1use std::fmt;
45use std::str::FromStr;
46
47use serde::{Deserialize, Deserializer, Serialize, Serializer};
48use serde_json::Value;
49use thiserror::Error;
50
51use crate::EvalError;
52
53#[derive(Debug, Clone, PartialEq, Eq, Hash)]
57enum Segment {
58 Key(String),
61}
62
63#[derive(Debug, Clone, PartialEq, Eq, Hash)]
74pub struct Path(Vec<Segment>);
75
76#[derive(Debug, Clone, PartialEq, Eq, Error)]
80#[error("invalid path syntax '{path}': {reason}")]
81pub struct PathParseError {
82 pub path: String,
84 pub reason: String,
86}
87
88impl PathParseError {
89 fn new(path: &str, reason: &str) -> Self {
90 Self {
91 path: path.to_string(),
92 reason: reason.to_string(),
93 }
94 }
95}
96
97impl FromStr for Path {
98 type Err = PathParseError;
99
100 fn from_str(path: &str) -> Result<Self, Self::Err> {
101 let rest = path
102 .strip_prefix('$')
103 .ok_or_else(|| PathParseError::new(path, "path must start with '$'"))?;
104 if rest.is_empty() {
105 return Ok(Path(Vec::new()));
107 }
108 let body = match rest.as_bytes()[0] {
109 b'.' => {
110 let after_dot = &rest[1..];
111 if after_dot.is_empty() {
112 return Err(PathParseError::new(
113 path,
114 "trailing '.' with no segment after it",
115 ));
116 }
117 after_dot
118 }
119 b'[' => rest,
120 _ => {
121 return Err(PathParseError::new(
122 path,
123 "expected '.', '[', or end-of-string right after '$'",
124 ))
125 }
126 };
127 let segments = if body.contains('[') {
128 parse_bracket_segments(body, path)?
129 } else {
130 parse_dot_segments(body, path)?
131 };
132 Ok(Path(segments.into_iter().map(Segment::Key).collect()))
133 }
134}
135
136fn parse_dot_segments(body: &str, original: &str) -> Result<Vec<String>, PathParseError> {
139 let mut segments = Vec::new();
140 for part in body.split('.') {
141 if part.is_empty() {
142 return Err(PathParseError::new(
143 original,
144 "empty path segment (leading, trailing, or consecutive '.')",
145 ));
146 }
147 segments.push(part.to_string());
148 }
149 Ok(segments)
150}
151
152fn parse_bracket_segments(body: &str, original: &str) -> Result<Vec<String>, PathParseError> {
169 fn invalid(original: &str, reason: &str) -> PathParseError {
170 PathParseError::new(original, reason)
171 }
172
173 let bytes = body.as_bytes();
174 let len = bytes.len();
175 let mut segments = Vec::new();
176 let mut i = 0usize;
177 let mut expect_segment_start = true;
180
181 while i < len {
182 match bytes[i] {
183 b'[' => {
184 if i + 1 >= len || bytes[i + 1] != b'"' {
185 return Err(invalid(original, "expected '\"' after '['"));
186 }
187 let name_start = i + 2;
188 let mut j = name_start;
189 while j < len && bytes[j] != b'"' {
190 j += 1;
191 }
192 if j >= len {
193 return Err(invalid(original, "unterminated bracket segment"));
194 }
195 let name = &body[name_start..j];
196 if name.is_empty() {
197 return Err(invalid(original, "empty bracket key"));
198 }
199 if j + 1 >= len || bytes[j + 1] != b']' {
200 return Err(invalid(original, "missing closing ']' after key"));
201 }
202 segments.push(name.to_string());
203 i = j + 2;
204 expect_segment_start = false;
205 if i < len && bytes[i] != b'.' && bytes[i] != b'[' {
209 return Err(invalid(
210 original,
211 "expected '.' or '[' after bracket segment",
212 ));
213 }
214 }
215 b'.' => {
216 if expect_segment_start {
217 return Err(invalid(original, "empty path segment"));
218 }
219 i += 1;
220 expect_segment_start = true;
221 if i >= len {
222 return Err(invalid(original, "empty path segment"));
223 }
224 }
225 _ => {
226 let start = i;
227 while i < len && bytes[i] != b'.' && bytes[i] != b'[' {
228 i += 1;
229 }
230 segments.push(body[start..i].to_string());
231 expect_segment_start = false;
232 }
233 }
234 }
235
236 if expect_segment_start {
237 return Err(invalid(original, "empty path segment"));
238 }
239
240 Ok(segments)
241}
242
243fn is_dot_safe(key: &str) -> bool {
248 !key.is_empty() && !key.contains(['.', '[', ']'])
249}
250
251impl fmt::Display for Path {
252 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
260 write!(f, "$")?;
261 for Segment::Key(key) in &self.0 {
262 if is_dot_safe(key) {
263 write!(f, ".{key}")?;
264 } else {
265 write!(f, "[\"{key}\"]")?;
266 }
267 }
268 Ok(())
269 }
270}
271
272impl Serialize for Path {
273 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
274 where
275 S: Serializer,
276 {
277 serializer.serialize_str(&self.to_string())
278 }
279}
280
281impl<'de> Deserialize<'de> for Path {
282 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
283 where
284 D: Deserializer<'de>,
285 {
286 let s = String::deserialize(deserializer)?;
287 s.parse::<Path>().map_err(serde::de::Error::custom)
288 }
289}
290
291impl Path {
292 pub fn read<'a>(&self, ctx: &'a Value) -> Result<&'a Value, EvalError> {
299 let mut cur = ctx;
300 for Segment::Key(key) in &self.0 {
301 cur = cur
302 .get(key)
303 .ok_or_else(|| EvalError::PathNotFound(self.to_string()))?;
304 }
305 Ok(cur)
306 }
307
308 pub fn write(&self, ctx: &mut Value, value: Value) -> Result<(), EvalError> {
324 if self.0.is_empty() {
325 *ctx = value;
326 return Ok(());
327 }
328 write_recursive(ctx, &self.0, value, self)
329 }
330}
331
332fn write_recursive(
333 node: &mut Value,
334 keys: &[Segment],
335 value: Value,
336 full_path: &Path,
337) -> Result<(), EvalError> {
338 let Segment::Key(key) = &keys[0];
339 ensure_writable_object(node, full_path, key)?;
340 let obj = node
341 .as_object_mut()
342 .expect("ensure_writable_object guarantees an object here");
343 if keys.len() == 1 {
344 obj.insert(key.clone(), value);
345 Ok(())
346 } else {
347 let entry = obj.entry(key.clone()).or_insert(Value::Null);
348 write_recursive(entry, &keys[1..], value, full_path)
349 }
350}
351
352fn ensure_writable_object(node: &mut Value, full_path: &Path, key: &str) -> Result<(), EvalError> {
357 if node.is_object() {
358 return Ok(());
359 }
360 if node.is_null() {
361 *node = Value::Object(serde_json::Map::new());
362 return Ok(());
363 }
364 Err(EvalError::TypeError {
365 op: "path.write".into(),
366 msg: format!(
367 "cannot write path '{full_path}' at segment '{key}': existing value at this \
368 position is not an object ({node:?})"
369 ),
370 })
371}
372
373#[cfg(test)]
374mod tests {
375 use super::*;
376 use serde_json::json;
377
378 #[test]
381 fn accepts_root() {
382 let p: Path = "$".parse().unwrap();
383 assert_eq!(p, Path(Vec::new()));
384 }
385
386 #[test]
387 fn accepts_single_dot_segment() {
388 let p: Path = "$.a".parse().unwrap();
389 assert_eq!(p, Path(vec![Segment::Key("a".into())]));
390 }
391
392 #[test]
393 fn accepts_multi_dot_segments() {
394 let p: Path = "$.a.b".parse().unwrap();
395 assert_eq!(
396 p,
397 Path(vec![Segment::Key("a".into()), Segment::Key("b".into())])
398 );
399 }
400
401 #[test]
402 fn accepts_bracket_forms() {
403 assert!("$.a[\"p.md\"]".parse::<Path>().is_ok());
404 assert!("$[\"x.y\"]".parse::<Path>().is_ok());
405 assert!("$[\"x.y\"].inner".parse::<Path>().is_ok());
406 assert!("$.a[\"x\"][\"y\"]".parse::<Path>().is_ok());
407 }
408
409 #[test]
410 fn rejects_missing_dollar_prefix() {
411 assert!("a.b".parse::<Path>().is_err());
412 assert!("".parse::<Path>().is_err());
413 }
414
415 #[test]
416 fn rejects_dollar_foo_no_dot() {
417 let err = "$foo".parse::<Path>().unwrap_err();
419 assert_eq!(err.path, "$foo");
420 }
421
422 #[test]
423 fn rejects_trailing_dot() {
424 assert!("$.".parse::<Path>().is_err());
425 assert!("$.a.".parse::<Path>().is_err());
426 }
427
428 #[test]
429 fn rejects_empty_middle_segment() {
430 assert!("$.a..b".parse::<Path>().is_err());
431 }
432
433 #[test]
434 fn rejects_empty_bracket_key() {
435 assert!("$.a[\"\"]".parse::<Path>().is_err());
436 assert!("$.a[]".parse::<Path>().is_err());
437 }
438
439 #[test]
440 fn rejects_unterminated_bracket() {
441 assert!("$.a[".parse::<Path>().is_err());
442 assert!("$.a[\"x".parse::<Path>().is_err());
443 }
444
445 #[test]
446 fn rejects_unquoted_bracket_key() {
447 assert!("$.a[p.md]".parse::<Path>().is_err());
448 }
449
450 #[test]
451 fn rejects_unseparated_plain_suffix_after_bracket() {
452 assert!("$.a[\"x\"]b".parse::<Path>().is_err());
453 }
454
455 #[test]
458 fn display_round_trip() {
459 for src in [
460 "$",
461 "$.a",
462 "$.a.b.c",
463 "$.a[\"p.md\"]",
464 "$[\"x.y\"]",
465 "$[\"x.y\"].inner",
466 "$.a[\"x\"][\"y\"]",
467 ] {
468 let parsed: Path = src.parse().unwrap();
469 let rendered = parsed.to_string();
470 let reparsed: Path = rendered.parse().unwrap_or_else(|e| {
471 panic!("canonical form '{rendered}' (from '{src}') failed to re-parse: {e}")
472 });
473 assert_eq!(
474 parsed, reparsed,
475 "round-trip mismatch for '{src}' -> '{rendered}'"
476 );
477 }
478 }
479
480 #[test]
481 fn display_dot_safe_key_renders_dot_form() {
482 let p: Path = "$.a".parse().unwrap();
483 assert_eq!(p.to_string(), "$.a");
484 }
485
486 #[test]
487 fn display_dotted_key_renders_bracket_form() {
488 let p: Path = "$.a[\"p.md\"]".parse().unwrap();
489 assert_eq!(p.to_string(), "$.a[\"p.md\"]");
490 }
491
492 #[test]
495 fn read_root_returns_whole_ctx() {
496 let p: Path = "$".parse().unwrap();
497 let ctx = json!({"a": 1});
498 assert_eq!(p.read(&ctx).unwrap(), &ctx);
499 }
500
501 #[test]
502 fn read_missing_key_errors_path_not_found() {
503 let p: Path = "$.a.missing".parse().unwrap();
504 let ctx = json!({"a": {}});
505 let err = p.read(&ctx).unwrap_err();
506 assert!(matches!(err, EvalError::PathNotFound(_)), "{err:?}");
507 }
508
509 #[test]
510 fn write_root_replaces_whole_ctx() {
511 let p: Path = "$".parse().unwrap();
512 let mut ctx = json!({"a": 1});
513 p.write(&mut ctx, json!({"b": 2})).unwrap();
514 assert_eq!(ctx, json!({"b": 2}));
515 }
516
517 #[test]
518 fn write_creates_missing_intermediate_objects() {
519 let p: Path = "$.a.b.c".parse().unwrap();
520 let mut ctx = json!({});
521 p.write(&mut ctx, json!(42)).unwrap();
522 assert_eq!(ctx, json!({"a": {"b": {"c": 42}}}));
523 }
524
525 #[test]
526 fn write_clobber_of_non_object_intermediate_errors_and_leaves_ctx_unchanged() {
527 let p: Path = "$.a.b".parse().unwrap();
528 let mut ctx = json!({"a": "string"});
529 let before = ctx.clone();
530 let err = p.write(&mut ctx, json!(1)).unwrap_err();
531 assert!(matches!(err, EvalError::TypeError { .. }), "{err:?}");
532 assert_eq!(ctx, before, "ctx must be unchanged after a rejected write");
533 }
534
535 #[test]
536 fn write_top_level_non_object_ctx_errors_and_leaves_ctx_unchanged() {
537 let p: Path = "$.a".parse().unwrap();
538 let mut ctx = json!("scalar");
539 let before = ctx.clone();
540 let err = p.write(&mut ctx, json!(1)).unwrap_err();
541 assert!(matches!(err, EvalError::TypeError { .. }), "{err:?}");
542 assert_eq!(ctx, before);
543 }
544
545 #[test]
546 fn write_existing_null_intermediate_still_promotes() {
547 let p: Path = "$.a.b".parse().unwrap();
548 let mut ctx = json!({"a": null});
549 p.write(&mut ctx, json!(1)).unwrap();
550 assert_eq!(ctx, json!({"a": {"b": 1}}));
551 }
552}