oxihuman_core/
json_pointer.rs1#![allow(dead_code)]
4
5#[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#[derive(Debug, Clone, PartialEq)]
29pub struct JsonPointer {
30 tokens: Vec<String>,
31}
32
33impl JsonPointer {
34 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 pub fn tokens(&self) -> &[String] {
51 &self.tokens
52 }
53
54 pub fn is_root(&self) -> bool {
56 self.tokens.is_empty()
57 }
58
59 pub fn depth(&self) -> usize {
61 self.tokens.len()
62 }
63
64 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
78pub fn escape_token(tok: &str) -> String {
80 tok.replace('~', "~0").replace('/', "~1")
81}
82
83pub fn unescape_token(tok: &str) -> String {
85 tok.replace("~1", "/").replace("~0", "~")
86}
87
88pub fn pointer_leaf(ptr: &JsonPointer) -> Option<&str> {
90 ptr.tokens().last().map(|s| s.as_str())
91}
92
93pub 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 let p = JsonPointer::parse("").expect("should succeed");
110 assert!(p.is_root());
111 }
112
113 #[test]
114 fn test_parse_simple() {
115 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 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 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 assert!(JsonPointer::parse("foo/bar").is_err());
138 }
139
140 #[test]
141 fn test_depth() {
142 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 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 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 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 let p = JsonPointer::parse("").expect("should succeed");
174 assert!(pointer_parent(&p).is_none());
175 }
176}