jsonptr_lite/lib.rs
1//! Tiny JSON Pointer lookups for `serde_json::Value`.
2//!
3//! Supports RFC 6901 escape rules for path tokens:
4//! - `~1` becomes `/`
5//! - `~0` becomes `~`
6//!
7//! This is intentionally minimal: one function, no allocations beyond token unescaping.
8//! Returns `None` for invalid pointers or missing paths.
9//!
10//! # Examples
11//! ```
12//! use serde_json::{json, Value};
13//! use jsonptr_lite::ptr;
14//!
15//! let v = json!({ "a": { "b": 3 }});
16//! assert_eq!(ptr(&v, "/a/b").and_then(Value::as_i64), Some(3));
17//!
18//! // array index
19//! let v = json!({ "items": [10, 20, 30] });
20//! assert_eq!(ptr(&v, "/items/1").and_then(Value::as_i64), Some(20));
21//!
22//! // escaped slash in key name: "/"
23//! let v = json!({ "a/b": 7 });
24//! assert_eq!(ptr(&v, "/a~1b").and_then(Value::as_i64), Some(7));
25//!
26//! // empty pointer returns the whole value
27//! let v = json!(42);
28//! assert_eq!(ptr(&v, "").and_then(Value::as_i64), Some(42));
29//! ```
30
31//! change value via pointer
32//! use serde_json::{json, Value};
33//! use jsonptr_lite::{ptr, ptr_mut};
34//! let mut v = json!({"a":{"b":0}});
35//! *ptr_mut(&mut v, "/a/b").unwrap() = json!(42);
36//! assert_eq!(ptr(&v, "/a/b").and_then(Value::as_i64), Some(42));
37use serde_json::Value;
38
39/// Lookup a JSON value by JSON Pointer (RFC 6901).
40///
41/// - empty string `""` returns the input `value`
42/// - each path segment is separated by `/`
43/// - escape rules in segments: `~1` → `/`, `~0` → `~`
44/// - returns `None` if the path cannot be followed
45pub fn ptr<'a>(value: &'a Value, pointer: &str) -> Option<&'a Value> {
46 // empty pointer means the whole document
47 if pointer.is_empty() {
48 return Some(value);
49 }
50 // pointer must begin with "/" per RFC 6901
51 if !pointer.starts_with('/') {
52 return None;
53 }
54
55 let mut current = value;
56 // skip the leading empty segment before the first "/"
57 for raw_token in pointer.split('/').skip(1) {
58 // decode "~1" -> "/" and "~0" -> "~"
59 let token = unescape_token(raw_token)?;
60
61 match current {
62 Value::Object(map) => {
63 // step into object by key
64 current = map.get(&token)?;
65 }
66 Value::Array(items) => {
67 // step into array by index
68 let idx: usize = token.parse().ok()?;
69 current = items.get(idx)?;
70 }
71 _ => {
72 // cannot descend into primitives
73 return None;
74 }
75 }
76 }
77 Some(current)
78}
79
80// turn token escapes into their characters
81// "~1" becomes "/" and "~0" becomes "~"
82// any other "~" sequence is invalid
83fn unescape_token(token: &str) -> Option<String> {
84 // fast path: no tilde, nothing to unescape
85 if !token.contains('~') {
86 return Some(token.to_owned());
87 }
88
89 let mut out = String::with_capacity(token.len());
90 let mut chars = token.chars();
91 while let Some(c) = chars.next() {
92 if c == '~' {
93 match chars.next() {
94 Some('0') => out.push('~'),
95 Some('1') => out.push('/'),
96 _ => return None,
97 }
98 } else {
99 out.push(c);
100 }
101 }
102 Some(out)
103}
104
105pub fn ptr_mut<'a>(value: &'a mut Value, pointer: &str) -> Option<&'a mut Value> {
106 // empty pointer means the whole document
107 if pointer.is_empty() {
108 return Some(value);
109 }
110 // pointer must begin with "/" per RFC 6901
111 if !pointer.starts_with('/') {
112 return None;
113 }
114
115 let mut current = value;
116 // skip the leading empty segment before the first "/"
117 for raw_token in pointer.split('/').skip(1) {
118 // decode "~1" -> "/" and "~0" -> "~"
119 let token = unescape_token(raw_token)?;
120
121 match current {
122 Value::Object(map) => {
123 // step into object by key
124 current = map.get_mut(&token)?;
125 }
126 Value::Array(items) => {
127 // step into array by index
128 let idx: usize = token.parse().ok()?;
129 current = items.get_mut(idx)?;
130 }
131 _ => {
132 // cannot descend into primitives
133 return None;
134 }
135 }
136 }
137 Some(current)
138}
139
140#[cfg(test)]
141mod tests {
142 use super::ptr;
143 use serde_json::json;
144
145 #[test]
146 fn root_pointer() {
147 let v = json!(123);
148 assert_eq!(ptr(&v, "").and_then(|x| x.as_i64()), Some(123));
149 }
150
151 #[test]
152 fn object_path() {
153 let v = json!({"a":{"b":3}});
154 assert_eq!(ptr(&v, "/a/b").and_then(|x| x.as_i64()), Some(3));
155 assert!(ptr(&v, "/a/x").is_none());
156 }
157
158 #[test]
159 fn array_index() {
160 let v = json!({"items":[10,20,30]});
161 assert_eq!(ptr(&v, "/items/0").and_then(|x| x.as_i64()), Some(10));
162 assert_eq!(ptr(&v, "/items/2").and_then(|x| x.as_i64()), Some(30));
163 assert!(ptr(&v, "/items/3").is_none());
164 assert!(ptr(&v, "/items/-1").is_none());
165 }
166
167 #[test]
168 fn escapes() {
169 let v = json!({"a/b": 7, "x~y": 9});
170 assert_eq!(ptr(&v, "/a~1b").and_then(|x| x.as_i64()), Some(7));
171 assert_eq!(ptr(&v, "/x~0y").and_then(|x| x.as_i64()), Some(9));
172 }
173
174 #[test]
175 fn invalid_pointer() {
176 let v = json!({"a": 1});
177 assert!(ptr(&v, "a").is_none()); // missing leading slash
178 assert!(ptr(&v, "/~").is_none()); // bad escape
179 assert!(ptr(&v, "/a/0").is_none()); // descending into non-container
180 }
181}