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()[..start]
300            .iter()
301            .rposition(|byte| *byte == b',')
302        {
303            (comma, item.end, false)
304        } else {
305            (start, item.end, false)
306        };
307    if remove_following_line {
308        if content.as_bytes().get(remove_end) == Some(&b'\r') {
309            remove_end += 1;
310        }
311        if content.as_bytes().get(remove_end) == Some(&b'\n') {
312            remove_end += 1;
313        }
314    }
315    if remove_start > remove_end {
316        std::mem::swap(&mut remove_start, &mut remove_end);
317    }
318    let mut output = content.to_string();
319    output.replace_range(remove_start..remove_end, "");
320    serde_json::from_str::<serde_json::Value>(&output).map_err(|error| {
321        DocumentError::ParseError {
322            format: "JSON".to_string(),
323            detail: error.to_string(),
324        }
325    })?;
326    Ok(output)
327}
328
329fn skip_ws_bytes(source: &[u8], mut position: usize) -> usize {
330    while source
331        .get(position)
332        .is_some_and(|byte| matches!(byte, b' ' | b'\n' | b'\r' | b'\t'))
333    {
334        position += 1;
335    }
336    position
337}
338
339#[derive(Debug, Clone)]
340pub(crate) struct Node {
341    start: usize,
342    end: usize,
343    member_start: Option<usize>,
344    kind: NodeKind,
345}
346
347#[derive(Debug, Clone)]
348enum NodeKind {
349    Scalar,
350    Object(Vec<(String, Node)>),
351    Array(Vec<Node>),
352}
353
354fn resolve<'a>(node: &'a Node, segments: &[String], index: usize) -> Option<&'a Node> {
355    if index == segments.len() {
356        return Some(node);
357    }
358    match &node.kind {
359        NodeKind::Object(entries) => entries
360            .iter()
361            .rev()
362            .find(|(key, _)| key == &segments[index])
363            .and_then(|(_, child)| resolve(child, segments, index + 1)),
364        NodeKind::Array(items) => segments[index]
365            .parse::<usize>()
366            .ok()
367            .and_then(|item| items.get(item))
368            .and_then(|child| resolve(child, segments, index + 1)),
369        NodeKind::Scalar => None,
370    }
371}
372
373struct Parser<'a> {
374    source: &'a [u8],
375}
376
377impl<'a> Parser<'a> {
378    fn new(source: &'a str) -> Self {
379        Self {
380            source: source.as_bytes(),
381        }
382    }
383
384    fn parse_value(&mut self, mut position: usize) -> DocumentResult<Node> {
385        position = self.skip_ws(position);
386        let start = position;
387        let Some(byte) = self.source.get(position).copied() else {
388            return self.error(position, "expected JSON value");
389        };
390        let kind = match byte {
391            b'{' => self.parse_object(&mut position)?,
392            b'[' => self.parse_array(&mut position)?,
393            b'"' => {
394                position = self.parse_string(position)?;
395                NodeKind::Scalar
396            }
397            _ => {
398                position = self.parse_scalar(position)?;
399                NodeKind::Scalar
400            }
401        };
402        Ok(Node {
403            start,
404            end: position,
405            member_start: None,
406            kind,
407        })
408    }
409
410    fn parse_object(&mut self, position: &mut usize) -> DocumentResult<NodeKind> {
411        *position += 1;
412        let mut entries = Vec::new();
413        loop {
414            *position = self.skip_ws(*position);
415            if self.source.get(*position) == Some(&b'}') {
416                *position += 1;
417                return Ok(NodeKind::Object(entries));
418            }
419            let key_start = *position;
420            let key_end = self.parse_string(*position)?;
421            let key = serde_json::from_slice::<String>(&self.source[key_start..key_end]).map_err(
422                |error| DocumentError::ParseError {
423                    format: "JSON".to_string(),
424                    detail: error.to_string(),
425                },
426            )?;
427            *position = self.skip_ws(key_end);
428            if self.source.get(*position) != Some(&b':') {
429                return self.error(*position, "expected `:` after object key");
430            }
431            *position += 1;
432            let child = self.parse_value(*position)?;
433            *position = child.end;
434            let mut child = child;
435            child.member_start = Some(key_start);
436            entries.push((key, child));
437            *position = self.skip_ws(*position);
438            match self.source.get(*position) {
439                Some(b',') => *position += 1,
440                Some(b'}') => {
441                    *position += 1;
442                    return Ok(NodeKind::Object(entries));
443                }
444                _ => return self.error(*position, "expected `,` or `}` in object"),
445            }
446        }
447    }
448
449    fn parse_array(&mut self, position: &mut usize) -> DocumentResult<NodeKind> {
450        *position += 1;
451        let mut items = Vec::new();
452        loop {
453            *position = self.skip_ws(*position);
454            if self.source.get(*position) == Some(&b']') {
455                *position += 1;
456                return Ok(NodeKind::Array(items));
457            }
458            let child = self.parse_value(*position)?;
459            *position = child.end;
460            items.push(child);
461            *position = self.skip_ws(*position);
462            match self.source.get(*position) {
463                Some(b',') => *position += 1,
464                Some(b']') => {
465                    *position += 1;
466                    return Ok(NodeKind::Array(items));
467                }
468                _ => return self.error(*position, "expected `,` or `]` in array"),
469            }
470        }
471    }
472
473    fn parse_string(&self, mut position: usize) -> DocumentResult<usize> {
474        if self.source.get(position) != Some(&b'"') {
475            return self.error(position, "expected JSON string");
476        }
477        position += 1;
478        let mut escaped = false;
479        while let Some(byte) = self.source.get(position).copied() {
480            position += 1;
481            if escaped {
482                escaped = false;
483            } else if byte == b'\\' {
484                escaped = true;
485            } else if byte == b'"' {
486                return Ok(position);
487            }
488        }
489        self.error(position, "unterminated JSON string")
490    }
491
492    fn parse_scalar(&self, mut position: usize) -> DocumentResult<usize> {
493        let start = position;
494        while let Some(byte) = self.source.get(position).copied() {
495            if matches!(byte, b',' | b']' | b'}' | b' ' | b'\n' | b'\r' | b'\t') {
496                break;
497            }
498            position += 1;
499        }
500        if position == start {
501            return self.error(position, "empty JSON scalar");
502        }
503        serde_json::from_slice::<serde_json::Value>(&self.source[start..position])
504            .map_err(|error| DocumentError::ParseError {
505                format: "JSON".to_string(),
506                detail: error.to_string(),
507            })
508            .map(|_| position)
509    }
510
511    fn skip_ws(&self, mut position: usize) -> usize {
512        while self
513            .source
514            .get(position)
515            .is_some_and(|byte| matches!(byte, b' ' | b'\n' | b'\r' | b'\t'))
516        {
517            position += 1;
518        }
519        position
520    }
521
522    fn error<T>(&self, position: usize, detail: &str) -> DocumentResult<T> {
523        Err(DocumentError::ParseError {
524            format: "JSON".to_string(),
525            detail: format!("at byte {position}: {detail}"),
526        })
527    }
528}
529
530pub fn load(content: &str) -> DocumentResult<Value> {
531    serde_json::from_str::<serde_json::Value>(content)
532        .map(Value::from)
533        .map_err(|e| DocumentError::ParseError {
534            format: "JSON".to_string(),
535            detail: e.to_string(),
536        })
537}
538
539pub fn save(value: &Value) -> DocumentResult<String> {
540    let json_val: serde_json::Value = value.clone().into();
541    serde_json::to_string_pretty(&json_val).map_err(|e| DocumentError::ParseError {
542        format: "JSON".to_string(),
543        detail: e.to_string(),
544    })
545}