Skip to main content

agent_first_data/document/format/
json.rs

1//! JSON format backend (via serde_json).
2
3use crate::document::{DocumentError, DocumentResult, Value};
4
5/// Lossless JSON source document. The parsed node tree stores byte spans for
6/// every value/member, so editors can replace only the requested span while
7/// retaining untouched ordering, whitespace, escapes, and number spelling.
8#[derive(Debug)]
9pub struct JsonDocument<'a> {
10    source: &'a str,
11    root: Node,
12}
13
14impl<'a> JsonDocument<'a> {
15    pub fn parse(source: &'a str) -> DocumentResult<Self> {
16        let mut parser = Parser::new(source)?;
17        let root = parser.parse_value(0)?;
18        if parser.skip_ws(root.end) != source.len() {
19            return Err(DocumentError::ParseError {
20                format: "JSON".to_string(),
21                detail: "trailing bytes after root value".to_string(),
22            });
23        }
24        Ok(Self { source, root })
25    }
26
27    #[must_use]
28    pub fn source(&self) -> &'a str {
29        self.source
30    }
31
32    #[must_use]
33    pub(crate) fn root(&self) -> &Node {
34        &self.root
35    }
36}
37
38/// Set a JSON value at `path` while retaining every byte outside the edited
39/// region. JSON has no comments, but this still preserves key order,
40/// whitespace, line endings, escapes, and untouched number spellings.
41///
42/// Matches the in-memory [`crate::document::set_path`] capability:
43/// - an existing value (scalar *or* collection) is replaced in place;
44/// - a missing leaf, and any missing intermediate parent objects, are created
45///   under the deepest existing ancestor object.
46pub fn set_preserving(content: &str, path: &str, value: &Value) -> DocumentResult<String> {
47    let segments = crate::document::parse_path(path)?;
48    let document = JsonDocument::parse(content)?;
49    let root = document.root();
50    let mut output = content.to_string();
51    if segments.last().is_some_and(|segment| segment == "-")
52        && let Some(parent) = resolve(root, &segments[..segments.len() - 1], 0)
53        && matches!(parent.kind, NodeKind::Array(_))
54    {
55        return append_array_item_preserving(
56            content,
57            &crate::document::join_path(&segments[..segments.len() - 1]),
58            value,
59        );
60    }
61    match resolve(root, &segments, 0) {
62        Some(target) => {
63            // Replace the whole existing node span — scalar or collection. A
64            // replaced collection is re-rendered compactly; everything outside
65            // its span (and key order) is untouched.
66            let replacement = compact_json(value)?;
67            output.replace_range(target.start..target.end, &replacement);
68        }
69        None => {
70            // The full path is absent. Find the deepest existing ancestor
71            // object, then splice one new member holding the remaining path
72            // chain wrapped around the leaf value — creating every missing
73            // intermediate object along the way.
74            let (insert_at, tail) = deepest_existing_object(root, &segments).ok_or_else(|| {
75                DocumentError::UnsupportedOperation {
76                    format: "JSON".to_string(),
77                    operation: "set".to_string(),
78                    detail: "cannot insert a new key into a non-object JSON value".to_string(),
79                }
80            })?;
81            let NodeKind::Object(entries) = &insert_at.kind else {
82                unreachable!("deepest_existing_object only returns object nodes");
83            };
84            let (member_key, rest) = tail.split_first().ok_or(DocumentError::EmptyPath)?;
85            let new_key =
86                serde_json::to_string(member_key).map_err(|error| DocumentError::ParseError {
87                    format: "JSON".to_string(),
88                    detail: error.to_string(),
89                })?;
90            let (position, fragment) = match entries.last() {
91                Some((_, last_node)) => {
92                    let anchor = last_node.member_start.unwrap_or(last_node.start);
93                    let (indent, multiline) = member_indent(content.as_bytes(), anchor);
94                    let member_value = nested_member_value(rest, value, multiline, &indent)?;
95                    let fragment = if multiline {
96                        format!(",\n{indent}{new_key}: {member_value}")
97                    } else {
98                        format!(", {new_key}: {member_value}")
99                    };
100                    (last_node.end, fragment)
101                }
102                None => {
103                    // Empty object: no sibling to mirror, so stay inline compact.
104                    let member_value = nested_member_value(rest, value, false, "")?;
105                    (insert_at.start + 1, format!("{new_key}: {member_value}"))
106                }
107            };
108            output.insert_str(position, &fragment);
109        }
110    }
111    serde_json::from_str::<serde_json::Value>(&output).map_err(|error| {
112        DocumentError::ParseError {
113            format: "JSON".to_string(),
114            detail: error.to_string(),
115        }
116    })?;
117    Ok(output)
118}
119
120/// Compact one-line JSON rendering of a document value.
121fn compact_json(value: &Value) -> DocumentResult<String> {
122    let json_value = serde_json::Value::try_from(value)?;
123    serde_json::to_string(&json_value).map_err(|error| DocumentError::UnsupportedOperation {
124        format: "JSON".to_string(),
125        operation: "set".to_string(),
126        detail: error.to_string(),
127    })
128}
129
130/// The deepest node reachable along `segments` that is an object, paired with
131/// the still-missing tail below it. Returns `None` if that deepest existing
132/// node is not an object (so a child key cannot be created under it).
133fn deepest_existing_object<'a>(
134    root: &'a Node,
135    segments: &'a [String],
136) -> Option<(&'a Node, &'a [String])> {
137    // `segments` itself does not resolve (checked by the caller), so start one
138    // level up and walk toward the root; the root (empty prefix) always exists.
139    for split in (0..segments.len()).rev() {
140        if let Some(node) = resolve(root, &segments[..split], 0) {
141            return match node.kind {
142                NodeKind::Object(_) => Some((node, &segments[split..])),
143                _ => None,
144            };
145        }
146    }
147    None
148}
149
150/// Render the value for a spliced member: the leaf `value` wrapped in an object
151/// for each remaining `tail` segment (`["a","b"] , v` → `{"a":{"b":v}}`).
152/// Multiline output is re-indented to sit at `base_indent`.
153fn nested_member_value(
154    tail: &[String],
155    value: &Value,
156    multiline: bool,
157    base_indent: &str,
158) -> DocumentResult<String> {
159    let mut nested = serde_json::Value::try_from(value)?;
160    for segment in tail.iter().rev() {
161        let mut object = serde_json::Map::new();
162        object.insert(segment.clone(), nested);
163        nested = serde_json::Value::Object(object);
164    }
165    if multiline {
166        let pretty =
167            serde_json::to_string_pretty(&nested).map_err(|error| DocumentError::ParseError {
168                format: "JSON".to_string(),
169                detail: error.to_string(),
170            })?;
171        Ok(pretty.replace('\n', &format!("\n{base_indent}")))
172    } else {
173        serde_json::to_string(&nested).map_err(|error| DocumentError::ParseError {
174            format: "JSON".to_string(),
175            detail: error.to_string(),
176        })
177    }
178}
179
180/// Indentation of the member whose key begins at `anchor`, and whether that
181/// member sits on its own line (so a spliced sibling should too).
182fn member_indent(bytes: &[u8], anchor: usize) -> (String, bool) {
183    match bytes[..anchor].iter().rposition(|byte| *byte == b'\n') {
184        Some(newline) => {
185            let indent = bytes[newline + 1..anchor]
186                .iter()
187                .take_while(|byte| matches!(byte, b' ' | b'\t'))
188                .map(|byte| *byte as char)
189                .collect();
190            (indent, true)
191        }
192        None => (String::new(), false),
193    }
194}
195
196/// Remove one existing JSON member or array item while preserving the rest of
197/// the source layout. Removing the root value is intentionally unsupported.
198pub fn unset_preserving(content: &str, path: &str) -> DocumentResult<String> {
199    let segments = crate::document::parse_path(path)?;
200    let document = JsonDocument::parse(content)?;
201    let root = document.root();
202    let target = resolve(root, &segments, 0).ok_or_else(|| DocumentError::PathNotFound {
203        path: path.to_string(),
204    })?;
205    if target.member_start.is_none()
206        && matches!(target.kind, NodeKind::Scalar)
207        && segments.is_empty()
208    {
209        return Err(DocumentError::UnsupportedOperation {
210            format: "JSON".to_string(),
211            operation: "unset".to_string(),
212            detail: "cannot remove the JSON root value".to_string(),
213        });
214    }
215    let mut start = target.member_start.unwrap_or(target.start);
216    if target.member_start.is_some()
217        && let Some(newline) = content.as_bytes()[..start]
218            .iter()
219            .rposition(|byte| *byte == b'\n')
220    {
221        let candidate = newline + 1;
222        if content.as_bytes()[candidate..start]
223            .iter()
224            .all(|byte| byte.is_ascii_whitespace())
225        {
226            start = candidate;
227        }
228    }
229    let end = target.end;
230    let after = skip_ws_bytes(content.as_bytes(), end);
231    // Removing a last/only member also removes the comma *before* it — but that
232    // search must stay inside the target's own parent container. A comma
233    // belonging to an enclosing object/array (e.g. a sibling earlier in the
234    // document) is not ours to take, or we splice across a container boundary.
235    let parent_body_start =
236        resolve(root, &segments[..segments.len() - 1], 0).map_or(0, |parent| parent.start + 1);
237    let (remove_start, mut remove_end, remove_following_line) =
238        if content.as_bytes().get(after) == Some(&b',') {
239            (start, after + 1, true)
240        } else if let Some(offset) = content.as_bytes()[parent_body_start..start]
241            .iter()
242            .rposition(|byte| *byte == b',')
243        {
244            (parent_body_start + offset, end, false)
245        } else {
246            (start, end, false)
247        };
248    if remove_following_line {
249        if content.as_bytes().get(remove_end) == Some(&b'\r') {
250            remove_end += 1;
251        }
252        if content.as_bytes().get(remove_end) == Some(&b'\n') {
253            remove_end += 1;
254        }
255    }
256    let mut output = content.to_string();
257    output.replace_range(remove_start..remove_end, "");
258    serde_json::from_str::<serde_json::Value>(&output).map_err(|error| {
259        DocumentError::ParseError {
260            format: "JSON".to_string(),
261            detail: error.to_string(),
262        }
263    })?;
264    Ok(output)
265}
266
267/// Append one JSON array item while retaining the existing array's trailing
268/// whitespace and all bytes outside the insertion point.
269pub fn append_array_item_preserving(
270    content: &str,
271    path: &str,
272    item: &Value,
273) -> DocumentResult<String> {
274    let segments = array_path_segments(path)?;
275    let mut parser = Parser::new(content)?;
276    let root = parser.parse_value(0)?;
277    let target = resolve(&root, &segments, 0).ok_or_else(|| DocumentError::PathNotFound {
278        path: path.to_string(),
279    })?;
280    let NodeKind::Array(items) = &target.kind else {
281        return Err(DocumentError::UnsupportedOperation {
282            format: "JSON".to_string(),
283            operation: "add".to_string(),
284            detail: "target is not an array".to_string(),
285        });
286    };
287    let json_item = serde_json::Value::try_from(item)?;
288    let fragment =
289        serde_json::to_string(&json_item).map_err(|error| DocumentError::UnsupportedOperation {
290            format: "JSON".to_string(),
291            operation: "add".to_string(),
292            detail: error.to_string(),
293        })?;
294    let close = target
295        .end
296        .checked_sub(1)
297        .ok_or_else(|| DocumentError::ParseError {
298            format: "JSON".to_string(),
299            detail: "invalid array span".to_string(),
300        })?;
301    let whitespace_start = content.as_bytes()[target.start + 1..close]
302        .iter()
303        .rposition(|byte| !byte.is_ascii_whitespace())
304        .map(|index| target.start + 2 + index)
305        .unwrap_or(target.start + 1);
306    let insertion = if items.is_empty() {
307        fragment
308    } else {
309        format!(", {fragment}")
310    };
311    let mut output = content.to_string();
312    output.insert_str(whitespace_start, &insertion);
313    serde_json::from_str::<serde_json::Value>(&output).map_err(|error| {
314        DocumentError::ParseError {
315            format: "JSON".to_string(),
316            detail: error.to_string(),
317        }
318    })?;
319    Ok(output)
320}
321
322/// Remove one item from a JSON array by numeric index without rebuilding the
323/// document.
324pub fn remove_array_item_preserving(
325    content: &str,
326    path: &str,
327    index: usize,
328) -> DocumentResult<String> {
329    let segments = array_path_segments(path)?;
330    let mut parser = Parser::new(content)?;
331    let root = parser.parse_value(0)?;
332    let target = resolve(&root, &segments, 0).ok_or_else(|| DocumentError::PathNotFound {
333        path: path.to_string(),
334    })?;
335    let NodeKind::Array(items) = &target.kind else {
336        return Err(DocumentError::UnsupportedOperation {
337            format: "JSON".to_string(),
338            operation: "remove".to_string(),
339            detail: "target is not an array".to_string(),
340        });
341    };
342    let item = items
343        .get(index)
344        .ok_or_else(|| DocumentError::IndexOutOfBounds {
345            path: path.to_string(),
346            index,
347            len: items.len(),
348        })?;
349    let mut start = item.start;
350    if let Some(newline) = content.as_bytes()[..start]
351        .iter()
352        .rposition(|byte| *byte == b'\n')
353    {
354        let candidate = newline + 1;
355        if content.as_bytes()[candidate..start]
356            .iter()
357            .all(|byte| byte.is_ascii_whitespace())
358        {
359            start = candidate;
360        }
361    }
362    let after = skip_ws_bytes(content.as_bytes(), item.end);
363    let (mut remove_start, mut remove_end, remove_following_line) =
364        if content.as_bytes().get(after) == Some(&b',') {
365            (start, after + 1, true)
366        } else if let Some(comma) = content.as_bytes()[target.start..start]
367            .iter()
368            .rposition(|byte| *byte == b',')
369            .map(|offset| target.start + offset)
370        {
371            // Scoped to the array's own span (`target.start..start`), not
372            // the whole document: an unscoped backward search can walk past
373            // the array's opening `[` and land on an unrelated preceding
374            // sibling's comma (e.g. `{"a":1,"items":[{"id":"x"}]}` removing
375            // the sole item), corrupting the document instead of just
376            // collapsing the array to `[]`.
377            (comma, item.end, false)
378        } else {
379            (start, item.end, false)
380        };
381    if remove_following_line {
382        if content.as_bytes().get(remove_end) == Some(&b'\r') {
383            remove_end += 1;
384        }
385        if content.as_bytes().get(remove_end) == Some(&b'\n') {
386            remove_end += 1;
387        }
388    }
389    if remove_start > remove_end {
390        std::mem::swap(&mut remove_start, &mut remove_end);
391    }
392    let mut output = content.to_string();
393    output.replace_range(remove_start..remove_end, "");
394    serde_json::from_str::<serde_json::Value>(&output).map_err(|error| {
395        DocumentError::ParseError {
396            format: "JSON".to_string(),
397            detail: error.to_string(),
398        }
399    })?;
400    Ok(output)
401}
402
403/// An empty keyed-list prefix names a root array. Other document operations
404/// continue to reject an empty dot-path because they require an addressed
405/// child.
406fn array_path_segments(path: &str) -> DocumentResult<Vec<String>> {
407    if path.is_empty() {
408        Ok(Vec::new())
409    } else {
410        crate::document::parse_path(path)
411    }
412}
413
414fn skip_ws_bytes(source: &[u8], mut position: usize) -> usize {
415    while source
416        .get(position)
417        .is_some_and(|byte| matches!(byte, b' ' | b'\n' | b'\r' | b'\t'))
418    {
419        position += 1;
420    }
421    position
422}
423
424#[derive(Debug, Clone)]
425pub(crate) struct Node {
426    start: usize,
427    end: usize,
428    member_start: Option<usize>,
429    kind: NodeKind,
430}
431
432#[derive(Debug, Clone)]
433enum NodeKind {
434    Scalar,
435    Object(Vec<(String, Node)>),
436    Array(Vec<Node>),
437}
438
439fn resolve<'a>(node: &'a Node, segments: &[String], index: usize) -> Option<&'a Node> {
440    if index == segments.len() {
441        return Some(node);
442    }
443    match &node.kind {
444        NodeKind::Object(entries) => entries
445            .iter()
446            .rev()
447            .find(|(key, _)| key == &segments[index])
448            .and_then(|(_, child)| resolve(child, segments, index + 1)),
449        NodeKind::Array(items) => segments[index]
450            .parse::<usize>()
451            .ok()
452            .and_then(|item| items.get(item))
453            .and_then(|child| resolve(child, segments, index + 1)),
454        NodeKind::Scalar => None,
455    }
456}
457
458struct Parser<'a> {
459    source: &'a [u8],
460}
461
462impl<'a> Parser<'a> {
463    fn new(source: &'a str) -> DocumentResult<Self> {
464        // Validate the complete grammar and recursion budget before scanning
465        // spans. Otherwise an edit could repair malformed input silently, or
466        // unbounded nesting could exhaust the stack before semantic loading.
467        serde_json::from_str::<serde_json::Value>(source).map_err(|error| {
468            DocumentError::ParseError {
469                format: "JSON".to_string(),
470                detail: error.to_string(),
471            }
472        })?;
473        Ok(Self {
474            source: source.as_bytes(),
475        })
476    }
477
478    fn parse_value(&mut self, mut position: usize) -> DocumentResult<Node> {
479        position = self.skip_ws(position);
480        let start = position;
481        let Some(byte) = self.source.get(position).copied() else {
482            return self.error(position, "expected JSON value");
483        };
484        let kind = match byte {
485            b'{' => self.parse_object(&mut position)?,
486            b'[' => self.parse_array(&mut position)?,
487            b'"' => {
488                position = self.parse_string(position)?;
489                NodeKind::Scalar
490            }
491            _ => {
492                position = self.parse_scalar(position)?;
493                NodeKind::Scalar
494            }
495        };
496        Ok(Node {
497            start,
498            end: position,
499            member_start: None,
500            kind,
501        })
502    }
503
504    fn parse_object(&mut self, position: &mut usize) -> DocumentResult<NodeKind> {
505        *position += 1;
506        let mut entries = Vec::new();
507        let mut keys = std::collections::HashSet::new();
508        loop {
509            *position = self.skip_ws(*position);
510            if self.source.get(*position) == Some(&b'}') {
511                *position += 1;
512                return Ok(NodeKind::Object(entries));
513            }
514            let key_start = *position;
515            let key_end = self.parse_string(*position)?;
516            let key = serde_json::from_slice::<String>(&self.source[key_start..key_end]).map_err(
517                |error| DocumentError::ParseError {
518                    format: "JSON".to_string(),
519                    detail: error.to_string(),
520                },
521            )?;
522            if !keys.insert(key.clone()) {
523                return self.error(key_start, "duplicate object key");
524            }
525            *position = self.skip_ws(key_end);
526            if self.source.get(*position) != Some(&b':') {
527                return self.error(*position, "expected `:` after object key");
528            }
529            *position += 1;
530            let child = self.parse_value(*position)?;
531            *position = child.end;
532            let mut child = child;
533            child.member_start = Some(key_start);
534            entries.push((key, child));
535            *position = self.skip_ws(*position);
536            match self.source.get(*position) {
537                Some(b',') => *position += 1,
538                Some(b'}') => {
539                    *position += 1;
540                    return Ok(NodeKind::Object(entries));
541                }
542                _ => return self.error(*position, "expected `,` or `}` in object"),
543            }
544        }
545    }
546
547    fn parse_array(&mut self, position: &mut usize) -> DocumentResult<NodeKind> {
548        *position += 1;
549        let mut items = Vec::new();
550        loop {
551            *position = self.skip_ws(*position);
552            if self.source.get(*position) == Some(&b']') {
553                *position += 1;
554                return Ok(NodeKind::Array(items));
555            }
556            let child = self.parse_value(*position)?;
557            *position = child.end;
558            items.push(child);
559            *position = self.skip_ws(*position);
560            match self.source.get(*position) {
561                Some(b',') => *position += 1,
562                Some(b']') => {
563                    *position += 1;
564                    return Ok(NodeKind::Array(items));
565                }
566                _ => return self.error(*position, "expected `,` or `]` in array"),
567            }
568        }
569    }
570
571    fn parse_string(&self, mut position: usize) -> DocumentResult<usize> {
572        if self.source.get(position) != Some(&b'"') {
573            return self.error(position, "expected JSON string");
574        }
575        position += 1;
576        let mut escaped = false;
577        while let Some(byte) = self.source.get(position).copied() {
578            position += 1;
579            if escaped {
580                escaped = false;
581            } else if byte == b'\\' {
582                escaped = true;
583            } else if byte == b'"' {
584                return Ok(position);
585            }
586        }
587        self.error(position, "unterminated JSON string")
588    }
589
590    fn parse_scalar(&self, mut position: usize) -> DocumentResult<usize> {
591        let start = position;
592        while let Some(byte) = self.source.get(position).copied() {
593            if matches!(byte, b',' | b']' | b'}' | b' ' | b'\n' | b'\r' | b'\t') {
594                break;
595            }
596            position += 1;
597        }
598        if position == start {
599            return self.error(position, "empty JSON scalar");
600        }
601        serde_json::from_slice::<serde_json::Value>(&self.source[start..position])
602            .map_err(|error| DocumentError::ParseError {
603                format: "JSON".to_string(),
604                detail: error.to_string(),
605            })
606            .map(|_| position)
607    }
608
609    fn skip_ws(&self, mut position: usize) -> usize {
610        while self
611            .source
612            .get(position)
613            .is_some_and(|byte| matches!(byte, b' ' | b'\n' | b'\r' | b'\t'))
614        {
615            position += 1;
616        }
617        position
618    }
619
620    fn error<T>(&self, position: usize, detail: &str) -> DocumentResult<T> {
621        Err(DocumentError::ParseError {
622            format: "JSON".to_string(),
623            detail: format!("at byte {position}: {detail}"),
624        })
625    }
626}
627
628pub fn load(content: &str) -> DocumentResult<Value> {
629    JsonDocument::parse(content)?;
630    serde_json::from_str::<serde_json::Value>(content)
631        .map(Value::from)
632        .map_err(|e| DocumentError::ParseError {
633            format: "JSON".to_string(),
634            detail: e.to_string(),
635        })
636}
637
638pub fn save(value: &Value) -> DocumentResult<String> {
639    let json_val = serde_json::Value::try_from(value)?;
640    serde_json::to_string_pretty(&json_val).map_err(|e| DocumentError::ParseError {
641        format: "JSON".to_string(),
642        detail: e.to_string(),
643    })
644}
645
646#[cfg(test)]
647mod tests {
648    use super::{JsonDocument, load, save};
649    use crate::document::Value;
650
651    #[test]
652    fn duplicate_keys_are_rejected_by_load_and_editor_parser() {
653        let source = r#"{"same":1,"same":2}"#;
654        assert!(load(source).is_err());
655        assert!(JsonDocument::parse(source).is_err());
656    }
657
658    #[test]
659    fn source_parser_rejects_invalid_strings_and_trailing_commas_before_editing() {
660        for source in [r#"{"key":"\q"}"#, "[1,]", r#"{"key":1,}"#] {
661            assert!(JsonDocument::parse(source).is_err(), "{source}");
662        }
663        assert!(
664            super::set_preserving(
665                r#"{"key":"\q"}"#,
666                "key",
667                &Value::String("replacement".into()),
668            )
669            .is_err()
670        );
671    }
672
673    #[test]
674    fn source_parser_bounds_nesting_before_scanning_spans() {
675        for depth in [256, 10_000] {
676            let source = format!("{}0{}", "[".repeat(depth), "]".repeat(depth));
677            assert!(JsonDocument::parse(&source).is_err());
678        }
679    }
680
681    #[test]
682    fn save_rejects_non_finite_float() {
683        let error = save(&Value::Float(f64::INFINITY)).expect_err("infinity must fail");
684        assert!(error.to_string().contains("non-finite"));
685    }
686}