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
+
+
[ ](https://github.com/chanced/jsonptr)
[ ](https://crates.io/crates/jsonptr)
@@ -6,126 +10,155 @@
[ ](https://github.com/chanced/jsonptr/actions?query=branch%3Amain)
[ ](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 }
- }
-}