Skip to main content

knf/
set.rs

1//! `key.path=value` expressions: the one inline-layer spelling.
2//!
3//! [`PathLeaf`] pairs a [`RefPath`] with a leaf value. The path is always
4//! typed; the leaf type `V` is chosen by the caller. [`FromStr`] for
5//! [`PathLeaf<String>`] keeps the right-hand side raw, and the
6//! [`serde_json::Value`] impl parses it as JSON with a string fallback — a rule
7//! exported on its own as [`json_or_string`], for callers that need the same
8//! typing without a path.
9//!
10//! [`TryFrom<PathLeaf<Value>>`](TryFrom) expands to a nested object:
11//! `server.port=8080` → `{"server":{"port":8080}}`. There are deliberately no
12//! `Serialize`/`Deserialize` impls — a `PathLeaf` is an expression, and
13//! serializing one could reasonably mean either the string or the object, so
14//! callers pick explicitly via [`Display`](fmt::Display) or the conversion.
15
16use std::fmt;
17use std::str::FromStr;
18
19use crate::{ConfigFormat, ConfigObject, PathError, RefPath, Seg};
20use serde_json::{Map, Value};
21
22/// A leaf value addressed by a parsed path.
23///
24/// [`FromStr`] for [`PathLeaf<String>`] splits `key.path=value`, parses the LHS
25/// as a [`RefPath`], and stores the RHS as-is. The [`serde_json::Value`] impl
26/// parses that RHS as JSON, falling back to a string:
27/// `port=8080` is a number, `name=foo` is a string.
28///
29/// The grammar accepts bracket steps — `a[0]=1` parses — because it is the
30/// one spelling references also use. Whether such a path may *write* is
31/// [`RefPath::try_into_keys`]' question, asked at conversion time.
32///
33/// `Display` of a typed leaf is canonical — path, `=`, compact JSON of the
34/// leaf — so `name=foo` displays as `name="foo"`. [`FromStr`] ∘ [`Display`](fmt::Display)
35/// preserves path and leaf, not the original spelling. [`PathLeaf<String>`]
36/// displays the raw RHS.
37#[derive(Debug, Clone, PartialEq)]
38pub struct PathLeaf<V> {
39    path: RefPath,
40    leaf: V,
41}
42
43impl<V> PathLeaf<V> {
44    /// Build from path segments and a leaf. Rejects an empty path or any empty segment.
45    pub fn new(path: Vec<String>, leaf: V) -> Result<Self, PathError> {
46        Ok(Self {
47            path: RefPath::from_keys(path)?,
48            leaf,
49        })
50    }
51
52    /// The path as steps. May hold [`Index`](Seg::Index) steps from the
53    /// grammar — consumers that write run
54    /// [`try_into_keys`](RefPath::try_into_keys).
55    pub fn path(&self) -> &[Seg] {
56        self.path.segs()
57    }
58
59    /// The RHS value, not yet wrapped in nested objects.
60    pub fn leaf(&self) -> &V {
61        &self.leaf
62    }
63
64    /// Replace the leaf, keeping the path. The path is already valid, so this
65    /// cannot fail the way [`new`](Self::new) can.
66    pub fn map_leaf<T>(self, f: impl FnOnce(V) -> T) -> PathLeaf<T> {
67        PathLeaf {
68            path: self.path,
69            leaf: f(self.leaf),
70        }
71    }
72
73    /// [`map_leaf`](Self::map_leaf) when the conversion can fail.
74    pub fn try_map_leaf<T, E>(self, f: impl FnOnce(V) -> Result<T, E>) -> Result<PathLeaf<T>, E> {
75        Ok(PathLeaf {
76            path: self.path,
77            leaf: f(self.leaf)?,
78        })
79    }
80
81    /// Nests the leaf under every key in the path, innermost first.
82    fn try_into_nested(self, nest: impl Fn(String, V) -> V) -> Result<V, PathError> {
83        let keys = self.path.try_into_keys()?;
84        Ok(keys
85            .into_iter()
86            .rev()
87            .fold(self.leaf, |acc, key| nest(key, acc)))
88    }
89}
90
91impl PathLeaf<String> {
92    /// Validate this writer's path before reading inputs or selecting a format.
93    pub fn validate_keys(&self) -> Result<(), PathError> {
94        self.path.clone().try_into_keys().map(|_| ())
95    }
96
97    /// Type the RHS in the native format and expand it into a nested object.
98    pub fn into_layer<V: ConfigFormat>(self) -> Result<V, PathError> {
99        let keys = self.path.try_into_keys()?;
100        let leaf = V::parse_inline(self.leaf);
101        Ok(keys.into_iter().rev().fold(leaf, |value, key| {
102            let mut object = V::Object::new();
103            object.insert(key, value);
104            V::object(object)
105        }))
106    }
107}
108
109/// Parse a standalone TOML value, falling back to the original text as a string.
110/// Surrounding TOML whitespace is ignored when parsing a literal. Invalid and
111/// out-of-range literals, including `null`, retain the original text as strings.
112pub fn toml_or_string(text: String) -> toml::Value {
113    text.trim_matches([' ', '\t', '\r', '\n'])
114        .parse()
115        .unwrap_or_else(|_| toml::Value::String(text))
116}
117
118impl FromStr for PathLeaf<String> {
119    type Err = PathError;
120
121    fn from_str(expr: &str) -> Result<Self, Self::Err> {
122        // Split on the first `=` so the RHS may contain more of them.
123        let Some((lhs, rhs)) = expr.split_once('=') else {
124            return Err(PathError::MissingEquals);
125        };
126        Ok(Self {
127            path: lhs.parse()?,
128            leaf: rhs.to_string(),
129        })
130    }
131}
132
133impl fmt::Display for PathLeaf<String> {
134    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
135        write!(f, "{}={}", self.path, self.leaf)
136    }
137}
138impl FromStr for PathLeaf<Value> {
139    type Err = PathError;
140
141    fn from_str(expr: &str) -> Result<Self, Self::Err> {
142        Ok(PathLeaf::<String>::from_str(expr)?.into())
143    }
144}
145
146/// Parses text as JSON, falling back to the string itself.
147///
148/// `8080` is a number, `true` is a bool, `foo` is the string `"foo"` because it
149/// is not valid JSON, and `[a,b]` is the string `"[a,b]"` for the same reason.
150///
151/// Public because more than one caller needs *this* rule rather than a rule like
152/// it: `--set`'s RHS and `${env:VAR}` in a whole-string position must type
153/// identically, and two matching implementations would only agree until one of
154/// them was edited.
155pub fn json_or_string(text: String) -> Value {
156    serde_json::from_str(&text).unwrap_or_else(|_| Value::String(text))
157}
158
159impl From<PathLeaf<String>> for PathLeaf<Value> {
160    fn from(path_leaf: PathLeaf<String>) -> Self {
161        path_leaf.map_leaf(json_or_string)
162    }
163}
164
165impl fmt::Display for PathLeaf<Value> {
166    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
167        // Infallible for a `Value`: only maps with non-string keys and
168        // non-finite floats can fail, and neither survives a JSON parse.
169        let rhs = serde_json::to_string(&self.leaf).expect("a Value always serializes");
170        write!(f, "{}={rhs}", self.path)
171    }
172}
173
174impl TryFrom<PathLeaf<Value>> for Value {
175    type Error = PathError;
176
177    /// Expands to a nested object. Fallible because the grammar accepts
178    /// bracket steps (`a[0]=1` parses) that a writer cannot use: an index
179    /// never reaches the nested-object expansion.
180    fn try_from(path_leaf: PathLeaf<Value>) -> Result<Self, Self::Error> {
181        path_leaf.try_into_nested(|key, acc| {
182            let mut obj = Map::new();
183            obj.insert(key, acc);
184            Value::Object(obj)
185        })
186    }
187}
188
189#[cfg(test)]
190mod tests {
191    use serde_json::json;
192
193    use super::*;
194
195    #[test]
196    fn toml_inline_literals_ignore_surrounding_whitespace() {
197        for literal in [
198            "8080",
199            "true",
200            "1.0",
201            "1979-05-27",
202            "inf",
203            "[1, 2]",
204            "{a=1}",
205            "' name '",
206        ] {
207            for padding in [" ", "\t", "\n", "\r\n", " \t\r\n"] {
208                let expected = toml_or_string(literal.into());
209                for text in [
210                    format!("{padding}{literal}"),
211                    format!("{literal}{padding}"),
212                    format!("{padding}{literal}{padding}"),
213                ] {
214                    assert_eq!(toml_or_string(text.clone()), expected, "{text:?}");
215                }
216            }
217        }
218        for text in [
219            " \tnull\n",
220            " 9223372036854775808\n",
221            " [a,b] ",
222            " text\n",
223            " \t\r\n",
224            "\u{a0}8080\u{a0}",
225        ] {
226            assert_eq!(
227                toml_or_string(text.into()),
228                toml::Value::String(text.into())
229            );
230        }
231    }
232
233    #[test]
234    fn rejects_malformed_expressions() {
235        for (bad, want) in [
236            ("noequals", PathError::MissingEquals),
237            ("=1", PathError::EmptyPath),
238            (
239                "a..b=1",
240                PathError::EmptySegment {
241                    path: "a..b".into(),
242                },
243            ),
244            (".a=1", PathError::EmptySegment { path: ".a".into() }),
245            ("a.=1", PathError::EmptySegment { path: "a.".into() }),
246            ("a[]=1", PathError::BadIndex { path: "a[]".into() }),
247        ] {
248            assert_eq!(bad.parse::<PathLeaf<String>>().unwrap_err(), want, "{bad}");
249        }
250    }
251
252    #[test]
253    fn new_rejects_empty_path_and_empty_segments() {
254        assert_eq!(
255            PathLeaf::<String>::new(vec![], "1".into()).unwrap_err(),
256            PathError::EmptyPath
257        );
258        assert_eq!(
259            PathLeaf::<String>::new(vec!["".into()], "1".into()).unwrap_err(),
260            PathError::EmptySegment { path: "".into() }
261        );
262        assert_eq!(
263            PathLeaf::<String>::new(vec!["a".into(), "".into()], "1".into()).unwrap_err(),
264            PathError::EmptySegment { path: "a.".into() }
265        );
266    }
267
268    #[test]
269    fn raw_fromstr_keeps_the_rhs_unparsed() {
270        let path_leaf: PathLeaf<String> = "port=8080".parse().unwrap();
271        assert_eq!(path_leaf.path(), [Seg::Key("port".into())]);
272        assert_eq!(path_leaf.leaf(), "8080");
273        assert_eq!(path_leaf.to_string(), "port=8080");
274    }
275
276    /// The grammar accepts bracket steps; only writers reject them.
277    #[test]
278    fn path_leaf_accepts_brackets_its_writers_reject() {
279        let parsed: PathLeaf<String> = "a[0]=1".parse().unwrap();
280        assert_eq!(parsed.path(), [Seg::Key("a".into()), Seg::Index(0)]);
281        assert_eq!(parsed.to_string(), "a[0]=1");
282    }
283
284    #[test]
285    fn map_leaf_preserves_the_path() {
286        let path_leaf = PathLeaf::new(vec!["a".into()], "xy".to_string())
287            .unwrap()
288            .map_leaf(|s| s.len());
289        assert_eq!(path_leaf.path(), [Seg::Key("a".into())]);
290        assert_eq!(*path_leaf.leaf(), 2);
291    }
292
293    fn parse(expr: &str) -> PathLeaf<Value> {
294        expr.parse().expect("valid PathLeaf")
295    }
296
297    fn nested(expr: &str) -> Value {
298        Value::try_from(parse(expr)).expect("all-key path")
299    }
300
301    /// The §4.2 table, verbatim.
302    #[test]
303    fn value_typing() {
304        assert_eq!(nested("port=8080"), json!({"port": 8080}));
305        assert_eq!(nested("debug=true"), json!({"debug": true}));
306        assert_eq!(nested("name=foo"), json!({"name": "foo"}));
307        assert_eq!(nested("proxy=null"), json!({"proxy": null}));
308        assert_eq!(nested(r#"tags=["a","b"]"#), json!({"tags": ["a", "b"]}));
309        assert_eq!(nested("tags=[a,b]"), json!({"tags": "[a,b]"}));
310    }
311
312    /// The sharp edge: a bare `1.0` is a number.
313    #[test]
314    fn numeric_looking_strings() {
315        assert_eq!(nested("version=1.0"), json!({"version": 1.0}));
316        assert_eq!(nested(r#"version="1.0""#), json!({"version": "1.0"}));
317    }
318
319    #[test]
320    fn dotted_paths_nest() {
321        assert_eq!(
322            nested("server.port=8080"),
323            json!({"server": {"port": 8080}})
324        );
325        assert_eq!(nested("a.b.c=1"), json!({"a": {"b": {"c": 1}}}));
326    }
327
328    #[test]
329    fn splits_on_the_first_equals_only() {
330        assert_eq!(nested("q=a=b"), json!({"q": "a=b"}));
331        assert_eq!(nested("q="), json!({"q": ""}));
332    }
333
334    #[test]
335    fn display_is_canonical() {
336        assert_eq!(parse("name=foo").to_string(), r#"name="foo""#);
337        assert_eq!(parse("port=8080").to_string(), "port=8080");
338        assert_eq!(parse("q=").to_string(), r#"q="""#);
339        assert_eq!(parse("q=a=b").to_string(), r#"q="a=b""#);
340        assert_eq!(parse("server.port=8080").to_string(), "server.port=8080");
341    }
342
343    #[test]
344    fn fromstr_display_preserves_path_and_leaf() {
345        for expr in [
346            "port=8080",
347            "name=foo",
348            r#"name="foo""#,
349            "debug=true",
350            "proxy=null",
351            r#"tags=["a","b"]"#,
352            "q=",
353            "q=a=b",
354            "server.port=8080",
355        ] {
356            let parsed = parse(expr);
357            let round = parsed.to_string().parse::<PathLeaf<Value>>().unwrap();
358            assert_eq!(round.path(), parsed.path(), "{expr}");
359            assert_eq!(round.leaf(), parsed.leaf(), "{expr}");
360        }
361    }
362
363    #[test]
364    fn from_raw_path_leaf_parses_the_rhs() {
365        let raw: PathLeaf<String> = "server.port=8080".parse().unwrap();
366        let typed = PathLeaf::<Value>::from(raw);
367        assert_eq!(
368            Value::try_from(typed).unwrap(),
369            json!({"server": {"port": 8080}})
370        );
371    }
372
373    /// Brackets parse — a reference may read an element — but a `--set`-shaped
374    /// expression can never expand one into a writer's nested object.
375    #[test]
376    fn bracketed_paths_parse_but_cannot_write() {
377        let err = Value::try_from(parse("servers[0].host=x")).unwrap_err();
378        assert_eq!(
379            err,
380            PathError::IndexInKeyPath {
381                path: "servers[0].host".into()
382            }
383        );
384        assert_eq!(
385            err.to_string(),
386            "`servers[0].host` contains an array index; merge paths take keys only"
387        );
388    }
389}