From b42348985b3f072f93e5c0f0f2a7db2f393c6671 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Mello?= <3285133+asmello@users.noreply.github.com> Date: Mon, 21 Oct 2024 02:43:46 +0100 Subject: [PATCH] Slicing API for `Pointer` (#84) * implement slicing api * update changelog * add note about complexity of indexing tokens --- CHANGELOG.md | 2 + src/pointer.rs | 27 ++- src/pointer/slice.rs | 500 +++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 523 insertions(+), 6 deletions(-) create mode 100644 src/pointer/slice.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index f447fe3..18e83a7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Changed signature of `PathBuf::parse` to avoid requiring allocation. - Bumps minimum Rust version to 1.79. +- `Pointer::get` now accepts ranges and can produce `Pointer` segments as output (similar to + `slice::get`). ### Fixed diff --git a/src/pointer.rs b/src/pointer.rs index ad5cff5..8b1f0f7 100644 --- a/src/pointer.rs +++ b/src/pointer.rs @@ -7,6 +7,9 @@ use alloc::{ vec::Vec, }; use core::{borrow::Borrow, cmp::Ordering, ops::Deref, str::FromStr}; +use slice::SlicePointer; + +mod slice; /* ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ @@ -277,23 +280,35 @@ impl Pointer { .map(|s| unsafe { Self::new_unchecked(s) }) } - /// Attempts to get a `Token` by the index. Returns `None` if the index is - /// out of bounds. + /// Attempts to get a `Token` or a segment of the `Pointer`, depending on + /// the type of index. + /// + /// Returns `None` if the index is out of bounds. + /// + /// Note that this operation is O(n). /// /// ## Example /// ```rust /// use jsonptr::{Pointer, Token}; /// - /// let ptr = Pointer::from_static("/foo/bar"); + /// let ptr = Pointer::from_static("/foo/bar/qux"); /// assert_eq!(ptr.get(0), Some("foo".into())); /// assert_eq!(ptr.get(1), Some("bar".into())); - /// assert_eq!(ptr.get(2), None); + /// assert_eq!(ptr.get(3), None); + /// assert_eq!(ptr.get(..), Some(Pointer::from_static("/foo/bar/qux"))); + /// assert_eq!(ptr.get(..1), Some(Pointer::from_static("/foo"))); + /// assert_eq!(ptr.get(1..3), Some(Pointer::from_static("/bar/qux"))); + /// assert_eq!(ptr.get(1..=2), Some(Pointer::from_static("/bar/qux"))); /// /// let ptr = Pointer::root(); /// assert_eq!(ptr.get(0), None); + /// assert_eq!(ptr.get(..), Some(Pointer::root())); /// ``` - pub fn get(&self, index: usize) -> Option { - self.tokens().nth(index).clone() + pub fn get<'p, I>(&'p self, index: I) -> Option + where + I: SlicePointer<'p>, + { + index.get(self) } /// Attempts to resolve a [`R::Value`] based on the path in this [`Pointer`]. diff --git a/src/pointer/slice.rs b/src/pointer/slice.rs new file mode 100644 index 0000000..26f4dd6 --- /dev/null +++ b/src/pointer/slice.rs @@ -0,0 +1,500 @@ +use super::Pointer; +use crate::Token; +use core::ops::Bound; + +pub trait SlicePointer<'p>: private::Sealed { + type Output: 'p; + + fn get(self, pointer: &'p Pointer) -> Option; +} + +impl<'p> SlicePointer<'p> for usize { + type Output = Token<'p>; + + fn get(self, pointer: &'p Pointer) -> Option { + pointer.tokens().nth(self) + } +} + +impl<'p> SlicePointer<'p> for core::ops::Range { + type Output = &'p Pointer; + + fn get(self, pointer: &'p Pointer) -> Option { + if self.end < self.start { + // never valid + return None; + } + + let mut idx = 0; + let mut offset = 0; + let mut start_offset = None; + let mut end_offset = None; + + for token in pointer.tokens() { + if idx == self.start { + start_offset = Some(offset); + } + if idx == self.end { + end_offset = Some(offset); + break; + } + idx += 1; + // also include the `/` separator + offset += token.encoded().len() + 1; + } + + // edge case where end is last token index + 1 + // this is valid because range is exclusive + if idx == self.end { + end_offset = Some(offset); + } + + let slice = &pointer.0.as_bytes()[start_offset?..end_offset?]; + // SAFETY: start and end offsets are token boundaries, so the slice is + // valid utf-8 (and also a valid json pointer!) + Some(unsafe { Pointer::new_unchecked(core::str::from_utf8_unchecked(slice)) }) + } +} + +impl<'p> SlicePointer<'p> for core::ops::RangeFrom { + type Output = &'p Pointer; + + fn get(self, pointer: &'p Pointer) -> Option { + { + let mut offset = 0; + let mut start_offset = None; + + for (idx, token) in pointer.tokens().enumerate() { + if idx == self.start { + start_offset = Some(offset); + break; + } + // also include the `/` separator + offset += token.encoded().len() + 1; + } + + let slice = &pointer.0.as_bytes()[start_offset?..]; + // SAFETY: start offset is token boundary, so the slice is valid + // utf-8 (and also a valid json pointer!) + Some(unsafe { Pointer::new_unchecked(core::str::from_utf8_unchecked(slice)) }) + } + } +} + +impl<'p> SlicePointer<'p> for core::ops::RangeTo { + type Output = &'p Pointer; + + fn get(self, pointer: &'p Pointer) -> Option { + { + let mut idx = 0; + let mut offset = 0; + let mut end_offset = None; + + for token in pointer.tokens() { + if idx == self.end { + end_offset = Some(offset); + break; + } + idx += 1; + // also include the `/` separator + offset += token.encoded().len() + 1; + } + + // edge case where end is last token index + 1 + // this is valid because range is exclusive + if idx == self.end { + end_offset = Some(offset); + } + + let slice = &pointer.0.as_bytes()[..end_offset?]; + // SAFETY: start and end offsets are token boundaries, so the slice is + // valid utf-8 (and also a valid json pointer!) + Some(unsafe { Pointer::new_unchecked(core::str::from_utf8_unchecked(slice)) }) + } + } +} + +impl<'p> SlicePointer<'p> for core::ops::RangeFull { + type Output = &'p Pointer; + + fn get(self, pointer: &'p Pointer) -> Option { + Some(pointer) + } +} + +impl<'p> SlicePointer<'p> for core::ops::RangeInclusive { + type Output = &'p Pointer; + + fn get(self, pointer: &'p Pointer) -> Option { + let (start, end) = self.into_inner(); + if end < start { + // never valid + return None; + } + + let mut offset = 0; + let mut start_offset = None; + let mut end_offset = None; + + for (idx, token) in pointer.tokens().enumerate() { + if idx == start { + start_offset = Some(offset); + } + // also include the `/` separator + offset += token.encoded().len() + 1; + // since the range is inclusive, we wish to slice up until the end + // of the token whose index is `end`, so we increment offset first + // before checking for a match + if idx == end { + end_offset = Some(offset); + break; + } + } + + // notice that we don't use an inclusive range here, because we already + // acounted for the included end token when computing `end_offset` above + let slice = &pointer.0.as_bytes()[start_offset?..end_offset?]; + // SAFETY: start and end offsets are token boundaries, so the slice is + // valid utf-8 (and also a valid json pointer!) + Some(unsafe { Pointer::new_unchecked(core::str::from_utf8_unchecked(slice)) }) + } +} + +impl<'p> SlicePointer<'p> for core::ops::RangeToInclusive { + type Output = &'p Pointer; + + fn get(self, pointer: &'p Pointer) -> Option { + { + let mut offset = 0; + let mut end_offset = None; + + for (idx, token) in pointer.tokens().enumerate() { + // also include the `/` separator + offset += token.encoded().len() + 1; + // since the range is inclusive, we wish to slice up until the end + // of the token whose index is `end`, so we increment offset first + // before checking for a match + if idx == self.end { + end_offset = Some(offset); + break; + } + } + + // notice that we don't use an inclusive range here, because we already + // acounted for the included end token when computing `end_offset` above + let slice = &pointer.0.as_bytes()[..end_offset?]; + // SAFETY: start and end offsets are token boundaries, so the slice is + // valid utf-8 (and also a valid json pointer!) + Some(unsafe { Pointer::new_unchecked(core::str::from_utf8_unchecked(slice)) }) + } + } +} + +impl<'p> SlicePointer<'p> for (Bound, Bound) { + type Output = &'p Pointer; + + fn get(self, pointer: &'p Pointer) -> Option { + match self { + (Bound::Included(start), Bound::Included(end)) => pointer.get(start..=end), + (Bound::Included(start), Bound::Excluded(end)) => pointer.get(start..end), + (Bound::Included(start), Bound::Unbounded) => pointer.get(start..), + (Bound::Excluded(start), Bound::Included(end)) => pointer.get(start + 1..=end), + (Bound::Excluded(start), Bound::Excluded(end)) => pointer.get(start + 1..end), + (Bound::Excluded(start), Bound::Unbounded) => pointer.get(start + 1..), + (Bound::Unbounded, Bound::Included(end)) => pointer.get(..=end), + (Bound::Unbounded, Bound::Excluded(end)) => pointer.get(..end), + (Bound::Unbounded, Bound::Unbounded) => pointer.get(..), + } + } +} + +mod private { + use core::ops; + + pub trait Sealed {} + impl Sealed for usize {} + impl Sealed for ops::Range {} + impl Sealed for ops::RangeTo {} + impl Sealed for ops::RangeFrom {} + impl Sealed for ops::RangeFull {} + impl Sealed for ops::RangeInclusive {} + impl Sealed for ops::RangeToInclusive {} + impl Sealed for (ops::Bound, ops::Bound) {} +} + +#[cfg(test)] +mod tests { + use core::ops::Bound; + + use crate::{Pointer, Token}; + + #[test] + fn get_single() { + let ptr = Pointer::from_static("/foo/bar/qux"); + let s = ptr.get(0); + assert_eq!(s, Some(Token::new("foo"))); + let s = ptr.get(1); + assert_eq!(s, Some(Token::new("bar"))); + let s = ptr.get(2); + assert_eq!(s, Some(Token::new("qux"))); + let s = ptr.get(3); + assert_eq!(s, None); + + let ptr = Pointer::from_static("/"); + let s = ptr.get(0); + assert_eq!(s, Some(Token::new(""))); + let s = ptr.get(1); + assert_eq!(s, None); + + let ptr = Pointer::from_static(""); + let s = ptr.get(0); + assert_eq!(s, None); + let s = ptr.get(1); + assert_eq!(s, None); + } + + #[allow(clippy::reversed_empty_ranges)] + #[test] + fn get_range() { + let ptr = Pointer::from_static("/foo/bar/qux"); + let s = ptr.get(0..3); + assert_eq!(s, Some(ptr)); + let s = ptr.get(0..2); + assert_eq!(s, Some(Pointer::from_static("/foo/bar"))); + let s = ptr.get(0..1); + assert_eq!(s, Some(Pointer::from_static("/foo"))); + let s = ptr.get(0..0); + assert_eq!(s, Some(Pointer::from_static(""))); + let s = ptr.get(1..3); + assert_eq!(s, Some(Pointer::from_static("/bar/qux"))); + let s = ptr.get(1..2); + assert_eq!(s, Some(Pointer::from_static("/bar"))); + let s = ptr.get(1..1); + assert_eq!(s, Some(Pointer::from_static(""))); + let s = ptr.get(1..0); + assert_eq!(s, None); + let s = ptr.get(0..4); + assert_eq!(s, None); + let s = ptr.get(2..4); + assert_eq!(s, None); + + let ptr = Pointer::from_static("/"); + let s = ptr.get(0..1); + assert_eq!(s, Some(ptr)); + let s = ptr.get(0..0); + assert_eq!(s, Some(Pointer::root())); + let s = ptr.get(1..0); + assert_eq!(s, None); + let s = ptr.get(0..2); + assert_eq!(s, None); + let s = ptr.get(1..2); + assert_eq!(s, None); + let s = ptr.get(1..1); + assert_eq!(s, None); + + let ptr = Pointer::root(); + let s = ptr.get(0..1); + assert_eq!(s, None); + let s = ptr.get(0..0); + assert_eq!(s, None); + let s = ptr.get(1..0); + assert_eq!(s, None); + let s = ptr.get(1..1); + assert_eq!(s, None); + } + + #[test] + fn get_from_range() { + let ptr = Pointer::from_static("/foo/bar/qux"); + let s = ptr.get(0..); + assert_eq!(s, Some(ptr)); + let s = ptr.get(1..); + assert_eq!(s, Some(Pointer::from_static("/bar/qux"))); + let s = ptr.get(2..); + assert_eq!(s, Some(Pointer::from_static("/qux"))); + let s = ptr.get(3..); + assert_eq!(s, None); + + let ptr = Pointer::from_static("/"); + let s = ptr.get(0..); + assert_eq!(s, Some(ptr)); + let s = ptr.get(1..); + assert_eq!(s, None); + + let ptr = Pointer::from_static(""); + let s = ptr.get(0..); + assert_eq!(s, None); + } + + #[test] + fn get_to_range() { + let ptr = Pointer::from_static("/foo/bar/qux"); + let s = ptr.get(..4); + assert_eq!(s, None); + let s = ptr.get(..3); + assert_eq!(s, Some(ptr)); + let s = ptr.get(..2); + assert_eq!(s, Some(Pointer::from_static("/foo/bar"))); + let s = ptr.get(..1); + assert_eq!(s, Some(Pointer::from_static("/foo"))); + let s = ptr.get(..0); + assert_eq!(s, Some(Pointer::from_static(""))); + + let ptr = Pointer::from_static("/"); + let s = ptr.get(..0); + assert_eq!(s, Some(Pointer::from_static(""))); + let s = ptr.get(..1); + assert_eq!(s, Some(ptr)); + let s = ptr.get(..2); + assert_eq!(s, None); + + let ptr = Pointer::from_static(""); + let s = ptr.get(..0); + assert_eq!(s, Some(ptr)); + let s = ptr.get(..1); + assert_eq!(s, None); + } + + #[test] + fn get_full_range() { + let ptr = Pointer::from_static("/foo/bar"); + let s = ptr.get(..); + assert_eq!(s, Some(ptr)); + + let ptr = Pointer::from_static("/"); + let s = ptr.get(..); + assert_eq!(s, Some(ptr)); + + let ptr = Pointer::from_static(""); + let s = ptr.get(..); + assert_eq!(s, Some(ptr)); + } + + #[allow(clippy::reversed_empty_ranges)] + #[test] + fn get_range_inclusive() { + let ptr = Pointer::from_static("/foo/bar/qux"); + let s = ptr.get(0..=3); + assert_eq!(s, None); + let s = ptr.get(0..=2); + assert_eq!(s, Some(ptr)); + let s = ptr.get(0..=1); + assert_eq!(s, Some(Pointer::from_static("/foo/bar"))); + let s = ptr.get(0..=0); + assert_eq!(s, Some(Pointer::from_static("/foo"))); + let s = ptr.get(1..=3); + assert_eq!(s, None); + let s = ptr.get(1..=2); + assert_eq!(s, Some(Pointer::from_static("/bar/qux"))); + let s = ptr.get(1..=1); + assert_eq!(s, Some(Pointer::from_static("/bar"))); + let s = ptr.get(1..=0); + assert_eq!(s, None); + + let ptr = Pointer::from_static("/"); + let s = ptr.get(0..=0); + assert_eq!(s, Some(ptr)); + let s = ptr.get(1..=0); + assert_eq!(s, None); + let s = ptr.get(0..=1); + assert_eq!(s, None); + let s = ptr.get(1..=1); + assert_eq!(s, None); + + let ptr = Pointer::root(); + let s = ptr.get(0..=1); + assert_eq!(s, None); + let s = ptr.get(0..=0); + assert_eq!(s, None); + let s = ptr.get(1..=0); + assert_eq!(s, None); + let s = ptr.get(1..=1); + assert_eq!(s, None); + } + + #[test] + fn get_to_range_inclusive() { + let ptr = Pointer::from_static("/foo/bar/qux"); + let s = ptr.get(..=3); + assert_eq!(s, None); + let s = ptr.get(..=2); + assert_eq!(s, Some(ptr)); + let s = ptr.get(..=1); + assert_eq!(s, Some(Pointer::from_static("/foo/bar"))); + let s = ptr.get(..=0); + assert_eq!(s, Some(Pointer::from_static("/foo"))); + + let ptr = Pointer::from_static("/"); + let s = ptr.get(..=0); + assert_eq!(s, Some(ptr)); + let s = ptr.get(..=1); + assert_eq!(s, None); + + let ptr = Pointer::from_static(""); + let s = ptr.get(..=0); + assert_eq!(s, None); + let s = ptr.get(..=1); + assert_eq!(s, None); + } + + #[test] + fn get_by_explicit_bounds() { + let ptr = Pointer::from_static("/foo/bar/qux"); + let s = ptr.get((Bound::Excluded(0), Bound::Included(2))); + assert_eq!(s, Some(Pointer::from_static("/bar/qux"))); + let s = ptr.get((Bound::Excluded(0), Bound::Excluded(2))); + assert_eq!(s, Some(Pointer::from_static("/bar"))); + let s = ptr.get((Bound::Excluded(0), Bound::Unbounded)); + assert_eq!(s, Some(Pointer::from_static("/bar/qux"))); + let s = ptr.get((Bound::Included(0), Bound::Included(2))); + assert_eq!(s, Some(Pointer::from_static("/foo/bar/qux"))); + let s = ptr.get((Bound::Included(0), Bound::Excluded(2))); + assert_eq!(s, Some(Pointer::from_static("/foo/bar"))); + let s = ptr.get((Bound::Included(0), Bound::Unbounded)); + assert_eq!(s, Some(Pointer::from_static("/foo/bar/qux"))); + let s = ptr.get((Bound::Unbounded, Bound::Included(2))); + assert_eq!(s, Some(Pointer::from_static("/foo/bar/qux"))); + let s = ptr.get((Bound::Unbounded, Bound::Excluded(2))); + assert_eq!(s, Some(Pointer::from_static("/foo/bar"))); + let s = ptr.get((Bound::Unbounded, Bound::Unbounded)); + assert_eq!(s, Some(Pointer::from_static("/foo/bar/qux"))); + + let ptr = Pointer::from_static("/foo/bar"); + let s = ptr.get((Bound::Excluded(0), Bound::Included(2))); + assert_eq!(s, None); + let s = ptr.get((Bound::Excluded(0), Bound::Excluded(2))); + assert_eq!(s, Some(Pointer::from_static("/bar"))); + let s = ptr.get((Bound::Excluded(0), Bound::Unbounded)); + assert_eq!(s, Some(Pointer::from_static("/bar"))); + let s = ptr.get((Bound::Included(0), Bound::Included(2))); + assert_eq!(s, None); + let s = ptr.get((Bound::Included(0), Bound::Excluded(2))); + assert_eq!(s, Some(ptr)); + let s = ptr.get((Bound::Included(0), Bound::Unbounded)); + assert_eq!(s, Some(ptr)); + let s = ptr.get((Bound::Unbounded, Bound::Included(2))); + assert_eq!(s, None); + let s = ptr.get((Bound::Unbounded, Bound::Excluded(2))); + assert_eq!(s, Some(ptr)); + let s = ptr.get((Bound::Unbounded, Bound::Unbounded)); + assert_eq!(s, Some(ptr)); + + // testing only the start excluded case a bit more exhaustively since + // other cases just delegate directly (so are covered by other tests) + let ptr = Pointer::from_static("/"); + let s = ptr.get((Bound::Excluded(0), Bound::Included(0))); + assert_eq!(s, None); + let s = ptr.get((Bound::Excluded(0), Bound::Excluded(0))); + assert_eq!(s, None); + let s = ptr.get((Bound::Excluded(0), Bound::Unbounded)); + assert_eq!(s, None); + + let ptr = Pointer::from_static(""); + let s = ptr.get((Bound::Excluded(0), Bound::Included(0))); + assert_eq!(s, None); + let s = ptr.get((Bound::Excluded(0), Bound::Excluded(0))); + assert_eq!(s, None); + let s = ptr.get((Bound::Excluded(0), Bound::Unbounded)); + assert_eq!(s, None); + } +}