Skip to content

introduce Index type for less error-prone index resolution #39

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Jun 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
114 changes: 54 additions & 60 deletions src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ use alloc::{
vec::Vec,
};
use core::{
// error::Error as StdError,
fmt::{Debug, Display, Formatter},
num::ParseIntError,
};
Expand All @@ -23,8 +22,6 @@ pub enum Error {
Unresolvable(UnresolvableError),
/// Indicates that a Pointer was not found in the data.
NotFound(NotFoundError),
// /// Indicates that a Pointer was malformed.
// MalformedPointer(MalformedPointerError),
}

impl Error {
Expand All @@ -40,33 +37,20 @@ impl Error {
pub fn is_not_found(&self) -> bool {
matches!(self, Error::NotFound(_))
}
// /// Returns `true` if the error is `Error::MalformedPointerError`.
// pub fn is_malformed_pointer(&self) -> bool {
// matches!(self, Error::MalformedPointer(_))
// }
}
// impl From<MalformedPointerError> for Error {
// fn from(err: MalformedPointerError) -> Self {
// Error::MalformedPointer(err)
// }
// }
}

impl From<IndexError> for Error {
fn from(err: IndexError) -> Self {
Error::Index(err)
}
}

impl From<NotFoundError> for Error {
fn from(err: NotFoundError) -> Self {
Error::NotFound(err)
}
}

impl From<OutOfBoundsError> for Error {
fn from(err: OutOfBoundsError) -> Self {
Error::Index(IndexError::from(err))
}
}

impl From<UnresolvableError> for Error {
fn from(err: UnresolvableError) -> Self {
Error::Unresolvable(err)
Expand All @@ -79,7 +63,6 @@ impl Display for Error {
Error::Index(err) => Display::fmt(err, f),
Error::Unresolvable(err) => Display::fmt(err, f),
Error::NotFound(err) => Display::fmt(err, f),
// Error::MalformedPointer(err) => Display::fmt(err, f),
}
}
}
Expand Down Expand Up @@ -137,70 +120,76 @@ impl Display for UnresolvableError {
}
}

