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::{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 FromStr for PathLeaf<String> {
92    type Err = PathError;
93
94    fn from_str(expr: &str) -> Result<Self, Self::Err> {
95        // Split on the first `=` so the RHS may contain more of them.
96        let Some((lhs, rhs)) = expr.split_once('=') else {
97            return Err(PathError::MissingEquals);
98        };
99        Ok(Self {
100            path: lhs.parse()?,
101            leaf: rhs.to_string(),
102        })
103    }
104}
105
106impl fmt::Display for PathLeaf<String> {
107    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
108        write!(f, "{}={}", self.path, self.leaf)
109    }
110}
111impl FromStr for PathLeaf<Value> {
112    type Err = PathError;
113
114    fn from_str(expr: &str) -> Result<Self, Self::Err> {
115        Ok(PathLeaf::<String>::from_str(expr)?.into())
116    }
117}
118
119/// Parses text as JSON, falling back to the string itself.
120///
121/// `8080` is a number, `true` is a bool, `foo` is the string `"foo"` because it
122/// is not valid JSON, and `[a,b]` is the string `"[a,b]"` for the same reason.
123///
124/// Public because more than one caller needs *this* rule rather than a rule like
125/// it: `--set`'s RHS and `${env:VAR}` in a whole-string position must type
126/// identically, and two matching implementations would only agree until one of
127/// them was edited.
128pub fn json_or_string(text: String) -> Value {
129    serde_json::from_str(&text).unwrap_or_else(|_| Value::String(text))
130}
131
132impl From<PathLeaf<String>> for PathLeaf<Value> {
133    fn from(path_leaf: PathLeaf<String>) -> Self {
134        path_leaf.map_leaf(json_or_string)
135    }
136}
137
138impl fmt::Display for PathLeaf<Value> {
139    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
140        // Infallible for a `Value`: only maps with non-string keys and
141        // non-finite floats can fail, and neither survives a JSON parse.
142        let rhs = serde_json::to_string(&self.leaf).expect("a Value always serializes");
143        write!(f, "{}={rhs}", self.path)
144    }
145}
146
147impl TryFrom<PathLeaf<Value>> for Value {
148    type Error = PathError;
149
150    /// Expands to a nested object. Fallible because the grammar accepts
151    /// bracket steps (`a[0]=1` parses) that a writer cannot use: an index
152    /// never reaches the nested-object expansion.
153    fn try_from(path_leaf: PathLeaf<Value>) -> Result<Self, Self::Error> {
154        path_leaf.try_into_nested(|key, acc| {
155            let mut obj = Map::new();
156            obj.insert(key, acc);
157            Value::Object(obj)
158        })
159    }
160}
161
162#[cfg(test)]
163mod tests {
164    use serde_json::json;
165
166    use super::*;
167
168    #[test]
169    fn rejects_malformed_expressions() {
170        for (bad, want) in [
171            ("noequals", PathError::MissingEquals),
172            ("=1", PathError::EmptyPath),
173            (
174                "a..b=1",
175                PathError::EmptySegment {
176                    path: "a..b".into(),
177                },
178            ),
179            (".a=1", PathError::EmptySegment { path: ".a".into() }),
180            ("a.=1", PathError::EmptySegment { path: "a.".into() }),
181            ("a[]=1", PathError::BadIndex { path: "a[]".into() }),
182        ] {
183            assert_eq!(bad.parse::<PathLeaf<String>>().unwrap_err(), want, "{bad}");
184        }
185    }
186
187    #[test]
188    fn new_rejects_empty_path_and_empty_segments() {
189        assert_eq!(
190            PathLeaf::<String>::new(vec![], "1".into()).unwrap_err(),
191            PathError::EmptyPath
192        );
193        assert_eq!(
194            PathLeaf::<String>::new(vec!["".into()], "1".into()).unwrap_err(),
195            PathError::EmptySegment { path: "".into() }
196        );
197        assert_eq!(
198            PathLeaf::<String>::new(vec!["a".into(), "".into()], "1".into()).unwrap_err(),
199            PathError::EmptySegment { path: "a.".into() }
200        );
201    }
202
203    #[test]
204    fn raw_fromstr_keeps_the_rhs_unparsed() {
205        let path_leaf: PathLeaf<String> = "port=8080".parse().unwrap();
206        assert_eq!(path_leaf.path(), [Seg::Key("port".into())]);
207        assert_eq!(path_leaf.leaf(), "8080");
208        assert_eq!(path_leaf.to_string(), "port=8080");
209    }
210
211    /// The grammar accepts bracket steps; only writers reject them.
212    #[test]
213    fn path_leaf_accepts_brackets_its_writers_reject() {
214        let parsed: PathLeaf<String> = "a[0]=1".parse().unwrap();
215        assert_eq!(parsed.path(), [Seg::Key("a".into()), Seg::Index(0)]);
216        assert_eq!(parsed.to_string(), "a[0]=1");
217    }
218
219    #[test]
220    fn map_leaf_preserves_the_path() {
221        let path_leaf = PathLeaf::new(vec!["a".into()], "xy".to_string())
222            .unwrap()
223            .map_leaf(|s| s.len());
224        assert_eq!(path_leaf.path(), [Seg::Key("a".into())]);
225        assert_eq!(*path_leaf.leaf(), 2);
226    }
227
228    fn parse(expr: &str) -> PathLeaf<Value> {
229        expr.parse().expect("valid PathLeaf")
230    }
231
232    fn nested(expr: &str) -> Value {
233        Value::try_from(parse(expr)).expect("all-key path")
234    }
235
236    /// The §4.2 table, verbatim.
237    #[test]
238    fn value_typing() {
239        assert_eq!(nested("port=8080"), json!({"port": 8080}));
240        assert_eq!(nested("debug=true"), json!({"debug": true}));
241        assert_eq!(nested("name=foo"), json!({"name": "foo"}));
242        assert_eq!(nested("proxy=null"), json!({"proxy": null}));
243        assert_eq!(nested(r#"tags=["a","b"]"#), json!({"tags": ["a", "b"]}));
244        assert_eq!(nested("tags=[a,b]"), json!({"tags": "[a,b]"}));
245    }
246
247    /// The sharp edge: a bare `1.0` is a number.
248    #[test]
249    fn numeric_looking_strings() {
250        assert_eq!(nested("version=1.0"), json!({"version": 1.0}));
251        assert_eq!(nested(r#"version="1.0""#), json!({"version": "1.0"}));
252    }
253
254    #[test]
255    fn dotted_paths_nest() {
256        assert_eq!(
257            nested("server.port=8080"),
258            json!({"server": {"port": 8080}})
259        );
260        assert_eq!(nested("a.b.c=1"), json!({"a": {"b": {"c": 1}}}));
261    }
262
263    #[test]
264    fn splits_on_the_first_equals_only() {
265        assert_eq!(nested("q=a=b"), json!({"q": "a=b"}));
266        assert_eq!(nested("q="), json!({"q": ""}));
267    }
268
269    #[test]
270    fn display_is_canonical() {
271        assert_eq!(parse("name=foo").to_string(), r#"name="foo""#);
272        assert_eq!(parse("port=8080").to_string(), "port=8080");
273        assert_eq!(parse("q=").to_string(), r#"q="""#);
274        assert_eq!(parse("q=a=b").to_string(), r#"q="a=b""#);
275        assert_eq!(parse("server.port=8080").to_string(), "server.port=8080");
276    }
277
278    #[test]
279    fn fromstr_display_preserves_path_and_leaf() {
280        for expr in [
281            "port=8080",
282            "name=foo",
283            r#"name="foo""#,
284            "debug=true",
285            "proxy=null",
286            r#"tags=["a","b"]"#,
287            "q=",
288            "q=a=b",
289            "server.port=8080",
290        ] {
291            let parsed = parse(expr);
292            let round = parsed.to_string().parse::<PathLeaf<Value>>().unwrap();
293            assert_eq!(round.path(), parsed.path(), "{expr}");
294            assert_eq!(round.leaf(), parsed.leaf(), "{expr}");
295        }
296    }
297
298    #[test]
299    fn from_raw_path_leaf_parses_the_rhs() {
300        let raw: PathLeaf<String> = "server.port=8080".parse().unwrap();
301        let typed = PathLeaf::<Value>::from(raw);
302        assert_eq!(
303            Value::try_from(typed).unwrap(),
304            json!({"server": {"port": 8080}})
305        );
306    }
307
308    /// Brackets parse — a reference may read an element — but a `--set`-shaped
309    /// expression can never expand one into a writer's nested object.
310    #[test]
311    fn bracketed_paths_parse_but_cannot_write() {
312        let err = Value::try_from(parse("servers[0].host=x")).unwrap_err();
313        assert_eq!(
314            err,
315            PathError::IndexInKeyPath {
316                path: "servers[0].host".into()
317            }
318        );
319        assert_eq!(
320            err.to_string(),
321            "`servers[0].host` contains an array index; merge paths take keys only"
322        );
323    }
324}