jsonptr/
index.rs

1//! Abstract index representation for RFC 6901.
2//!
3//! [RFC 6901](https://datatracker.ietf.org/doc/html/rfc6901) defines two valid
4//! ways to represent array indices as Pointer tokens: non-negative integers,
5//! and the character `-`, which stands for the index after the last existing
6//! array member. While attempting to use `-` to resolve an array value will
7//! always be out of bounds, the token can be useful when paired with utilities
8//! which can mutate a value, such as this crate's [`assign`](crate::assign)
9//! functionality or JSON Patch [RFC
10//! 6902](https://datatracker.ietf.org/doc/html/rfc6902), as it provides a way
11//! to express where to put the new element when extending an array.
12//!
13//! While this crate doesn't implement RFC 6902, it still must consider
14//! non-numerical indices as valid, and provide a mechanism for manipulating
15//! them. This is what this module provides.
16//!
17//! The main use of the `Index` type is when resolving a [`Token`] instance as a
18//! concrete index for a given array length:
19//!
20//! ```
21//! # use jsonptr::{index::Index, Token};
22//! assert_eq!(Token::new("1").to_index(), Ok(Index::Num(1)));
23//! assert_eq!(Token::new("-").to_index(), Ok(Index::Next));
24//! assert!(Token::new("a").to_index().is_err());
25//!
26//! assert_eq!(Index::Num(0).for_len(1), Ok(0));
27//! assert!(Index::Num(1).for_len(1).is_err());
28//! assert!(Index::Next.for_len(1).is_err());
29//!
30//! assert_eq!(Index::Num(1).for_len_incl(1), Ok(1));
31//! assert_eq!(Index::Next.for_len_incl(1), Ok(1));
32//! assert!(Index::Num(2).for_len_incl(1).is_err());
33//!
34//! assert_eq!(Index::Num(42).for_len_unchecked(30), 42);
35//! assert_eq!(Index::Next.for_len_unchecked(30), 30);
36//! ```
37
38use crate::Token;
39use alloc::string::String;
40use core::{fmt, num::ParseIntError, str::FromStr};
41
42/// Represents an abstract index into an array.
43///
44/// If provided an upper bound with [`Self::for_len`] or [`Self::for_len_incl`],
45/// will produce a concrete numerical index.
46#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
47pub enum Index {
48    /// A non-negative integer value
49    Num(usize),
50    /// The `-` token, the position of the next would-be item in the array
51    Next,
52}
53
54impl Index {
55    /// Bounds the index for a given array length (exclusive).
56    ///
57    /// The upper range is exclusive, so only indices that are less than
58    /// the given length will be accepted as valid. This ensures that
59    /// the resolved numerical index can be used to access an existing array
60    /// element.
61    ///
62    /// [`Self::Next`], by consequence, is always considered *invalid*, since
63    /// it resolves to the array length itself.
64    ///
65    /// See also [`Self::for_len_incl`] for an alternative if you wish to accept
66    /// [`Self::Next`] (or its numerical equivalent) as valid.
67    ///
68    /// # Examples
69    ///
70    /// ```
71    /// # use jsonptr::index::Index;
72    /// assert_eq!(Index::Num(0).for_len(1), Ok(0));
73    /// assert!(Index::Num(1).for_len(1).is_err());
74    /// assert!(Index::Next.for_len(1).is_err());
75    /// ```
76    /// # Errors
77    /// Returns [`OutOfBoundsError`] if the index is out of bounds.
78    pub fn for_len(&self, length: usize) -> Result<usize, OutOfBoundsError> {
79        match *self {
80            Self::Num(index) if index < length => Ok(index),
81            Self::Num(index) => Err(OutOfBoundsError { length, index }),
82            Self::Next => Err(OutOfBoundsError {
83                length,
84                index: length,
85            }),
86        }
87    }
88
89    /// Bounds the index for a given array length (inclusive).
90    ///
91    /// The upper range is inclusive, so an index pointing to the position
92    /// _after_ the last element will be considered valid. Be careful when using
93    /// the resulting numerical index for accessing an array.
94    ///
95    /// [`Self::Next`] is always considered valid.
96    ///
97    /// See also [`Self::for_len`] for an alternative if you wish to ensure that
98    /// the resolved index can be used to access an existing array element.
99    ///
100    /// # Examples
101    ///
102    /// ```
103    /// # use jsonptr::index::Index;
104    /// assert_eq!(Index::Num(1).for_len_incl(1), Ok(1));
105    /// assert_eq!(Index::Next.for_len_incl(1), Ok(1));
106    /// assert!(Index::Num(2).for_len_incl(1).is_err());
107    /// ```
108    ///
109    /// # Errors
110    /// Returns [`OutOfBoundsError`] if the index is out of bounds.
111    pub fn for_len_incl(&self, length: usize) -> Result<usize, OutOfBoundsError> {
112        match *self {
113            Self::Num(index) if index <= length => Ok(index),
114            Self::Num(index) => Err(OutOfBoundsError { length, index }),
115            Self::Next => Ok(length),
116        }
117    }
118
119    /// Resolves the index for a given array length.
120    ///
121    /// No bound checking will take place. If you wish to ensure the
122    /// index can be used to access an existing element in the array, use
123    /// [`Self::for_len`] - or use [`Self::for_len_incl`] if you wish to accept
124    /// [`Self::Next`] as valid as well.
125    ///
126    /// # Examples
127    ///
128    /// ```
129    /// # use jsonptr::index::Index;
130    /// assert_eq!(Index::Num(42).for_len_unchecked(30), 42);
131    /// assert_eq!(Index::Next.for_len_unchecked(30), 30);
132    ///
133    /// // no bounds checks
134    /// assert_eq!(Index::Num(34).for_len_unchecked(40), 34);
135    /// assert_eq!(Index::Next.for_len_unchecked(34), 34);
136    /// ```
137    pub fn for_len_unchecked(&self, length: usize) -> usize {
138        match *self {
139            Self::Num(idx) => idx,
140            Self::Next => length,
141        }
142    }
143}
144
145impl fmt::Display for Index {
146    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
147        match *self {
148            Self::Num(index) => write!(f, "{index}"),
149            Self::Next => f.write_str("-"),
150        }
151    }
152}
153
154impl From<usize> for Index {
155    fn from(value: usize) -> Self {
156        Self::Num(value)
157    }
158}
159
160impl FromStr for Index {
161    type Err = ParseIndexError;
162
163    fn from_str(s: &str) -> Result<Self, Self::Err> {
164        if s == "-" {
165            Ok(Index::Next)
166        } else if s.starts_with('0') && s != "0" {
167            Err(ParseIndexError::LeadingZeros)
168        } else {
169            s.chars().position(|c| !c.is_ascii_digit()).map_or_else(
170                || {
171                    s.parse::<usize>()
172                        .map(Index::Num)
173                        .map_err(ParseIndexError::from)
174                },
175                |offset| {
176                    // this comes up with the `+` sign which is valid for
177                    // representing a `usize` but not allowed in RFC 6901 array
178                    // indices
179                    Err(ParseIndexError::InvalidCharacter(InvalidCharacterError {
180                        source: String::from(s),
181                        offset,
182                    }))
183                },
184            )
185        }
186    }
187}
188
189impl TryFrom<&Token<'_>> for Index {
190    type Error = ParseIndexError;
191
192    fn try_from(value: &Token) -> Result<Self, Self::Error> {
193        Index::from_str(value.encoded())
194    }
195}
196
197impl TryFrom<&str> for Index {
198    type Error = ParseIndexError;
199
200    fn try_from(value: &str) -> Result<Self, Self::Error> {
201        Index::from_str(value)
202    }
203}
204
205impl TryFrom<Token<'_>> for Index {
206    type Error = ParseIndexError;
207
208    fn try_from(value: Token) -> Result<Self, Self::Error> {
209        Index::from_str(value.encoded())
210    }
211}
212
213macro_rules! derive_try_from {
214    ($($t:ty),+ $(,)?) => {
215        $(
216            impl TryFrom<$t> for Index {
217                type Error = ParseIndexError;
218
219                fn try_from(value: $t) -> Result<Self, Self::Error> {
220                    Index::from_str(&value)
221                }
222            }
223        )*
224    }
225}
226
227derive_try_from!(String, &String);
228
229/*
230░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
231╔══════════════════════════════════════════════════════════════════════════════╗
232║                                                                              ║
233║                               OutOfBoundsError                               ║
234║                              ¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯                              ║
235╚══════════════════════════════════════════════════════════════════════════════╝
236░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
237*/
238
239/// Indicates that an `Index` is not within the given bounds.
240#[derive(Debug, Clone, PartialEq, Eq)]
241pub struct OutOfBoundsError {
242    /// The provided array length.
243    ///
244    /// If the range is inclusive, the resolved numerical index will be strictly
245    /// less than this value, otherwise it could be equal to it.
246    pub length: usize,
247
248    /// The resolved numerical index.
249    ///
250    /// Note that [`Index::Next`] always resolves to the given array length,
251    /// so it is only valid when the range is inclusive.
252    pub index: usize,
253}
254
255impl fmt::Display for OutOfBoundsError {
256    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
257        write!(
258            f,
259            "index {} out of bounds (len: {})",
260            self.index, self.length
261        )
262    }
263}
264
265#[cfg(feature = "std")]
266impl std::error::Error for OutOfBoundsError {}
267
268/*
269░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
270╔══════════════════════════════════════════════════════════════════════════════╗
271║                                                                              ║
272║                               ParseIndexError                                ║
273║                              ¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯                               ║
274╚══════════════════════════════════════════════════════════════════════════════╝
275░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
276*/
277
278/// Indicates that the `Token` could not be parsed as valid RFC 6901 array index.
279#[derive(Debug, Clone, PartialEq, Eq)]
280pub enum ParseIndexError {
281    /// The Token does not represent a valid integer.
282    InvalidInteger(ParseIntError),
283    /// The Token contains leading zeros.
284    LeadingZeros,
285    /// The Token contains a non-digit character.
286    InvalidCharacter(InvalidCharacterError),
287}
288
289impl From<ParseIntError> for ParseIndexError {
290    fn from(source: ParseIntError) -> Self {
291        Self::InvalidInteger(source)
292    }
293}
294
295impl fmt::Display for ParseIndexError {
296    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
297        match self {
298            ParseIndexError::InvalidInteger(_) => {
299                write!(f, "failed to parse token as an integer")
300            }
301            ParseIndexError::LeadingZeros => write!(
302                f,
303                "token contained leading zeros, which are disallowed by RFC 6901"
304            ),
305            ParseIndexError::InvalidCharacter(_) => {
306                write!(f, "failed to parse token as an index")
307            }
308        }
309    }
310}
311
312#[cfg(feature = "std")]
313impl std::error::Error for ParseIndexError {
314    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
315        match self {
316            ParseIndexError::InvalidInteger(source) => Some(source),
317            ParseIndexError::InvalidCharacter(source) => Some(source),
318            ParseIndexError::LeadingZeros => None,
319        }
320    }
321}
322
323/// Indicates that a non-digit character was found when parsing the RFC 6901 array index.
324#[derive(Debug, Clone, PartialEq, Eq)]
325pub struct InvalidCharacterError {
326    pub(crate) source: String,
327    pub(crate) offset: usize,
328}
329
330impl InvalidCharacterError {
331    /// Returns the offset of the character in the string.
332    ///
333    /// This offset is given in characters, not in bytes.
334    pub fn offset(&self) -> usize {
335        self.offset
336    }
337
338    /// Returns the source string.
339    pub fn source(&self) -> &str {
340        &self.source
341    }
342
343    /// Returns the offending character.
344    #[allow(clippy::missing_panics_doc)]
345    pub fn char(&self) -> char {
346        self.source
347            .chars()
348            .nth(self.offset)
349            .expect("char was found at offset")
350    }
351}
352
353impl fmt::Display for InvalidCharacterError {
354    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
355        write!(
356            f,
357            "token contains the non-digit character '{}', \
358                which is disallowed by RFC 6901",
359            self.char()
360        )
361    }
362}
363
364#[cfg(feature = "std")]
365impl std::error::Error for InvalidCharacterError {}
366
367/*
368░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
369╔══════════════════════════════════════════════════════════════════════════════╗
370║                                                                              ║
371║                                    Tests                                     ║
372║                                   ¯¯¯¯¯¯¯                                    ║
373╚══════════════════════════════════════════════════════════════════════════════╝
374░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
375*/
376
377#[cfg(test)]
378mod tests {
379    use super::*;
380    use crate::Token;
381
382    #[test]
383    fn index_from_usize() {
384        let index = Index::from(5usize);
385        assert_eq!(index, Index::Num(5));
386    }
387
388    #[test]
389    fn index_try_from_token_num() {
390        let token = Token::new("3");
391        let index = Index::try_from(&token).unwrap();
392        assert_eq!(index, Index::Num(3));
393    }
394
395    #[test]
396    fn index_try_from_token_next() {
397        let token = Token::new("-");
398        let index = Index::try_from(&token).unwrap();
399        assert_eq!(index, Index::Next);
400    }
401
402    #[test]
403    fn index_try_from_str_num() {
404        let index = Index::try_from("42").unwrap();
405        assert_eq!(index, Index::Num(42));
406    }
407
408    #[test]
409    fn index_try_from_str_next() {
410        let index = Index::try_from("-").unwrap();
411        assert_eq!(index, Index::Next);
412    }
413
414    #[test]
415    fn index_try_from_string_num() {
416        let index = Index::try_from(String::from("7")).unwrap();
417        assert_eq!(index, Index::Num(7));
418    }
419
420    #[test]
421    fn index_try_from_string_next() {
422        let index = Index::try_from(String::from("-")).unwrap();
423        assert_eq!(index, Index::Next);
424    }
425
426    #[test]
427    fn index_for_len_incl_valid() {
428        assert_eq!(Index::Num(0).for_len_incl(1), Ok(0));
429        assert_eq!(Index::Next.for_len_incl(2), Ok(2));
430    }
431
432    #[test]
433    fn index_for_len_incl_out_of_bounds() {
434        Index::Num(2).for_len_incl(1).unwrap_err();
435    }
436
437    #[test]
438    fn index_for_len_unchecked() {
439        assert_eq!(Index::Num(10).for_len_unchecked(5), 10);
440        assert_eq!(Index::Next.for_len_unchecked(3), 3);
441    }
442
443    #[test]
444    fn display_index_num() {
445        let index = Index::Num(5);
446        assert_eq!(index.to_string(), "5");
447    }
448
449    #[test]
450    fn display_index_next() {
451        assert_eq!(Index::Next.to_string(), "-");
452    }
453
454    #[test]
455    fn for_len() {
456        assert_eq!(Index::Num(0).for_len(1), Ok(0));
457        assert!(Index::Num(1).for_len(1).is_err());
458        assert!(Index::Next.for_len(1).is_err());
459    }
460
461    #[test]
462    fn try_from_token() {
463        let token = Token::new("3");
464        let index = <Index as TryFrom<Token>>::try_from(token).unwrap();
465        assert_eq!(index, Index::Num(3));
466        let token = Token::new("-");
467        let index = Index::try_from(&token).unwrap();
468        assert_eq!(index, Index::Next);
469    }
470}