Skip to main content

json_schema_rs/
json_pointer.rs

1//! JSON Pointer (RFC 6901): type and helpers for building and parsing pointer strings.
2//!
3//! A pointer is either the empty string (whole document) or a sequence of
4//! reference tokens separated by `/`. In each token, `~0` represents `~` and
5//! `~1` represents `/`. Segments are stored decoded; encoding is applied when
6//! producing the pointer string.
7
8use std::fmt;
9
10/// Error when parsing a string as a JSON Pointer (RFC 6901).
11#[derive(Debug, Clone, PartialEq, Eq)]
12pub enum JsonPointerError {
13    /// Invalid escape: `~` not followed by `0` or `1`.
14    InvalidEscape,
15    /// Input is not valid UTF-8.
16    InvalidUtf8,
17}
18
19impl fmt::Display for JsonPointerError {
20    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
21        match self {
22            JsonPointerError::InvalidEscape => {
23                write!(
24                    f,
25                    "invalid JSON Pointer escape: ~ must be followed by 0 or 1"
26                )
27            }
28            JsonPointerError::InvalidUtf8 => write!(f, "JSON Pointer is not valid UTF-8"),
29        }
30    }
31}
32
33impl std::error::Error for JsonPointerError {}
34
35/// Encode one segment for RFC 6901: `~` → `~0`, `/` → `~1`.
36fn encode_segment(segment: &str) -> String {
37    segment.replace('~', "~0").replace('/', "~1")
38}
39
40/// Decode one reference token: first `~1` → `/`, then `~0` → `~`.
41fn decode_token(token: &str) -> Result<String, JsonPointerError> {
42    let mut out = String::with_capacity(token.len());
43    let mut chars = token.chars().peekable();
44    while let Some(c) = chars.next() {
45        if c == '~' {
46            let next = chars.next().ok_or(JsonPointerError::InvalidEscape)?;
47            match next {
48                '0' => out.push('~'),
49                '1' => out.push('/'),
50                _ => return Err(JsonPointerError::InvalidEscape),
51            }
52        } else {
53            out.push(c);
54        }
55    }
56    Ok(out)
57}
58
59/// A JSON Pointer (RFC 6901): identifies a value within a JSON document.
60///
61/// Stored as decoded segments; the pointer string is produced by encoding
62/// when needed. Root has zero segments.
63#[derive(Debug, Clone, PartialEq, Eq)]
64pub struct JsonPointer {
65    /// Decoded reference tokens; empty = root.
66    segments: Vec<String>,
67    /// Cached RFC 6901 string ("" or "/seg1/seg2/...") for `as_str()` and Display.
68    encoded: String,
69}
70
71impl JsonPointer {
72    /// Root pointer (whole document). Equal to the empty string.
73    #[must_use]
74    pub fn root() -> Self {
75        Self {
76            segments: Vec::new(),
77            encoded: String::new(),
78        }
79    }
80
81    fn from_segments_and_encoded(segments: Vec<String>, encoded: String) -> Self {
82        Self { segments, encoded }
83    }
84
85    /// Returns a new pointer with one more segment. The segment is stored
86    /// decoded; encoding is applied when producing the pointer string.
87    #[must_use]
88    pub fn push(&self, segment: &str) -> Self {
89        let mut new_segments = self.segments.clone();
90        new_segments.push(segment.to_string());
91        let encoded = if new_segments.is_empty() {
92            String::new()
93        } else {
94            format!(
95                "/{}",
96                new_segments
97                    .iter()
98                    .map(String::as_str)
99                    .map(encode_segment)
100                    .collect::<Vec<_>>()
101                    .join("/")
102            )
103        };
104        Self {
105            segments: new_segments,
106            encoded,
107        }
108    }
109
110    /// Returns a new pointer with the last segment removed. Root unchanged.
111    #[must_use]
112    pub fn pop(&self) -> Self {
113        if self.segments.is_empty() {
114            return self.clone();
115        }
116        let mut segs = self.segments.clone();
117        segs.pop();
118        let enc = if segs.is_empty() {
119            String::new()
120        } else {
121            format!(
122                "/{}",
123                segs.iter()
124                    .map(String::as_str)
125                    .map(encode_segment)
126                    .collect::<Vec<_>>()
127                    .join("/")
128            )
129        };
130        Self {
131            segments: segs,
132            encoded: enc,
133        }
134    }
135
136    /// Returns the parent pointer (same as popping the last segment).
137    #[must_use]
138    pub fn parent(&self) -> Self {
139        self.pop()
140    }
141
142    /// Returns a new pointer with only the first `len` segments.
143    #[must_use]
144    pub fn truncate(&self, len: usize) -> Self {
145        if len >= self.segments.len() {
146            return self.clone();
147        }
148        let segs: Vec<String> = self.segments[..len].to_vec();
149        let enc = if segs.is_empty() {
150            String::new()
151        } else {
152            format!(
153                "/{}",
154                segs.iter()
155                    .map(String::as_str)
156                    .map(encode_segment)
157                    .collect::<Vec<_>>()
158                    .join("/")
159            )
160        };
161        Self {
162            segments: segs,
163            encoded: enc,
164        }
165    }
166
167    /// Number of segments (0 for root).
168    #[must_use]
169    pub fn len(&self) -> usize {
170        self.segments.len()
171    }
172
173    /// Returns true if this pointer has no segments (root).
174    #[must_use]
175    pub fn is_empty(&self) -> bool {
176        self.segments.is_empty()
177    }
178
179    /// Returns true if this is the root pointer.
180    #[must_use]
181    pub fn is_root(&self) -> bool {
182        self.segments.is_empty()
183    }
184
185    /// Returns an iterator over the decoded segments.
186    pub fn segments(&self) -> impl Iterator<Item = &str> {
187        self.segments.iter().map(String::as_str)
188    }
189
190    /// Returns the segment at `index`, or `None` if out of bounds.
191    #[must_use]
192    pub fn segment_at(&self, index: usize) -> Option<&str> {
193        self.segments.get(index).map(String::as_str)
194    }
195
196    /// Returns a new pointer with the segment at `index` removed.
197    #[must_use]
198    pub fn remove(&self, index: usize) -> Self {
199        if index >= self.segments.len() {
200            return self.clone();
201        }
202        let mut segs = self.segments.clone();
203        segs.remove(index);
204        let enc = if segs.is_empty() {
205            String::new()
206        } else {
207            format!(
208                "/{}",
209                segs.iter()
210                    .map(String::as_str)
211                    .map(encode_segment)
212                    .collect::<Vec<_>>()
213                    .join("/")
214            )
215        };
216        Self {
217            segments: segs,
218            encoded: enc,
219        }
220    }
221
222    /// Returns the pointer as a string ("" for root, "/a/b" for children).
223    #[must_use]
224    pub fn as_str(&self) -> &str {
225        self.encoded.as_str()
226    }
227
228    /// Returns a display-friendly location: "root" when empty, otherwise the pointer string.
229    #[must_use]
230    pub fn display_root_or_path(&self) -> &str {
231        if self.encoded.is_empty() {
232            "root"
233        } else {
234            self.encoded.as_str()
235        }
236    }
237}
238
239impl fmt::Display for JsonPointer {
240    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
241        write!(f, "{}", self.encoded)
242    }
243}
244
245/// Parse a non-empty pointer: must start with `/`; split by `/`, decode each token.
246fn try_parse(s: &str) -> Result<JsonPointer, JsonPointerError> {
247    if s.is_empty() {
248        return Ok(JsonPointer::root());
249    }
250    if !s.starts_with('/') {
251        return Err(JsonPointerError::InvalidEscape);
252    }
253    let parts: Vec<&str> = s.split('/').collect();
254    let mut segments = Vec::with_capacity(parts.len().saturating_sub(1));
255    for (i, part) in parts.iter().enumerate() {
256        if i == 0 {
257            continue;
258        }
259        segments.push(decode_token(part)?);
260    }
261    let encoded = s.to_string();
262    Ok(JsonPointer::from_segments_and_encoded(segments, encoded))
263}
264
265impl TryFrom<&str> for JsonPointer {
266    type Error = JsonPointerError;
267
268    fn try_from(s: &str) -> Result<Self, Self::Error> {
269        try_parse(s)
270    }
271}
272
273impl TryFrom<String> for JsonPointer {
274    type Error = JsonPointerError;
275
276    fn try_from(s: String) -> Result<Self, Self::Error> {
277        try_parse(&s)
278    }
279}
280
281impl TryFrom<&[u8]> for JsonPointer {
282    type Error = JsonPointerError;
283
284    fn try_from(bytes: &[u8]) -> Result<Self, Self::Error> {
285        let s = std::str::from_utf8(bytes).map_err(|_| JsonPointerError::InvalidUtf8)?;
286        try_parse(s)
287    }
288}
289
290impl TryFrom<Vec<u8>> for JsonPointer {
291    type Error = JsonPointerError;
292
293    fn try_from(bytes: Vec<u8>) -> Result<Self, Self::Error> {
294        let s = String::from_utf8(bytes).map_err(|_| JsonPointerError::InvalidUtf8)?;
295        try_parse(&s)
296    }
297}
298
299impl From<Vec<String>> for JsonPointer {
300    fn from(segments: Vec<String>) -> Self {
301        let encoded = if segments.is_empty() {
302            String::new()
303        } else {
304            format!(
305                "/{}",
306                segments
307                    .iter()
308                    .map(String::as_str)
309                    .map(encode_segment)
310                    .collect::<Vec<_>>()
311                    .join("/")
312            )
313        };
314        Self { segments, encoded }
315    }
316}
317
318impl From<JsonPointer> for String {
319    fn from(p: JsonPointer) -> Self {
320        p.encoded
321    }
322}
323
324#[cfg(test)]
325mod tests {
326    use super::{JsonPointer, JsonPointerError};
327    use std::convert::TryFrom;
328
329    #[test]
330    fn root_is_empty() {
331        let p: JsonPointer = JsonPointer::root();
332        let expected = "";
333        let actual = p.as_str();
334        assert_eq!(expected, actual);
335        assert_eq!(p.to_string(), "");
336        assert!(p.is_root());
337        assert_eq!(p.len(), 0);
338    }
339
340    #[test]
341    fn push_one_segment() {
342        let p: JsonPointer = JsonPointer::root().push("foo");
343        let expected = "/foo";
344        let actual = p.as_str();
345        assert_eq!(expected, actual);
346    }
347
348    #[test]
349    fn push_multiple_segments() {
350        let p: JsonPointer = JsonPointer::root().push("a").push("b").push("c");
351        let expected = "/a/b/c";
352        let actual = p.as_str();
353        assert_eq!(expected, actual);
354    }
355
356    #[test]
357    fn segment_with_slash_encoded_as_tilde1() {
358        let p: JsonPointer = JsonPointer::root().push("a/b");
359        let expected = "/a~1b";
360        let actual = p.as_str();
361        assert_eq!(expected, actual);
362    }
363
364    #[test]
365    fn segment_with_tilde_encoded_as_tilde0() {
366        let p: JsonPointer = JsonPointer::root().push("a~b");
367        let expected = "/a~0b";
368        let actual = p.as_str();
369        assert_eq!(expected, actual);
370    }
371
372    #[test]
373    fn segment_with_both_tilde_and_slash() {
374        let p: JsonPointer = JsonPointer::root().push("~1").push("a/b");
375        let expected = "/~01/a~1b";
376        let actual = p.as_str();
377        assert_eq!(expected, actual);
378    }
379
380    #[test]
381    fn display_root_or_path() {
382        let root: JsonPointer = JsonPointer::root();
383        let expected = "root";
384        let actual = root.display_root_or_path();
385        assert_eq!(expected, actual);
386        let child: JsonPointer = JsonPointer::root().push("x");
387        let expected_path = "/x";
388        let actual_path = child.display_root_or_path();
389        assert_eq!(expected_path, actual_path);
390    }
391
392    #[test]
393    fn pop_from_one_segment_gives_root() {
394        let p: JsonPointer = JsonPointer::root().push("a");
395        let expected = JsonPointer::root();
396        let actual = p.pop();
397        assert_eq!(expected.as_str(), actual.as_str());
398        assert_eq!(actual.len(), 0);
399    }
400
401    #[test]
402    fn pop_from_three_segments_gives_two() {
403        let p: JsonPointer = JsonPointer::root().push("a").push("b").push("c");
404        let expected = JsonPointer::root().push("a").push("b");
405        let actual = p.pop();
406        assert_eq!(expected.as_str(), actual.as_str());
407    }
408
409    #[test]
410    fn pop_from_root_leaves_root() {
411        let root: JsonPointer = JsonPointer::root();
412        let actual = root.pop();
413        assert_eq!(root.as_str(), actual.as_str());
414    }
415
416    #[test]
417    fn parent_matches_pop() {
418        let p: JsonPointer = JsonPointer::root().push("x").push("y");
419        let expected = p.pop();
420        let actual = p.parent();
421        assert_eq!(expected.as_str(), actual.as_str());
422    }
423
424    #[test]
425    fn truncate_to_zero_gives_root() {
426        let p: JsonPointer = JsonPointer::root().push("a").push("b");
427        let actual = p.truncate(0);
428        assert_eq!(JsonPointer::root().as_str(), actual.as_str());
429    }
430
431    #[test]
432    fn truncate_to_one() {
433        let p: JsonPointer = JsonPointer::root().push("a").push("b").push("c");
434        let expected = JsonPointer::root().push("a");
435        let actual = p.truncate(1);
436        assert_eq!(expected.as_str(), actual.as_str());
437    }
438
439    #[test]
440    fn truncate_to_current_len_unchanged() {
441        let p: JsonPointer = JsonPointer::root().push("a").push("b");
442        let actual = p.truncate(2);
443        assert_eq!(p.as_str(), actual.as_str());
444    }
445
446    #[test]
447    fn truncate_beyond_len_unchanged() {
448        let p: JsonPointer = JsonPointer::root().push("a");
449        let actual = p.truncate(10);
450        assert_eq!(p.as_str(), actual.as_str());
451    }
452
453    #[test]
454    fn segments_iterator() {
455        let p: JsonPointer = JsonPointer::root().push("a").push("b").push("c");
456        let expected: Vec<&str> = vec!["a", "b", "c"];
457        let actual: Vec<&str> = p.segments().collect();
458        assert_eq!(expected, actual);
459    }
460
461    #[test]
462    fn segment_at_valid() {
463        let p: JsonPointer = JsonPointer::root().push("x").push("y");
464        let expected_first = Some("x");
465        let actual_first = p.segment_at(0);
466        assert_eq!(expected_first, actual_first);
467        let expected_second = Some("y");
468        let actual_second = p.segment_at(1);
469        assert_eq!(expected_second, actual_second);
470    }
471
472    #[test]
473    fn segment_at_out_of_bounds() {
474        let p: JsonPointer = JsonPointer::root().push("a");
475        let expected: Option<&str> = None;
476        let actual = p.segment_at(1);
477        assert_eq!(expected, actual);
478    }
479
480    #[test]
481    fn remove_segment() {
482        let p: JsonPointer = JsonPointer::root().push("a").push("b").push("c");
483        let actual = p.remove(1);
484        let expected = JsonPointer::root().push("a").push("c");
485        assert_eq!(expected.as_str(), actual.as_str());
486    }
487
488    #[test]
489    fn try_from_empty_string() {
490        let expected = JsonPointer::root();
491        let actual = JsonPointer::try_from("").unwrap();
492        assert_eq!(expected.as_str(), actual.as_str());
493    }
494
495    #[test]
496    fn try_from_slash_a() {
497        let expected = JsonPointer::root().push("a");
498        let actual = JsonPointer::try_from("/a").unwrap();
499        assert_eq!(expected.as_str(), actual.as_str());
500    }
501
502    #[test]
503    fn try_from_slash_a_slash_b() {
504        let expected = JsonPointer::root().push("a").push("b");
505        let actual = JsonPointer::try_from("/a/b").unwrap();
506        assert_eq!(expected.as_str(), actual.as_str());
507    }
508
509    #[test]
510    fn try_from_encoded_slash() {
511        let expected = JsonPointer::root().push("a/b");
512        let actual = JsonPointer::try_from("/a~1b").unwrap();
513        assert_eq!(expected.as_str(), actual.as_str());
514    }
515
516    #[test]
517    fn try_from_encoded_tilde() {
518        let expected = JsonPointer::root().push("a~b");
519        let actual = JsonPointer::try_from("/a~0b").unwrap();
520        assert_eq!(expected.as_str(), actual.as_str());
521    }
522
523    #[test]
524    fn try_from_encoded_tilde1_segment() {
525        let expected = JsonPointer::root().push("~1");
526        let actual = JsonPointer::try_from("/~01").unwrap();
527        assert_eq!(expected.as_str(), actual.as_str());
528    }
529
530    #[test]
531    fn try_from_invalid_escape_tilde_only() {
532        let actual = JsonPointer::try_from("~");
533        assert!(matches!(actual, Err(JsonPointerError::InvalidEscape)));
534    }
535
536    #[test]
537    fn try_from_invalid_escape_tilde_two() {
538        let actual = JsonPointer::try_from("/a~2b");
539        assert!(matches!(actual, Err(JsonPointerError::InvalidEscape)));
540    }
541
542    #[test]
543    fn try_from_no_leading_slash_rejected() {
544        let actual = JsonPointer::try_from("a");
545        assert!(matches!(actual, Err(JsonPointerError::InvalidEscape)));
546    }
547
548    #[test]
549    fn from_vec_string_empty() {
550        let expected = JsonPointer::root();
551        let actual: JsonPointer = Vec::<String>::new().into();
552        assert_eq!(expected.as_str(), actual.as_str());
553    }
554
555    #[test]
556    fn from_vec_string_two() {
557        let expected = JsonPointer::root().push("a").push("b");
558        let actual: JsonPointer = vec!["a".to_string(), "b".to_string()].into();
559        assert_eq!(expected.as_str(), actual.as_str());
560    }
561
562    #[test]
563    fn try_from_bytes_valid_utf8() {
564        let expected = JsonPointer::root().push("x");
565        let actual = JsonPointer::try_from("/x".as_bytes()).unwrap();
566        assert_eq!(expected.as_str(), actual.as_str());
567    }
568
569    #[test]
570    fn try_from_bytes_invalid_utf8() {
571        let actual = JsonPointer::try_from(vec![0xff, 0xfe].as_slice());
572        assert!(matches!(actual, Err(JsonPointerError::InvalidUtf8)));
573    }
574
575    #[test]
576    fn into_string() {
577        let p: JsonPointer = JsonPointer::root().push("foo").push("bar");
578        let expected = "/foo/bar";
579        let actual: String = p.into();
580        assert_eq!(expected, actual);
581    }
582
583    #[test]
584    fn round_trip_build_serialize_parse() {
585        let expected = JsonPointer::root().push("a").push("b").push("c");
586        let s = expected.to_string();
587        let actual = JsonPointer::try_from(s.as_str()).unwrap();
588        assert_eq!(expected.as_str(), actual.as_str());
589    }
590
591    #[test]
592    fn round_trip_parse_serialize_parse() {
593        let s = "/a~1b/c~0d";
594        let p = JsonPointer::try_from(s).unwrap();
595        let s2 = p.to_string();
596        let p2 = JsonPointer::try_from(s2.as_str()).unwrap();
597        assert_eq!(p.as_str(), p2.as_str());
598    }
599
600    #[test]
601    fn empty_segment() {
602        let p: JsonPointer = JsonPointer::root().push("");
603        let expected = "/";
604        let actual = p.as_str();
605        assert_eq!(expected, actual);
606    }
607
608    #[test]
609    fn len_after_push() {
610        let root = JsonPointer::root();
611        assert_eq!(root.len(), 0);
612        let one = root.push("a");
613        assert_eq!(one.len(), 1);
614        let three = one.push("b").push("c");
615        assert_eq!(three.len(), 3);
616    }
617}