Skip to main content

oxihuman_core/
json_pointer.rs

1// Copyright (C) 2026 COOLJAPAN OU (Team KitaSan)
2// SPDX-License-Identifier: Apache-2.0
3#![allow(dead_code)]
4
5//! JSON Pointer (RFC 6901) resolver stub.
6
7/// Error type for JSON Pointer operations.
8#[derive(Debug, Clone, PartialEq)]
9pub enum JsonPointerError {
10    InvalidSyntax(String),
11    KeyNotFound(String),
12    IndexOutOfRange(usize),
13    InvalidIndex(String),
14}
15
16impl std::fmt::Display for JsonPointerError {
17    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
18        match self {
19            Self::InvalidSyntax(s) => write!(f, "invalid JSON pointer syntax: {s}"),
20            Self::KeyNotFound(k) => write!(f, "key not found: {k}"),
21            Self::IndexOutOfRange(i) => write!(f, "index out of range: {i}"),
22            Self::InvalidIndex(s) => write!(f, "invalid array index: {s}"),
23        }
24    }
25}
26
27/// A parsed JSON Pointer (RFC 6901).
28#[derive(Debug, Clone, PartialEq)]
29pub struct JsonPointer {
30    tokens: Vec<String>,
31}
32
33impl JsonPointer {
34    /// Parse a JSON Pointer string (e.g. `"/foo/bar/0"`).
35    pub fn parse(s: &str) -> Result<Self, JsonPointerError> {
36        if s.is_empty() {
37            return Ok(JsonPointer { tokens: vec![] });
38        }
39        if !s.starts_with('/') {
40            return Err(JsonPointerError::InvalidSyntax(s.to_string()));
41        }
42        let tokens = s[1..]
43            .split('/')
44            .map(|tok| tok.replace("~1", "/").replace("~0", "~"))
45            .collect();
46        Ok(JsonPointer { tokens })
47    }
48
49    /// Return the reference tokens of this pointer.
50    pub fn tokens(&self) -> &[String] {
51        &self.tokens
52    }
53
54    /// Return whether this pointer is the root (empty).
55    pub fn is_root(&self) -> bool {
56        self.tokens.is_empty()
57    }
58
59    /// Return the depth (number of tokens).
60    pub fn depth(&self) -> usize {
61        self.tokens.len()
62    }
63
64    /// Serialize back to a JSON Pointer string.
65    pub fn to_string_repr(&self) -> String {
66        if self.tokens.is_empty() {
67            return String::new();
68        }
69        let mut out = String::new();
70        for tok in &self.tokens {
71            out.push('/');
72            out.push_str(&tok.replace('~', "~0").replace('/', "~1"));
73        }
74        out
75    }
76}
77
78/// Escape a single reference token per RFC 6901.
79pub fn escape_token(tok: &str) -> String {
80    tok.replace('~', "~0").replace('/', "~1")
81}
82
83/// Unescape a single reference token per RFC 6901.
84pub fn unescape_token(tok: &str) -> String {
85    tok.replace("~1", "/").replace("~0", "~")
86}
87
88/// Return the last token of a pointer, or `None` if root.
89pub fn pointer_leaf(ptr: &JsonPointer) -> Option<&str> {
90    ptr.tokens().last().map(|s| s.as_str())
91}
92
93/// Return a new pointer that is the parent of `ptr`, or `None` if root.
94pub fn pointer_parent(ptr: &JsonPointer) -> Option<JsonPointer> {
95    if ptr.is_root() {
96        return None;
97    }
98    let tokens = ptr.tokens[..ptr.tokens.len().saturating_sub(1)].to_vec();
99    Some(JsonPointer { tokens })
100}
101
102#[cfg(test)]
103mod tests {
104    use super::*;
105
106    #[test]
107    fn test_parse_root() {
108        /* root pointer is empty string */
109        let p = JsonPointer::parse("").expect("should succeed");
110        assert!(p.is_root());
111    }
112
113    #[test]
114    fn test_parse_simple() {
115        /* parse /foo/bar */
116        let p = JsonPointer::parse("/foo/bar").expect("should succeed");
117        assert_eq!(p.tokens(), &["foo", "bar"]);
118    }
119
120    #[test]
121    fn test_escape_tilde() {
122        /* ~ must be escaped as ~0 */
123        let p = JsonPointer::parse("/a~0b").expect("should succeed");
124        assert_eq!(p.tokens()[0], "a~b");
125    }
126
127    #[test]
128    fn test_escape_slash() {
129        /* / in token must be escaped as ~1 */
130        let p = JsonPointer::parse("/a~1b").expect("should succeed");
131        assert_eq!(p.tokens()[0], "a/b");
132    }
133
134    #[test]
135    fn test_invalid_no_leading_slash() {
136        /* must start with / */
137        assert!(JsonPointer::parse("foo/bar").is_err());
138    }
139
140    #[test]
141    fn test_depth() {
142        /* depth counts tokens */
143        let p = JsonPointer::parse("/a/b/c").expect("should succeed");
144        assert_eq!(p.depth(), 3);
145    }
146
147    #[test]
148    fn test_roundtrip() {
149        /* serialization roundtrip */
150        let s = "/foo~0bar/baz~1qux";
151        let p = JsonPointer::parse(s).expect("should succeed");
152        assert_eq!(p.to_string_repr(), s);
153    }
154
155    #[test]
156    fn test_leaf() {
157        /* leaf returns last token */
158        let p = JsonPointer::parse("/x/y/z").expect("should succeed");
159        assert_eq!(pointer_leaf(&p), Some("z"));
160    }
161
162    #[test]
163    fn test_parent() {
164        /* parent drops last token */
165        let p = JsonPointer::parse("/a/b").expect("should succeed");
166        let parent = pointer_parent(&p).expect("should succeed");
167        assert_eq!(parent.tokens(), &["a"]);
168    }
169
170    #[test]
171    fn test_root_parent_none() {
172        /* root has no parent */
173        let p = JsonPointer::parse("").expect("should succeed");
174        assert!(pointer_parent(&p).is_none());
175    }
176}