diff --git a/.cargo/config.toml b/.cargo/config.toml new file mode 100644 index 0000000..6b77399 --- /dev/null +++ b/.cargo/config.toml @@ -0,0 +1,5 @@ +[alias] +cov = "llvm-cov --lcov --output-path lcov.info" + +[build] +rustdocflags = ["--cfg", "docsrs"] diff --git a/.github/codecov.yml b/.github/codecov.yml index cd5ce8f..657b95d 100644 --- a/.github/codecov.yml +++ b/.github/codecov.yml @@ -1,21 +1,23 @@ # ref: https://docs.codecov.com/docs/codecovyml-reference coverage: - # Hold ourselves to a high bar - range: 85..100 - round: down - precision: 1 - status: - # ref: https://docs.codecov.com/docs/commit-status - project: - default: - # Avoid false negatives - threshold: 1% + # Hold ourselves to a high bar + range: 85..100 + round: down + precision: 1 + status: + # ref: https://docs.codecov.com/docs/commit-status + project: + default: + # Avoid false negatives + threshold: 1% # Test files aren't important for coverage ignore: - - "tests" + - "tests" + - "arbitrary.rs" + - "src/arbitrary.rs" # Make comments less noisy comment: - layout: "files" - require_changes: true + layout: "files" + require_changes: true diff --git a/.gitignore b/.gitignore index a3d6b48..1ae19a2 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ /target # Cargo.lock +lcov.info diff --git a/CHANGELOG.md b/CHANGELOG.md index 50b57a6..114ac79 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,7 +20,7 @@ This is a breaking release including: - [Quickcheck](https://docs.rs/quickcheck/latest/quickcheck/index.html)-based testing - New methods: `Pointer::split_front`, `Pointer::split_back`, `Pointer::parent`, `Pointer::strip_suffix` - Implemented `Display` and `Debug` for `ParseError` -- Adds `Pointer::split_at` which utilizes character offsets to split a pointer at a seperator +- Adds `Pointer::split_at` which utilizes character offsets to split a pointer at a separator - Adds specific error types `ParseError`, `ResolveError`, `AssignError` ### Changed diff --git a/Cargo.toml b/Cargo.toml index 5efddef..14d4048 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -29,7 +29,6 @@ syn = { version = "1.0.109", optional = true } assign = [] default = ["std", "serde", "json", "resolve", "assign", "delete"] delete = ["resolve"] -impl = [] json = ["dep:serde_json", "serde"] resolve = [] std = ["serde/std", "serde_json?/std"] diff --git a/README.md b/README.md index 9cb2f52..c1951c3 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,8 @@ -# jsonptr - JSON Pointers for Rust +
+ +# jsonptr - JSON Pointers (RFC 6901) for Rust + +
[github](https://github.com/chanced/jsonptr) [crates.io](https://crates.io/crates/jsonptr) @@ -6,126 +10,155 @@ [build status](https://github.com/chanced/jsonptr/actions?query=branch%3Amain) [code coverage](https://codecov.io/gh/chanced/jsonptr) -Data structures and logic for resolving, assigning, and deleting by JSON Pointers ([RFC -6901](https://datatracker.ietf.org/doc/html/rfc6901)). +JSON Pointers ([RFC 6901](https://datatracker.ietf.org/doc/html/rfc6901)) +defines a string syntax for identifying a specific location within a JSON, or +similar, document. This crate provides two types, [`Pointer`] and [`PointerBuf`] +(akin to [`Path`] and [`PathBuf`]), for working with them abstractly. + +A pointer is composed of zero or more [`Token`]s, single segments which +represent a field of an object or an [`index`] of an array, and are bounded by +either `'/'` or the end of the string. Tokens are lightly encoded, where `'~'` +is escaped as `"~0"` due to it signaling encoding and `'/'` is escaped as `"~1"` +because `'/'` separates tokens and would split the token into two otherwise. + +[`Token`]s can be iterated over using either [`Tokens`], returned from the +[`tokens`] method of a pointer or [`Components`], returned from the +[`components`] method. The difference being that `Tokens` iterates over each +token in the pointer, while `Components` iterates over [`Component`]s, which can +represent the root of the document or a single token along with the offset of +the token from within the pointer. + +Operations [`resolve`], [`assign`] and [`delete`] are provided as traits with +corresponding methods on pointer types. Implementations of each trait are +provided for value types of the crates [`serde_json`] and [`toml`]. All +operations are enabled by default but are gated by [feature +flags](#feature-flags). ## Usage -JSON Pointers can be created either with a slice of strings or directly from a properly encoded string representing a JSON Pointer. - -### Resolve values - -#### `Pointer::resolve` +To parse a pointer from a string, use the [`parse`](Pointer::parse) method. +[`PointerBuf`] can use either the [`parse`](PointerBuf::parse) or +[`from_tokens`](PointerBuf::from_tokens) construct from an iterator of +[`Token`]s: ```rust -use jsonptr::Pointer; +use jsonptr::{Pointer, PointerBuf}; use serde_json::json; -let mut data = json!({ "foo": { "bar": "baz" } }); -let ptr = Pointer::from_static("/foo/bar"); -let bar = ptr.resolve(&data).unwrap(); -assert_eq!(bar, "baz"); -``` +let ptr = Pointer::parse("/examples/0/name").unwrap(); -#### `Resolve::resolve` +let buf = PointerBuf::from_tokens(["examples", "0", "name"]); +assert_eq!(ptr, &buf); -```rust -use jsonptr::{ Pointer, resolve::Resolve }; -use serde_json::json; - -let mut data = json!({ "foo": { "bar": "baz" } }); -let ptr = Pointer::from_static("/foo/bar"); -let bar = data.resolve(&ptr).unwrap(); -assert_eq!(bar, "baz"); - -``` - -#### `ResolveMut::resolve_mut` - -```rust -use jsonptr::{Pointer, resolve::ResolveMut}; -use serde_json::json; +let parent = ptr.parent().unwrap(); +assert_eq!(parent, Pointer::parse("/examples/0").unwrap()); -let ptr = Pointer::from_static("/foo/bar"); -let mut data = json!({ "foo": { "bar": "baz" }}); -let mut bar = data.resolve_mut(&ptr).unwrap(); -assert_eq!(bar, "baz"); +let (front, remaining) = ptr.split_front().unwrap(); +assert_eq!(front.decoded(), "examples"); +assert_eq!(remaining, Pointer::parse("/0/name").unwrap()); ``` -### Assign - -#### `Pointer::assign` +Values can be resolved by `Pointer`s using either [`Resolve`] or [`ResolveMut`] +traits. See the [`resolve`] mod for more information. ```rust use jsonptr::Pointer; use serde_json::json; -let ptr = Pointer::from_static("/foo/bar"); -let mut data = json!({}); -let _previous = ptr.assign(&mut data, "qux").unwrap(); -assert_eq!(data, json!({"foo": { "bar": "qux" }})) -``` - -#### `Assign::asign` - -```rust -use jsonptr::{assign::Assign, Pointer}; -use serde_json::json; - -let ptr = Pointer::from_static("/foo/bar"); -let mut data = json!({}); -let _previous = data.assign(&ptr, "qux").unwrap(); -assert_eq!(data, json!({ "foo": { "bar": "qux" }})) +let ptr = Pointer::parse("/foo/bar").unwrap(); +let data = json!({"foo": { "bar": 34 }}); +let bar = ptr.resolve(&data).unwrap(); +assert_eq!(bar, &json!(34)); ``` -### Delete - -#### `Pointer::delete` +Values can be assigned using the [`Assign`] trait. See [`assign`] for more +information. ```rust use jsonptr::Pointer; use serde_json::json; -let mut data = json!({ "foo": { "bar": { "baz": "qux" } } }); -let ptr = Pointer::from_static("/foo/bar/baz"); -assert_eq!(ptr.delete(&mut data), Some("qux".into())); -assert_eq!(data, json!({ "foo": { "bar": {} } })); - -// unresolved pointers return None -let mut data = json!({}); -assert_eq!(ptr.delete(&mut data), None); +let ptr = Pointer::parse("/secret/universe").unwrap(); +let mut data = json!({"secret": { "universe": 42 }}); +let replaced = ptr.assign(&mut data, json!(34)).unwrap(); +assert_eq!(replaced, Some(json!(42))); +assert_eq!(data, json!({"secret": { "universe": 34 }})); ``` -#### `Delete::delete` +Values can be deleted with the [`Delete`] trait. See [`delete`] for more +information. ```rust -use jsonptr::{ Pointer, delete::Delete }; +use jsonptr::Pointer; use serde_json::json; -let mut data = json!({ "foo": { "bar": { "baz": "qux" } } }); -let ptr = Pointer::from_static("/foo/bar/baz"); -assert_eq!(ptr.delete(&mut data), Some("qux".into())); -assert_eq!(data, json!({ "foo": { "bar": {} } })); - -// replacing a root pointer replaces data with `Value::Null` -let ptr = Pointer::root(); -let deleted = json!({ "foo": { "bar": {} } }); -assert_eq!(data.delete(&ptr), Some(deleted)); -assert!(data.is_null()); +let ptr = Pointer::parse("/secret/universe").unwrap(); +let mut data = json!({"secret": { "universe": 42 }}); +let replaced = ptr.assign(&mut data, json!(34)).unwrap(); +assert_eq!(replaced, Some(json!(42))); +assert_eq!(data, json!({"secret": { "universe": 34 }})); ``` ## Feature Flags -| Flag | Enables | -| :-----: | ----------------------------------------- | -| `"std"` | implements `std::error::Error` for errors | - -## Contributions / Issues - -Contributions and feedback are always welcome and appreciated. +| Flag | Description | Enables | Default | +| :---------: | ----------------------------------------------------------------------------------------------------------------------------------------- | --------------- | :-----: | +| `"std"` | Implements `std::error::Error` for error types | | ✓ | +| `"serde"` | Enables [`serde`] support for types | | ✓ | +| `"json"` | Implements ops for [`serde_json::Value`] | `"serde"` | ✓ | +| `"toml"` | Implements ops for [`toml::Value`] | `"std"`, `toml` | | +| `"assign"` | Enables the [`assign`] module and related pointer methods, providing a means to assign a value to a specific location within a document | | ✓ | +| `"resolve"` | Enables the [`resolve`] module and related pointer methods, providing a means to resolve a value at a specific location within a document | | ✓ | +| `"delete"` | Enables the [`delete`] module and related pointer methods, providing a means to delete a value at a specific location within a document | `"resolve"` | ✓ | -If you find an issue, please open a ticket or a pull request. +
## License -MIT or Apache 2.0. +Licensed under either of + +- Apache License, Version 2.0 + ([LICENSE-APACHE](LICENSE-APACHE) or http://www.apache.org/licenses/LICENSE-2.0) +- MIT license + ([LICENSE-MIT](LICENSE-MIT) or http://opensource.org/licenses/MIT) + +at your convenience. + +## Contribution + +Contributions and feedback are always welcome and appreciated. If you find an +issue, please open a ticket or a pull request. + +Unless you explicitly state otherwise, any contribution intentionally submitted +for inclusion in the work by you, as defined in the Apache-2.0 license, shall be +dual licensed as above, without any additional terms or conditions. + +[LICENSE-APACHE]: LICENSE-APACHE +[LICENSE-MIT]: LICENSE-MIT + +
+ +[`Pointer::components`]: (https://docs.rs/jsonptr/latest/jsonptrstruct.Pointer.html#method.components) +[`Pointer::tokens`]: (https://docs.rs/jsonptr/latest/jsonptrstruct.Pointer.html#method.tokens) +[`Pointer`]: https://docs.rs/jsonptr/latest/jsonptr/struct.Pointer.html +[`PointerBuf`]: https://docs.rs/jsonptr/latest/jsonptr/struct.PointerBuf.html +[`Token`]: https://docs.rs/jsonptr/latest/jsonptr/struct.Token.html +[`Tokens`]: https://docs.rs/jsonptr/latest/jsonptr/struct.Tokens.html +[`Components`]: https://docs.rs/jsonptr/latest/jsonptr/struct.Components.html +[`Component`]: https://docs.rs/jsonptr/latest/jsonptr/enum.Component.html +[`Root`]: https://docs.rs/jsonptr/latest/jsonptr/enum.Component.html#variant.Root +[`index`]: https://doc.rust-lang.org/std/primitive.usize.html +[`tokens`]: https://docs.rs/jsonptr/latest/jsonptr/struct.Pointer.html#method.tokens +[`components`]: https://docs.rs/jsonptr/latest/jsonptr/struct.Pointer.html#method.components +[`resolve`]: https://docs.rs/jsonptr/latest/jsonptr/resolve/index.html +[`assign`]: https://docs.rs/jsonptr/latest/jsonptr/assign/index.html +[`delete`]: https://docs.rs/jsonptr/latest/jsonptr/delete/index.html +[`Resolve`]: https://docs.rs/jsonptr/latest/jsonptr/resolve/trait.Resolve.html +[`ResolveMut`]: https://docs.rs/jsonptr/latest/jsonptr/resolve/trait.ResolveMut.html +[`Assign`]: https://docs.rs/jsonptr/latest/jsonptr/assign/trait.Assign.html +[`Delete`]: https://docs.rs/jsonptr/latest/jsonptr/delete/trait.Delete.html +[`serde`]: https://docs.rs/serde/1.0.120/serde/index +[`serde_json`]: https://docs.rs/serde_json/1.0.120/serde_json/enum.Value.html +[`toml`]: https://docs.rs/toml/0.8/toml/enum.Value.html +[`Path`]: https://doc.rust-lang.org/std/path/struct.Path.html +[`PathBuf`]: https://doc.rust-lang.org/std/path/struct.PathBuf.html diff --git a/src/assign.rs b/src/assign.rs index 7890b16..5ed22d4 100644 --- a/src/assign.rs +++ b/src/assign.rs @@ -3,16 +3,8 @@ //! This module provides the [`Assign`] trait which allows for the assignment of //! values based on a JSON Pointer. //! -//! ## Feature Flag //! This module is enabled by default with the `"assign"` feature flag. //! -//! ## Provided implementations -//! -//! | Lang | value type | feature flag | Default | -//! | ----- |: ----------------- :|: ---------- :| ------- | -//! | JSON | `serde_json::Value` | `"json"` | ✓ | -//! | TOML | `toml::Value` | `"toml"` | | -//! //! # Expansion //! The path will automatically be expanded if the [`Pointer`] is not fully //! exhausted before reaching a non-existent key in the case of objects, index @@ -23,20 +15,31 @@ //! - All tokens not equal to `"0"` or `"-"` will be considered keys of an //! object. //! +//! ## Usage +//! [`Assign`] can be used directly or through the [`assign`](Pointer::assign) +//! method of [`Pointer`]. //! -//! -//! ## Example //! ```rust -//! # use jsonptr::Pointer; -//! # use serde_json::json; +//! use jsonptr::Pointer; +//! use serde_json::json; //! let mut data = json!({"foo": "bar"}); //! let ptr = Pointer::from_static("/foo"); //! let replaced = ptr.assign(&mut data, "baz").unwrap(); //! assert_eq!(replaced, Some(json!("bar"))); //! assert_eq!(data, json!({"foo": "baz"})); //! ``` +//! ## Provided implementations +//! +//! | Lang | value type | feature flag | Default | +//! | ----- |: ----------------- :|: ---------- :| ------- | +//! | JSON | `serde_json::Value` | `"json"` | ✓ | +//! | TOML | `toml::Value` | `"toml"` | | +//! -use crate::{OutOfBoundsError, ParseIndexError, Pointer}; +use crate::{ + index::{OutOfBoundsError, ParseIndexError}, + Pointer, +}; use core::fmt::{self, Debug}; /* @@ -548,7 +551,10 @@ mod toml { #[allow(clippy::too_many_lines)] mod tests { use super::{Assign, AssignError}; - use crate::{OutOfBoundsError, ParseIndexError, Pointer}; + use crate::{ + index::{OutOfBoundsError, ParseIndexError}, + Pointer, + }; use alloc::str::FromStr; use core::fmt::{Debug, Display}; @@ -558,7 +564,7 @@ mod tests { ptr: &'static str, assign: V, expected_data: V, - expected_result: Result, V::Error>, + expected: Result, V::Error>, } impl Test @@ -577,7 +583,7 @@ mod tests { mut data, assign, expected_data, - expected_result, + expected, .. } = self; let ptr = Pointer::from_static(ptr); @@ -586,7 +592,7 @@ mod tests { &expected_data, &data, "test #{i}:\n\ndata: \n{data:#?}\n\nexpected_data\n{expected_data:#?}" ); - assert_eq!(&expected_result, &replaced); + assert_eq!(&expected, &replaced); } } @@ -598,7 +604,7 @@ mod tests { #[test] #[cfg(feature = "json")] - fn test_assign_json() { + fn assign_json() { use alloc::vec; use serde_json::json; Test::all([ @@ -607,90 +613,90 @@ mod tests { data: json!({}), assign: json!("bar"), expected_data: json!({"foo": "bar"}), - expected_result: Ok(None), + expected: Ok(None), }, Test { ptr: "", data: json!({"foo": "bar"}), assign: json!("baz"), expected_data: json!("baz"), - expected_result: Ok(Some(json!({"foo": "bar"}))), + expected: Ok(Some(json!({"foo": "bar"}))), }, Test { ptr: "/foo", data: json!({"foo": "bar"}), assign: json!("baz"), expected_data: json!({"foo": "baz"}), - expected_result: Ok(Some(json!("bar"))), + expected: Ok(Some(json!("bar"))), }, Test { ptr: "/foo/bar", data: json!({"foo": "bar"}), assign: json!("baz"), expected_data: json!({"foo": {"bar": "baz"}}), - expected_result: Ok(Some(json!("bar"))), + expected: Ok(Some(json!("bar"))), }, Test { ptr: "/foo/bar", data: json!({}), assign: json!("baz"), expected_data: json!({"foo": {"bar": "baz"}}), - expected_result: Ok(None), + expected: Ok(None), }, Test { ptr: "/", data: json!({}), assign: json!("foo"), expected_data: json!({"": "foo"}), - expected_result: Ok(None), + expected: Ok(None), }, Test { ptr: "/-", data: json!({}), assign: json!("foo"), expected_data: json!({"-": "foo"}), - expected_result: Ok(None), + expected: Ok(None), }, Test { ptr: "/-", data: json!(null), assign: json!(34), expected_data: json!([34]), - expected_result: Ok(Some(json!(null))), + expected: Ok(Some(json!(null))), }, Test { ptr: "/foo/-", data: json!({"foo": "bar"}), assign: json!("baz"), expected_data: json!({"foo": ["baz"]}), - expected_result: Ok(Some(json!("bar"))), + expected: Ok(Some(json!("bar"))), }, Test { ptr: "/foo/-/bar", assign: "baz".into(), data: json!({}), - expected_result: Ok(None), + expected: Ok(None), expected_data: json!({"foo":[{"bar": "baz"}]}), }, Test { ptr: "/foo/-/bar", assign: "qux".into(), data: json!({"foo":[{"bar":"baz" }]}), - expected_result: Ok(None), + expected: Ok(None), expected_data: json!({"foo":[{"bar":"baz"},{"bar":"qux"}]}), }, Test { ptr: "/foo/-/bar", data: json!({"foo":[{"bar":"baz"},{"bar":"qux"}]}), assign: "quux".into(), - expected_result: Ok(None), + expected: Ok(None), expected_data: json!({"foo":[{"bar":"baz"},{"bar":"qux"},{"bar":"quux"}]}), }, Test { ptr: "/foo/0/bar", data: json!({"foo":[{"bar":"baz"},{"bar":"qux"},{"bar":"quux"}]}), assign: "grault".into(), - expected_result: Ok(Some("baz".into())), + expected: Ok(Some("baz".into())), expected_data: json!({"foo":[{"bar":"grault"},{"bar":"qux"},{"bar":"quux"}]}), }, Test { @@ -698,34 +704,34 @@ mod tests { data: json!({}), assign: json!("foo"), expected_data: json!({"0": "foo"}), - expected_result: Ok(None), + expected: Ok(None), }, Test { ptr: "/1", data: json!(null), assign: json!("foo"), expected_data: json!({"1": "foo"}), - expected_result: Ok(Some(json!(null))), + expected: Ok(Some(json!(null))), }, Test { ptr: "/0", data: json!([]), expected_data: json!(["foo"]), assign: json!("foo"), - expected_result: Ok(None), + expected: Ok(None), }, Test { ptr: "///bar", data: json!({"":{"":{"bar": 42}}}), assign: json!(34), expected_data: json!({"":{"":{"bar":34}}}), - expected_result: Ok(Some(json!(42))), + expected: Ok(Some(json!(42))), }, Test { ptr: "/1", data: json!([]), assign: json!("foo"), - expected_result: Err(AssignError::OutOfBounds { + expected: Err(AssignError::OutOfBounds { offset: 0, source: OutOfBoundsError { index: 1, @@ -734,11 +740,18 @@ mod tests { }), expected_data: json!([]), }, + Test { + ptr: "/0", + data: json!(["foo"]), + assign: json!("bar"), + expected: Ok(Some(json!("foo"))), + expected_data: json!(["bar"]), + }, Test { ptr: "/a", data: json!([]), assign: json!("foo"), - expected_result: Err(AssignError::FailedToParseIndex { + expected: Err(AssignError::FailedToParseIndex { offset: 0, source: ParseIndexError { source: usize::from_str("foo").unwrap_err(), @@ -757,7 +770,7 @@ mod tests { #[test] #[cfg(feature = "toml")] - fn test_assign_toml() { + fn assign_toml() { use alloc::vec; use toml::{toml, Table, Value}; Test::all([ @@ -766,97 +779,97 @@ mod tests { ptr: "/foo", assign: "bar".into(), expected_data: toml! { "foo" = "bar" }.into(), - expected_result: Ok(None), + expected: Ok(None), }, Test { data: toml! {foo = "bar"}.into(), ptr: "", assign: "baz".into(), expected_data: "baz".into(), - expected_result: Ok(Some(toml! {foo = "bar"}.into())), + expected: Ok(Some(toml! {foo = "bar"}.into())), }, Test { data: toml! { foo = "bar"}.into(), ptr: "/foo", assign: "baz".into(), expected_data: toml! {foo = "baz"}.into(), - expected_result: Ok(Some("bar".into())), + expected: Ok(Some("bar".into())), }, Test { data: toml! { foo = "bar"}.into(), ptr: "/foo/bar", assign: "baz".into(), expected_data: toml! {foo = { bar = "baz"}}.into(), - expected_result: Ok(Some("bar".into())), + expected: Ok(Some("bar".into())), }, Test { data: Table::new().into(), ptr: "/", assign: "foo".into(), expected_data: toml! {"" = "foo"}.into(), - expected_result: Ok(None), + expected: Ok(None), }, Test { data: Table::new().into(), ptr: "/-", assign: "foo".into(), expected_data: toml! {"-" = "foo"}.into(), - expected_result: Ok(None), + expected: Ok(None), }, Test { data: "data".into(), ptr: "/-", assign: 34.into(), expected_data: Value::Array(vec![34.into()]), - expected_result: Ok(Some("data".into())), + expected: Ok(Some("data".into())), }, Test { data: toml! {foo = "bar"}.into(), ptr: "/foo/-", assign: "baz".into(), expected_data: toml! {foo = ["baz"]}.into(), - expected_result: Ok(Some("bar".into())), + expected: Ok(Some("bar".into())), }, Test { data: Table::new().into(), ptr: "/0", assign: "foo".into(), expected_data: toml! {"0" = "foo"}.into(), - expected_result: Ok(None), + expected: Ok(None), }, Test { data: 21.into(), ptr: "/1", assign: "foo".into(), expected_data: toml! {"1" = "foo"}.into(), - expected_result: Ok(Some(21.into())), + expected: Ok(Some(21.into())), }, Test { data: Value::Array(vec![]), ptr: "/0", expected_data: vec![Value::from("foo")].into(), assign: "foo".into(), - expected_result: Ok(None), + expected: Ok(None), }, Test { ptr: "/foo/-/bar", assign: "baz".into(), data: Table::new().into(), - expected_result: Ok(None), + expected: Ok(None), expected_data: toml! { "foo" = [{"bar" = "baz"}] }.into(), }, Test { ptr: "/foo/-/bar", assign: "qux".into(), data: toml! {"foo" = [{"bar" = "baz"}] }.into(), - expected_result: Ok(None), + expected: Ok(None), expected_data: toml! {"foo" = [{"bar" = "baz"}, {"bar" = "qux"}]}.into(), }, Test { ptr: "/foo/-/bar", data: toml! {"foo" = [{"bar" = "baz"}, {"bar" = "qux"}]}.into(), assign: "quux".into(), - expected_result: Ok(None), + expected: Ok(None), expected_data: toml! {"foo" = [{"bar" = "baz"}, {"bar" = "qux"}, {"bar" = "quux"}]} .into(), }, @@ -864,7 +877,7 @@ mod tests { ptr: "/foo/0/bar", data: toml! {"foo" = [{"bar" = "baz"}, {"bar" = "qux"}, {"bar" = "quux"}]}.into(), assign: "grault".into(), - expected_result: Ok(Some("baz".into())), + expected: Ok(Some("baz".into())), expected_data: toml! {"foo" = [{"bar" = "grault"}, {"bar" = "qux"}, {"bar" = "quux"}]}.into(), }, @@ -872,14 +885,14 @@ mod tests { data: Value::Array(vec![]), ptr: "/-", assign: "foo".into(), - expected_result: Ok(None), + expected: Ok(None), expected_data: vec!["foo"].into(), }, Test { data: Value::Array(vec![]), ptr: "/1", assign: "foo".into(), - expected_result: Err(AssignError::OutOfBounds { + expected: Err(AssignError::OutOfBounds { offset: 0, source: OutOfBoundsError { index: 1, @@ -892,7 +905,7 @@ mod tests { data: Value::Array(vec![]), ptr: "/a", assign: "foo".into(), - expected_result: Err(AssignError::FailedToParseIndex { + expected: Err(AssignError::FailedToParseIndex { offset: 0, source: ParseIndexError { source: usize::from_str("foo").unwrap_err(), diff --git a/src/component.rs b/src/component.rs new file mode 100644 index 0000000..9950161 --- /dev/null +++ b/src/component.rs @@ -0,0 +1,75 @@ +use crate::{Pointer, Token, Tokens}; + +/// A single [`Token`] or the root of a JSON Pointer +#[derive(Debug, PartialEq, Eq, PartialOrd, Ord)] +pub enum Component<'t> { + /// The document root + Root, + /// A segment of a JSON Pointer + Token(Token<'t>), +} +impl<'t> From> for Component<'t> { + fn from(token: Token<'t>) -> Self { + Self::Token(token) + } +} + +/// An iterator over the [`Component`]s of a JSON Pointer +#[derive(Debug)] +pub struct Components<'t> { + tokens: Tokens<'t>, + sent_root: bool, +} + +impl<'t> Iterator for Components<'t> { + type Item = Component<'t>; + fn next(&mut self) -> Option { + if !self.sent_root { + self.sent_root = true; + return Some(Component::Root); + } + self.tokens.next().map(Component::Token) + } +} + +impl<'t> From<&'t Pointer> for Components<'t> { + fn from(pointer: &'t Pointer) -> Self { + Self { + sent_root: false, + tokens: pointer.tokens(), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn components() { + let ptr = Pointer::from_static(""); + let components: Vec<_> = Components::from(ptr).collect(); + assert_eq!(components, vec![Component::Root]); + + let ptr = Pointer::from_static("/foo"); + let components = ptr.components().collect::>(); + assert_eq!( + components, + vec![Component::Root, Component::Token("foo".into())] + ); + + let ptr = Pointer::from_static("/foo/bar/-/0/baz"); + let components = ptr.components().collect::>(); + assert_eq!( + components, + vec![ + Component::Root, + Component::from(Token::from("foo")), + Component::Token("bar".into()), + Component::Token("-".into()), + Component::Token("0".into()), + Component::Token("baz".into()) + ] + ); + } +} diff --git a/src/delete.rs b/src/delete.rs index 8db5850..2909b5e 100644 --- a/src/delete.rs +++ b/src/delete.rs @@ -13,18 +13,10 @@ //! - `"json"` - `serde_json::Value::Null` //! - `"toml"` - `toml::Value::Table::Default` //! -//! ## Feature Flag -//! This module is enabled by default with the `"resolve"` feature flag. +//! This module is enabled by default with the `"delete"` feature flag. //! -//! ## Provided implementations -//! -//! | Lang | value type | feature flag | Default | -//! | ----- |: ----------------- :|: ---------- :| ------- | -//! | JSON | `serde_json::Value` | `"json"` | ✓ | -//! | TOML | `toml::Value` | `"toml"` | | -//! -//! ## Examples -//! ### Deleting a resolved pointer: +//! ## Usage +//! Deleting a resolved pointer: //! ```rust //! use jsonptr::{Pointer, delete::Delete}; //! use serde_json::json; @@ -34,7 +26,7 @@ //! assert_eq!(data.delete(&ptr), Some("qux".into())); //! assert_eq!(data, json!({ "foo": { "bar": {} } })); //! ``` -//! ### Deleting a non-existent Pointer returns `None`: +//! Deleting a non-existent Pointer returns `None`: //! ```rust //! use jsonptr::{ Pointer, delete::Delete }; //! use serde_json::json; @@ -44,7 +36,7 @@ //! assert_eq!(ptr.delete(&mut data), None); //! assert_eq!(data, json!({})); //! ``` -//! ### Deleting a root pointer replaces the value with `Value::Null`: +//! Deleting a root pointer replaces the value with `Value::Null`: //! ```rust //! use jsonptr::{Pointer, delete::Delete}; //! use serde_json::json; @@ -54,6 +46,14 @@ //! assert_eq!(data.delete(&ptr), Some(json!({ "foo": { "bar": "baz" } }))); //! assert!(data.is_null()); //! ``` +//! +//! ## Provided implementations +//! +//! | Lang | value type | feature flag | Default | +//! | ----- |: ----------------- :|: ---------- :| ------- | +//! | JSON | `serde_json::Value` | `"json"` | ✓ | +//! | TOML | `toml::Value` | `"toml"` | | + use crate::Pointer; /* @@ -183,7 +183,7 @@ mod tests { fn all(tests: impl IntoIterator>) { tests.into_iter().enumerate().for_each(|(i, t)| t.run(i)); } - fn run(self, i: usize) { + fn run(self, _i: usize) { let Test { mut data, ptr, @@ -193,16 +193,8 @@ mod tests { let ptr = Pointer::from_static(ptr); let deleted = ptr.delete(&mut data); - assert_eq!( - expected_data, - data, - "\ntest delete #{i} failed:\ndata not as expected\n\nptr: \"{ptr}\"\n\nexpected data:\n{expected_data:#?}\n\nactual data:\n{data:#?}\n\n" - ); - assert_eq!( - expected_deleted, - deleted, - "\ntest delete #{i} failed:\n\ndeleted value not as expected\nexpected deleted:{expected_data:#?}\n\nactual deleted:{deleted:#?}\n\n", - ); + assert_eq!(expected_data, data); + assert_eq!(expected_deleted, deleted); } } /* @@ -212,7 +204,7 @@ mod tests { */ #[test] #[cfg(feature = "json")] - fn test_delete_json() { + fn delete_json() { Test::all([ // 0 Test { @@ -271,6 +263,12 @@ mod tests { expected_data: json!({"test": "test"}), expected_deleted: Some(json!(21)), }, + Test { + ptr: "", + data: json!({"Example": 21, "test": "test"}), + expected_data: json!(null), + expected_deleted: Some(json!({"Example": 21, "test": "test"})), + }, ]); } /* @@ -280,7 +278,7 @@ mod tests { */ #[test] #[cfg(feature = "toml")] - fn test_delete_toml() { + fn delete_toml() { use toml::{toml, Table, Value}; Test::all([ diff --git a/src/index.rs b/src/index.rs index 2d770f2..e81acca 100644 --- a/src/index.rs +++ b/src/index.rs @@ -3,10 +3,12 @@ //! [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. +//! array member. While attempting to use `-` to resolve an array value will +//! always be out of bounds, the token can be useful when paired with utilities +//! which can mutate a value, such as this crate's [`assign`](crate::assign) +//! functionality or JSON Patch [RFC +//! 6902](https://datatracker.ietf.org/doc/html/rfc6902), as it provides 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 @@ -16,7 +18,7 @@ //! concrete index for a given array length: //! //! ``` -//! # use jsonptr::{Index, Token}; +//! # use jsonptr::{index::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()); @@ -33,9 +35,9 @@ //! assert_eq!(Index::Next.for_len_unchecked(30), 30); //! ```` -use crate::{OutOfBoundsError, ParseIndexError, Token}; +use crate::Token; use alloc::string::String; -use core::fmt::Display; +use core::{fmt, num::ParseIntError, str::FromStr}; /// Represents an abstract index into an array. /// @@ -66,7 +68,7 @@ impl Index { /// # Examples /// /// ``` - /// # use jsonptr::Index; + /// # use jsonptr::index::Index; /// assert_eq!(Index::Num(0).for_len(1), Ok(0)); /// assert!(Index::Num(1).for_len(1).is_err()); /// assert!(Index::Next.for_len(1).is_err()); @@ -98,7 +100,7 @@ impl Index { /// # Examples /// /// ``` - /// # use jsonptr::Index; + /// # use jsonptr::index::Index; /// assert_eq!(Index::Num(1).for_len_incl(1), Ok(1)); /// assert_eq!(Index::Next.for_len_incl(1), Ok(1)); /// assert!(Index::Num(2).for_len_incl(1).is_err()); @@ -124,11 +126,14 @@ impl Index { /// # Examples /// /// ``` - /// # use jsonptr::Index; + /// # use jsonptr::index::Index; /// assert_eq!(Index::Num(42).for_len_unchecked(30), 42); /// assert_eq!(Index::Next.for_len_unchecked(30), 30); + /// + /// // no bounds checks + /// assert_eq!(Index::Num(34).for_len_unchecked(40), 34); + /// assert_eq!(Index::Next.for_len_unchecked(34), 34); /// ```` - pub fn for_len_unchecked(&self, length: usize) -> usize { match *self { Self::Num(idx) => idx, @@ -137,7 +142,7 @@ impl Index { } } -impl Display for Index { +impl fmt::Display for Index { fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { match *self { Self::Num(index) => write!(f, "{index}"), @@ -152,28 +157,39 @@ impl From for Index { } } -impl TryFrom<&Token<'_>> for Index { - type Error = ParseIndexError; +impl FromStr for Index { + type Err = ParseIndexError; - fn try_from(value: &Token) -> Result { - // we don't need to decode because it's a single char - if value.encoded() == "-" { + fn from_str(s: &str) -> Result { + if s == "-" { Ok(Index::Next) } else { - Ok(value.decoded().parse::().map(Index::Num)?) + Ok(s.parse::().map(Index::Num)?) } } } +impl TryFrom<&Token<'_>> for Index { + type Error = ParseIndexError; + + fn try_from(value: &Token) -> Result { + Index::from_str(value.encoded()) + } +} + impl TryFrom<&str> for Index { type Error = ParseIndexError; fn try_from(value: &str) -> Result { - if value == "-" { - Ok(Index::Next) - } else { - Ok(value.parse::().map(Index::Num)?) - } + Index::from_str(value) + } +} + +impl TryFrom> for Index { + type Error = ParseIndexError; + + fn try_from(value: Token) -> Result { + Index::from_str(value.encoded()) } } @@ -184,11 +200,220 @@ macro_rules! derive_try_from { type Error = ParseIndexError; fn try_from(value: $t) -> Result { - value.try_into() + Index::from_str(&value) } } )* } } -derive_try_from!(Token<'_>, String, &String); +derive_try_from!(String, &String); + +/* +░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ +╔══════════════════════════════════════════════════════════════════════════════╗ +║ ║ +║ OutOfBoundsError ║ +║ ¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯ ║ +╚══════════════════════════════════════════════════════════════════════════════╝ +░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ +*/ + +/// Indicates that an `Index` is not within the given bounds. +#[derive(Debug, PartialEq, Eq)] +pub struct OutOfBoundsError { + /// 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, +} + +impl fmt::Display for OutOfBoundsError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "index {} out of bounds (limit: {})", + self.index, self.length + ) + } +} + +#[cfg(feature = "std")] +impl std::error::Error for OutOfBoundsError {} + +/* +░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ +╔══════════════════════════════════════════════════════════════════════════════╗ +║ ║ +║ ParseIndexError ║ +║ ¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯ ║ +╚══════════════════════════════════════════════════════════════════════════════╝ +░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ +*/ + +/// Indicates that the `Token` could not be parsed as valid RFC 6901 index. +#[derive(Debug, PartialEq, Eq)] +pub struct ParseIndexError { + /// The source `ParseIntError` + pub source: ParseIntError, +} + +impl From for ParseIndexError { + fn from(source: ParseIntError) -> Self { + Self { source } + } +} + +impl fmt::Display for ParseIndexError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "failed to parse token as an integer") + } +} + +#[cfg(feature = "std")] +impl std::error::Error for ParseIndexError { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + Some(&self.source) + } +} + +/* +░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ +╔══════════════════════════════════════════════════════════════════════════════╗ +║ ║ +║ Tests ║ +║ ¯¯¯¯¯¯¯ ║ +╚══════════════════════════════════════════════════════════════════════════════╝ +░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ +*/ + +#[cfg(test)] +mod tests { + use super::*; + use crate::Token; + + #[test] + fn index_from_usize() { + let index = Index::from(5usize); + assert_eq!(index, Index::Num(5)); + } + + #[test] + fn index_try_from_token_num() { + let token = Token::new("3"); + let index = Index::try_from(&token).unwrap(); + assert_eq!(index, Index::Num(3)); + } + + #[test] + fn index_try_from_token_next() { + let token = Token::new("-"); + let index = Index::try_from(&token).unwrap(); + assert_eq!(index, Index::Next); + } + + #[test] + fn index_try_from_str_num() { + let index = Index::try_from("42").unwrap(); + assert_eq!(index, Index::Num(42)); + } + + #[test] + fn index_try_from_str_next() { + let index = Index::try_from("-").unwrap(); + assert_eq!(index, Index::Next); + } + + #[test] + fn index_try_from_string_num() { + let index = Index::try_from(String::from("7")).unwrap(); + assert_eq!(index, Index::Num(7)); + } + + #[test] + fn index_try_from_string_next() { + let index = Index::try_from(String::from("-")).unwrap(); + assert_eq!(index, Index::Next); + } + + #[test] + fn index_for_len_incl_valid() { + assert_eq!(Index::Num(0).for_len_incl(1), Ok(0)); + assert_eq!(Index::Next.for_len_incl(2), Ok(2)); + } + + #[test] + fn index_for_len_incl_out_of_bounds() { + Index::Num(2).for_len_incl(1).unwrap_err(); + } + + #[test] + fn index_for_len_unchecked() { + assert_eq!(Index::Num(10).for_len_unchecked(5), 10); + assert_eq!(Index::Next.for_len_unchecked(3), 3); + } + + #[test] + fn display_index_num() { + let index = Index::Num(5); + assert_eq!(index.to_string(), "5"); + } + + #[test] + fn display_index_next() { + assert_eq!(Index::Next.to_string(), "-"); + } + + #[test] + fn for_len() { + assert_eq!(Index::Num(0).for_len(1), Ok(0)); + assert!(Index::Num(1).for_len(1).is_err()); + assert!(Index::Next.for_len(1).is_err()); + } + + #[test] + fn out_of_bounds_error_display() { + let err = OutOfBoundsError { + length: 5, + index: 10, + }; + assert_eq!(err.to_string(), "index 10 out of bounds (limit: 5)"); + } + + #[test] + fn parse_index_error_display() { + let err = ParseIndexError { + source: "not a number".parse::().unwrap_err(), + }; + assert_eq!(err.to_string(), "failed to parse token as an integer"); + } + + #[test] + #[cfg(feature = "std")] + fn parse_index_error_source() { + use std::error::Error; + let source = "not a number".parse::().unwrap_err(); + let err = ParseIndexError { source }; + assert_eq!( + err.source().unwrap().to_string(), + "not a number".parse::().unwrap_err().to_string() + ); + } + + #[test] + fn try_from_token() { + let token = Token::new("3"); + let index = >::try_from(token).unwrap(); + assert_eq!(index, Index::Num(3)); + let token = Token::new("-"); + let index = Index::try_from(&token).unwrap(); + assert_eq!(index, Index::Next); + } +} diff --git a/src/lib.rs b/src/lib.rs index 76b55fe..cbe7a7f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,3 +1,26 @@ +// rustdoc + README hack: https://linebender.org/blog/doc-include +//! +//! +//! [`Pointer`]: `crate::Pointer` +//! [`PointerBuf`]: `crate::PointerBuf` +//! [`Token`]: `crate::Token` +//! [`Tokens`]: `crate::Tokens` +//! [`Component`]: `crate::Component` +//! [`Components`]: `crate::Components` +//! [`Resolve`]: `crate::resolve::Resolve` +//! [`ResolveMut`]: `crate::resolve::ResolveMut` +//! [`resolve`]: `crate::resolve` +//! [`assign`]: `crate::assign` +//! [`delete`]: `crate::delete` +//! [`index`]: `crate::index` +//! [`Root`]: `crate::Component::Root` +//! [`str`]: `str` +//! [`String`]: `String` +//! [`serde_json::Value`]: `serde_json::Value` +//! [`toml::Value`]: https://docs.rs/toml/0.8/toml/enum.Value.html + #![doc = include_str!("../README.md")] #![warn(missing_docs)] #![deny(clippy::all, clippy::pedantic)] @@ -7,15 +30,13 @@ clippy::into_iter_without_iter, clippy::needless_pass_by_value, clippy::expect_fun_call, - clippy::must_use_candidate + clippy::must_use_candidate, + clippy::similar_names )] #[cfg_attr(not(feature = "std"), macro_use)] extern crate alloc; -use core::{fmt, num::ParseIntError}; -pub mod prelude; - #[cfg(feature = "assign")] pub mod assign; #[cfg(feature = "assign")] @@ -31,304 +52,16 @@ pub mod resolve; #[cfg(feature = "resolve")] pub use resolve::{Resolve, ResolveMut}; -mod tokens; -pub use tokens::*; - mod pointer; -pub use pointer::*; +pub use pointer::{ParseError, Pointer, PointerBuf, ReplaceTokenError}; mod token; -pub use token::*; +pub use token::{InvalidEncodingError, Token, Tokens}; pub mod index; -pub use index::Index; + +mod component; +pub use component::{Component, Components}; #[cfg(test)] mod arbitrary; - -/* -░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ -╔══════════════════════════════════════════════════════════════════════════════╗ -║ ║ -║ ParseError ║ -║ ¯¯¯¯¯¯¯¯¯¯¯¯ ║ -╚══════════════════════════════════════════════════════════════════════════════╝ -░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ -*/ - -/// Indicates that a `Pointer` was malformed and unable to be parsed. -#[derive(Debug, PartialEq)] -pub enum ParseError { - /// `Pointer` did not start with a backslash (`'/'`). - NoLeadingBackslash, - - /// `Pointer` contained invalid encoding (e.g. `~` not followed by `0` or - /// `1`). - InvalidEncoding { - /// Offset of the partial pointer starting with the token that contained - /// the invalid encoding - offset: usize, - /// The source `InvalidEncodingError` - source: InvalidEncodingError, - }, -} - -impl fmt::Display for ParseError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Self::NoLeadingBackslash { .. } => { - write!( - f, - "json pointer is malformed as it does not start with a backslash ('/')" - ) - } - Self::InvalidEncoding { source, .. } => write!(f, "{source}"), - } - } -} - -impl ParseError { - /// Returns `true` if this error is `NoLeadingBackslash`; otherwise returns - /// `false`. - pub fn is_no_leading_backslash(&self) -> bool { - matches!(self, Self::NoLeadingBackslash { .. }) - } - - /// Returns `true` if this error is `InvalidEncoding`; otherwise returns - /// `false`. - pub fn is_invalid_encoding(&self) -> bool { - matches!(self, Self::InvalidEncoding { .. }) - } - - /// Offset of the partial pointer starting with the token which caused the - /// error. - /// ```text - /// "/foo/invalid~tilde/invalid" - /// ↑ - /// 4 - /// ``` - /// ``` - /// # use jsonptr::PointerBuf; - /// let err = PointerBuf::parse("/foo/invalid~tilde/invalid").unwrap_err(); - /// assert_eq!(err.pointer_offset(), 4) - /// ``` - pub fn pointer_offset(&self) -> usize { - match *self { - Self::NoLeadingBackslash { .. } => 0, - Self::InvalidEncoding { offset, .. } => offset, - } - } - - /// Offset of the character index from within the first token of - /// [`Self::pointer_offset`]) - /// ```text - /// "/foo/invalid~tilde/invalid" - /// ↑ - /// 8 - /// ``` - /// ``` - /// # use jsonptr::PointerBuf; - /// let err = PointerBuf::parse("/foo/invalid~tilde/invalid").unwrap_err(); - /// assert_eq!(err.source_offset(), 8) - /// ``` - pub fn source_offset(&self) -> usize { - match self { - Self::NoLeadingBackslash { .. } => 0, - Self::InvalidEncoding { source, .. } => source.offset, - } - } - - /// Offset of the first invalid encoding from within the pointer. - /// ```text - /// "/foo/invalid~tilde/invalid" - /// ↑ - /// 12 - /// ``` - /// ``` - /// # use jsonptr::PointerBuf; - /// let err = PointerBuf::parse("/foo/invalid~tilde/invalid").unwrap_err(); - /// assert_eq!(err.pointer_offset(), 4) - /// ``` - pub fn complete_offset(&self) -> usize { - self.source_offset() + self.pointer_offset() - } -} - -#[cfg(feature = "std")] -impl std::error::Error for ParseError { - fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { - match self { - Self::InvalidEncoding { source, .. } => Some(source), - Self::NoLeadingBackslash => None, - } - } -} - -/* -░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ -╔══════════════════════════════════════════════════════════════════════════════╗ -║ ║ -║ ParseIndexError ║ -║ ¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯ ║ -╚══════════════════════════════════════════════════════════════════════════════╝ -░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ -*/ - -/// Indicates that the `Token` could not be parsed as valid RFC 6901 index. -#[derive(Debug, PartialEq, Eq)] -pub struct ParseIndexError { - /// The source `ParseIntError` - pub source: ParseIntError, -} - -impl From for ParseIndexError { - fn from(source: ParseIntError) -> Self { - Self { source } - } -} - -impl fmt::Display for ParseIndexError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "failed to parse token as an integer") - } -} - -#[cfg(feature = "std")] -impl std::error::Error for ParseIndexError { - fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { - Some(&self.source) - } -} - -/* -░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ -╔══════════════════════════════════════════════════════════════════════════════╗ -║ ║ -║ InvalidEncodingError ║ -║ ¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯ ║ -╚══════════════════════════════════════════════════════════════════════════════╝ -░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ -*/ - -/// A token within a json pointer contained invalid encoding (`~` not followed -/// by `0` or `1`). -/// -#[derive(Debug, PartialEq, Eq)] -pub struct InvalidEncodingError { - /// offset of the erroneous `~` from within the `Token` - pub offset: usize, -} - -impl InvalidEncodingError { - /// The byte offset of the first invalid `~`. - pub fn offset(&self) -> usize { - self.offset - } -} - -impl fmt::Display for InvalidEncodingError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!( - f, - "json pointer is malformed due to invalid encoding ('~' not followed by '0' or '1')" - ) - } -} - -#[cfg(feature = "std")] -impl std::error::Error for InvalidEncodingError {} - -/* -░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ -╔══════════════════════════════════════════════════════════════════════════════╗ -║ ║ -║ OutOfBoundsError ║ -║ ¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯ ║ -╚══════════════════════════════════════════════════════════════════════════════╝ -░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ -*/ - -/// Indicates that an `Index` is not within the given bounds. -#[derive(Debug, PartialEq, Eq)] -pub struct OutOfBoundsError { - /// 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, -} - -impl fmt::Display for OutOfBoundsError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!( - f, - "index {} out of bounds (limit: {})", - self.index, self.length - ) - } -} - -#[cfg(feature = "std")] -impl std::error::Error for OutOfBoundsError {} - -/* -░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ -╔══════════════════════════════════════════════════════════════════════════════╗ -║ ║ -║ NotFoundError ║ -║ ¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯ ║ -╚══════════════════════════════════════════════════════════════════════════════╝ -░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ -*/ - -/// An error that indicates a [`Pointer`]'s path was not found in the data. -#[derive(Debug, PartialEq, Eq)] -pub struct NotFoundError { - /// The starting offset of the [`Token`] within the [`Pointer`] which could not - /// be resolved. - pub offset: usize, -} - -impl fmt::Display for NotFoundError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "path starting at offset {} not found", self.offset) - } -} - -#[cfg(feature = "std")] -impl std::error::Error for NotFoundError {} - -/* -░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ -╔══════════════════════════════════════════════════════════════════════════════╗ -║ ║ -║ ReplaceTokenError ║ -║ ¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯ ║ -╚══════════════════════════════════════════════════════════════════════════════╝ -░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ -*/ - -/// Returned from `Pointer::replace_token` when the provided index is out of -/// bounds. -#[derive(Debug, PartialEq, Eq)] -pub struct ReplaceTokenError { - /// The index of the token that was out of bounds. - pub index: usize, - /// The number of tokens in the `Pointer`. - pub count: usize, -} - -impl fmt::Display for ReplaceTokenError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "index {} is out of bounds ({})", self.index, self.count) - } -} - -#[cfg(feature = "std")] -impl std::error::Error for ReplaceTokenError {} diff --git a/src/pointer.rs b/src/pointer.rs index b8501e2..0e56d88 100644 --- a/src/pointer.rs +++ b/src/pointer.rs @@ -1,6 +1,7 @@ -use crate::{InvalidEncodingError, ParseError, ReplaceTokenError, Token, Tokens}; +use crate::{token::InvalidEncodingError, Components, Token, Tokens}; use alloc::{ borrow::ToOwned, + fmt, string::{String, ToString}, vec::Vec, }; @@ -17,7 +18,7 @@ use core::{borrow::Borrow, cmp::Ordering, ops::Deref, str::FromStr}; */ /// A JSON Pointer is a string containing a sequence of zero or more reference -/// tokens, each prefixed by a '/' character. +/// [`Token`]s, each prefixed by a `'/'` character. /// /// See [RFC 6901 for more /// information](https://datatracker.ietf.org/doc/html/rfc6901). @@ -182,7 +183,7 @@ impl Pointer { .into() } /// Splits the `Pointer` at the given index if the character at the index is - /// a seperator backslash (`'/'`), returning `Some((head, tail))`. Otherwise, + /// a separator backslash (`'/'`), returning `Some((head, tail))`. Otherwise, /// returns `None`. /// /// For the following JSON Pointer, the following splits are possible (0, 4, 8): @@ -228,6 +229,10 @@ impl Pointer { self.0.strip_suffix(&suffix.0).map(Self::new) } + /// Returns the pointer stripped of the given prefix. + pub fn strip_prefix<'a>(&'a self, prefix: &Self) -> Option<&'a Self> { + self.0.strip_prefix(&prefix.0).map(Self::new) + } /// Attempts to get a `Token` by the index. Returns `None` if the index is /// out of bounds. /// @@ -247,43 +252,54 @@ impl Pointer { self.tokens().nth(index).clone() } - /// Attempts to resolve a `Value` based on the path in this `Pointer`. + /// Attempts to resolve a [`R::Value`] based on the path in this [`Pointer`]. /// /// ## Errors - /// Returns [`R::Error`](`Resolve::Error`) if an error occurs while - /// resolving. + /// Returns [`R::Error`] if an error occurs while resolving. /// /// The rules of such are determined by the `R`'s implementation of - /// [`Resolve`] but provided implementations return - /// [`ResolveError`](crate::resolve::ResolveError) if: + /// [`Resolve`] but provided implementations return [`ResolveError`] if: /// - The path is unreachable (e.g. a scalar is encountered prior to the end /// of the path) /// - The path is not found (e.g. a key in an object or an index in an array /// does not exist) - /// - An [`Token`] cannot be parsed as an array - /// [`Index`](crate::index::Index) - /// - An array [`Index`](crate::index::Index) is out of bounds + /// - A [`Token`] cannot be parsed as an array [`Index`] + /// - An array [`Index`] is out of bounds + /// + /// [`R::Value`]: `crate::resolve::Resolve::Value` + /// [`R::Error`]: `crate::resolve::Resolve::Error` + /// [`Resolve`]: `crate::resolve::Resolve` + /// [`ResolveError`]: `crate::resolve::ResolveError` + /// [`Token`]: `crate::Token` + /// [`Index`]: `crate::index::Index` #[cfg(feature = "resolve")] pub fn resolve<'v, R: crate::Resolve>(&self, value: &'v R) -> Result<&'v R::Value, R::Error> { value.resolve(self) } - /// Attempts to resolve a mutable `Value` based on the path in this `Pointer`. + /// Attempts to resolve a mutable [`R::Value`] based on the path in this + /// `Pointer`. /// /// ## Errors - /// Returns [`R::Error`](`ResolveMut::Error`) if an error occurs while + /// Returns [`R::Error`] if an error occurs while /// resolving. /// /// The rules of such are determined by the `R`'s implementation of - /// [`ResolveMut`] but provided implementations return - /// [`ResolveError`](crate::resolve::ResolveError) if: + /// [`ResolveMut`] but provided implementations return [`ResolveError`] if: /// - The path is unreachable (e.g. a scalar is encountered prior to the end /// of the path) /// - The path is not found (e.g. a key in an object or an index in an array /// does not exist) - /// - An [`Token`] cannot be parsed as an array - /// [`Index`](crate::index::Index) - /// - An array [`Index`](crate::index::Index) is out of bounds + /// - A [`Token`] cannot be parsed as an array [`Index`] + /// - An array [`Index`] is out of bounds + /// + /// [`R::Value`]: `crate::resolve::ResolveMut::Value` + /// [`R::Error`]: `crate::resolve::ResolveMut::Error` + /// [`ResolveMut`]: `crate::resolve::ResolveMut` + /// [`ResolveError`]: `crate::resolve::ResolveError` + /// [`Token`]: `crate::Token` + /// [`Index`]: `crate::index::Index` + #[cfg(feature = "resolve")] pub fn resolve_mut<'v, R: crate::ResolveMut>( &self, @@ -385,6 +401,24 @@ impl Pointer { { dest.assign(self, src) } + + /// Returns [`Components`] of this JSON Pointer. + /// + /// A [`Component`](crate::Component) is either [`Token`] or the root + /// location of a document. + /// ## Example + /// ``` + /// # use jsonptr::{Component, Pointer}; + /// let ptr = Pointer::parse("/a/b").unwrap(); + /// let mut components = ptr.components(); + /// assert_eq!(components.next(), Some(Component::Root)); + /// assert_eq!(components.next(), Some(Component::Token("a".into()))); + /// assert_eq!(components.next(), Some(Component::Token("b".into()))); + /// assert_eq!(components.next(), None); + /// ``` + pub fn components(&self) -> Components { + self.into() + } } #[cfg(feature = "serde")] @@ -441,13 +475,29 @@ impl PartialEq<&str> for Pointer { &&self.0 == other } } - +impl<'p> PartialEq for &'p Pointer { + fn eq(&self, other: &String) -> bool { + self.0.eq(other) + } +} impl PartialEq for Pointer { fn eq(&self, other: &str) -> bool { &self.0 == other } } +impl PartialEq for &str { + fn eq(&self, other: &Pointer) -> bool { + *self == (&other.0) + } +} + +impl PartialEq for String { + fn eq(&self, other: &Pointer) -> bool { + self == &other.0 + } +} + impl PartialEq for str { fn eq(&self, other: &Pointer) -> bool { self == &other.0 @@ -481,6 +531,18 @@ impl PartialEq for PointerBuf { &self.0 == other } } + +impl PartialEq for str { + fn eq(&self, other: &PointerBuf) -> bool { + self == other.0 + } +} +impl PartialEq for &str { + fn eq(&self, other: &PointerBuf) -> bool { + *self == other.0 + } +} + impl AsRef for Pointer { fn as_ref(&self) -> &Pointer { self @@ -529,6 +591,62 @@ impl AsRef<[u8]> for Pointer { } } +impl PartialOrd for Pointer { + fn partial_cmp(&self, other: &PointerBuf) -> Option { + self.0.partial_cmp(other.0.as_str()) + } +} + +impl PartialOrd for PointerBuf { + fn partial_cmp(&self, other: &Pointer) -> Option { + self.0.as_str().partial_cmp(&other.0) + } +} +impl PartialOrd<&Pointer> for PointerBuf { + fn partial_cmp(&self, other: &&Pointer) -> Option { + self.0.as_str().partial_cmp(&other.0) + } +} + +impl PartialOrd for String { + fn partial_cmp(&self, other: &Pointer) -> Option { + self.as_str().partial_cmp(&other.0) + } +} +impl PartialOrd for &Pointer { + fn partial_cmp(&self, other: &String) -> Option { + self.0.partial_cmp(other.as_str()) + } +} + +impl PartialOrd for String { + fn partial_cmp(&self, other: &PointerBuf) -> Option { + self.as_str().partial_cmp(other.0.as_str()) + } +} + +impl PartialOrd for str { + fn partial_cmp(&self, other: &Pointer) -> Option { + self.partial_cmp(&other.0) + } +} + +impl PartialOrd for str { + fn partial_cmp(&self, other: &PointerBuf) -> Option { + self.partial_cmp(other.0.as_str()) + } +} +impl PartialOrd for &str { + fn partial_cmp(&self, other: &PointerBuf) -> Option { + (*self).partial_cmp(other.0.as_str()) + } +} +impl PartialOrd for &str { + fn partial_cmp(&self, other: &Pointer) -> Option { + (*self).partial_cmp(&other.0) + } +} + impl PartialOrd<&str> for &Pointer { fn partial_cmp(&self, other: &&str) -> Option { PartialOrd::partial_cmp(&self.0[..], &other[..]) @@ -541,6 +659,24 @@ impl PartialOrd for Pointer { } } +impl PartialOrd<&str> for PointerBuf { + fn partial_cmp(&self, other: &&str) -> Option { + PartialOrd::partial_cmp(&self.0[..], &other[..]) + } +} + +impl<'p> PartialOrd for &'p Pointer { + fn partial_cmp(&self, other: &PointerBuf) -> Option { + self.0.partial_cmp(other.0.as_str()) + } +} + +impl PartialOrd for PointerBuf { + fn partial_cmp(&self, other: &String) -> Option { + self.0.partial_cmp(other) + } +} + impl<'a> IntoIterator for &'a Pointer { type Item = Token<'a>; type IntoIter = Tokens<'a>; @@ -559,7 +695,7 @@ impl<'a> IntoIterator for &'a Pointer { ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ */ -/// An owned, mutable Pointer (akin to String). +/// An owned, mutable [`Pointer`] (akin to `String`). /// /// This type provides methods like [`PointerBuf::push_back`] and /// [`PointerBuf::replace_token`] that mutate the pointer in place. It also @@ -577,7 +713,7 @@ impl PointerBuf { /// Attempts to parse a string into a `PointerBuf`. /// /// ## Errors - /// Returns a `ParseError` if the string is not a valid JSON Pointer. + /// Returns a [`ParseError`] if the string is not a valid JSON Pointer. pub fn parse + ?Sized>(s: &S) -> Result { Pointer::parse(&s).map(Pointer::to_buf) } @@ -666,7 +802,7 @@ impl PointerBuf { }); } let mut tokens = self.tokens().collect::>(); - if index > tokens.len() { + if index >= tokens.len() { return Err(ReplaceTokenError { count: tokens.len(), index, @@ -794,7 +930,7 @@ const fn validate(value: &str) -> Result<&str, ParseError> { if bytes[0] != b'/' { return Err(ParseError::NoLeadingBackslash); } - let mut ptr_offset = 0; // offset within the pointer of the most recent '/' seperator + let mut ptr_offset = 0; // offset within the pointer of the most recent '/' separator let mut tok_offset = 0; // offset within the current token let bytes = value.as_bytes(); @@ -802,7 +938,7 @@ const fn validate(value: &str) -> Result<&str, ParseError> { while i < bytes.len() { match bytes[i] { b'/' => { - // backslashes ('/') seperate tokens + // backslashes ('/') separate tokens // we increment the ptr_offset to point to this character ptr_offset = i; // and reset the token offset @@ -815,7 +951,7 @@ const fn validate(value: &str) -> Result<&str, ParseError> { // the pointer is not properly encoded // // we use the pointer offset, which points to the last - // encountered seperator, as the offset of the error. + // encountered separator, as the offset of the error. // The source `InvalidEncodingError` then uses the token // offset. // @@ -839,12 +975,158 @@ const fn validate(value: &str) -> Result<&str, ParseError> { _ => {} } i += 1; - // not a seperator so we increment the token offset + // not a separator so we increment the token offset tok_offset += 1; } Ok(value) } +/* +░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ +╔══════════════════════════════════════════════════════════════════════════════╗ +║ ║ +║ ParseError ║ +║ ¯¯¯¯¯¯¯¯¯¯¯¯ ║ +╚══════════════════════════════════════════════════════════════════════════════╝ +░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ +*/ + +/// Indicates that a `Pointer` was malformed and unable to be parsed. +#[derive(Debug, PartialEq)] +pub enum ParseError { + /// `Pointer` did not start with a backslash (`'/'`). + NoLeadingBackslash, + + /// `Pointer` contained invalid encoding (e.g. `~` not followed by `0` or + /// `1`). + InvalidEncoding { + /// Offset of the partial pointer starting with the token that contained + /// the invalid encoding + offset: usize, + /// The source `InvalidEncodingError` + source: InvalidEncodingError, + }, +} + +impl fmt::Display for ParseError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::NoLeadingBackslash { .. } => { + write!( + f, + "json pointer is malformed as it does not start with a backslash ('/')" + ) + } + Self::InvalidEncoding { source, .. } => write!(f, "{source}"), + } + } +} + +impl ParseError { + /// Returns `true` if this error is `NoLeadingBackslash` + pub fn is_no_leading_backslash(&self) -> bool { + matches!(self, Self::NoLeadingBackslash { .. }) + } + + /// Returns `true` if this error is `InvalidEncoding` + pub fn is_invalid_encoding(&self) -> bool { + matches!(self, Self::InvalidEncoding { .. }) + } + + /// Offset of the partial pointer starting with the token which caused the error. + /// + /// ```text + /// "/foo/invalid~tilde/invalid" + /// ↑ + /// ``` + /// + /// ``` + /// # use jsonptr::PointerBuf; + /// let err = PointerBuf::parse("/foo/invalid~tilde/invalid").unwrap_err(); + /// assert_eq!(err.pointer_offset(), 4) + /// ``` + pub fn pointer_offset(&self) -> usize { + match *self { + Self::NoLeadingBackslash { .. } => 0, + Self::InvalidEncoding { offset, .. } => offset, + } + } + + /// Offset of the character index from within the first token of + /// [`Self::pointer_offset`]) + /// + /// ```text + /// "/foo/invalid~tilde/invalid" + /// ↑ + /// 8 + /// ``` + /// ``` + /// # use jsonptr::PointerBuf; + /// let err = PointerBuf::parse("/foo/invalid~tilde/invalid").unwrap_err(); + /// assert_eq!(err.source_offset(), 8) + /// ``` + pub fn source_offset(&self) -> usize { + match self { + Self::NoLeadingBackslash { .. } => 0, + Self::InvalidEncoding { source, .. } => source.offset, + } + } + + /// Offset of the first invalid encoding from within the pointer. + /// ```text + /// "/foo/invalid~tilde/invalid" + /// ↑ + /// 12 + /// ``` + /// ``` + /// use jsonptr::PointerBuf; + /// let err = PointerBuf::parse("/foo/invalid~tilde/invalid").unwrap_err(); + /// assert_eq!(err.complete_offset(), 12) + /// ``` + pub fn complete_offset(&self) -> usize { + self.source_offset() + self.pointer_offset() + } +} + +#[cfg(feature = "std")] +impl std::error::Error for ParseError { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + match self { + Self::InvalidEncoding { source, .. } => Some(source), + Self::NoLeadingBackslash => None, + } + } +} + +/* +░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ +╔══════════════════════════════════════════════════════════════════════════════╗ +║ ║ +║ ReplaceTokenError ║ +║ ¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯ ║ +╚══════════════════════════════════════════════════════════════════════════════╝ +░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ +*/ + +/// Returned from `Pointer::replace_token` when the provided index is out of +/// bounds. +#[derive(Debug, PartialEq, Eq)] +pub struct ReplaceTokenError { + /// The index of the token that was out of bounds. + pub index: usize, + /// The number of tokens in the `Pointer`. + pub count: usize, +} + +impl fmt::Display for ReplaceTokenError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "index {} is out of bounds ({})", self.index, self.count) + } +} + +#[cfg(feature = "std")] +impl std::error::Error for ReplaceTokenError {} + /* ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ ╔══════════════════════════════════════════════════════════════════════════════╗ @@ -857,6 +1139,8 @@ const fn validate(value: &str) -> Result<&str, ParseError> { #[cfg(test)] mod tests { + use std::error::Error; + use super::*; use quickcheck::TestResult; use quickcheck_macros::quickcheck; @@ -868,7 +1152,38 @@ mod tests { } #[test] - fn test_parse() { + fn strip_suffix() { + let p = Pointer::new("/example/pointer/to/some/value"); + let stripped = p.strip_suffix(Pointer::new("/to/some/value")).unwrap(); + assert_eq!(stripped, "/example/pointer"); + } + + #[test] + fn strip_prefix() { + let p = Pointer::new("/example/pointer/to/some/value"); + let stripped = p.strip_prefix(Pointer::new("/example/pointer")).unwrap(); + assert_eq!(stripped, "/to/some/value"); + } + + #[test] + fn parse_error_is_no_leading_backslash() { + let err = ParseError::NoLeadingBackslash; + assert!(err.is_no_leading_backslash()); + assert!(!err.is_invalid_encoding()); + } + + #[test] + fn parse_error_is_invalid_encoding() { + let err = ParseError::InvalidEncoding { + offset: 0, + source: InvalidEncodingError { offset: 1 }, + }; + assert!(!err.is_no_leading_backslash()); + assert!(err.is_invalid_encoding()); + } + + #[test] + fn parse() { let tests = [ ("", Ok("")), ("/", Ok("/")), @@ -906,18 +1221,53 @@ mod tests { ]; for (input, expected) in tests { let actual = Pointer::parse(input).map(Pointer::as_str); - assert_eq!( - actual, expected, - "pointer parsing failed to meet expectations - \ninput: {input} - \nexpected:\n{expected:#?} - \nactual:\n{actual:#?}", - ); + assert_eq!(actual, expected); } } #[test] - fn test_push_pop_back() { + fn parse_error_offsets() { + let err = Pointer::parse("/foo/invalid~encoding").unwrap_err(); + assert_eq!(err.pointer_offset(), 4); + assert_eq!(err.source_offset(), 8); + assert_eq!(err.complete_offset(), 12); + + let err = Pointer::parse("invalid~encoding").unwrap_err(); + assert_eq!(err.pointer_offset(), 0); + assert_eq!(err.source_offset(), 0); + + let err = Pointer::parse("no-leading/slash").unwrap_err(); + assert!(err.source().is_none()); + } + + #[test] + #[cfg(feature = "std")] + fn parse_error_source() { + use std::error::Error; + let err = Pointer::parse("/foo/invalid~encoding").unwrap_err(); + assert!(err.source().is_some()); + let source = err.source().unwrap(); + assert!(source.is::()); + + let err = Pointer::parse("no-leading/slash").unwrap_err(); + assert!(err.source().is_none()); + } + + #[test] + fn pointerbuf_as_pointer_returns_pointer() { + let ptr = PointerBuf::parse("/foo/bar").unwrap(); + assert_eq!(ptr.as_ptr(), ptr); + } + + #[test] + fn pointer_buf_clear() { + let mut ptr = PointerBuf::from_tokens(["foo", "bar"]); + ptr.clear(); + assert_eq!(ptr, ""); + } + + #[test] + fn push_pop_back() { let mut ptr = PointerBuf::default(); assert_eq!(ptr, "", "default, root pointer should equal \"\""); assert_eq!(ptr.count(), 0, "default pointer should have 0 tokens"); @@ -926,37 +1276,24 @@ mod tests { assert_eq!(ptr, "/foo", "pointer should equal \"/foo\" after push_back"); ptr.push_back("bar".into()); - assert_eq!( - ptr, "/foo/bar", - "pointer should equal \"/foo/bar\" after push_back" - ); + assert_eq!(ptr, "/foo/bar"); ptr.push_back("/baz".into()); - assert_eq!( - ptr, "/foo/bar/~1baz", - "pointer should equal \"/foo/bar/~1baz\" after push_back" - ); + assert_eq!(ptr, "/foo/bar/~1baz"); let mut ptr = PointerBuf::from_tokens(["foo", "bar"]); assert_eq!(ptr.pop_back(), Some("bar".into())); assert_eq!(ptr, "/foo", "pointer should equal \"/foo\" after pop_back"); - assert_eq!( - ptr.pop_back(), - Some("foo".into()), - "\"foo\" should have been popped from the back" - ); + assert_eq!(ptr.pop_back(), Some("foo".into())); assert_eq!(ptr, "", "pointer should equal \"\" after pop_back"); } #[test] - fn test_replace_token() { + fn replace_token() { let mut ptr = PointerBuf::try_from("/test/token").unwrap(); let res = ptr.replace_token(0, "new".into()); assert!(res.is_ok()); - assert_eq!( - ptr, "/new/token", - "pointer should equal \"/new/token\" after replace_token" - ); + assert_eq!(ptr, "/new/token"); let res = ptr.replace_token(3, "invalid".into()); @@ -964,7 +1301,7 @@ mod tests { } #[test] - fn test_push_pop_front() { + fn push_pop_front() { let mut ptr = PointerBuf::default(); assert_eq!(ptr, ""); assert_eq!(ptr.count(), 0); @@ -991,6 +1328,12 @@ mod tests { assert_eq!(ptr, ""); } + #[test] + fn display_replace_token_error() { + let err = ReplaceTokenError { index: 3, count: 2 }; + assert_eq!(format!("{err}"), "index 3 is out of bounds (2)"); + } + #[test] fn pop_front_works_with_empty_strings() { { @@ -1036,7 +1379,7 @@ mod tests { } #[test] - fn test_formatting() { + fn formatting() { assert_eq!(PointerBuf::from_tokens(["foo", "bar"]), "/foo/bar"); assert_eq!( PointerBuf::from_tokens(["~/foo", "~bar", "/baz"]), @@ -1044,10 +1387,13 @@ mod tests { ); assert_eq!(PointerBuf::from_tokens(["field", "", "baz"]), "/field//baz"); assert_eq!(PointerBuf::default(), ""); + + let ptr = PointerBuf::from_tokens(["foo", "bar", "baz"]); + assert_eq!(ptr.to_string(), "/foo/bar/baz"); } #[test] - fn test_last() { + fn last() { let ptr = Pointer::from_static("/foo/bar"); assert_eq!(ptr.last(), Some("bar".into())); @@ -1066,7 +1412,7 @@ mod tests { } #[test] - fn test_first() { + fn first() { let ptr = Pointer::from_static("/foo/bar"); assert_eq!(ptr.first(), Some("foo".into())); @@ -1078,7 +1424,7 @@ mod tests { } #[test] - fn test_pointerbuf_try_from() { + fn pointerbuf_try_from() { let ptr = PointerBuf::from_tokens(["foo", "bar", "~/"]); assert_eq!(PointerBuf::try_from("/foo/bar/~0~1").unwrap(), ptr); @@ -1086,6 +1432,125 @@ mod tests { assert_eq!(ptr, into); } + #[test] + fn default() { + let ptr = PointerBuf::default(); + assert_eq!(ptr, ""); + assert_eq!(ptr.count(), 0); + + let ptr = <&Pointer>::default(); + assert_eq!(ptr, ""); + } + + #[test] + #[cfg(all(feature = "serde", feature = "json"))] + fn to_json_value() { + use serde_json::Value; + let ptr = Pointer::from_static("/foo/bar"); + assert_eq!(ptr.to_json_value(), Value::String(String::from("/foo/bar"))); + } + + #[cfg(all(feature = "resolve", feature = "json"))] + #[test] + fn resolve() { + // full tests in resolve.rs + use serde_json::json; + let value = json!({ + "foo": { + "bar": { + "baz": "qux" + } + } + }); + let ptr = Pointer::from_static("/foo/bar/baz"); + let resolved = ptr.resolve(&value).unwrap(); + assert_eq!(resolved, &json!("qux")); + } + + #[cfg(all(feature = "delete", feature = "json"))] + #[test] + fn delete() { + use serde_json::json; + let mut value = json!({ + "foo": { + "bar": { + "baz": "qux" + } + } + }); + let ptr = Pointer::from_static("/foo/bar/baz"); + let deleted = ptr.delete(&mut value).unwrap(); + assert_eq!(deleted, json!("qux")); + assert_eq!( + value, + json!({ + "foo": { + "bar": {} + } + }) + ); + } + + #[cfg(all(feature = "assign", feature = "json"))] + #[test] + fn assign() { + use serde_json::json; + let mut value = json!({}); + let ptr = Pointer::from_static("/foo/bar"); + let replaced = ptr.assign(&mut value, json!("baz")).unwrap(); + assert_eq!(replaced, None); + assert_eq!( + value, + json!({ + "foo": { + "bar": "baz" + } + }) + ); + } + + #[test] + fn get() { + let ptr = Pointer::from_static("/0/1/2/3/4/5/6/7/8/9"); + for i in 0..10 { + assert_eq!(ptr.get(i).unwrap().decoded(), i.to_string()); + } + } + + #[test] + fn replace_token_success() { + let mut ptr = PointerBuf::from_tokens(["foo", "bar", "baz"]); + assert!(ptr.replace_token(1, "qux".into()).is_ok()); + assert_eq!(ptr, PointerBuf::from_tokens(["foo", "qux", "baz"])); + + assert!(ptr.replace_token(0, "corge".into()).is_ok()); + assert_eq!(ptr, PointerBuf::from_tokens(["corge", "qux", "baz"])); + + assert!(ptr.replace_token(2, "quux".into()).is_ok()); + assert_eq!(ptr, PointerBuf::from_tokens(["corge", "qux", "quux"])); + } + + #[test] + fn replace_token_out_of_bounds() { + let mut ptr = PointerBuf::from_tokens(["foo", "bar"]); + assert!(ptr.replace_token(2, "baz".into()).is_err()); + assert_eq!(ptr, PointerBuf::from_tokens(["foo", "bar"])); // Ensure original pointer is unchanged + } + + #[test] + fn replace_token_with_empty_string() { + let mut ptr = PointerBuf::from_tokens(["foo", "bar", "baz"]); + assert!(ptr.replace_token(1, "".into()).is_ok()); + assert_eq!(ptr, PointerBuf::from_tokens(["foo", "", "baz"])); + } + + #[test] + fn replace_token_in_empty_pointer() { + let mut ptr = PointerBuf::default(); + assert!(ptr.replace_token(0, "foo".into()).is_err()); + assert_eq!(ptr, PointerBuf::default()); // Ensure the pointer remains empty + } + #[test] fn pop_back_works_with_empty_strings() { { @@ -1130,6 +1595,48 @@ mod tests { } } + #[test] + // `clippy::useless_asref` is tripping here because the `as_ref` is being + // called on the same type (`&Pointer`). This is just to ensure that the + // `as_ref` method is implemented correctly and stays that way. + #[allow(clippy::useless_asref)] + fn pointerbuf_as_ref_returns_pointer() { + let ptr_str = "/foo/bar"; + let ptr = Pointer::from_static(ptr_str); + let ptr_buf = ptr.to_buf(); + assert_eq!(ptr_buf.as_ref(), ptr); + let r: &Pointer = ptr.as_ref(); + assert_eq!(ptr, r); + + let s: &str = ptr.as_ref(); + assert_eq!(s, ptr_str); + + let b: &[u8] = ptr.as_ref(); + assert_eq!(b, ptr_str.as_bytes()); + } + + #[test] + fn from_tokens() { + let ptr = PointerBuf::from_tokens(["foo", "bar", "baz"]); + assert_eq!(ptr, "/foo/bar/baz"); + } + + #[test] + fn pointer_borrow() { + let ptr = Pointer::from_static("/foo/bar"); + let borrowed: &str = ptr.borrow(); + assert_eq!(borrowed, "/foo/bar"); + } + + #[test] + #[cfg(feature = "json")] + fn into_value() { + use serde_json::Value; + let ptr = Pointer::from_static("/foo/bar"); + let value: Value = ptr.into(); + assert_eq!(value, Value::String("/foo/bar".to_string())); + } + #[test] fn intersect() { let base = Pointer::from_static("/foo/bar"); @@ -1275,14 +1782,132 @@ mod tests { let mut b = base.clone(); b.append(&suffix_1); let isect = a.intersection(&b); - if isect != base { - println!("\nintersection failed\nbase: {base:?}\nisect: {isect:?}\nsuffix_0: {suffix_0:?}\nsuffix_1: {suffix_1:?}\n"); - } TestResult::from_bool(isect == base) } + #[cfg(all(feature = "json", feature = "std", feature = "serde"))] + #[test] + fn serde() { + use serde::Deserialize; + let ptr = PointerBuf::from_tokens(["foo", "bar"]); + let json = serde_json::to_string(&ptr).unwrap(); + assert_eq!(json, "\"/foo/bar\""); + let deserialized: PointerBuf = serde_json::from_str(&json).unwrap(); + assert_eq!(deserialized, ptr); + + let ptr = Pointer::from_static("/foo/bar"); + let json = serde_json::to_string(&ptr).unwrap(); + assert_eq!(json, "\"/foo/bar\""); + + let mut de = serde_json::Deserializer::from_str("\"/foo/bar\""); + let p = <&Pointer>::deserialize(&mut de).unwrap(); + assert_eq!(p, ptr); + let s = serde_json::to_string(p).unwrap(); + assert_eq!(json, s); + + let invalid = serde_json::from_str::<&Pointer>("\"foo/bar\""); + assert!(invalid.is_err()); + assert_eq!( + invalid.unwrap_err().to_string(), + "failed to parse json pointer\n\ncaused by:\njson pointer is malformed as it does not start with a backslash ('/') at line 1 column 9" + ); + } + + #[test] + fn to_owned() { + let ptr = Pointer::from_static("/bread/crumbs"); + let buf = ptr.to_owned(); + assert_eq!(buf, "/bread/crumbs"); + } + + #[test] + #[allow(clippy::cmp_owned, unused_must_use)] + fn partial_eq() { + let ptr_string = String::from("/bread/crumbs"); + let ptr_str = "/bread/crumbs"; + let ptr = Pointer::from_static(ptr_str); + let ptr_buf = ptr.to_buf(); + <&Pointer as PartialEq<&Pointer>>::eq(&ptr, &ptr); + >::eq(ptr, &ptr_str); + <&Pointer as PartialEq>::eq(&ptr, &ptr_string); + >::eq(ptr, &ptr_string); + >::eq(ptr, &ptr_buf); + <&str as PartialEq>::eq(&ptr_str, ptr); + >::eq(&ptr_string, ptr); + >::eq(ptr_str, ptr); + >::eq(&ptr_buf, ptr_str); + >::eq(&ptr_buf, &ptr_buf); + >::eq(&ptr_buf, ptr); + >::eq(ptr, &ptr_buf); + >::eq(&ptr_buf, &ptr); + >::eq(&ptr_buf, &ptr_str); + >::eq(&ptr_buf, &ptr_string); + <&Pointer as PartialEq>::eq(&ptr, &ptr_buf); + >::eq(ptr_str, &ptr_buf); + <&str as PartialEq>::eq(&ptr_str, &ptr_buf); + >::eq(&ptr_string, &ptr_buf); + } + + #[test] + fn partial_ord() { + let a_str = "/foo/bar"; + let a_string = a_str.to_string(); + let a_ptr = Pointer::from_static(a_str); + let a_buf = a_ptr.to_buf(); + let b_str = "/foo/bar"; + let b_string = b_str.to_string(); + let b_ptr = Pointer::from_static(b_str); + let b_buf = b_ptr.to_buf(); + let c_str = "/foo/bar/baz"; + let c_string = c_str.to_string(); + let c_ptr = Pointer::from_static(c_str); + let c_buf = c_ptr.to_buf(); + + assert!(>::lt(a_ptr, &c_buf)); + assert!(>::lt(&a_buf, c_ptr)); + assert!(>::lt(&a_string, c_ptr)); + assert!(>::lt(a_str, c_ptr)); + assert!(>::lt(a_str, &c_buf)); + assert!(<&str as PartialOrd>::lt(&a_str, c_ptr)); + assert!(<&str as PartialOrd>::lt(&a_str, &c_buf)); + assert!(<&Pointer as PartialOrd>::lt(&a_ptr, &c_buf)); + assert!(<&Pointer as PartialOrd<&str>>::lt(&b_ptr, &c_str)); + assert!(>::lt(a_ptr, &c_string)); + assert!(>::lt(&a_buf, &c_str)); + assert!(>::lt(&a_buf, &c_string)); + assert!(a_ptr < c_buf); + assert!(c_buf > a_ptr); + assert!(a_buf < c_ptr); + assert!(a_ptr < c_buf); + assert!(a_ptr < c_ptr); + assert!(a_ptr <= c_ptr); + assert!(c_ptr > a_ptr); + assert!(c_ptr >= a_ptr); + assert!(a_ptr == b_ptr); + assert!(a_ptr <= b_ptr); + assert!(a_ptr >= b_ptr); + assert!(a_string < c_buf); + assert!(a_string <= c_buf); + assert!(c_string > a_buf); + assert!(c_string >= a_buf); + assert!(a_string == b_buf); + assert!(a_ptr < c_buf); + assert!(a_ptr <= c_buf); + assert!(c_ptr > a_buf); + assert!(c_ptr >= a_buf); + assert!(a_ptr == b_buf); + assert!(a_ptr <= b_buf); + assert!(a_ptr >= b_buf); + assert!(a_ptr < c_buf); + assert!(c_ptr > b_string); + // couldn't inline this + #[allow(clippy::nonminimal_bool)] + let not = !(a_ptr > c_buf); + assert!(not); + } + #[test] - fn test_intersection() { + fn intersection() { struct Test { base: &'static str, a_suffix: &'static str, @@ -1295,6 +1920,11 @@ mod tests { a_suffix: "/", b_suffix: "/a/b/c", }, + Test { + base: "", + a_suffix: "", + b_suffix: "", + }, Test { base: "/a", a_suffix: "/", @@ -1324,10 +1954,53 @@ mod tests { a.append(&PointerBuf::parse(a_suffix).unwrap()); b.append(&PointerBuf::parse(b_suffix).unwrap()); let intersection = a.intersection(&b); - assert_eq!( - intersection, base, - "\nintersection failed\n\nbase:{base}\na_suffix:{a_suffix}\nb_suffix:{b_suffix} expected: \"{base}\"\n actual: \"{intersection}\"\n" - ); + assert_eq!(intersection, base); } } + #[test] + fn parse_error_display() { + assert_eq!( + ParseError::NoLeadingBackslash.to_string(), + "json pointer is malformed as it does not start with a backslash ('/')" + ); + } + + #[test] + fn into_iter() { + use core::iter::IntoIterator; + + let ptr = PointerBuf::from_tokens(["foo", "bar", "baz"]); + let tokens: Vec = ptr.into_iter().collect(); + let from_tokens = PointerBuf::from_tokens(tokens); + assert_eq!(ptr, from_tokens); + + let ptr = Pointer::from_static("/foo/bar/baz"); + let tokens: Vec<_> = ptr.into_iter().collect(); + assert_eq!(ptr, PointerBuf::from_tokens(tokens)); + } + + #[test] + fn from_str() { + let p = PointerBuf::from_str("/foo/bar").unwrap(); + assert_eq!(p, "/foo/bar"); + } + + #[test] + fn from_token() { + let p = PointerBuf::from(Token::new("foo")); + assert_eq!(p, "/foo"); + } + + #[test] + fn from_usize() { + let p = PointerBuf::from(0); + assert_eq!(p, "/0"); + } + + #[test] + fn borrow() { + let ptr = PointerBuf::from_tokens(["foo", "bar"]); + let borrowed: &Pointer = ptr.borrow(); + assert_eq!(borrowed, "/foo/bar"); + } } diff --git a/src/prelude.rs b/src/prelude.rs deleted file mode 100644 index 4aa5e2f..0000000 --- a/src/prelude.rs +++ /dev/null @@ -1,7 +0,0 @@ -//! Exposes the traits `Assign`, `Delete`, `Resolve`, `ResolveMut` if enabled. -#[cfg(feature = "assign")] -pub use crate::assign::Assign; -#[cfg(feature = "delete")] -pub use crate::delete::Delete; -#[cfg(feature = "resolve")] -pub use crate::resolve::{Resolve, ResolveMut}; diff --git a/src/resolve.rs b/src/resolve.rs index f11bbd9..17ee9e0 100644 --- a/src/resolve.rs +++ b/src/resolve.rs @@ -4,9 +4,27 @@ //! implemented by types that can internally resolve a value based on a JSON //! Pointer. //! -//! ## Feature Flag //! This module is enabled by default with the `"resolve"` feature flag. //! +//! ## Usage +//! [`Resolve`] and [`ResolveMut`] can be used directly or through the +//! [`resolve`](Pointer::resolve) and [`resolve_mut`](Pointer::resolve_mut) +//! methods on [`Pointer`] and [`PointerBuf`](crate::PointerBuf). +//! +//! ```rust +//! use jsonptr::{Pointer, Resolve, ResolveMut}; +//! use serde_json::json; +//! +//! let ptr = Pointer::from_static("/foo/1"); +//! let mut data = json!({"foo": ["bar", "baz"]}); +//! +//! let value = ptr.resolve(&data).unwrap(); +//! assert_eq!(value, &json!("baz")); +//! +//! let value = data.resolve_mut(ptr).unwrap(); +//! assert_eq!(value, &json!("baz")); +//! ``` +//! //! ## Provided implementations //! //! | Lang | value type | feature flag | Default | @@ -15,7 +33,10 @@ //! | TOML | `toml::Value` | `"toml"` | | //! //! -use crate::{OutOfBoundsError, ParseIndexError, Pointer, Token}; +use crate::{ + index::{OutOfBoundsError, ParseIndexError}, + Pointer, Token, +}; /* ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ @@ -55,7 +76,7 @@ pub trait Resolve { */ /// A trait implemented by types which can resolve a mutable reference to a -/// `serde_json::Value` from the path in a JSON [`Pointer`]. +/// value type from a path represented by a JSON [`Pointer`]. pub trait ResolveMut { /// The type of value that is being resolved. type Value; @@ -395,55 +416,198 @@ mod toml { #[cfg(test)] mod tests { use super::{Resolve, ResolveError, ResolveMut}; - use crate::Pointer; + use crate::{ + index::{OutOfBoundsError, ParseIndexError}, + Pointer, + }; use core::fmt; - struct Test<'v, V> { - ptr: &'static str, - expected_result: Result<&'v V, ResolveError>, - data: &'v V, + #[cfg(feature = "std")] + #[test] + fn resolve_error_source() { + use std::error::Error; + let err = ResolveError::FailedToParseIndex { + offset: 0, + source: ParseIndexError { + source: "invalid".parse::().unwrap_err(), + }, + }; + assert!(err.source().is_some()); + + let err = ResolveError::OutOfBounds { + offset: 0, + source: OutOfBoundsError { + index: 1, + length: 0, + }, + }; + assert!(err.source().is_some()); + + let err = ResolveError::NotFound { offset: 0 }; + assert!(err.source().is_none()); + + let err = ResolveError::Unreachable { offset: 0 }; + assert!(err.source().is_none()); } - impl<'v, V> Test<'v, V> - where - V: Resolve - + ResolveMut - + Clone - + PartialEq - + fmt::Display - + fmt::Debug, - { - fn all(tests: impl IntoIterator>) { - tests.into_iter().enumerate().for_each(|(i, t)| t.run(i)); - } + #[test] + fn resolve_error_display() { + let err = ResolveError::FailedToParseIndex { + offset: 0, + source: ParseIndexError { + source: "invalid".parse::().unwrap_err(), + }, + }; + assert_eq!(format!("{err}"), "failed to parse index at offset 0"); + + let err = ResolveError::OutOfBounds { + offset: 0, + source: OutOfBoundsError { + index: 1, + length: 0, + }, + }; + assert_eq!(format!("{err}"), "index at offset 0 out of bounds"); - fn run(self, i: usize) { - _ = self; - let Test { - ptr, - data, - expected_result, - } = self; - let ptr = Pointer::from_static(ptr); + let err = ResolveError::NotFound { offset: 0 }; - // cloning the data & expected_result to make comparison easier - let mut data = data.clone(); - let expected_result = expected_result.cloned(); + assert_eq!(format!("{err}"), "pointer starting at offset 0 not found"); - // testing Resolve - let res = data.resolve(ptr).cloned(); - assert_eq!( - &res, &expected_result, - "test #{i} failed:\n\nexpected\n{expected_result:#?}\n\nactual:\n{res:#?}", - ); + let err = ResolveError::Unreachable { offset: 0 }; + assert_eq!( + format!("{err}"), + "pointer starting at offset 0 is unreachable" + ); + } - // testing ResolveMut - let res = data.resolve_mut(ptr).cloned(); - assert_eq!( - &res, &expected_result, - "test #{i} failed:\n\nexpected\n{expected_result:#?}\n\nactual:\n{res:#?}", - ); - } + #[test] + fn resolve_error_offset() { + let err = ResolveError::FailedToParseIndex { + offset: 0, + source: ParseIndexError { + source: "invalid".parse::().unwrap_err(), + }, + }; + assert_eq!(err.offset(), 0); + + let err = ResolveError::OutOfBounds { + offset: 0, + source: OutOfBoundsError { + index: 1, + length: 0, + }, + }; + assert_eq!(err.offset(), 0); + + let err = ResolveError::NotFound { offset: 0 }; + assert_eq!(err.offset(), 0); + + let err = ResolveError::Unreachable { offset: 0 }; + assert_eq!(err.offset(), 0); + } + + #[test] + fn resolve_error_is_unreachable() { + let err = ResolveError::FailedToParseIndex { + offset: 0, + source: ParseIndexError { + source: "invalid".parse::().unwrap_err(), + }, + }; + assert!(!err.is_unreachable()); + + let err = ResolveError::OutOfBounds { + offset: 0, + source: OutOfBoundsError { + index: 1, + length: 0, + }, + }; + assert!(!err.is_unreachable()); + + let err = ResolveError::NotFound { offset: 0 }; + assert!(!err.is_unreachable()); + + let err = ResolveError::Unreachable { offset: 0 }; + assert!(err.is_unreachable()); + } + + #[test] + fn resolve_error_is_not_found() { + let err = ResolveError::FailedToParseIndex { + offset: 0, + source: ParseIndexError { + source: "invalid".parse::().unwrap_err(), + }, + }; + assert!(!err.is_not_found()); + + let err = ResolveError::OutOfBounds { + offset: 0, + source: OutOfBoundsError { + index: 1, + length: 0, + }, + }; + assert!(!err.is_not_found()); + + let err = ResolveError::NotFound { offset: 0 }; + assert!(err.is_not_found()); + + let err = ResolveError::Unreachable { offset: 0 }; + assert!(!err.is_not_found()); + } + + #[test] + fn resolve_error_is_out_of_bounds() { + let err = ResolveError::FailedToParseIndex { + offset: 0, + source: ParseIndexError { + source: "invalid".parse::().unwrap_err(), + }, + }; + assert!(!err.is_out_of_bounds()); + + let err = ResolveError::OutOfBounds { + offset: 0, + source: OutOfBoundsError { + index: 1, + length: 0, + }, + }; + assert!(err.is_out_of_bounds()); + + let err = ResolveError::NotFound { offset: 0 }; + assert!(!err.is_out_of_bounds()); + + let err = ResolveError::Unreachable { offset: 0 }; + assert!(!err.is_out_of_bounds()); + } + + #[test] + fn resolve_error_is_failed_to_parse_index() { + let err = ResolveError::FailedToParseIndex { + offset: 0, + source: ParseIndexError { + source: "invalid".parse::().unwrap_err(), + }, + }; + assert!(err.is_failed_to_parse_index()); + + let err = ResolveError::OutOfBounds { + offset: 0, + source: OutOfBoundsError { + index: 1, + length: 0, + }, + }; + assert!(!err.is_failed_to_parse_index()); + + let err = ResolveError::NotFound { offset: 0 }; + assert!(!err.is_failed_to_parse_index()); + + let err = ResolveError::Unreachable { offset: 0 }; + assert!(!err.is_failed_to_parse_index()); } /* @@ -454,7 +618,7 @@ mod tests { #[test] #[cfg(feature = "json")] - fn test_resolve_json() { + fn resolve_json() { use serde_json::json; let data = &json!({ @@ -476,86 +640,86 @@ mod tests { " ": 7, "m~n": 8 }); - // let data = &test_data; + // let data = &data; Test::all([ // 0 Test { ptr: "", data, - expected_result: Ok(data), + expected: Ok(data), }, // 1 Test { ptr: "/array", data, - expected_result: Ok(data.get("array").unwrap()), // ["bar", "baz"] + expected: Ok(data.get("array").unwrap()), // ["bar", "baz"] }, // 2 Test { ptr: "/array/0", data, - expected_result: Ok(data.get("array").unwrap().get(0).unwrap()), // "bar" + expected: Ok(data.get("array").unwrap().get(0).unwrap()), // "bar" }, // 3 Test { ptr: "/a~1b", data, - expected_result: Ok(data.get("a/b").unwrap()), // 1 + expected: Ok(data.get("a/b").unwrap()), // 1 }, // 4 Test { ptr: "/c%d", data, - expected_result: Ok(data.get("c%d").unwrap()), // 2 + expected: Ok(data.get("c%d").unwrap()), // 2 }, // 5 Test { ptr: "/e^f", data, - expected_result: Ok(data.get("e^f").unwrap()), // 3 + expected: Ok(data.get("e^f").unwrap()), // 3 }, // 6 Test { ptr: "/g|h", data, - expected_result: Ok(data.get("g|h").unwrap()), // 4 + expected: Ok(data.get("g|h").unwrap()), // 4 }, // 7 Test { ptr: "/i\\j", data, - expected_result: Ok(data.get("i\\j").unwrap()), // 5 + expected: Ok(data.get("i\\j").unwrap()), // 5 }, // 8 Test { ptr: "/k\"l", data, - expected_result: Ok(data.get("k\"l").unwrap()), // 6 + expected: Ok(data.get("k\"l").unwrap()), // 6 }, // 9 Test { ptr: "/ ", data, - expected_result: Ok(data.get(" ").unwrap()), // 7 + expected: Ok(data.get(" ").unwrap()), // 7 }, // 10 Test { ptr: "/m~0n", data, - expected_result: Ok(data.get("m~n").unwrap()), // 8 + expected: Ok(data.get("m~n").unwrap()), // 8 }, // 11 Test { ptr: "/object/bool/unresolvable", data, - expected_result: Err(ResolveError::Unreachable { offset: 12 }), + expected: Err(ResolveError::Unreachable { offset: 12 }), }, // 12 Test { ptr: "/object/not_found", data, - expected_result: Err(ResolveError::NotFound { offset: 7 }), + expected: Err(ResolveError::NotFound { offset: 7 }), }, ]); } @@ -567,7 +731,7 @@ mod tests { */ #[test] #[cfg(feature = "toml")] - fn test_resolve_toml() { + fn resolve_toml() { use toml::{toml, Value}; let data = &Value::Table(toml! { @@ -588,87 +752,115 @@ mod tests { " " = 7 "m~n" = 8 }); - // let data = &test_data; + // let data = &data; Test::all([ - // 0 Test { ptr: "", data, - expected_result: Ok(data), + expected: Ok(data), }, - // 1 Test { ptr: "/array", data, - expected_result: Ok(data.get("array").unwrap()), // ["bar", "baz"] + expected: Ok(data.get("array").unwrap()), // ["bar", "baz"] }, - // 2 Test { ptr: "/array/0", data, - expected_result: Ok(data.get("array").unwrap().get(0).unwrap()), // "bar" + expected: Ok(data.get("array").unwrap().get(0).unwrap()), // "bar" }, - // 3 Test { ptr: "/a~1b", data, - expected_result: Ok(data.get("a/b").unwrap()), // 1 + expected: Ok(data.get("a/b").unwrap()), // 1 }, - // 4 Test { ptr: "/c%d", data, - expected_result: Ok(data.get("c%d").unwrap()), // 2 + expected: Ok(data.get("c%d").unwrap()), // 2 }, - // 5 Test { ptr: "/e^f", data, - expected_result: Ok(data.get("e^f").unwrap()), // 3 + expected: Ok(data.get("e^f").unwrap()), // 3 }, - // 6 Test { ptr: "/g|h", data, - expected_result: Ok(data.get("g|h").unwrap()), // 4 + expected: Ok(data.get("g|h").unwrap()), // 4 }, - // 7 Test { ptr: "/i\\j", data, - expected_result: Ok(data.get("i\\j").unwrap()), // 5 + expected: Ok(data.get("i\\j").unwrap()), // 5 }, - // 8 Test { ptr: "/k\"l", data, - expected_result: Ok(data.get("k\"l").unwrap()), // 6 + expected: Ok(data.get("k\"l").unwrap()), // 6 }, - // 9 Test { ptr: "/ ", data, - expected_result: Ok(data.get(" ").unwrap()), // 7 + expected: Ok(data.get(" ").unwrap()), // 7 }, - // 10 Test { ptr: "/m~0n", data, - expected_result: Ok(data.get("m~n").unwrap()), // 8 + expected: Ok(data.get("m~n").unwrap()), // 8 }, - // 11 Test { ptr: "/object/bool/unresolvable", data, - expected_result: Err(ResolveError::Unreachable { offset: 12 }), + expected: Err(ResolveError::Unreachable { offset: 12 }), }, - // 12 Test { ptr: "/object/not_found", data, - expected_result: Err(ResolveError::NotFound { offset: 7 }), + expected: Err(ResolveError::NotFound { offset: 7 }), }, ]); } + struct Test<'v, V> { + ptr: &'static str, + expected: Result<&'v V, ResolveError>, + data: &'v V, + } + + impl<'v, V> Test<'v, V> + where + V: Resolve + + ResolveMut + + Clone + + PartialEq + + fmt::Display + + fmt::Debug, + { + fn all(tests: impl IntoIterator>) { + tests.into_iter().enumerate().for_each(|(i, t)| t.run(i)); + } + + fn run(self, _i: usize) { + _ = self; + let Test { + ptr, + data, + expected, + } = self; + let ptr = Pointer::from_static(ptr); + + // cloning the data & expected to make comparison easier + let mut data = data.clone(); + let expected = expected.cloned(); + + // testing Resolve + let res = data.resolve(ptr).cloned(); + assert_eq!(&res, &expected); + + // testing ResolveMut + let res = data.resolve_mut(ptr).cloned(); + assert_eq!(&res, &expected); + } + } } diff --git a/src/token.rs b/src/token.rs index bafa5b1..5f89f10 100644 --- a/src/token.rs +++ b/src/token.rs @@ -1,6 +1,9 @@ -use crate::{index::Index, InvalidEncodingError, ParseIndexError}; +use core::str::Split; + +use crate::index::{Index, ParseIndexError}; use alloc::{ borrow::Cow, + fmt, string::{String, ToString}, vec::Vec, }; @@ -22,8 +25,9 @@ const SLASH_ENC: u8 = b'1'; ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ */ -/// A `Token` is a segment of a JSON Pointer, seperated by '/' (%x2F). It can -/// represent a key in a JSON object or an index in a JSON array. +/// A `Token` is a segment of a JSON [`Pointer`](crate::Token), preceded by `'/'` (`%x2F`). +/// +/// `Token`s can represent a key in a JSON object or an index in an array. /// /// - Indexes should not contain leading zeros. /// - When dealing with arrays or path expansion for assignment, `"-"` represent @@ -232,7 +236,7 @@ impl<'a> Token<'a> { /// ## Examples /// /// ``` - /// # use jsonptr::{Index, Token}; + /// # use jsonptr::{index::Index, Token}; /// assert_eq!(Token::new("-").to_index(), Ok(Index::Next)); /// assert_eq!(Token::new("0").to_index(), Ok(Index::Num(0))); /// assert_eq!(Token::new("2").to_index(), Ok(Index::Num(2))); @@ -315,6 +319,72 @@ impl alloc::fmt::Display for Token<'_> { } } +/* +░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ +╔══════════════════════════════════════════════════════════════════════════════╗ +║ ║ +║ Tokens ║ +║ ¯¯¯¯¯¯¯¯ ║ +╚══════════════════════════════════════════════════════════════════════════════╝ +░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ +*/ + +/// An iterator over the [`Token`]s of a [`Pointer`](crate::Pointer). +#[derive(Debug)] +pub struct Tokens<'a> { + inner: Split<'a, char>, +} + +impl<'a> Iterator for Tokens<'a> { + type Item = Token<'a>; + fn next(&mut self) -> Option { + self.inner.next().map(Token::from_encoded_unchecked) + } +} +impl<'t> Tokens<'t> { + pub(crate) fn new(inner: Split<'t, char>) -> Self { + Self { inner } + } +} + +/* +░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ +╔══════════════════════════════════════════════════════════════════════════════╗ +║ ║ +║ InvalidEncodingError ║ +║ ¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯ ║ +╚══════════════════════════════════════════════════════════════════════════════╝ +░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ +*/ + +/// A token within a json pointer contained invalid encoding (`~` not followed +/// by `0` or `1`). +/// +#[derive(Debug, PartialEq, Eq)] +pub struct InvalidEncodingError { + /// offset of the erroneous `~` from within the `Token` + pub offset: usize, +} + +impl InvalidEncodingError { + /// The byte offset of the first invalid `~`. + pub fn offset(&self) -> usize { + self.offset + } +} + +impl fmt::Display for InvalidEncodingError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "json pointer is malformed due to invalid encoding ('~' not followed by '0' or '1')" + ) + } +} + +#[cfg(feature = "std")] +impl std::error::Error for InvalidEncodingError {} + /* ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ ╔══════════════════════════════════════════════════════════════════════════════╗ @@ -327,22 +397,116 @@ impl alloc::fmt::Display for Token<'_> { #[cfg(test)] mod tests { + use crate::{assign::AssignError, index::OutOfBoundsError, Pointer}; + use super::*; use quickcheck_macros::quickcheck; #[test] - fn test_from() { + fn from() { assert_eq!(Token::from("/").encoded(), "~1"); assert_eq!(Token::from("~/").encoded(), "~0~1"); + assert_eq!(Token::from(34u32).encoded(), "34"); + assert_eq!(Token::from(34u64).encoded(), "34"); + assert_eq!(Token::from(String::from("foo")).encoded(), "foo"); + assert_eq!(Token::from(&Token::new("foo")).encoded(), "foo"); + } + + #[test] + fn to_index() { + assert_eq!(Token::new("-").to_index(), Ok(Index::Next)); + assert_eq!(Token::new("0").to_index(), Ok(Index::Num(0))); + assert_eq!(Token::new("2").to_index(), Ok(Index::Num(2))); + assert!(Token::new("a").to_index().is_err()); + assert!(Token::new("-1").to_index().is_err()); } #[test] - fn test_from_encoded() { + fn new() { + assert_eq!(Token::new("~1").encoded(), "~01"); + assert_eq!(Token::new("a/b").encoded(), "a~1b"); + } + + #[test] + fn serde() { + let token = Token::from_encoded("foo~0").unwrap(); + let json = serde_json::to_string(&token).unwrap(); + assert_eq!(json, "\"foo~\""); + let deserialized: Token = serde_json::from_str(&json).unwrap(); + assert_eq!(deserialized, token); + } + + #[test] + fn assign_error_display() { + let err = AssignError::FailedToParseIndex { + offset: 3, + source: ParseIndexError { + source: "a".parse::().unwrap_err(), + }, + }; + assert_eq!( + err.to_string(), + "assignment failed due to an invalid index at offset 3" + ); + + let err = AssignError::OutOfBounds { + offset: 3, + source: OutOfBoundsError { + index: 3, + length: 2, + }, + }; + + assert_eq!( + err.to_string(), + "assignment failed due to index at offset 3 being out of bounds" + ); + } + + #[test] + #[cfg(feature = "std")] + fn assign_error_source() { + use std::error::Error; + let err = AssignError::FailedToParseIndex { + offset: 3, + source: ParseIndexError { + source: "a".parse::().unwrap_err(), + }, + }; + assert!(err.source().is_some()); + assert!(err.source().unwrap().is::()); + + let err = AssignError::OutOfBounds { + offset: 3, + source: OutOfBoundsError { + index: 3, + length: 2, + }, + }; + + assert!(err.source().unwrap().is::()); + } + + #[test] + fn from_encoded() { assert_eq!(Token::from_encoded("~1").unwrap().encoded(), "~1"); assert_eq!(Token::from_encoded("~0~1").unwrap().encoded(), "~0~1"); let t = Token::from_encoded("a~1b").unwrap(); assert_eq!(t.decoded(), "a/b"); - let _ = Token::from_encoded("a/b").unwrap_err(); + assert!(Token::from_encoded("a/b").is_err()); + assert!(Token::from_encoded("a~a").is_err()); + } + + #[test] + fn invalid_encoding_offset() { + let err = InvalidEncodingError { offset: 3 }; + assert_eq!(err.offset(), 3); + } + + #[test] + fn into_owned() { + let token = Token::from_encoded("foo~0").unwrap().into_owned(); + assert_eq!(token.encoded(), "foo~0"); } #[quickcheck] @@ -351,4 +515,26 @@ mod tests { let decoded = Token::from_encoded(token.encoded()).unwrap(); token == decoded } + + #[test] + fn invalid_encoding_error_display() { + assert_eq!( + Token::from_encoded("~").unwrap_err().to_string(), + "json pointer is malformed due to invalid encoding ('~' not followed by '0' or '1')" + ); + } + + #[test] + fn tokens() { + let pointer = Pointer::from_static("/a/b/c"); + let tokens: Vec = pointer.tokens().collect(); + assert_eq!( + tokens, + vec![ + Token::from_encoded_unchecked("a"), + Token::from_encoded_unchecked("b"), + Token::from_encoded_unchecked("c") + ] + ); + } } diff --git a/src/tokens.rs b/src/tokens.rs deleted file mode 100644 index ae9c3c6..0000000 --- a/src/tokens.rs +++ /dev/null @@ -1,31 +0,0 @@ -use core::str::Split; - -use crate::Token; - -/* -░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ -╔══════════════════════════════════════════════════════════════════════════════╗ -║ ║ -║ Tokens ║ -║ ¯¯¯¯¯¯¯¯ ║ -╚══════════════════════════════════════════════════════════════════════════════╝ -░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ -*/ - -/// An iterator over the tokens in a Pointer. -#[derive(Debug)] -pub struct Tokens<'a> { - inner: Split<'a, char>, -} - -impl<'a> Iterator for Tokens<'a> { - type Item = Token<'a>; - fn next(&mut self) -> Option { - self.inner.next().map(Token::from_encoded_unchecked) - } -} -impl<'t> Tokens<'t> { - pub(crate) fn new(inner: Split<'t, char>) -> Self { - Self { inner } - } -}