1use std::fmt;
17use std::str::FromStr;
18
19use crate::{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 FromStr for PathLeaf<String> {
92 type Err = PathError;
93
94 fn from_str(expr: &str) -> Result<Self, Self::Err> {
95 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
119pub 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 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 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 #[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 #[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 #[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 #[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}