/// Indicates an error occurred while parsing a `usize` (`ParseError`) or the
/// parsed value was out of bounds for the targeted array.
#[derive(PartialEq, Eq, Clone)]
pub enum IndexError {
/// Represents an that an error occurred when parsing an index.
Parse(ParseError),
/// Indicates that the Pointer contains an index of an array that is out of
/// bounds.
OutOfBounds(OutOfBoundsError),
/// Indicates that the `Token` could not be parsed as valid RFC 6901 index.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ParseIndexError {
source: ParseIntError,
}
impl Display for IndexError {
fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result {
match self {
IndexError::Parse(err) => Display::fmt(&err, f),
IndexError::OutOfBounds(err) => Display::fmt(&err, f),
}

impl ParseIndexError {
pub(crate) fn new(source: ParseIntError) -> Self {
Self { source }
}
}
impl Debug for IndexError {

impl Display for ParseIndexError {
fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result {
Display::fmt(self, f)
write!(f, "{}", self.source)
}
}
#[cfg(feature = "std")]
impl std::error::Error for IndexError {}

impl From<OutOfBoundsError> for IndexError {
fn from(err: OutOfBoundsError) -> Self {
IndexError::OutOfBounds(err)
#[cfg(feature = "std")]
impl std::error::Error for ParseIndexError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
Some(&self.source)
}
}

/// ParseError represents an that an error occurred when parsing an index.
/// Indicates an issue while accessing an array item by index.
#[derive(Debug, PartialEq, Eq, Clone)]
pub struct ParseError {
source: ParseIntError,
pub enum IndexError {
/// Represents an error while parsing the index from a token.
Parse(ParseIndexError),
/// Indicates that the resolved index was out of bounds.
OutOfBounds(OutOfBoundsError),
}

impl ParseError {
pub(crate) fn new(source: ParseIntError) -> Self {
Self { source }
impl Display for IndexError {
fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result {
match self {
IndexError::Parse(err) => Display::fmt(err, f),
IndexError::OutOfBounds(err) => Display::fmt(err, f),
}
}
}

#[cfg(feature = "std")]
impl std::error::Error for ParseError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
Some(&self.source)
impl std::error::Error for IndexError {}

impl From<OutOfBoundsError> for Error {
fn from(err: OutOfBoundsError) -> Self {
Error::Index(IndexError::OutOfBounds(err))
}
}

impl Display for ParseError {
fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result {
write!(f, "{}", self.source)
impl From<ParseIndexError> for Error {
fn from(value: ParseIndexError) -> Self {
Error::Index(IndexError::Parse(value))
}
}

/// Indicates that the `Pointer` contains an index of an array that is out of
/// bounds.
/// Indicates that an `Index` is not within the given bounds.
#[derive(Debug, PartialEq, Eq, Clone)]
pub struct OutOfBoundsError {
/// The length of the array.
pub len: usize,
/// The index of the array that was out of bounds.
/// The provided array length.
///
/// If the range is inclusive, the resolved numerical index will be strictly
/// less than this value, otherwise it could be equal to it.
pub length: usize,
/// The resolved numerical index.
///
/// Note that [`Index::Next`] always resolves to the given array length,
/// so it is only valid when the range is inclusive.
pub index: usize,
}

Expand All @@ -209,7 +198,11 @@ impl std::error::Error for OutOfBoundsError {}

impl Display for OutOfBoundsError {
fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result {
write!(f, "index {} out of bounds", self.index)
write!(
f,
"index {} out of bounds (limit: {})",
self.index, self.length
)
}
}

Expand Down Expand Up @@ -256,6 +249,7 @@ impl From<FromUtf8Error> for MalformedPointerError {
})
}
}

impl Display for MalformedPointerError {
fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result {
match self {
Expand Down
194 changes: 194 additions & 0 deletions src/index.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
//! Abstract index representation for RFC 6901.
//!
//! [RFC 6901](https://datatracker.ietf.org/doc/html/rfc6901) defines two valid
//! ways to represent array indices as Pointer tokens: non-negative integers, and
//! the character `-`, which stands for the index after the last existing array
//! member. While attempting to use `-` to access an array is an error, the token
//! can be useful when paired with [RFC 6902](https://datatracker.ietf.org/doc/
//! html/rfc6902) as a way to express where to put the new element when extending
//! an array.
//!
//! While this crate doesn't implement RFC 6902, it still must consider
//! non-numerical indices as valid, and provide a mechanism for manipulating them.
//! This is what this module provides.
//!
//! The main use of the `Index` type is when resolving a [`Token`] instance as a
//! concrete index for a given array length:
//!
//! ```
//! # use jsonptr::{Index, Token};
//! assert_eq!(Token::new("1").to_index(), Ok(Index::Num(1)));
//! assert_eq!(Token::new("-").to_index(), Ok(Index::Next));
//! assert!(Token::new("a").to_index().is_err());
//!
//! assert_eq!(Index::Num(0).inside(1), Ok(0));
//! assert!(Index::Num(1).inside(1).is_err());
//! assert!(Index::Next.inside(1).is_err());
//!
//! assert_eq!(Index::Num(1).inside_incl(1), Ok(1));
//! assert_eq!(Index::Next.inside_incl(1), Ok(1));
//! assert!(Index::Num(2).inside_incl(1).is_err());
//!
//! assert_eq!(Index::Num(42).for_len(30), 42);
//! assert_eq!(Index::Next.for_len(30), 30);
//! ````

use crate::{OutOfBoundsError, ParseIndexError, Token};
use core::fmt::Display;

/// Represents an abstract index into an array.
///
/// If provided an upper bound with [`Self::inside`] or [`Self::inside_incl`],
/// will produce a concrete numerical index.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum Index {
/// A non-negative integer value
Num(usize),
/// The `-` token, the position of the next would-be item in the array
Next,
}

impl Index {
/// Bounds the index for a given array length (exclusive).
///
/// The upper range is exclusive, so only indices that are less than
/// the given length will be accepted as valid. This ensures that
/// the resolved numerical index can be used to access an existing array
/// element.
///
/// [`Self::Next`], by consequence, is always considered *invalid*, since
/// it resolves to the array length itself.
///
/// See also [`Self::inside_incl`] for an alternative if you wish to accept
/// [`Self::Next`] (or its numerical equivalent) as valid.
///
/// # Examples
///
/// ```
/// # use jsonptr::Index;
/// assert_eq!(Index::Num(0).inside(1), Ok(0));
/// assert!(Index::Num(1).inside(1).is_err());
/// assert!(Index::Next.inside(1).is_err());
/// ```
pub fn inside(&self, length: usize) -> Result<usize, OutOfBoundsError> {
match *self {
Self::Num(index) if index < length => Ok(index),
Self::Num(index) => Err(OutOfBoundsError { length, index }),
Self::Next => Err(OutOfBoundsError {
length,
index: length,
}),
}
}

/// Bounds the index for a given array length (inclusive).
///
/// The upper range is inclusive, so an index pointing to the position
/// _after_ the last element will be considered valid. Be careful when using
/// the resulting numerical index for accessing an array.
///
/// [`Self::Next`] is always considered valid.
///
/// See also [`Self::inside`] for an alternative if you wish to ensure that
/// the resolved index can be used to access an existing array element.
///
/// # Examples
///
/// ```
/// # use jsonptr::Index;
/// assert_eq!(Index::Num(1).inside_incl(1), Ok(1));
/// assert_eq!(Index::Next.inside_incl(1), Ok(1));
/// assert!(Index::Num(2).inside_incl(1).is_err());
/// ```
pub fn inside_incl(&self, length: usize) -> Result<usize, OutOfBoundsError> {
match *self {
Self::Num(index) if index <= length => Ok(index),
Self::Num(index) => Err(OutOfBoundsError { length, index }),
Self::Next => Ok(length),
}
}

/// Resolves the index for a given array length.
///
/// No bound checking will take place. If you wish to ensure the index can
/// be used to access an existing element in the array, use [`Self::inside`]
/// - or use [`Self::inside_incl`] if you wish to accept [`Self::Next`] as
/// valid as well.
///
/// # Examples
///
/// ```
/// # use jsonptr::Index;
/// assert_eq!(Index::Num(42).for_len(30), 42);
/// assert_eq!(Index::Next.for_len(30), 30);
/// ````
pub fn for_len(&self, length: usize) -> usize {
match *self {
Self::Num(idx) => idx,
Self::Next => length,
}
}
}

impl Display for Index {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match *self {
Self::Num(index) => write!(f, "{index}"),
Self::Next => f.write_str("-"),
}
}
}

impl From<usize> for Index {
fn from(value: usize) -> Self {
Self::Num(value)
}
}

impl TryFrom<&Token<'_>> for Index {
type Error = ParseIndexError;

fn try_from(value: &Token) -> Result<Self, Self::Error> {
// we don't need to decode because it's a single char
if value.encoded() == "-" {
Ok(Index::Next)
} else {
value
.decoded()
.parse::<usize>()
.map(Index::Num)
.map_err(ParseIndexError::new)
}
}
}

impl TryFrom<&str> for Index {
type Error = ParseIndexError;

fn try_from(value: &str) -> Result<Self, Self::Error> {
if value == "-" {
Ok(Index::Next)
} else {
value
.parse::<usize>()
.map(Index::Num)
.map_err(ParseIndexError::new)
}
}
}

macro_rules! derive_try_from {
($($t:ty),+ $(,)?) => {
$(
impl TryFrom<$t> for Index {
type Error = ParseIndexError;

fn try_from(value: $t) -> Result<Self, Self::Error> {
value.try_into()
}
}
)*
}
}

derive_try_from!(Token<'_>, String, &String);
Loading
Loading