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