json_archive/
pointer.rs

1// json-archive is a tool for tracking JSON file changes over time
2// Copyright (C) 2025  Peoples Grocers LLC
3//
4// This program is free software: you can redistribute it and/or modify
5// it under the terms of the GNU Affero General Public License as published
6// by the Free Software Foundation, either version 3 of the License, or
7// (at your option) any later version.
8//
9// This program is distributed in the hope that it will be useful,
10// but WITHOUT ANY WARRANTY; without even the implied warranty of
11// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12// GNU Affero General Public License for more details.
13//
14// You should have received a copy of the GNU Affero General Public License
15// along with this program.  If not, see <https://www.gnu.org/licenses/>.
16//
17// To purchase a license under different terms contact admin@peoplesgrocers.com
18// To request changes, report bugs, or give user feedback contact
19// marxism@peoplesgrocers.com
20//
21
22use crate::diagnostics::{Diagnostic, DiagnosticCode};
23use crate::pointer_errors::{
24    build_array_index_out_of_bounds_error, build_invalid_array_index_error,
25    build_key_not_found_error, build_type_mismatch_error,
26};
27use serde_json::Value;
28
29#[derive(Debug, Clone, PartialEq)]
30pub struct JsonPointer {
31    tokens: Vec<String>,
32}
33
34impl JsonPointer {
35    pub fn new(path: &str) -> Result<Self, Diagnostic> {
36        if path.is_empty() {
37            return Ok(JsonPointer { tokens: vec![] });
38        }
39
40        if !path.starts_with('/') {
41            return Err(Diagnostic::fatal(
42                DiagnosticCode::InvalidPointerSyntax,
43                format!(
44                    "I couldn't parse the path '{}': Path must start with '/'",
45                    path
46                ),
47            ));
48        }
49
50        let tokens = path[1..]
51            .split('/')
52            .map(|token| token.replace("~1", "/").replace("~0", "~"))
53            .collect();
54
55        Ok(JsonPointer { tokens })
56    }
57
58    /// Traverse the JSON value following this pointer, returning a mutable reference.
59    ///
60    /// Errors include rich context: the full path, which segment failed, the value
61    /// at that point, and suggestions for typos. See `pointer_errors` module for details.
62    pub fn get_mut<'a>(&self, value: &'a mut Value) -> Result<&'a mut Value, Diagnostic> {
63        let mut current = value;
64
65        for (token_index, token) in self.tokens.iter().enumerate() {
66            match current {
67                Value::Object(obj) => {
68                    if obj.contains_key(token) {
69                        current = obj.get_mut(token).unwrap();
70                    } else {
71                        let keys: Vec<String> = obj.keys().cloned().collect();
72                        let key_refs: Vec<&str> = keys.iter().map(|s| s.as_str()).collect();
73                        return Err(build_key_not_found_error(
74                            &self.tokens,
75                            token_index,
76                            token,
77                            &key_refs,
78                        ));
79                    }
80                }
81                Value::Array(arr) => {
82                    let arr_len = arr.len();
83                    let index = token.parse::<usize>().map_err(|_| {
84                        build_invalid_array_index_error(&self.tokens, token_index, token, arr)
85                    })?;
86                    if index < arr_len {
87                        current = &mut arr[index];
88                    } else {
89                        return Err(build_array_index_out_of_bounds_error(
90                            &self.tokens,
91                            token_index,
92                            index,
93                            arr_len,
94                            arr,
95                        ));
96                    }
97                }
98                _ => {
99                    return Err(build_type_mismatch_error(
100                        &self.tokens,
101                        token_index,
102                        token,
103                        current,
104                    ));
105                }
106            }
107        }
108
109        Ok(current)
110    }
111
112    /// Returns the parent pointer (all tokens except the last).
113    ///
114    /// Used by `set` and `remove`: to modify a value, we need a mutable reference
115    /// to its parent container (object or array), then operate on the final key/index.
116    fn parent(&self) -> JsonPointer {
117        JsonPointer {
118            tokens: self.tokens[..self.tokens.len() - 1].to_vec(),
119        }
120    }
121
122    pub fn set(&self, value: &mut Value, new_value: Value) -> Result<(), Diagnostic> {
123        if self.tokens.is_empty() {
124            *value = new_value;
125            return Ok(());
126        }
127
128        let last_token = &self.tokens[self.tokens.len() - 1];
129        let parent = self.parent().get_mut(value)?;
130
131        match parent {
132            Value::Object(obj) => {
133                obj.insert(last_token.clone(), new_value);
134            }
135            Value::Array(arr) => {
136                let index = last_token.parse::<usize>().map_err(|_| {
137                    Diagnostic::fatal(
138                        DiagnosticCode::InvalidArrayIndex,
139                        format!("I couldn't parse '{}' as an array index", last_token),
140                    )
141                })?;
142
143                if index == arr.len() {
144                    arr.push(new_value);
145                } else if index < arr.len() {
146                    arr[index] = new_value;
147                } else {
148                    return Err(Diagnostic::fatal(
149                        DiagnosticCode::PathNotFound,
150                        format!(
151                            "I couldn't set index {} (array length is {})",
152                            index,
153                            arr.len()
154                        ),
155                    ));
156                }
157            }
158            _ => {
159                return Err(Diagnostic::fatal(
160                    DiagnosticCode::TypeMismatch,
161                    format!(
162                        "I can't set property '{}' on {}",
163                        last_token,
164                        parent.type_name()
165                    ),
166                ));
167            }
168        }
169
170        Ok(())
171    }
172
173    pub fn remove(&self, value: &mut Value) -> Result<Value, Diagnostic> {
174        if self.tokens.is_empty() {
175            return Err(Diagnostic::fatal(
176                DiagnosticCode::InvalidPointerSyntax,
177                "I can't remove the root value".to_string(),
178            ));
179        }
180
181        let last_token = &self.tokens[self.tokens.len() - 1];
182        let parent = self.parent().get_mut(value)?;
183
184        match parent {
185            Value::Object(obj) => obj.remove(last_token).ok_or_else(|| {
186                Diagnostic::fatal(
187                    DiagnosticCode::PathNotFound,
188                    format!("I couldn't find the key '{}' to remove", last_token),
189                )
190            }),
191            Value::Array(arr) => {
192                let index = last_token.parse::<usize>().map_err(|_| {
193                    Diagnostic::fatal(
194                        DiagnosticCode::InvalidArrayIndex,
195                        format!("I couldn't parse '{}' as an array index", last_token),
196                    )
197                })?;
198
199                if index < arr.len() {
200                    Ok(arr.remove(index))
201                } else {
202                    Err(Diagnostic::fatal(
203                        DiagnosticCode::PathNotFound,
204                        format!(
205                            "I couldn't remove index {} (array length is {})",
206                            index,
207                            arr.len()
208                        ),
209                    ))
210                }
211            }
212            _ => Err(Diagnostic::fatal(
213                DiagnosticCode::TypeMismatch,
214                format!(
215                    "I can't remove property '{}' from {}",
216                    last_token,
217                    parent.type_name()
218                ),
219            )),
220        }
221    }
222
223    pub fn to_string(&self) -> String {
224        if self.tokens.is_empty() {
225            return "".to_string();
226        }
227
228        let escaped_tokens: Vec<String> = self
229            .tokens
230            .iter()
231            .map(|token| token.replace("~", "~0").replace("/", "~1"))
232            .collect();
233
234        format!("/{}", escaped_tokens.join("/"))
235    }
236}
237
238trait ValueTypeExt {
239    fn type_name(&self) -> &'static str;
240}
241
242impl ValueTypeExt for Value {
243    fn type_name(&self) -> &'static str {
244        match self {
245            Value::Null => "null",
246            Value::Bool(_) => "boolean",
247            Value::Number(_) => "number",
248            Value::String(_) => "string",
249            Value::Array(_) => "array",
250            Value::Object(_) => "object",
251        }
252    }
253}
254
255#[cfg(test)]
256mod tests {
257    use super::*;
258    use serde_json::json;
259
260    #[test]
261    fn test_empty_pointer() {
262        let pointer = JsonPointer::new("").unwrap();
263        let mut value = json!({"foo": "bar"});
264        assert_eq!(pointer.get_mut(&mut value).unwrap(), &json!({"foo": "bar"}));
265    }
266
267    #[test]
268    fn test_simple_object_access() {
269        let pointer = JsonPointer::new("/foo").unwrap();
270        let mut value = json!({"foo": "bar"});
271        assert_eq!(pointer.get_mut(&mut value).unwrap(), &json!("bar"));
272    }
273
274    #[test]
275    fn test_nested_object_access() {
276        let pointer = JsonPointer::new("/foo/bar").unwrap();
277        let mut value = json!({"foo": {"bar": "baz"}});
278        assert_eq!(pointer.get_mut(&mut value).unwrap(), &json!("baz"));
279    }
280
281    #[test]
282    fn test_array_access() {
283        let pointer = JsonPointer::new("/items/0").unwrap();
284        let mut value = json!({"items": ["first", "second"]});
285        assert_eq!(pointer.get_mut(&mut value).unwrap(), &json!("first"));
286    }
287
288    #[test]
289    fn test_escape_sequences() {
290        let pointer = JsonPointer::new("/foo~1bar").unwrap();
291        let mut value = json!({"foo/bar": "baz"});
292        assert_eq!(pointer.get_mut(&mut value).unwrap(), &json!("baz"));
293
294        let pointer = JsonPointer::new("/foo~0bar").unwrap();
295        let mut value = json!({"foo~bar": "baz"});
296        assert_eq!(pointer.get_mut(&mut value).unwrap(), &json!("baz"));
297    }
298
299    #[test]
300    fn test_set_object() {
301        let pointer = JsonPointer::new("/foo").unwrap();
302        let mut value = json!({"foo": "bar"});
303        pointer.set(&mut value, json!("new_value")).unwrap();
304        assert_eq!(value, json!({"foo": "new_value"}));
305    }
306
307    #[test]
308    fn test_set_array_append() {
309        let pointer = JsonPointer::new("/items/2").unwrap();
310        let mut value = json!({"items": ["first", "second"]});
311        pointer.set(&mut value, json!("third")).unwrap();
312        assert_eq!(value, json!({"items": ["first", "second", "third"]}));
313    }
314
315    #[test]
316    fn test_remove_object() {
317        let pointer = JsonPointer::new("/foo").unwrap();
318        let mut value = json!({"foo": "bar", "baz": "qux"});
319        let removed = pointer.remove(&mut value).unwrap();
320        assert_eq!(removed, json!("bar"));
321        assert_eq!(value, json!({"baz": "qux"}));
322    }
323
324    #[test]
325    fn test_remove_array() {
326        let pointer = JsonPointer::new("/items/0").unwrap();
327        let mut value = json!({"items": ["first", "second", "third"]});
328        let removed = pointer.remove(&mut value).unwrap();
329        assert_eq!(removed, json!("first"));
330        assert_eq!(value, json!({"items": ["second", "third"]}));
331    }
332}