Skip to main content

usage_config/
value.rs

1//! What a setting holds, at runtime and as a declared default.
2//!
3//! Two types rather than one, because they answer to different masters. [`Value`] is owned:
4//! it comes from a file or an environment variable while the process runs, so it has to be
5//! allocated. [`Const`] is what a *declared* default is, and every field of it is
6//! `const`-constructible so a generated registry costs nothing to load — no parsing, no
7//! allocation, nothing done per process start for a setting nobody reads.
8
9use std::collections::BTreeMap;
10use std::fmt;
11
12/// Text on one line, whatever it contains.
13///
14/// Everything this crate prints is line-oriented — one setting per line for a listing, one fact
15/// per line for an explanation, one warning or failure per line — so anything interpolated into a
16/// line has to stay on it. Three kinds of thing can carry a newline and all three did: a value (a
17/// multi-line string is perfectly ordinary in TOML), an origin (a path may contain one), and a
18/// message that quotes either of them. Lives here beside [`Value::display`] because that is what
19/// it is usually wrapped around, and one rule shared by every renderer is one rule.
20pub(crate) fn one_line(text: &str) -> String {
21    // Newlines only. Escaping backslashes as well made `C:\Users\me\hk.toml` render with
22    // doubled separators — a path a reader would copy and find nothing at, which is a worse
23    // failure than the ambiguity it bought: this output is for a human, and the one thing it
24    // needs is that a record stays on its line.
25    text.replace('\n', "\\n").replace('\r', "\\r")
26}
27
28/// A value as one line of output, with its shape shown when there is nothing in it.
29///
30/// [`Value::display`] writes a value the way a user would type it, and three values are typed as
31/// nothing at all: the empty string, the empty list, the empty map. Interpolated into `key = {}`
32/// that produced a line ending after the `=` — a trailing space and a truncated look for a value
33/// that is perfectly ordinary, since clearing a list is how a declared default is turned off
34/// (`HK_EXCLUDE=`). Emptiness is a fact about a value and worth a word, so it gets the spelling
35/// its own format would use.
36///
37/// Not folded into `display` itself, which is also what `config get` prints: there, an empty
38/// setting printing nothing is exactly right, and `[]` would be a value nobody wrote.
39pub(crate) fn shown(value: &Value) -> String {
40    match value {
41        // Asked of the *value*, not of its text. Read from the text, a one-item list holding the
42        // empty string looked exactly like a cleared one — `[]` for a list that has something in
43        // it, which says the opposite of what is true.
44        Value::List(items) if items.is_empty() => "[]".to_string(),
45        Value::Map(entries) if entries.is_empty() => "{}".to_string(),
46        Value::String(text) if text.is_empty() => "\"\"".to_string(),
47        // A list is joined with commas, which hides any item whose own text is empty: one empty
48        // string came out as nothing (a cleared list), and two came out as `,`. So when an item
49        // would disappear, the whole list is written out item by item instead — `[""]` is one item
50        // and reads as one, `[a,""]` is two.
51        Value::List(items) if items.iter().any(|item| item.display().is_empty()) => {
52            let items: Vec<String> = items.iter().map(shown).collect();
53            format!("[{}]", items.join(","))
54        }
55        // Everything else writes something: a number and a boolean always do, and a map's keys are
56        // in its text even when its values are empty.
57        other => one_line(&other.display()),
58    }
59}
60
61/// A resolved configuration value.
62#[derive(Debug, Clone, PartialEq)]
63pub enum Value {
64    Bool(bool),
65    Int(i64),
66    Float(f64),
67    String(String),
68    List(Vec<Value>),
69    /// A table. Ordered by key so a resolution is reproducible and two runs of `config
70    /// explain` cannot disagree about what came first.
71    Map(BTreeMap<String, Value>),
72}
73
74impl Value {
75    /// The name of this shape, for an error a human has to read.
76    pub fn type_name(&self) -> &'static str {
77        match self {
78            Self::Bool(_) => "a boolean",
79            Self::Int(_) => "an integer",
80            Self::Float(_) => "a number",
81            Self::String(_) => "a string",
82            Self::List(_) => "a list",
83            Self::Map(_) => "a table",
84        }
85    }
86
87    /// This value written the way a user would type it.
88    pub fn display(&self) -> String {
89        match self {
90            Self::Bool(b) => b.to_string(),
91            Self::Int(i) => i.to_string(),
92            // With its point, because `1` is how an *integer* is written and this is not one. Left
93            // as `f64::to_string` gives it, a whole-number float and an integer were the same text —
94            // so a `choice 1.0` accepted `1` and refused the `1.0` the spec had written, and the
95            // list of what a setting allows said `1` back to an author who had not typed that.
96            Self::Float(f) => {
97                let text = f.to_string();
98                match f.is_finite() && !text.contains(['.', 'e', 'E']) {
99                    true => format!("{text}.0"),
100                    false => text,
101                }
102            }
103            Self::String(s) => s.clone(),
104            Self::List(items) => items
105                .iter()
106                .map(Self::display)
107                .collect::<Vec<_>>()
108                .join(","),
109            Self::Map(entries) => entries
110                .iter()
111                .map(|(key, value)| format!("{key}={}", value.display()))
112                .collect::<Vec<_>>()
113                .join(","),
114        }
115    }
116}
117
118impl fmt::Display for Value {
119    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
120        f.write_str(&self.display())
121    }
122}
123
124impl From<bool> for Value {
125    fn from(value: bool) -> Self {
126        Self::Bool(value)
127    }
128}
129
130impl From<i64> for Value {
131    fn from(value: i64) -> Self {
132        Self::Int(value)
133    }
134}
135
136impl From<f64> for Value {
137    fn from(value: f64) -> Self {
138        Self::Float(value)
139    }
140}
141
142impl From<&str> for Value {
143    fn from(value: &str) -> Self {
144        Self::String(value.to_string())
145    }
146}
147
148impl From<String> for Value {
149    fn from(value: String) -> Self {
150        Self::String(value)
151    }
152}
153
154/// A declared default, in the form a generated registry can hold as a `const`.
155///
156/// The same shapes as [`Value`], with borrowed strings and slices so nothing is allocated
157/// until somebody actually asks for the default of a setting no layer supplied.
158#[derive(Debug, Copy, Clone, PartialEq)]
159pub enum Const {
160    Bool(bool),
161    Int(i64),
162    Float(f64),
163    Str(&'static str),
164    List(&'static [Const]),
165    /// Key-value pairs, ordered by the generator so the `Value` it becomes is too.
166    Map(&'static [(&'static str, Const)]),
167}
168
169impl Const {
170    /// Whether `value` is this constant, or is written the same way.
171    ///
172    /// The same shape compares directly, without building the `Value` this stands for: the strict
173    /// case is the common one, it runs once per declared choice for every value supplied, and a
174    /// setting with choices is usually a string — where the comparison would otherwise allocate a
175    /// copy of the choice to throw away.
176    ///
177    /// The shapes differing is not a mismatch, though, and this is the part worth explaining. A spec
178    /// writes `choice 4` under `type="string"` as readily as `choice "4"`, and by the time a value
179    /// reaches here it has been coerced to the *declared* type — so the choice is an integer and the
180    /// value is the string `4`, and a strict comparison refuses a value the spec plainly allows.
181    /// Comparing what they are written as is the same question the coercion already answered:
182    /// `Ty::String` turns `4` into `"4"`, and `Ty::Float` turns `1` into `1.0`, whose text is `1`
183    /// either way. Only scalars get here — [`PropMeta::refuses`](crate::PropMeta::refuses) walks a
184    /// list or a table item by item first — so there is no way for `a,b` the string to be mistaken
185    /// for `[a, b]` the list.
186    pub fn matches(self, value: &Value) -> bool {
187        match (self, value) {
188            (Self::Bool(a), Value::Bool(b)) => a == *b,
189            (Self::Int(a), Value::Int(b)) => a == *b,
190            (Self::Float(a), Value::Float(b)) => a == *b,
191            (Self::Str(a), Value::String(b)) => a == b,
192            // A list or a table is not something a `choice` node can hold, and one of those is not a
193            // scalar written differently.
194            (Self::List(_) | Self::Map(_), _) | (_, Value::List(_) | Value::Map(_)) => false,
195            (choice, value) => choice.to_value().display() == value.display(),
196        }
197    }
198
199    /// The owned value this stands for.
200    pub fn to_value(self) -> Value {
201        match self {
202            Self::Bool(b) => Value::Bool(b),
203            Self::Int(i) => Value::Int(i),
204            Self::Float(f) => Value::Float(f),
205            Self::Str(s) => Value::String(s.to_string()),
206            Self::List(items) => Value::List(items.iter().map(|item| item.to_value()).collect()),
207            Self::Map(entries) => Value::Map(
208                entries
209                    .iter()
210                    .map(|(key, value)| ((*key).to_string(), value.to_value()))
211                    .collect(),
212            ),
213        }
214    }
215}
216
217#[cfg(test)]
218mod tests {
219    use super::*;
220
221    #[test]
222    fn a_declared_default_becomes_the_value_it_names() {
223        // The registry holds these as consts, so this conversion is the only cost a default
224        // ever has — and only for a setting whose default is actually reached for.
225        const NESTED: &[Const] = &[Const::Int(80), Const::Int(443)];
226        const PAIRS: &[(&str, Const)] = &[("a", Const::Bool(true))];
227        assert_eq!(Const::Bool(true).to_value(), Value::Bool(true));
228        assert_eq!(Const::Str("x").to_value(), Value::String("x".into()));
229        assert_eq!(
230            Const::List(NESTED).to_value(),
231            Value::List(vec![Value::Int(80), Value::Int(443)])
232        );
233        assert_eq!(
234            Const::Map(PAIRS).to_value(),
235            Value::Map([("a".to_string(), Value::Bool(true))].into_iter().collect())
236        );
237    }
238
239    #[test]
240    fn an_empty_value_is_told_apart_from_a_value_that_writes_as_nothing() {
241        // The distinction `explain`, `list` and every type error rest on. A cleared list is a
242        // supported state — `HK_EXCLUDE=` turns a declared default off — and a list holding one
243        // empty string is a different one, though their text is identical.
244        assert_eq!(shown(&Value::List(Vec::new())), "[]");
245        assert_eq!(shown(&Value::List(vec![Value::from("")])), "[\"\"]");
246        assert_eq!(
247            shown(&Value::List(vec![Value::from(""), Value::from("")])),
248            "[\"\",\"\"]"
249        );
250        assert_eq!(shown(&Value::from("")), "\"\"");
251        assert_eq!(shown(&Value::Map(BTreeMap::new())), "{}");
252
253        // And anything with text of its own is that text, unquoted and unbracketed: this is output
254        // a person reads, not a serializer.
255        assert_eq!(shown(&Value::from("git")), "git");
256        assert_eq!(shown(&Value::Int(0)), "0");
257        assert_eq!(shown(&Value::Bool(false)), "false");
258        assert_eq!(
259            shown(&Value::List(vec![Value::from("a"), Value::from("b")])),
260            "a,b"
261        );
262        // A list with one empty item among several is written out too, since that item is the one
263        // the comma-joined form would lose.
264        assert_eq!(
265            shown(&Value::List(vec![Value::from("a"), Value::from("")])),
266            "[a,\"\"]"
267        );
268        // A map whose value is empty still has its key in the text, which is enough to read.
269        assert_eq!(
270            shown(&Value::Map(
271                [("k".to_string(), Value::from(""))].into_iter().collect()
272            )),
273            "k="
274        );
275    }
276
277    #[test]
278    fn a_choice_and_a_value_written_the_same_way_are_the_same_choice() {
279        // A spec writes `choice 4` under `type="string"` as readily as `choice "4"`, and by the time
280        // a value reaches the check it has been coerced to the *declared* type — so the choice is an
281        // integer, the value is the string `4`, and comparing shapes refused a value the spec plainly
282        // allows.
283        assert!(Const::Int(4).matches(&Value::from("4")));
284        assert!(Const::Str("4").matches(&Value::Int(4)));
285        assert!(Const::Bool(true).matches(&Value::from("true")));
286        // A float and an integer are *not* the same text, and should not be: `1.0` is a float and
287        // `1` is not one. The pair that matters — `type="float"` with `choice 1` — is settled before
288        // this by the coercion, which reads the choice as a float, and `PropMeta::refuses` has a test
289        // for that.
290        assert!(!Const::Int(1).matches(&Value::Float(1.0)));
291        assert_eq!(Value::Float(1.0).display(), "1.0");
292        assert_eq!(Value::Float(0.5).display(), "0.5");
293        assert_eq!(Value::Int(1).display(), "1");
294
295        // The same shape still compares as itself, and different values are still different.
296        assert!(Const::Str("git").matches(&Value::from("git")));
297        assert!(!Const::Str("git").matches(&Value::from("svn")));
298        assert!(!Const::Int(4).matches(&Value::Int(5)));
299
300        // A collection is never compared to a scalar: `PropMeta::refuses` walks one item by item
301        // first, so `a,b` the string cannot be mistaken for `[a, b]` the list.
302        const ITEMS: &[Const] = &[Const::Str("a"), Const::Str("b")];
303        let list = Value::List(vec![Value::from("a"), Value::from("b")]);
304        assert!(!Const::Str("a,b").matches(&list));
305        assert!(!Const::List(ITEMS).matches(&Value::from("a,b")));
306    }
307
308    #[test]
309    fn a_value_can_be_written_the_way_it_was_typed() {
310        // What `config get` prints and what an error quotes back, so a list has to read as
311        // one rather than as its debug form.
312        assert_eq!(Value::Bool(false).display(), "false");
313        assert_eq!(
314            Value::List(vec![Value::String("a".into()), Value::Int(2)]).display(),
315            "a,2"
316        );
317        assert_eq!(
318            Value::Map([("k".to_string(), Value::Int(1))].into_iter().collect()).display(),
319            "k=1"
320        );
321    }
322}