1use std::fmt;
17use std::str::FromStr;
18
19use crate::{ConfigFormat, ConfigObject, PathError, RefPath, Seg};
20use serde_json::{Map, Value};
21
22#[derive(Debug, Clone, PartialEq)]
38pub struct PathLeaf<V> {
39 path: RefPath,
40 leaf: V,
41}
42
43impl<V> PathLeaf<V> {
44 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 pub fn path(&self) -> &[Seg] {
56 self.path.segs()
57 }
58
59 pub fn leaf(&self) -> &V {
61 &self.leaf
62 }
63
64 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 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 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 pub fn validate_keys(&self) -> Result<(), PathError> {
94 self.path.clone().try_into_keys().map(|_| ())
95 }
96
97 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
109pub 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 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
146pub 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 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 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 #[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 #[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 #[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 #[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}