rpick/config.rs
1/* Copyright © 2019-2023 Randy Barlow
2This program is free software: you can redistribute it and/or modify
3it under the terms of the GNU General Public License as published by
4the Free Software Foundation, version 3 of the License.
5
6This program is distributed in the hope that it will be useful,
7but WITHOUT ANY WARRANTY; without even the implied warranty of
8MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9GNU General Public License for more details.
10
11You should have received a copy of the GNU General Public License
12along with this program. If not, see <http://www.gnu.org/licenses/>.*/
13//! # Configuration
14//!
15//! This module defines the rpick configuration.
16//!
17//! The configuration defines the pick categories, their algorithms, and their choices.
18use std::collections::BTreeMap;
19use std::error;
20use std::fs::{File, OpenOptions};
21use std::io::{BufReader, Write};
22
23use serde::{Deserialize, Serialize};
24
25/// Return the user's config as a BTreeMap.
26///
27/// # Arguments
28///
29/// * `config_file_path` - A filesystem path to a YAML file that should be read.
30///
31/// # Returns
32///
33/// Returns a mapping of YAML to [`ConfigCategory`]'s, or an Error.
34pub fn read_config(
35 config_file_path: &str,
36) -> Result<BTreeMap<String, ConfigCategory>, Box<dyn error::Error>> {
37 let f = File::open(config_file_path)?;
38 let reader = BufReader::new(f);
39
40 let config: BTreeMap<String, ConfigCategory> = serde_yaml::from_reader(reader)?;
41 Ok(config)
42}
43
44/// Save the data from the given BTreeMap to the user's config file.
45///
46/// # Arguments
47///
48/// * `config_file_path` - A filesystem path that the config should be written to.
49/// * `config` - The config that should be serialized as YAML.
50pub fn write_config(
51 config_file_path: &str,
52 config: BTreeMap<String, ConfigCategory>,
53) -> Result<(), Box<dyn error::Error>> {
54 let mut f = OpenOptions::new()
55 .write(true)
56 .create(true)
57 .truncate(true)
58 .open(config_file_path)?;
59 let yaml = serde_yaml::to_string(&config).unwrap();
60
61 f.write_all(&yaml.into_bytes())?;
62 Ok(())
63}
64
65/// A category of items that can be chosen from.
66///
67/// Each variant of this Enum maps to one of the supported algorithms.
68#[derive(Debug, PartialEq, Serialize, Deserialize)]
69#[serde(deny_unknown_fields)]
70#[serde(rename_all = "snake_case")]
71#[serde(tag = "model")]
72pub enum ConfigCategory {
73 /// The Even variant picks from its choices with even distribution.
74 ///
75 /// # Attributes
76 ///
77 /// * `choices` - The list of choices to pick from.
78 Even { choices: Vec<String> },
79 /// The Gaussian variant uses a
80 /// [Gaussian distribution](https://en.wikipedia.org/wiki/Normal_distribution) to prefer choices
81 /// near the beginning of the list of choices over those at the end. Once a choice has been
82 /// accepted, it is moved to the end of the list.
83 ///
84 /// # Attributes
85 ///
86 /// * `stddev_scaling_factor` - This is used to derive the standard deviation; the standard
87 /// deviation is the length of the list of choices, divided by this scaling factor.
88 /// * `choices` - The list of choices to pick from.
89 Gaussian {
90 #[serde(default = "default_stddev_scaling_factor")]
91 stddev_scaling_factor: f64,
92 choices: Vec<String>,
93 },
94 /// The Inventory variant uses a weighted distribution to pick items, with each items chances
95 /// being tied to how many tickets it has. When a choice is accepted, that choice's ticket
96 /// count is reduced by 1.
97 ///
98 /// # Attributes
99 ///
100 /// * `choices` - The list of choices to pick from.
101 Inventory { choices: Vec<InventoryChoice> },
102 /// The Lru variant picks the Least Recently Used item from the list of choices. The least
103 /// recently used choice is found at the beginning of the list. Once a choice has been
104 /// accepted, it is moved to the end of the list.
105 ///
106 /// # Attributes
107 ///
108 /// * `choices` - The list of choices to pick from.
109 #[serde(rename = "lru")]
110 Lru { choices: Vec<String> },
111 /// The Lottery variant uses a weighted distribution to pick items, with each items chances
112 /// being tied to how many tickets it has. When a choice is accepted, that choice's ticket
113 /// count is set to 0, and every choice not chosen receives its weight in additional tickets.
114 ///
115 /// # Attributes
116 ///
117 /// * `choices` - The list of choices to pick from.
118 Lottery { choices: Vec<LotteryChoice> },
119 /// The Weighted variant is a simple weighted distribution.
120 ///
121 /// # Attributes
122 ///
123 /// * `choices` - The list of choices to pick from.
124 Weighted { choices: Vec<WeightedChoice> },
125}
126
127/// Represents an individual choice for the inventory model.
128///
129/// # Attributes
130///
131/// * `name` - The name of the choice.
132/// * `tickets` - The current number of tickets the choice has.
133#[derive(Debug, Eq, PartialEq, Serialize, Deserialize)]
134pub struct InventoryChoice {
135 pub name: String,
136 #[serde(default = "default_weight")]
137 pub tickets: u64,
138}
139
140/// Represents an individual choice for the lottery model.
141///
142/// # Attributes
143///
144#[derive(Debug, Eq, PartialEq, Serialize, Deserialize)]
145pub struct LotteryChoice {
146 /// The name of the choice
147 pub name: String,
148
149 /// How many tickets the choice should be reset to when it is chosen.
150 #[serde(default = "default_reset")]
151 pub reset: u64,
152
153 /// The current number of tickets the choice has.
154 #[serde(default = "default_weight")]
155 pub tickets: u64,
156
157 /// The number of tickets that will be added to `tickets` each time this choice is not picked.
158 #[serde(default = "default_weight")]
159 pub weight: u64,
160}
161
162/// Represents an individual choice for the weighted model.
163///
164/// # Attributes
165///
166/// * `name` - The name of the choice
167/// * `weight` - How much chance this choice has of being chosen, relative to the other choices.
168#[derive(Debug, Eq, PartialEq, Serialize, Deserialize)]
169pub struct WeightedChoice {
170 pub name: String,
171 #[serde(default = "default_weight")]
172 pub weight: u64,
173}
174
175/// Define the default for the stddev_scaling_factor setting as 3.0.
176fn default_stddev_scaling_factor() -> f64 {
177 3.0
178}
179
180/// Reset to 0 by default.
181fn default_reset() -> u64 {
182 0
183}
184
185/// Define the default for the weight setting as 1.
186fn default_weight() -> u64 {
187 1
188}
189
190#[cfg(test)]
191mod tests {
192 use super::*;
193
194 #[test]
195 fn test_defaults() {
196 assert!((default_stddev_scaling_factor() - 3.0).abs() < 0.000_001);
197 assert_eq!(default_weight(), 1);
198 assert_eq!(default_reset(), 0);
199 }
200}