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}