Skip to main content

knf/
inline.rs

1//! `key.path=value` inline layers.
2
3use std::fmt;
4use std::str::FromStr;
5
6use crate::{ConfigFormat, ConfigObject, PathError, RefPath, Seg};
7use serde_json::{Map, Value};
8
9/// A leaf value addressed by a parsed path: `server.port=8080`.
10///
11/// [`FromStr`] for `PathLeaf<String>` keeps the RHS raw; the
12/// [`serde_json::Value`] impl parses it as JSON with a string fallback.
13/// Bracket steps parse, but conversion to a layer rejects them.
14#[derive(Debug, Clone, PartialEq)]
15pub struct PathLeaf<V> {
16    path: RefPath,
17    leaf: V,
18}
19
20impl<V> PathLeaf<V> {
21    /// Build from path segments and a leaf. Rejects an empty path or any empty segment.
22    pub fn new(path: Vec<String>, leaf: V) -> Result<Self, PathError> {
23        Ok(Self {
24            path: RefPath::from_keys(path)?,
25            leaf,
26        })
27    }
28
29    /// The path as steps; may include index steps.
30    pub fn path(&self) -> &[Seg] {
31        self.path.segs()
32    }
33
34    /// The RHS value, not yet wrapped in nested objects.
35    pub fn leaf(&self) -> &V {
36        &self.leaf
37    }
38
39    /// Replace the leaf, keeping the path.
40    pub fn map_leaf<T>(self, f: impl FnOnce(V) -> T) -> PathLeaf<T> {
41        PathLeaf {
42            path: self.path,
43            leaf: f(self.leaf),
44        }
45    }
46
47    /// [`map_leaf`](Self::map_leaf) when the conversion can fail.
48    pub fn try_map_leaf<T, E>(self, f: impl FnOnce(V) -> Result<T, E>) -> Result<PathLeaf<T>, E> {
49        Ok(PathLeaf {
50            path: self.path,
51            leaf: f(self.leaf)?,
52        })
53    }
54
55    /// Nests the leaf under every key in the path, innermost first.
56    fn try_into_nested(self, nest: impl Fn(String, V) -> V) -> Result<V, PathError> {
57        let keys = self.path.try_into_keys()?;
58        Ok(keys
59            .into_iter()
60            .rev()
61            .fold(self.leaf, |acc, key| nest(key, acc)))
62    }
63}
64
65impl PathLeaf<String> {
66    /// Check that the path contains keys only.
67    pub fn validate_keys(&self) -> Result<(), PathError> {
68        self.path.clone().try_into_keys().map(|_| ())
69    }
70
71    /// Type the RHS in the native format and expand it into a nested object.
72    pub fn into_layer<V: ConfigFormat>(self) -> Result<V, PathError> {
73        let keys = self.path.try_into_keys()?;
74        let leaf = V::parse_inline(self.leaf);
75        Ok(keys.into_iter().rev().fold(leaf, |value, key| {
76            let mut object = V::Object::new();
77            object.insert(key, value);
78            V::object(object)
79        }))
80    }
81}
82
83/// Parses text as a TOML value, falling back to the original text as a string.
84pub fn toml_or_string(text: String) -> toml::Value {
85    text.trim_matches([' ', '\t', '\r', '\n'])
86        .parse()
87        .unwrap_or_else(|_| toml::Value::String(text))
88}
89
90impl FromStr for PathLeaf<String> {
91    type Err = PathError;
92
93    fn from_str(expr: &str) -> Result<Self, Self::Err> {
94        // Split on the first `=` so the RHS may contain more of them.
95        let Some((lhs, rhs)) = expr.split_once('=') else {
96            return Err(PathError::MissingEquals);
97        };
98        Ok(Self {
99            path: lhs.parse()?,
100            leaf: rhs.to_string(),
101        })
102    }
103}
104
105impl fmt::Display for PathLeaf<String> {
106    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
107        write!(f, "{}={}", self.path, self.leaf)
108    }
109}
110impl FromStr for PathLeaf<Value> {
111    type Err = PathError;
112
113    fn from_str(expr: &str) -> Result<Self, Self::Err> {
114        Ok(PathLeaf::<String>::from_str(expr)?.into())
115    }
116}
117
118/// Parses text as JSON, falling back to the string itself: `8080` is a number,
119/// `foo` is `"foo"`.
120pub fn json_or_string(text: String) -> Value {
121    serde_json::from_str(&text).unwrap_or_else(|_| Value::String(text))
122}
123
124impl From<PathLeaf<String>> for PathLeaf<Value> {
125    fn from(path_leaf: PathLeaf<String>) -> Self {
126        path_leaf.map_leaf(json_or_string)
127    }
128}
129
130impl fmt::Display for PathLeaf<Value> {
131    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
132        let rhs = serde_json::to_string(&self.leaf).expect("a Value always serializes");
133        write!(f, "{}={rhs}", self.path)
134    }
135}
136
137impl TryFrom<PathLeaf<Value>> for Value {
138    type Error = PathError;
139
140    /// Expands to a nested object. Fails on index steps.
141    fn try_from(path_leaf: PathLeaf<Value>) -> Result<Self, Self::Error> {
142        path_leaf.try_into_nested(|key, acc| {
143            let mut obj = Map::new();
144            obj.insert(key, acc);
145            Value::Object(obj)
146        })
147    }
148}
149
150#[cfg(test)]
151mod tests {
152    use serde_json::json;
153
154    use super::*;
155
156    #[test]
157    fn toml_inline_literals_ignore_surrounding_whitespace() {
158        for literal in [
159            "8080",
160            "true",
161            "1.0",
162            "1979-05-27",
163            "inf",
164            "[1, 2]",
165            "{a=1}",
166            "' name '",
167        ] {
168            for padding in [" ", "\t", "\n", "\r\n", " \t\r\n"] {
169                let expected = toml_or_string(literal.into());
170                for text in [
171                    format!("{padding}{literal}"),
172                    format!("{literal}{padding}"),
173                    format!("{padding}{literal}{padding}"),
174                ] {
175                    assert_eq!(toml_or_string(text.clone()), expected, "{text:?}");
176                }
177            }
178        }
179        for text in [
180            " \tnull\n",
181            " 9223372036854775808\n",
182            " [a,b] ",
183            " text\n",
184            " \t\r\n",
185            "\u{a0}8080\u{a0}",
186        ] {
187            assert_eq!(
188                toml_or_string(text.into()),
189                toml::Value::String(text.into())
190            );
191        }
192    }
193
194    #[test]
195    fn rejects_malformed_expressions() {
196        for (bad, want) in [
197            ("noequals", PathError::MissingEquals),
198            ("=1", PathError::EmptyPath),
199            (
200                "a..b=1",
201                PathError::EmptySegment {
202                    path: "a..b".into(),
203                },
204            ),
205            (".a=1", PathError::EmptySegment { path: ".a".into() }),
206            ("a.=1", PathError::EmptySegment { path: "a.".into() }),
207            ("a[]=1", PathError::BadIndex { path: "a[]".into() }),
208        ] {
209            assert_eq!(bad.parse::<PathLeaf<String>>().unwrap_err(), want, "{bad}");
210        }
211    }
212
213    #[test]
214    fn new_rejects_empty_path_and_empty_segments() {
215        assert_eq!(
216            PathLeaf::<String>::new(vec![], "1".into()).unwrap_err(),
217            PathError::EmptyPath
218        );
219        assert_eq!(
220            PathLeaf::<String>::new(vec!["".into()], "1".into()).unwrap_err(),
221            PathError::EmptySegment { path: "".into() }
222        );
223        assert_eq!(
224            PathLeaf::<String>::new(vec!["a".into(), "".into()], "1".into()).unwrap_err(),
225            PathError::EmptySegment { path: "a.".into() }
226        );
227    }
228
229    #[test]
230    fn raw_fromstr_keeps_the_rhs_unparsed() {
231        let path_leaf: PathLeaf<String> = "port=8080".parse().unwrap();
232        assert_eq!(path_leaf.path(), [Seg::Key("port".into())]);
233        assert_eq!(path_leaf.leaf(), "8080");
234        assert_eq!(path_leaf.to_string(), "port=8080");
235    }
236
237    /// The grammar accepts bracket steps; only writers reject them.
238    #[test]
239    fn path_leaf_accepts_brackets_its_writers_reject() {
240        let parsed: PathLeaf<String> = "a[0]=1".parse().unwrap();
241        assert_eq!(parsed.path(), [Seg::Key("a".into()), Seg::Index(0)]);
242        assert_eq!(parsed.to_string(), "a[0]=1");
243    }
244
245    #[test]
246    fn map_leaf_preserves_the_path() {
247        let path_leaf = PathLeaf::new(vec!["a".into()], "xy".to_string())
248            .unwrap()
249            .map_leaf(|s| s.len());
250        assert_eq!(path_leaf.path(), [Seg::Key("a".into())]);
251        assert_eq!(*path_leaf.leaf(), 2);
252    }
253
254    fn parse(expr: &str) -> PathLeaf<Value> {
255        expr.parse().expect("valid PathLeaf")
256    }
257
258    fn nested(expr: &str) -> Value {
259        Value::try_from(parse(expr)).expect("all-key path")
260    }
261
262    #[test]
263    fn value_typing() {
264        assert_eq!(nested("port=8080"), json!({"port": 8080}));
265        assert_eq!(nested("debug=true"), json!({"debug": true}));
266        assert_eq!(nested("name=foo"), json!({"name": "foo"}));
267        assert_eq!(nested("proxy=null"), json!({"proxy": null}));
268        assert_eq!(nested(r#"tags=["a","b"]"#), json!({"tags": ["a", "b"]}));
269        assert_eq!(nested("tags=[a,b]"), json!({"tags": "[a,b]"}));
270    }
271
272    /// The sharp edge: a bare `1.0` is a number.
273    #[test]
274    fn numeric_looking_strings() {
275        assert_eq!(nested("version=1.0"), json!({"version": 1.0}));
276        assert_eq!(nested(r#"version="1.0""#), json!({"version": "1.0"}));
277    }
278
279    #[test]
280    fn dotted_paths_nest() {
281        assert_eq!(
282            nested("server.port=8080"),
283            json!({"server": {"port": 8080}})
284        );
285        assert_eq!(nested("a.b.c=1"), json!({"a": {"b": {"c": 1}}}));
286    }
287
288    #[test]
289    fn splits_on_the_first_equals_only() {
290        assert_eq!(nested("q=a=b"), json!({"q": "a=b"}));
291        assert_eq!(nested("q="), json!({"q": ""}));
292    }
293
294    #[test]
295    fn display_is_canonical() {
296        assert_eq!(parse("name=foo").to_string(), r#"name="foo""#);
297        assert_eq!(parse("port=8080").to_string(), "port=8080");
298        assert_eq!(parse("q=").to_string(), r#"q="""#);
299        assert_eq!(parse("q=a=b").to_string(), r#"q="a=b""#);
300        assert_eq!(parse("server.port=8080").to_string(), "server.port=8080");
301    }
302
303    #[test]
304    fn fromstr_display_preserves_path_and_leaf() {
305        for expr in [
306            "port=8080",
307            "name=foo",
308            r#"name="foo""#,
309            "debug=true",
310            "proxy=null",
311            r#"tags=["a","b"]"#,
312            "q=",
313            "q=a=b",
314            "server.port=8080",
315        ] {
316            let parsed = parse(expr);
317            let round = parsed.to_string().parse::<PathLeaf<Value>>().unwrap();
318            assert_eq!(round.path(), parsed.path(), "{expr}");
319            assert_eq!(round.leaf(), parsed.leaf(), "{expr}");
320        }
321    }
322
323    #[test]
324    fn from_raw_path_leaf_parses_the_rhs() {
325        let raw: PathLeaf<String> = "server.port=8080".parse().unwrap();
326        let typed = PathLeaf::<Value>::from(raw);
327        assert_eq!(
328            Value::try_from(typed).unwrap(),
329            json!({"server": {"port": 8080}})
330        );
331    }
332
333    /// Bracket steps parse but never expand into a layer.
334    #[test]
335    fn bracketed_paths_parse_but_cannot_write() {
336        let err = Value::try_from(parse("servers[0].host=x")).unwrap_err();
337        assert_eq!(
338            err,
339            PathError::IndexInKeyPath {
340                path: "servers[0].host".into()
341            }
342        );
343        assert_eq!(
344            err.to_string(),
345            "`servers[0].host` contains an array index; merge paths take keys only"
346        );
347    }
348}