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