1use std::fmt;
4use std::str::FromStr;
5
6use crate::{ConfigFormat, ConfigObject, PathError, RefPath, Seg};
7use serde_json::{Map, Value};
8
9#[derive(Debug, Clone, PartialEq)]
15pub struct PathLeaf<V> {
16 path: RefPath,
17 leaf: V,
18}
19
20impl<V> PathLeaf<V> {
21 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 pub fn path(&self) -> &[Seg] {
31 self.path.segs()
32 }
33
34 pub fn leaf(&self) -> &V {
36 &self.leaf
37 }
38
39 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 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 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 pub fn validate_keys(&self) -> Result<(), PathError> {
68 self.path.clone().try_into_keys().map(|_| ())
69 }
70
71 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
83pub 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 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
118pub 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 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 #[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 #[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 #[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}