Skip to main content

qframe/storage/
value.rs

1//! Setting values, the typed conversions applications use, and writing them as TOML.
2
3use std::fmt::Write as _;
4
5/// A value stored in settings.
6#[derive(Debug, Clone, PartialEq)]
7pub enum SettingValue {
8    /// `true` or `false`.
9    Bool(bool),
10    /// A whole number.
11    Integer(i64),
12    /// A number with a fraction.
13    Float(f64),
14    /// Text.
15    Text(String),
16    /// A list of values.
17    List(Vec<SettingValue>),
18}
19
20impl SettingValue {
21    /// The TOML type name, for diagnostics.
22    #[must_use]
23    pub fn type_name(&self) -> &'static str {
24        match self {
25            Self::Bool(_) => "boolean",
26            Self::Integer(_) => "integer",
27            Self::Float(_) => "float",
28            Self::Text(_) => "string",
29            Self::List(_) => "array",
30        }
31    }
32
33    /// The value as it is written in a file, for diagnostics.
34    pub(crate) fn literal(&self) -> String {
35        let mut out = String::new();
36        self.write(&mut out);
37        out
38    }
39
40    /// Writes the value as TOML.
41    pub(crate) fn write(&self, out: &mut String) {
42        // Writing into a `String` cannot fail, so there is no error here to carry anywhere; the
43        // results are dropped for that reason and no other.
44        match self {
45            Self::Bool(value) => out.push_str(if *value { "true" } else { "false" }),
46            Self::Integer(value) => {
47                let _ = write!(out, "{value}");
48            }
49            Self::Float(value) => out.push_str(&float(*value)),
50            Self::Text(text) => quote(text, out),
51            Self::List(items) => {
52                out.push('[');
53                for (index, item) in items.iter().enumerate() {
54                    if index > 0 {
55                        out.push_str(", ");
56                    }
57                    item.write(out);
58                }
59                out.push(']');
60            }
61        }
62    }
63}
64
65fn float(value: f64) -> String {
66    if value.is_nan() {
67        "nan".to_owned()
68    } else if value.is_infinite() {
69        if value > 0.0 { "inf".to_owned() } else { "-inf".to_owned() }
70    } else {
71        // `Debug` always keeps a fraction or an exponent, which TOML needs to read a float back.
72        format!("{value:?}")
73    }
74}
75
76/// Writes `text` as a TOML basic string.
77pub(crate) fn quote(text: &str, out: &mut String) {
78    out.push('"');
79    for c in text.chars() {
80        match c {
81            '"' => out.push_str("\\\""),
82            '\\' => out.push_str("\\\\"),
83            '\n' => out.push_str("\\n"),
84            '\r' => out.push_str("\\r"),
85            '\t' => out.push_str("\\t"),
86            // As in `write` above: the target is a `String`, which has no failure to report.
87            c if c.is_control() => {
88                let _ = write!(out, "\\u{:04X}", u32::from(c));
89            }
90            c => out.push(c),
91        }
92    }
93    out.push('"');
94}
95
96/// Writes one key segment: bare when TOML allows it, quoted otherwise.
97pub(crate) fn key(segment: &str, out: &mut String) {
98    let bare = !segment.is_empty() && segment.chars().all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_');
99    if bare {
100        out.push_str(segment);
101    } else {
102        quote(segment, out);
103    }
104}
105
106/// A Rust type that can be read from and written to settings.
107///
108/// Implemented for `bool`, `String`, the integer types, `f32`, `f64` and `Vec<String>`. Integers
109/// are stored as `i64`; a `u64` or `usize` above `i64::MAX` is stored as `i64::MAX`.
110pub trait Setting: Sized {
111    /// The stored form.
112    fn to_setting(self) -> SettingValue;
113
114    /// Reads a stored value; `None` when it has another type or does not fit.
115    fn from_setting(value: &SettingValue) -> Option<Self>;
116}
117
118impl Setting for bool {
119    fn to_setting(self) -> SettingValue {
120        SettingValue::Bool(self)
121    }
122
123    fn from_setting(value: &SettingValue) -> Option<Self> {
124        match value {
125            SettingValue::Bool(value) => Some(*value),
126            _ => None,
127        }
128    }
129}
130
131impl Setting for String {
132    fn to_setting(self) -> SettingValue {
133        SettingValue::Text(self)
134    }
135
136    fn from_setting(value: &SettingValue) -> Option<Self> {
137        match value {
138            SettingValue::Text(text) => Some(text.clone()),
139            _ => None,
140        }
141    }
142}
143
144impl Setting for f64 {
145    fn to_setting(self) -> SettingValue {
146        SettingValue::Float(self)
147    }
148
149    fn from_setting(value: &SettingValue) -> Option<Self> {
150        match value {
151            SettingValue::Float(value) => Some(*value),
152            // A whole number written without a fraction is still a number the user meant.
153            SettingValue::Integer(value) => i32::try_from(*value).ok().map(f64::from),
154            _ => None,
155        }
156    }
157}
158
159impl Setting for f32 {
160    fn to_setting(self) -> SettingValue {
161        SettingValue::Float(f64::from(self))
162    }
163
164    fn from_setting(value: &SettingValue) -> Option<Self> {
165        // Settings hold small numbers such as ratios; precision beyond f32 is not meaningful.
166        f64::from_setting(value).map(|value| value as f32)
167    }
168}
169
170macro_rules! integer_setting {
171    ($($ty:ty),*) => {$(
172        impl Setting for $ty {
173            fn to_setting(self) -> SettingValue {
174                SettingValue::Integer(i64::try_from(self).unwrap_or(i64::MAX))
175            }
176
177            fn from_setting(value: &SettingValue) -> Option<Self> {
178                match value {
179                    SettingValue::Integer(value) => <$ty>::try_from(*value).ok(),
180                    _ => None,
181                }
182            }
183        }
184    )*};
185}
186
187integer_setting!(i8, i16, i32, i64, u8, u16, u32, u64, usize);
188
189impl Setting for Vec<String> {
190    fn to_setting(self) -> SettingValue {
191        SettingValue::List(self.into_iter().map(SettingValue::Text).collect())
192    }
193
194    fn from_setting(value: &SettingValue) -> Option<Self> {
195        match value {
196            SettingValue::List(items) => items.iter().map(String::from_setting).collect(),
197            _ => None,
198        }
199    }
200}
201
202#[cfg(test)]
203mod tests {
204    use super::*;
205
206    #[test]
207    fn writes_toml_literals() {
208        assert_eq!(SettingValue::Text("a \"b\"\n\\".into()).literal(), r#""a \"b\"\n\\""#);
209        assert_eq!(SettingValue::Float(1.0).literal(), "1.0");
210        assert_eq!(SettingValue::Float(f64::NAN).literal(), "nan");
211        assert_eq!(vec!["x".to_owned(), "y".to_owned()].to_setting().literal(), r#"["x", "y"]"#);
212        let mut out = String::new();
213        key("tab width", &mut out);
214        key("tab-width", &mut out);
215        assert_eq!(out, "\"tab width\"tab-width");
216    }
217
218    #[test]
219    fn typed_reads_check_types_and_ranges() {
220        assert_eq!(u8::from_setting(&SettingValue::Integer(300)), None);
221        assert_eq!(u16::from_setting(&SettingValue::Integer(300)), Some(300));
222        assert_eq!(f64::from_setting(&SettingValue::Integer(2)), Some(2.0));
223        assert_eq!(bool::from_setting(&SettingValue::Text("true".into())), None);
224    }
225}