Skip to main content

tokmd_gate/
pointer.rs

1//! RFC 6901 JSON Pointer implementation.
2
3use serde_json::Value;
4
5/// Resolve a JSON Pointer against a JSON value.
6///
7/// Implements RFC 6901 JSON Pointer syntax:
8/// - Empty string "" refers to the whole document
9/// - "/" refers to an empty key
10/// - "/foo/bar" navigates to obj["foo"]["bar"]
11/// - "/0" navigates to array index 0
12/// - "~0" escapes to "~"
13/// - "~1" escapes to "/"
14///
15/// # Examples
16///
17/// ```
18/// use serde_json::json;
19/// use tokmd_gate::resolve_pointer;
20///
21/// let doc = json!({"foo": {"bar": 42}});
22/// assert_eq!(resolve_pointer(&doc, "/foo/bar"), Some(&json!(42)));
23///
24/// let arr = json!({"items": [1, 2, 3]});
25/// assert_eq!(resolve_pointer(&arr, "/items/1"), Some(&json!(2)));
26/// ```
27pub fn resolve_pointer<'a>(value: &'a Value, pointer: &str) -> Option<&'a Value> {
28    // Empty pointer refers to whole document
29    if pointer.is_empty() {
30        return Some(value);
31    }
32
33    // Pointer must start with /
34    if !pointer.starts_with('/') {
35        return None;
36    }
37
38    let mut current = value;
39
40    for token in pointer[1..].split('/') {
41        // Unescape tokens per RFC 6901
42        let unescaped = unescape_token(token);
43
44        current = match current {
45            Value::Object(map) => map.get(&unescaped)?,
46            Value::Array(arr) => {
47                // Try to parse as array index
48                let idx: usize = unescaped.parse().ok()?;
49                arr.get(idx)?
50            }
51            _ => return None,
52        };
53    }
54
55    Some(current)
56}
57
58/// Unescape a JSON Pointer token per RFC 6901.
59/// ~1 -> /
60/// ~0 -> ~
61fn unescape_token(token: &str) -> String {
62    token.replace("~1", "/").replace("~0", "~")
63}
64
65/// Escape a string for use in a JSON Pointer.
66/// / -> ~1
67/// ~ -> ~0
68#[allow(dead_code)]
69pub fn escape_token(s: &str) -> String {
70    s.replace('~', "~0").replace('/', "~1")
71}
72
73#[cfg(test)]
74mod tests {
75    use super::*;
76    use serde_json::json;
77
78    #[test]
79    fn test_empty_pointer() {
80        let doc = json!({"foo": 1});
81        assert_eq!(resolve_pointer(&doc, ""), Some(&doc));
82    }
83
84    #[test]
85    fn test_simple_path() {
86        let doc = json!({"foo": {"bar": 42}});
87        assert_eq!(resolve_pointer(&doc, "/foo"), Some(&json!({"bar": 42})));
88        assert_eq!(resolve_pointer(&doc, "/foo/bar"), Some(&json!(42)));
89    }
90
91    #[test]
92    fn test_array_index() {
93        let doc = json!({"items": [10, 20, 30]});
94        assert_eq!(resolve_pointer(&doc, "/items/0"), Some(&json!(10)));
95        assert_eq!(resolve_pointer(&doc, "/items/2"), Some(&json!(30)));
96        assert_eq!(resolve_pointer(&doc, "/items/3"), None);
97    }
98
99    #[test]
100    fn test_escaped_tokens() {
101        let doc = json!({"a/b": {"c~d": 1}});
102        assert_eq!(resolve_pointer(&doc, "/a~1b/c~0d"), Some(&json!(1)));
103    }
104
105    #[test]
106    fn test_invalid_pointer() {
107        let doc = json!({"foo": 1});
108        // Missing leading slash
109        assert_eq!(resolve_pointer(&doc, "foo"), None);
110        // Non-existent path
111        assert_eq!(resolve_pointer(&doc, "/bar"), None);
112    }
113
114    #[test]
115    fn test_nested_arrays() {
116        let doc = json!({"matrix": [[1, 2], [3, 4]]});
117        assert_eq!(resolve_pointer(&doc, "/matrix/0/1"), Some(&json!(2)));
118        assert_eq!(resolve_pointer(&doc, "/matrix/1/0"), Some(&json!(3)));
119    }
120
121    #[test]
122    fn test_escape_token() {
123        assert_eq!(escape_token("a/b"), "a~1b");
124        assert_eq!(escape_token("a~b"), "a~0b");
125        assert_eq!(escape_token("a/b~c"), "a~1b~0c");
126    }
127}