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}