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/// Replace one existing JSON scalar while retaining every byte outside its
39/// value span. JSON has no comments, but this still preserves key order,
40/// whitespace, line endings, escapes, and untouched number spellings.
41pub fn set_scalar_preserving(content: &str, path: &str, value: &Value) -> DocumentResult<String> {
42    let segments = crate::document::parse_path(path)?;
43    let document = JsonDocument::parse(content)?;
44    let root = document.root();
45    let replacement =
46        serde_json::to_string(&serde_json::Value::from(value.clone())).map_err(|error| {
47            DocumentError::UnsupportedOperation {
48                format: "JSON".to_string(),
49                operation: "set".to_string(),
50                detail: error.to_string(),
51            }
52        })?;
53    let mut output = content.to_string();
54    match resolve(root, &segments, 0) {
55        Some(target) => {
56            if !matches!(target.kind, NodeKind::Scalar) {
57                return Err(DocumentError::UnsupportedOperation {
58                    format: "JSON".to_string(),
59                    operation: "set".to_string(),
60                    detail: "only existing scalar JSON values are supported by the source editor"
61                        .to_string(),
62                });
63            }
64            output.replace_range(target.start..target.end, &replacement);
65        }
66        None => {
67            // Missing leaf: splice a new member into the (existing) parent
68            // object, mirroring the neighbouring member's indentation. A
69            // missing intermediate parent fails before any write.
70            let (last, parents) = segments.split_last().ok_or(DocumentError::EmptyPath)?;
71            let parent = resolve(root, parents, 0).ok_or_else(|| DocumentError::PathNotFound {
72                path: path.to_string(),
73            })?;
74            let NodeKind::Object(entries) = &parent.kind else {
75                return Err(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 new_key =
82                serde_json::to_string(last).map_err(|error| DocumentError::ParseError {
83                    format: "JSON".to_string(),
84                    detail: error.to_string(),
85                })?;
86            let (position, fragment) = match entries.last() {
87                Some((_, last_node)) => {
88                    let anchor = last_node.member_start.unwrap_or(last_node.start);
89                    let (indent, multiline) = member_indent(content.as_bytes(), anchor);
90                    let fragment = if multiline {
91                        format!(",\n{indent}{new_key}: {replacement}")
92                    } else {
93                        format!(", {new_key}: {replacement}")
94                    };
95                    (last_node.end, fragment)
96                }
97                None => (parent.start + 1, format!("{new_key}: {replacement}")),
98            };
99            output.insert_str(position, &fragment);
100        }
101    }
102    serde_json::from_str::<serde_json::Value>(&output).map_err(|error| {
103        DocumentError::ParseError {
104            format: "JSON".to_string(),
105            detail: error.to_string(),
106        }
107    })?;
108    Ok(output)
109}
110
111/// Indentation of the member whose key begins at `anchor`, and whether that
112/// member sits on its own line (so a spliced sibling should too).
113fn member_indent(bytes: &[u8], anchor: usize) -> (String, bool) {
114    match bytes[..anchor].iter().rposition(|byte| *byte == b'\n') {
115        Some(newline) => {
116            let indent = bytes[newline + 1..anchor]
117                .iter()
118                .take_while(|byte| matches!(byte, b' ' | b'\t'))
119                .map(|byte| *byte as char)
120                .collect();
121            (indent, true)
122        }
123        None => (String::new(), false),
124    }
125}
126
127/// Remove one existing JSON member or array item while preserving the rest of
128/// the source layout. Removing the root value is intentionally unsupported.
129pub fn unset_preserving(content: &str, path: &str) -> DocumentResult<String> {
130    let segments = crate::document::parse_path(path)?;
131    let document = JsonDocument::parse(content)?;
132    let root = document.root();
133    let target = resolve(root, &segments, 0).ok_or_else(|| DocumentError::PathNotFound {
134        path: path.to_string(),
135    })?;
136    if target.member_start.is_none()
137        && matches!(target.kind, NodeKind::Scalar)
138        && segments.is_empty()
139    {
140        return Err(DocumentError::UnsupportedOperation {
141            format: "JSON".to_string(),
142            operation: "unset".to_string(),
143            detail: "cannot remove the JSON root value".to_string(),
144        });
145    }
146    let mut start = target.member_start.unwrap_or(target.start);
147    if target.member_start.is_some()
148        && let Some(newline) = content.as_bytes()[..start]
149            .iter()
150            .rposition(|byte| *byte == b'\n')
151    {
152        let candidate = newline + 1;
153        if content.as_bytes()[candidate..start]
154            .iter()
155            .all(|byte| byte.is_ascii_whitespace())
156        {
157            start = candidate;
158        }
159    }
160    let end = target.end;
161    let after = skip_ws_bytes(content.as_bytes(), end);
162    let (remove_start, mut remove_end, remove_following_line) =
163        if content.as_bytes().get(after) == Some(&b',') {
164            (start, after + 1, true)
165        } else if let Some(comma) = content.as_bytes()[..start]
166            .iter()
167            .rposition(|byte| *byte == b',')
168        {
169            (comma, end, false)
170        } else {
171            (start, end, false)
172        };
173    if remove_following_line {
174        if content.as_bytes().get(remove_end) == Some(&b'\r') {
175            remove_end += 1;
176        }
177        if content.as_bytes().get(remove_end) == Some(&b'\n') {
178            remove_end += 1;
179        }
180    }
181    let mut output = content.to_string();
182    output.replace_range(remove_start..remove_end, "");
183    serde_json::from_str::<serde_json::Value>(&output).map_err(|error| {
184        DocumentError::ParseError {
185            format: "JSON".to_string(),
186            detail: error.to_string(),
187        }
188    })?;
189    Ok(output)
190}
191
192/// Append one JSON array item while retaining the existing array's trailing
193/// whitespace and all bytes outside the insertion point.
194pub fn append_array_item_preserving(
195    content: &str,
196    path: &str,
197    item: &Value,
198) -> DocumentResult<String> {
199    let segments = crate::document::parse_path(path)?;
200    let mut parser = Parser::new(content);
201    let root = parser.parse_value(0)?;
202    let target = resolve(&root, &segments, 0).ok_or_else(|| DocumentError::PathNotFound {
203        path: path.to_string(),
204    })?;
205    let NodeKind::Array(items) = &target.kind else {
206        return Err(DocumentError::UnsupportedOperation {
207            format: "JSON".to_string(),
208            operation: "add".to_string(),
209            detail: "target is not an array".to_string(),
210        });
211    };
212    let fragment =
213        serde_json::to_string(&serde_json::Value::from(item.clone())).map_err(|error| {
214            DocumentError::UnsupportedOperation {
215                format: "JSON".to_string(),
216                operation: "add".to_string(),
217                detail: error.to_string(),
218            }
219        })?;
220    let close = target
221        .end
222        .checked_sub(1)
223        .ok_or_else(|| DocumentError::ParseError {
224            format: "JSON".to_string(),
225            detail: "invalid array span".to_string(),
226        })?;
227    let whitespace_start = content.as_bytes()[target.start + 1..close]
228        .iter()
229        .rposition(|byte| !byte.is_ascii_whitespace())
230        .map(|index| target.start + 2 + index)
231        .unwrap_or(target.start + 1);
232    let insertion = if items.is_empty() {
233        fragment
234    } else {
235        format!(", {fragment}")
236    };
237    let mut output = content.to_string();
238    output.insert_str(whitespace_start, &insertion);
239    serde_json::from_str::<serde_json::Value>(&output).map_err(|error| {
240        DocumentError::ParseError {
241            format: "JSON".to_string(),
242            detail: error.to_string(),
243        }
244    })?;
245    Ok(output)
246}
247
248/// Remove a keyed object from a JSON array without rebuilding the document.
249pub fn remove_array_item_preserving(
250    content: &str,
251    path: &str,
252    slug: &str,
253    slug_field: &str,
254) -> DocumentResult<String> {
255    let segments = crate::document::parse_path(path)?;
256    let mut parser = Parser::new(content);
257    let root = parser.parse_value(0)?;
258    let target = resolve(&root, &segments, 0).ok_or_else(|| DocumentError::PathNotFound {
259        path: path.to_string(),
260    })?;
261    let NodeKind::Array(items) = &target.kind else {
262        return Err(DocumentError::UnsupportedOperation {
263            format: "JSON".to_string(),
264            operation: "remove".to_string(),
265            detail: "target is not an array".to_string(),
266        });
267    };
268    let item = items
269        .iter()
270        .find(|item| {
271            let Ok(value) =
272                serde_json::from_str::<serde_json::Value>(&content[item.start..item.end])
273            else {
274                return false;
275            };
276            value.get(slug_field).and_then(serde_json::Value::as_str) == Some(slug)
277        })
278        .ok_or_else(|| DocumentError::SlugNotFound {
279            prefix: path.to_string(),
280            slug: slug.to_string(),
281        })?;
282    let mut start = item.start;
283    if let Some(newline) = content.as_bytes()[..start]
284        .iter()
285        .rposition(|byte| *byte == b'\n')
286    {
287        let candidate = newline + 1;
288        if content.as_bytes()[candidate..start]
289            .iter()
290            .all(|byte| byte.is_ascii_whitespace())
291        {
292            start = candidate;
293        }
294    }
295    let after = skip_ws_bytes(content.as_bytes(), item.end);
296    let (mut remove_start, mut remove_end, remove_following_line) =
297        if content.as_bytes().get(after) == Some(&b',') {
298            (start, after + 1, true)
299        } else if let Some(comma) = content.as_bytes()[target.start..start]
300            .iter()
301            .rposition(|byte| *byte == b',')
302            .map(|offset| target.start + offset)
303        {
304            // Scoped to the array's own span (`target.start..start`), not
305            // the whole document: an unscoped backward search can walk past
306            // the array's opening `[` and land on an unrelated preceding
307            // sibling's comma (e.g. `{"a":1,"items":[{"id":"x"}]}` removing
308            // the sole item), corrupting the document instead of just
309            // collapsing the array to `[]`.
310            (comma, item.end, false)
311        } else {
312            (start, item.end, false)
313        };
314    if remove_following_line {
315        if content.as_bytes().get(remove_end) == Some(&b'\r') {
316            remove_end += 1;
317        }
318        if content.as_bytes().get(remove_end) == Some(&b'\n') {
319            remove_end += 1;
320        }
321    }
322    if remove_start > remove_end {
323        std::mem::swap(&mut remove_start, &mut remove_end);
324    }
325    let mut output = content.to_string();
326    output.replace_range(remove_start..remove_end, "");
327    serde_json::from_str::<serde_json::Value>(&output).map_err(|error| {
328        DocumentError::ParseError {
329            format: "JSON".to_string(),
330            detail: error.to_string(),
331        }
332    })?;
333    Ok(output)
334}
335
336fn skip_ws_bytes(source: &[u8], mut position: usize) -> usize {
337    while source
338        .get(position)
339        .is_some_and(|byte| matches!(byte, b' ' | b'\n' | b'\r' | b'\t'))
340    {
341        position += 1;
342    }
343    position
344}
345
346#[derive(Debug, Clone)]
347pub(crate) struct Node {
348    start: usize,
349    end: usize,
350    member_start: Option<usize>,
351    kind: NodeKind,
352}
353
354#[derive(Debug, Clone)]
355enum NodeKind {
356    Scalar,
357    Object(Vec<(String, Node)>),
358    Array(Vec<Node>),
359}
360
361fn resolve<'a>(node: &'a Node, segments: &[String], index: usize) -> Option<&'a Node> {
362    if index == segments.len() {
363        return Some(node);
364    }
365    match &node.kind {
366        NodeKind::Object(entries) => entries
367            .iter()
368            .rev()
369            .find(|(key, _)| key == &segments[index])
370            .and_then(|(_, child)| resolve(child, segments, index + 1)),
371        NodeKind::Array(items) => segments[index]
372            .parse::<usize>()
373            .ok()
374            .and_then(|item| items.get(item))
375            .and_then(|child| resolve(child, segments, index + 1)),
376        NodeKind::Scalar => None,
377    }
378}
379
380struct Parser<'a> {
381    source: &'a [u8],
382}
383
384impl<'a> Parser<'a> {
385    fn new(source: &'a str) -> Self {
386        Self {
387            source: source.as_bytes(),
388        }
389    }
390
391    fn parse_value(&mut self, mut position: usize) -> DocumentResult<Node> {
392        position = self.skip_ws(position);
393        let start = position;
394        let Some(byte) = self.source.get(position).copied() else {
395            return self.error(position, "expected JSON value");
396        };
397        let kind = match byte {
398            b'{' => self.parse_object(&mut position)?,
399            b'[' => self.parse_array(&mut position)?,
400            b'"' => {
401                position = self.parse_string(position)?;
402                NodeKind::Scalar
403            }
404            _ => {
405                position = self.parse_scalar(position)?;
406                NodeKind::Scalar
407            }
408        };
409        Ok(Node {
410            start,
411            end: position,
412            member_start: None,
413            kind,
414        })
415    }
416
417    fn parse_object(&mut self, position: &mut usize) -> DocumentResult<NodeKind> {
418        *position += 1;
419        let mut entries = Vec::new();
420        loop {
421            *position = self.skip_ws(*position);
422            if self.source.get(*position) == Some(&b'}') {
423                *position += 1;
424                return Ok(NodeKind::Object(entries));
425            }
426            let key_start = *position;
427            let key_end = self.parse_string(*position)?;
428            let key = serde_json::from_slice::<String>(&self.source[key_start..key_end]).map_err(
429                |error| DocumentError::ParseError {
430                    format: "JSON".to_string(),
431                    detail: error.to_string(),
432                },
433            )?;
434            *position = self.skip_ws(key_end);
435            if self.source.get(*position) != Some(&b':') {
436                return self.error(*position, "expected `:` after object key");
437            }
438            *position += 1;
439            let child = self.parse_value(*position)?;
440            *position = child.end;
441            let mut child = child;
442            child.member_start = Some(key_start);
443            entries.push((key, child));
444            *position = self.skip_ws(*position);
445            match self.source.get(*position) {
446                Some(b',') => *position += 1,
447                Some(b'}') => {
448                    *position += 1;
449                    return Ok(NodeKind::Object(entries));
450                }
451                _ => return self.error(*position, "expected `,` or `}` in object"),
452            }
453        }
454    }
455
456    fn parse_array(&mut self, position: &mut usize) -> DocumentResult<NodeKind> {
457        *position += 1;
458        let mut items = Vec::new();
459        loop {
460            *position = self.skip_ws(*position);
461            if self.source.get(*position) == Some(&b']') {
462                *position += 1;
463                return Ok(NodeKind::Array(items));
464            }
465            let child = self.parse_value(*position)?;
466            *position = child.end;
467            items.push(child);
468            *position = self.skip_ws(*position);
469            match self.source.get(*position) {
470                Some(b',') => *position += 1,
471                Some(b']') => {
472                    *position += 1;
473                    return Ok(NodeKind::Array(items));
474                }
475                _ => return self.error(*position, "expected `,` or `]` in array"),
476            }
477        }
478    }
479
480    fn parse_string(&self, mut position: usize) -> DocumentResult<usize> {
481        if self.source.get(position) != Some(&b'"') {
482            return self.error(position, "expected JSON string");
483        }
484        position += 1;
485        let mut escaped = false;
486        while let Some(byte) = self.source.get(position).copied() {
487            position += 1;
488            if escaped {
489                escaped = false;
490            } else if byte == b'\\' {
491                escaped = true;
492            } else if byte == b'"' {
493                return Ok(position);
494            }
495        }
496        self.error(position, "unterminated JSON string")
497    }
498
499    fn parse_scalar(&self, mut position: usize) -> DocumentResult<usize> {
500        let start = position;
501        while let Some(byte) = self.source.get(position).copied() {
502            if matches!(byte, b',' | b']' | b'}' | b' ' | b'\n' | b'\r' | b'\t') {
503                break;
504            }
505            position += 1;
506        }
507        if position == start {
508            return self.error(position, "empty JSON scalar");
509        }
510        serde_json::from_slice::<serde_json::Value>(&self.source[start..position])
511            .map_err(|error| DocumentError::ParseError {
512                format: "JSON".to_string(),
513                detail: error.to_string(),
514            })
515            .map(|_| position)
516    }
517
518    fn skip_ws(&self, mut position: usize) -> usize {
519        while self
520            .source
521            .get(position)
522            .is_some_and(|byte| matches!(byte, b' ' | b'\n' | b'\r' | b'\t'))
523        {
524            position += 1;
525        }
526        position
527    }
528
529    fn error<T>(&self, position: usize, detail: &str) -> DocumentResult<T> {
530        Err(DocumentError::ParseError {
531            format: "JSON".to_string(),
532            detail: format!("at byte {position}: {detail}"),
533        })
534    }
535}
536
537pub fn load(content: &str) -> DocumentResult<Value> {
538    serde_json::from_str::<serde_json::Value>(content)
539        .map(Value::from)
540        .map_err(|e| DocumentError::ParseError {
541            format: "JSON".to_string(),
542            detail: e.to_string(),
543        })
544}
545
546pub fn save(value: &Value) -> DocumentResult<String> {
547    let json_val: serde_json::Value = value.clone().into();
548    serde_json::to_string_pretty(&json_val).map_err(|e| DocumentError::ParseError {
549        format: "JSON".to_string(),
550        detail: e.to_string(),
551    })
552}