use std::fmt;
use std::str::FromStr;
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use serde_json::Value;
use thiserror::Error;
use crate::EvalError;
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
enum Segment {
Key(String),
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct Path(Vec<Segment>);
#[derive(Debug, Clone, PartialEq, Eq, Error)]
#[error("invalid path syntax '{path}': {reason}")]
pub struct PathParseError {
pub path: String,
pub reason: String,
}
impl PathParseError {
fn new(path: &str, reason: &str) -> Self {
Self {
path: path.to_string(),
reason: reason.to_string(),
}
}
}
impl FromStr for Path {
type Err = PathParseError;
fn from_str(path: &str) -> Result<Self, Self::Err> {
let rest = path
.strip_prefix('$')
.ok_or_else(|| PathParseError::new(path, "path must start with '$'"))?;
if rest.is_empty() {
return Ok(Path(Vec::new()));
}
let body = match rest.as_bytes()[0] {
b'.' => {
let after_dot = &rest[1..];
if after_dot.is_empty() {
return Err(PathParseError::new(
path,
"trailing '.' with no segment after it",
));
}
after_dot
}
b'[' => rest,
_ => {
return Err(PathParseError::new(
path,
"expected '.', '[', or end-of-string right after '$'",
))
}
};
let segments = if body.contains('[') {
parse_bracket_segments(body, path)?
} else {
parse_dot_segments(body, path)?
};
Ok(Path(segments.into_iter().map(Segment::Key).collect()))
}
}
fn parse_dot_segments(body: &str, original: &str) -> Result<Vec<String>, PathParseError> {
let mut segments = Vec::new();
for part in body.split('.') {
if part.is_empty() {
return Err(PathParseError::new(
original,
"empty path segment (leading, trailing, or consecutive '.')",
));
}
segments.push(part.to_string());
}
Ok(segments)
}
fn parse_bracket_segments(body: &str, original: &str) -> Result<Vec<String>, PathParseError> {
fn invalid(original: &str, reason: &str) -> PathParseError {
PathParseError::new(original, reason)
}
let bytes = body.as_bytes();
let len = bytes.len();
let mut segments = Vec::new();
let mut i = 0usize;
let mut expect_segment_start = true;
while i < len {
match bytes[i] {
b'[' => {
if i + 1 >= len || bytes[i + 1] != b'"' {
return Err(invalid(original, "expected '\"' after '['"));
}
let name_start = i + 2;
let mut j = name_start;
while j < len && bytes[j] != b'"' {
j += 1;
}
if j >= len {
return Err(invalid(original, "unterminated bracket segment"));
}
let name = &body[name_start..j];
if name.is_empty() {
return Err(invalid(original, "empty bracket key"));
}
if j + 1 >= len || bytes[j + 1] != b']' {
return Err(invalid(original, "missing closing ']' after key"));
}
segments.push(name.to_string());
i = j + 2;
expect_segment_start = false;
if i < len && bytes[i] != b'.' && bytes[i] != b'[' {
return Err(invalid(
original,
"expected '.' or '[' after bracket segment",
));
}
}
b'.' => {
if expect_segment_start {
return Err(invalid(original, "empty path segment"));
}
i += 1;
expect_segment_start = true;
if i >= len {
return Err(invalid(original, "empty path segment"));
}
}
_ => {
let start = i;
while i < len && bytes[i] != b'.' && bytes[i] != b'[' {
i += 1;
}
segments.push(body[start..i].to_string());
expect_segment_start = false;
}
}
}
if expect_segment_start {
return Err(invalid(original, "empty path segment"));
}
Ok(segments)
}
fn is_dot_safe(key: &str) -> bool {
!key.is_empty() && !key.contains(['.', '[', ']'])
}
impl fmt::Display for Path {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "$")?;
for Segment::Key(key) in &self.0 {
if is_dot_safe(key) {
write!(f, ".{key}")?;
} else {
write!(f, "[\"{key}\"]")?;
}
}
Ok(())
}
}
impl Serialize for Path {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_str(&self.to_string())
}
}
impl<'de> Deserialize<'de> for Path {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let s = String::deserialize(deserializer)?;
s.parse::<Path>().map_err(serde::de::Error::custom)
}
}
impl Path {
pub fn read<'a>(&self, ctx: &'a Value) -> Result<&'a Value, EvalError> {
let mut cur = ctx;
for Segment::Key(key) in &self.0 {
cur = cur
.get(key)
.ok_or_else(|| EvalError::PathNotFound(self.to_string()))?;
}
Ok(cur)
}
pub fn write(&self, ctx: &mut Value, value: Value) -> Result<(), EvalError> {
if self.0.is_empty() {
*ctx = value;
return Ok(());
}
write_recursive(ctx, &self.0, value, self)
}
}
fn write_recursive(
node: &mut Value,
keys: &[Segment],
value: Value,
full_path: &Path,
) -> Result<(), EvalError> {
let Segment::Key(key) = &keys[0];
ensure_writable_object(node, full_path, key)?;
let obj = node
.as_object_mut()
.expect("ensure_writable_object guarantees an object here");
if keys.len() == 1 {
obj.insert(key.clone(), value);
Ok(())
} else {
let entry = obj.entry(key.clone()).or_insert(Value::Null);
write_recursive(entry, &keys[1..], value, full_path)
}
}
fn ensure_writable_object(node: &mut Value, full_path: &Path, key: &str) -> Result<(), EvalError> {
if node.is_object() {
return Ok(());
}
if node.is_null() {
*node = Value::Object(serde_json::Map::new());
return Ok(());
}
Err(EvalError::TypeError {
op: "path.write".into(),
msg: format!(
"cannot write path '{full_path}' at segment '{key}': existing value at this \
position is not an object ({node:?})"
),
})
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn accepts_root() {
let p: Path = "$".parse().unwrap();
assert_eq!(p, Path(Vec::new()));
}
#[test]
fn accepts_single_dot_segment() {
let p: Path = "$.a".parse().unwrap();
assert_eq!(p, Path(vec![Segment::Key("a".into())]));
}
#[test]
fn accepts_multi_dot_segments() {
let p: Path = "$.a.b".parse().unwrap();
assert_eq!(
p,
Path(vec![Segment::Key("a".into()), Segment::Key("b".into())])
);
}
#[test]
fn accepts_bracket_forms() {
assert!("$.a[\"p.md\"]".parse::<Path>().is_ok());
assert!("$[\"x.y\"]".parse::<Path>().is_ok());
assert!("$[\"x.y\"].inner".parse::<Path>().is_ok());
assert!("$.a[\"x\"][\"y\"]".parse::<Path>().is_ok());
}
#[test]
fn rejects_missing_dollar_prefix() {
assert!("a.b".parse::<Path>().is_err());
assert!("".parse::<Path>().is_err());
}
#[test]
fn rejects_dollar_foo_no_dot() {
let err = "$foo".parse::<Path>().unwrap_err();
assert_eq!(err.path, "$foo");
}
#[test]
fn rejects_trailing_dot() {
assert!("$.".parse::<Path>().is_err());
assert!("$.a.".parse::<Path>().is_err());
}
#[test]
fn rejects_empty_middle_segment() {
assert!("$.a..b".parse::<Path>().is_err());
}
#[test]
fn rejects_empty_bracket_key() {
assert!("$.a[\"\"]".parse::<Path>().is_err());
assert!("$.a[]".parse::<Path>().is_err());
}
#[test]
fn rejects_unterminated_bracket() {
assert!("$.a[".parse::<Path>().is_err());
assert!("$.a[\"x".parse::<Path>().is_err());
}
#[test]
fn rejects_unquoted_bracket_key() {
assert!("$.a[p.md]".parse::<Path>().is_err());
}
#[test]
fn rejects_unseparated_plain_suffix_after_bracket() {
assert!("$.a[\"x\"]b".parse::<Path>().is_err());
}
#[test]
fn display_round_trip() {
for src in [
"$",
"$.a",
"$.a.b.c",
"$.a[\"p.md\"]",
"$[\"x.y\"]",
"$[\"x.y\"].inner",
"$.a[\"x\"][\"y\"]",
] {
let parsed: Path = src.parse().unwrap();
let rendered = parsed.to_string();
let reparsed: Path = rendered.parse().unwrap_or_else(|e| {
panic!("canonical form '{rendered}' (from '{src}') failed to re-parse: {e}")
});
assert_eq!(
parsed, reparsed,
"round-trip mismatch for '{src}' -> '{rendered}'"
);
}
}
#[test]
fn display_dot_safe_key_renders_dot_form() {
let p: Path = "$.a".parse().unwrap();
assert_eq!(p.to_string(), "$.a");
}
#[test]
fn display_dotted_key_renders_bracket_form() {
let p: Path = "$.a[\"p.md\"]".parse().unwrap();
assert_eq!(p.to_string(), "$.a[\"p.md\"]");
}
#[test]
fn read_root_returns_whole_ctx() {
let p: Path = "$".parse().unwrap();
let ctx = json!({"a": 1});
assert_eq!(p.read(&ctx).unwrap(), &ctx);
}
#[test]
fn read_missing_key_errors_path_not_found() {
let p: Path = "$.a.missing".parse().unwrap();
let ctx = json!({"a": {}});
let err = p.read(&ctx).unwrap_err();
assert!(matches!(err, EvalError::PathNotFound(_)), "{err:?}");
}
#[test]
fn write_root_replaces_whole_ctx() {
let p: Path = "$".parse().unwrap();
let mut ctx = json!({"a": 1});
p.write(&mut ctx, json!({"b": 2})).unwrap();
assert_eq!(ctx, json!({"b": 2}));
}
#[test]
fn write_creates_missing_intermediate_objects() {
let p: Path = "$.a.b.c".parse().unwrap();
let mut ctx = json!({});
p.write(&mut ctx, json!(42)).unwrap();
assert_eq!(ctx, json!({"a": {"b": {"c": 42}}}));
}
#[test]
fn write_clobber_of_non_object_intermediate_errors_and_leaves_ctx_unchanged() {
let p: Path = "$.a.b".parse().unwrap();
let mut ctx = json!({"a": "string"});
let before = ctx.clone();
let err = p.write(&mut ctx, json!(1)).unwrap_err();
assert!(matches!(err, EvalError::TypeError { .. }), "{err:?}");
assert_eq!(ctx, before, "ctx must be unchanged after a rejected write");
}
#[test]
fn write_top_level_non_object_ctx_errors_and_leaves_ctx_unchanged() {
let p: Path = "$.a".parse().unwrap();
let mut ctx = json!("scalar");
let before = ctx.clone();
let err = p.write(&mut ctx, json!(1)).unwrap_err();
assert!(matches!(err, EvalError::TypeError { .. }), "{err:?}");
assert_eq!(ctx, before);
}
#[test]
fn write_existing_null_intermediate_still_promotes() {
let p: Path = "$.a.b".parse().unwrap();
let mut ctx = json!({"a": null});
p.write(&mut ctx, json!(1)).unwrap();
assert_eq!(ctx, json!({"a": {"b": 1}}));
}
}