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}