diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..a3e7991 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,58 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [0.4.0] 2023-05-31 + +### Added + +- Adds `CHANGELOG.md` which will be better upkept moving forward. +- Adds `MaybePointer` to assist with deserialization which should not fail fast. + +### Changed + +- `Pointer::new` now accepts a generic list, so `&["example"]` can be replaced by `["example"]`. For untyped, empty slices (i.e. `Pointer::new(&[])`), use `Pointer::default()`. +- `std` is now enabled by default. + +### Removed + +- Removes optional `MalformedPointerError` from `Pointer`. + +## [0.3.6] 2023-05-23 + +### Changed + +- Adds quotes around `Pointer` debug output (#11) + +### Fixed + +- Adds missing `impl std::error::Error` for `Error`, `NotFoundError`, `MalformedError` +- Fixes build for `std` feature flag + +## [0.3.4] 2023-05-11 + +### Added + +- Adds feature flag `fluent-uri` for `From` impl (#3) + +## [0.2.0] 2023-02-24 + +### Changed + +- `std` is now optional +- Adds feature flags `"uniresid"`, `"url"` to enable implementing `From`, `From` (respectively). + +### Removed + +- Removes `Cargo.lock` +- Makes `uniresid` and `uri` optional + +## [0.1.0] - 2022-06-12 + +### Fixed + +- Fixes root pointer representation `""` rather than the erroneous `"/"` +- Fixes an issue where encoded tokens were not being resolved properly diff --git a/Cargo.toml b/Cargo.toml index 92bcf2b..08ae96b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,7 +8,7 @@ keywords = ["json-pointer", "rfc-6901", "6901"] license = "MIT OR Apache-2.0" name = "jsonptr" repository = "https://github.com/chanced/jsonptr" -version = "0.3.6" +version = "0.4.0" [dependencies] fluent-uri = { version = "0.1.4", optional = true, default-features = false } @@ -18,5 +18,5 @@ uniresid = { version = "0.1.4", optional = true } url = { version = "2", optional = true } [features] -default = [] +default = ["std"] std = ["serde/std", "serde_json/std", "fluent-uri?/std"] diff --git a/README.md b/README.md index db4690d..0a7935f 100644 --- a/README.md +++ b/README.md @@ -12,71 +12,105 @@ Data structures and logic for resolving, assigning, and deleting by JSON Pointer JSON Pointers can be created either with a slice of strings or directly from a properly encoded string representing a JSON Pointer. -### Resolve +### Resolve values + +#### `Pointer::resolve` + +```rust +use jsonptr::Pointer; +use serde_json::json; + +let mut data = json!({"foo": { "bar": "baz" }}); +let ptr = Pointer::new(["foo", "bar"]); +let bar = ptr.resolve(&data).unwrap(); +assert_eq!(bar, "baz"); +``` + +#### `Resolve::resolve` ```rust -use jsonptr::{Pointer, Resolve, ResolveMut}; +use jsonptr::{Pointer, Resolve}; use serde_json::json; -fn main() { - let mut data = json!({ - "foo": { - "bar": "baz" - } - }); +let mut data = json!({ "foo": { "bar": "baz" }}); +let ptr = Pointer::new(["foo", "bar"]); +let bar = data.resolve(&ptr).unwrap(); +assert_eq!(bar, "baz"); - let ptr = Pointer::new(&["foo", "bar"]); - let bar = ptr.resolve(&data).unwrap(); - assert_eq!(bar, "baz"); +``` - let bar = data.resolve(&ptr).unwrap(); - assert_eq!(bar, "baz"); +#### `ResolveMut::resolve_mut` - let ptr = Pointer::try_from("/foo/bar").unwrap(); - let mut bar = data.resolve_mut(&ptr).unwrap(); - assert_eq!(bar, "baz"); -} +```rust +use jsonptr::{Pointer, ResolveMut}; +use serde_json::json; +let ptr = Pointer::try_from("/foo/bar").unwrap(); +let mut data = json!({ "foo": { "bar": "baz" }}); +let mut bar = data.resolve_mut(&ptr).unwrap(); +assert_eq!(bar, "baz"); ``` ### Assign +#### `Pointer::assign` + +```rust +use jsonptr::Pointer; +use serde_json::json; + +let ptr = Pointer::try_from("/foo/bar").unwrap(); +let mut data = json!({}); +let _previous = ptr.assign(&mut data, "qux").unwrap(); +assert_eq!(data, json!({ "foo": { "bar": "qux" }})) +``` + +#### `Assign::asign` + ```rust -use jsonptr::{Pointer, Assign}; +use jsonptr::{Assign, Pointer}; use serde_json::json; -fn main() { - let ptr = Pointer::try_from("/foo/bar").unwrap(); - let mut data = json!({}); - let assignment = data.assign(&ptr, "qux"); - assert_eq!(data, json!({ "foo": { "bar": "qux" }})) -} +let ptr = Pointer::try_from("/foo/bar").unwrap(); +let mut data = json!({}); +let _previous = data.assign(&ptr, "qux").unwrap(); +assert_eq!(data, json!({ "foo": { "bar": "qux" }})) ``` ### Delete +#### `Pointer::delete` + +```rust +use jsonptr::Pointer; +use serde_json::json; + +let mut data = json!({ "foo": { "bar": { "baz": "qux" } } }); +let ptr = Pointer::new(&["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); +``` + +#### `Delete::delete` + ```rust - use jsonptr::{Pointer, Delete}; - use serde_json::json; -fn main() { - let mut data = json!({ "foo": { "bar": { "baz": "qux" } } }); - let ptr = Pointer::new(&["foo", "bar", "baz"]); - assert_eq!(data.delete(&ptr), Ok(Some("qux".into()))); - assert_eq!(data, json!({ "foo": { "bar": {} } })); - - // unresolved pointers return Ok(None) - let mut data = json!({}); - let ptr = Pointer::new(&["foo", "bar", "baz"]); - assert_eq!(ptr.delete(&mut data), Ok(None)); - assert_eq!(data, json!({})); - - // replacing a root pointer replaces data with `Value::Null` - let mut data = json!({ "foo": { "bar": "baz" } }); - let ptr = Pointer::default(); - let expected = json!({ "foo": { "bar": "baz" } }); - assert_eq!(data.delete(&ptr), Ok(Some(expected))); - assert!(data.is_null()); -} +use jsonptr::{Pointer, Delete}; +use serde_json::json; + +let mut data = json!({ "foo": { "bar": { "baz": "qux" } } }); +let ptr = Pointer::new(["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::default(); +let deleted = json!({ "foo": { "bar": {} } }); +assert_eq!(data.delete(&ptr), Some(deleted)); +assert!(data.is_null()); ``` ## Feature Flags @@ -97,3 +131,7 @@ If you find an issue, please open a ticket or a pull request. ## License MIT or Apache 2.0. + +``` + +``` diff --git a/src/delete.rs b/src/delete.rs index de2cd2d..a22eeab 100644 --- a/src/delete.rs +++ b/src/delete.rs @@ -7,12 +7,12 @@ pub trait Delete { /// Error associated with `Delete` type Error; /// Attempts to internally delete a value based upon a [Pointer]. - fn delete(&mut self, ptr: &Pointer) -> Result, Self::Error>; + fn delete(&mut self, ptr: &Pointer) -> Option; } impl Delete for Value { type Error = MalformedPointerError; - fn delete(&mut self, ptr: &Pointer) -> Result, Self::Error> { + fn delete(&mut self, ptr: &Pointer) -> Option { ptr.delete(self) } } diff --git a/src/error.rs b/src/error.rs index bc6ea25..447fa2e 100644 --- a/src/error.rs +++ b/src/error.rs @@ -24,8 +24,8 @@ pub enum Error { Unresolvable(UnresolvableError), /// Indicates that a Pointer was not found in the data. NotFound(NotFoundError), - /// Indicates that a Pointer was malformed. - MalformedPointer(MalformedPointerError), + // /// Indicates that a Pointer was malformed. + // MalformedPointer(MalformedPointerError), } impl Error { @@ -41,16 +41,16 @@ impl Error { pub fn is_not_found(&self) -> bool { matches!(self, Error::NotFound(_)) } - /// Returns `true` if the error is `Error::MalformedPointerError`. - pub fn is_malformed_pointer(&self) -> bool { - matches!(self, Error::MalformedPointer(_)) - } -} -impl From for Error { - fn from(err: MalformedPointerError) -> Self { - Error::MalformedPointer(err) - } -} + // /// Returns `true` if the error is `Error::MalformedPointerError`. + // pub fn is_malformed_pointer(&self) -> bool { + // matches!(self, Error::MalformedPointer(_)) + // } +} +// impl From for Error { +// fn from(err: MalformedPointerError) -> Self { +// Error::MalformedPointer(err) +// } +// } impl From for Error { fn from(err: IndexError) -> Self { Error::Index(err) @@ -80,7 +80,7 @@ impl Display for Error { Error::Index(err) => Display::fmt(err, f), Error::Unresolvable(err) => Display::fmt(err, f), Error::NotFound(err) => Display::fmt(err, f), - Error::MalformedPointer(err) => Display::fmt(err, f), + // Error::MalformedPointer(err) => Display::fmt(err, f), } } } diff --git a/src/pointer.rs b/src/pointer.rs index ada4680..83e118a 100644 --- a/src/pointer.rs +++ b/src/pointer.rs @@ -12,13 +12,55 @@ use crate::{ }; use alloc::{ borrow::{Cow, ToOwned}, - boxed::Box, string::{String, ToString}, vec, vec::Vec, }; use serde::{Deserialize, Serialize}; use serde_json::Value; + +/// Helper type for deserialization. Either a valid [`Pointer`] or a string and +/// the parsing error +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum MaybePointer { + /// A valid [`Pointer`]. + Pointer(Pointer), + /// An invalid [`Pointer`] and the error that caused it. + Malformed(String, MalformedPointerError), +} +impl Deref for MaybePointer { + type Target = str; + + fn deref(&self) -> &Self::Target { + match self { + MaybePointer::Pointer(p) => p.as_str(), + MaybePointer::Malformed(s, _) => s, + } + } +} +impl<'de> Deserialize<'de> for MaybePointer { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + String::deserialize(deserializer).map(|s| match Pointer::from_str(&s) { + Ok(ptr) => MaybePointer::Pointer(ptr), + Err(e) => MaybePointer::Malformed(s, e), + }) + } +} +impl Serialize for MaybePointer { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + match self { + MaybePointer::Pointer(ptr) => serializer.serialize_str(ptr.as_str()), + MaybePointer::Malformed(s, _) => serializer.serialize_str(s), + } + } +} + /// A JSON Pointer is a string containing a sequence of zero or more reference /// tokens, each prefixed by a '/' character. /// @@ -50,7 +92,6 @@ assert_eq!(expected, Pointer::try_from(url).unwrap()) pub struct Pointer { inner: String, count: usize, - err: Option>, } impl Pointer { @@ -61,15 +102,20 @@ impl Pointer { Self::default() } /// Creates a new `Pointer` from a slice of non-encoded strings. - pub fn new(tokens: &[impl AsRef]) -> Self { + pub fn new(tokens: V) -> Self + where + V: AsRef<[T]>, + Token: for<'a> From<&'a T>, + { let mut inner = String::new(); - for t in tokens.iter().map(Token::new) { + let tokens = tokens.as_ref(); + + for t in tokens.iter().map(Into::::into) { inner.push('/'); inner.push_str(t.encoded()); } Pointer { inner, - err: None, count: tokens.len(), } } @@ -78,14 +124,6 @@ impl Pointer { &self.inner } - /// Returns `true` if the `Pointer` is valid. The only way an invalid - /// `Pointer` can occur is through deserialization. - /// - /// The JSON Pointer specification - pub fn is_valid(&self) -> bool { - self.err.is_none() - } - /// Pushes a `Token` onto the front of this `Pointer`. pub fn push_front(&mut self, token: Token) { self.count += 1; @@ -200,9 +238,6 @@ impl Pointer { /// Merges two `Pointer`s by appending `other` onto `self`. pub fn append(&mut self, other: &Pointer) -> &Pointer { self.count += other.count; - if self.err.is_none() && other.err.is_some() { - self.err = other.err.clone(); - } if self.is_root() { self.inner = other.inner.clone(); } else if !other.is_root() { @@ -279,14 +314,6 @@ impl Pointer { Tokens::new(self.split()) } - /// Returns the validation error which occurred during deserialization. - pub fn validate(&self) -> Result<(), MalformedPointerError> { - if let Some(err) = self.err.as_deref() { - Err(err.clone()) - } else { - Ok(()) - } - } /// Attempts to resolve a `Value` based on the path in this `Pointer`. pub fn resolve<'v>(&self, value: &'v Value) -> Result<&'v Value, Error> { let Resolved { @@ -398,7 +425,7 @@ impl Pointer { /// /// let mut data = json!({ "foo": { "bar": { "baz": "qux" } } }); /// let ptr = Pointer::new(&["foo", "bar", "baz"]); - /// assert_eq!(data.delete(&ptr), Ok(Some("qux".into()))); + /// assert_eq!(data.delete(&ptr), Some("qux".into())); /// assert_eq!(data, json!({ "foo": { "bar": {} } })); /// ``` /// ### Deleting a non-existent Pointer returns `None`: @@ -408,7 +435,7 @@ impl Pointer { /// /// let mut data = json!({}); /// let ptr = Pointer::new(&["foo", "bar", "baz"]); - /// assert_eq!(ptr.delete(&mut data), Ok(None)); + /// assert_eq!(ptr.delete(&mut data), None); /// assert_eq!(data, json!({})); /// ``` /// ### Deleting a root pointer replaces the value with `Value::Null`: @@ -418,13 +445,12 @@ impl Pointer { /// /// let mut data = json!({ "foo": { "bar": "baz" } }); /// let ptr = Pointer::default(); - /// assert_eq!(data.delete(&ptr), Ok(Some(json!({ "foo": { "bar": "baz" } })))); + /// assert_eq!(data.delete(&ptr), Some(json!({ "foo": { "bar": "baz" } }))); /// assert!(data.is_null()); /// ``` - pub fn delete(&self, value: &mut Value) -> Result, MalformedPointerError> { - self.validate()?; + pub fn delete(&self, value: &mut Value) -> Option { if self.is_root() { - return Ok(Some(mem::replace(value, Value::Null))); + return Some(mem::replace(value, Value::Null)); } let mut ptr = self.clone(); let last_token = ptr.pop_back().unwrap(); @@ -438,17 +464,17 @@ impl Pointer { if remaining.is_root() { match value { Value::Array(arr) => match last_token.as_index(arr.len()) { - Ok(idx) => Ok(Some(arr.remove(idx))), - Err(_) => Ok(None), + Ok(idx) => Some(arr.remove(idx)), + Err(_) => None, }, - Value::Object(obj) => Ok(obj.remove(last_token.as_key())), - _ => Ok(None), + Value::Object(obj) => obj.remove(last_token.as_key()), + _ => None, } } else { - Ok(None) + None } } - Err(_) => Ok(None), + Err(_) => None, } } @@ -649,7 +675,6 @@ impl Pointer { /// /// If the resolution is successful, the pointer will be empty. fn try_resolve_mut<'v>(&self, value: &'v mut Value) -> Result, Error> { - self.validate()?; let mut tokens = self.tokens(); let mut resolved = Pointer::default(); let res = tokens.try_fold((value, self.clone()), |(v, mut p), token| match v { @@ -851,14 +876,11 @@ impl<'de> Deserialize<'de> for Pointer { where D: serde::Deserializer<'de>, { + use serde::de::Error; let s = String::deserialize(deserializer)?; match Pointer::try_from(s.as_str()) { Ok(p) => Ok(p), - Err(err) => Ok(Pointer { - inner: s, - count: 0, - err: Some(Box::new(err)), - }), + Err(err) => Err(D::Error::custom(err)), } } } @@ -874,7 +896,7 @@ impl Borrow for Pointer { } impl From for Pointer { fn from(t: Token) -> Self { - Pointer::new(&[t]) + Pointer::new([t]) } } @@ -1020,7 +1042,7 @@ impl From<&Pointer> for Value { impl From for Pointer { fn from(value: usize) -> Self { - Pointer::new(&[value.to_string()]) + Pointer::new([value]) } } @@ -1028,11 +1050,7 @@ impl TryFrom<&str> for Pointer { type Error = MalformedPointerError; fn try_from(value: &str) -> Result { let (count, inner) = validate_and_format(value)?; - Ok(Pointer { - err: None, - count, - inner, - }) + Ok(Pointer { count, inner }) } } @@ -1328,12 +1346,12 @@ mod tests { #[test] fn test_formatting() { - assert_eq!(Pointer::new(&["foo", "bar"]), "/foo/bar"); + assert_eq!(Pointer::new(["foo", "bar"]), "/foo/bar"); assert_eq!( - Pointer::new(&["~/foo", "~bar", "/baz"]), + Pointer::new(["~/foo", "~bar", "/baz"]), "/~0~1foo/~0bar/~1baz" ); - assert_eq!(Pointer::new(&["field", "", "baz"]), "/field//baz"); + assert_eq!(Pointer::new(["field", "", "baz"]), "/field//baz"); assert_eq!(Pointer::default(), ""); } @@ -1458,7 +1476,7 @@ mod tests { #[test] fn test_resolve_not_found() { let mut data = test_data(); - let ptr = Pointer::new(&["foo", "not_found", "nope"]); + let ptr = Pointer::new(["foo", "not_found", "nope"]); let res = ptr.resolve_mut(&mut data); assert!(res.is_err()); let err = res.unwrap_err(); @@ -1483,7 +1501,7 @@ mod tests { } #[test] fn test_try_from() { - let ptr = Pointer::new(&["foo", "bar", "~/"]); + let ptr = Pointer::new(["foo", "bar", "~/"]); assert_eq!(Pointer::try_from("/foo/bar/~0~1").unwrap(), ptr); let into: Pointer = "/foo/bar/~0~1".try_into().unwrap(); diff --git a/src/token.rs b/src/token.rs index ba12ce8..7765ff0 100644 --- a/src/token.rs +++ b/src/token.rs @@ -147,6 +147,44 @@ impl From<&str> for Token { Token::new(s) } } +impl From<&String> for Token { + fn from(value: &String) -> Self { + Token::new(value.as_str()) + } +} + +impl From<&&str> for Token { + fn from(value: &&str) -> Self { + Token::new(*value) + } +} + +impl From<&usize> for Token { + fn from(value: &usize) -> Self { + Token::new(value.to_string()) + } +} +impl From for Token { + fn from(v: u32) -> Self { + Token::new(v.to_string()) + } +} +impl From<&u32> for Token { + fn from(v: &u32) -> Self { + Token::new(v.to_string()) + } +} +impl From for Token { + fn from(v: u64) -> Self { + Token::new(v.to_string()) + } +} +impl From<&u64> for Token { + fn from(v: &u64) -> Self { + Token::new(v.to_string()) + } +} + impl From for Token { fn from(value: String) -> Self { Token::new(value)