diff --git a/Cargo.toml b/Cargo.toml index 8ae49db31..a61dda79d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -83,7 +83,7 @@ static-iref = "3.0" rdf-types = "0.22.3" xsd-types = "0.9.4" locspan = "0.8" -json-syntax = "0.12.2" +json-syntax = "0.12.5" nquads-syntax = "0.19" multibase = "0.9.1" serde = "1.0" diff --git a/crates/claims/crates/vc/src/syntax/credential.rs b/crates/claims/crates/vc/src/syntax/credential.rs index 57a21fd9e..e93ecd5a1 100644 --- a/crates/claims/crates/vc/src/syntax/credential.rs +++ b/crates/claims/crates/vc/src/syntax/credential.rs @@ -14,7 +14,7 @@ use crate::{v1, v2, MaybeIdentified}; /// /// If you care about required context and/or type, use the /// [`AnySpecializedJsonCredential`] type directly. -pub type AnyJsonCredential = AnySpecializedJsonCredential; +pub type AnyJsonCredential = AnySpecializedJsonCredential; /// Any JSON credential using VCDM v1 or v2 with custom required contexts and /// types. @@ -29,7 +29,7 @@ pub type AnyJsonCredential = AnySpecializedJsonCredentia deserialize = "S: Deserialize<'de>, C: RequiredContextList, T: RequiredTypeSet" ) )] -pub enum AnySpecializedJsonCredential { +pub enum AnySpecializedJsonCredential { V1(v1::syntax::SpecializedJsonCredential), V2(v2::syntax::SpecializedJsonCredential), } diff --git a/crates/claims/crates/vc/src/syntax/mod.rs b/crates/claims/crates/vc/src/syntax/mod.rs index 914a2576a..ef2fecf1b 100644 --- a/crates/claims/crates/vc/src/syntax/mod.rs +++ b/crates/claims/crates/vc/src/syntax/mod.rs @@ -1,5 +1,6 @@ mod context; mod credential; +mod non_empty_object; mod presentation; mod types; @@ -8,6 +9,7 @@ use std::collections::BTreeMap; pub use context::*; pub use credential::*; use iref::{Uri, UriBuf}; +pub use non_empty_object::*; pub use presentation::*; use serde::{Deserialize, Serialize}; pub use types::*; diff --git a/crates/claims/crates/vc/src/syntax/non_empty_object.rs b/crates/claims/crates/vc/src/syntax/non_empty_object.rs new file mode 100644 index 000000000..17268fdd2 --- /dev/null +++ b/crates/claims/crates/vc/src/syntax/non_empty_object.rs @@ -0,0 +1,240 @@ +use json_syntax::{ + object::{ + Duplicate, Entry, Equivalent, IterMut, Key, RemovedByInsertFront, RemovedByInsertion, + RemovedEntries, ValuesMut, + }, + Object, Value, +}; +use serde::Serialize; +use std::{borrow::Borrow, hash::Hash, ops::Deref}; + +/// Non-empty JSON object. +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)] +#[serde(transparent)] +pub struct NonEmptyObject(Object); + +impl NonEmptyObject { + pub fn try_from_object(object: Object) -> Result { + if object.is_empty() { + Err(EmptyObject) + } else { + Ok(Self(object)) + } + } + + pub fn as_object(&self) -> &Object { + &self.0 + } + + pub fn into_object(self) -> Object { + self.0 + } + + pub fn iter_mut(&mut self) -> IterMut { + self.0.iter_mut() + } + + /// Returns an iterator over the values matching the given key. + /// + /// Runs in `O(1)` (average). + pub fn get_mut(&mut self, key: &Q) -> ValuesMut + where + Q: ?Sized + Hash + Equivalent, + { + self.0.get_mut(key) + } + + /// Returns the unique entry value matching the given key. + /// + /// Returns an error if multiple entries match the key. + /// + /// Runs in `O(1)` (average). + pub fn get_unique_mut(&mut self, key: &Q) -> Result, Duplicate<&Entry>> + where + Q: ?Sized + Hash + Equivalent, + { + self.0.get_unique_mut(key) + } + + /// Returns the (first) value associated to `key`, or insert a `key`-`value` + /// entry where `value` is returned by the given function `f`. + pub fn get_or_insert_with(&mut self, key: &Q, f: impl FnOnce() -> Value) -> &Value + where + Q: ?Sized + Hash + Equivalent + ToOwned, + Q::Owned: Into, + { + self.0.get_or_insert_with(key, f) + } + + /// Returns a mutable reference to the (first) value associated to `key`, or + /// insert a `key`-`value` entry where `value` is returned by the given + /// function `f`. + pub fn get_mut_or_insert_with(&mut self, key: &Q, f: impl FnOnce() -> Value) -> &mut Value + where + Q: ?Sized + Hash + Equivalent + ToOwned, + Q::Owned: Into, + { + self.0.get_mut_or_insert_with(key, f) + } + + /// Push the given key-value pair to the end of the object. + /// + /// Returns `true` if the key was not already present in the object, + /// and `false` otherwise. + /// Any previous entry matching the key is **not** overridden: duplicates + /// are preserved, in order. + /// + /// Runs in `O(1)`. + pub fn push(&mut self, key: Key, value: Value) -> bool { + self.0.push(key, value) + } + + pub fn push_entry(&mut self, entry: Entry) -> bool { + self.0.push_entry(entry) + } + + /// Push the given key-value pair to the top of the object. + /// + /// Returns `true` if the key was not already present in the object, + /// and `false` otherwise. + /// Any previous entry matching the key is **not** overridden: duplicates + /// are preserved, in order. + /// + /// Runs in `O(n)`. + pub fn push_front(&mut self, key: Key, value: Value) -> bool { + self.0.push_front(key, value) + } + + pub fn push_entry_front(&mut self, entry: Entry) -> bool { + self.0.push_entry_front(entry) + } + + /// Inserts the given key-value pair. + /// + /// If one or more entries are already matching the given key, + /// all of them are removed and returned in the resulting iterator. + /// Otherwise, `None` is returned. + pub fn insert(&mut self, key: Key, value: Value) -> Option { + self.0.insert(key, value) + } + + /// Inserts the given key-value pair on top of the object. + /// + /// If one or more entries are already matching the given key, + /// all of them are removed and returned in the resulting iterator. + pub fn insert_front(&mut self, key: Key, value: Value) -> RemovedByInsertFront { + self.0.insert_front(key, value) + } + + /// Sort the entries by key name. + /// + /// Entries with the same key are sorted by value. + pub fn sort(&mut self) { + self.0.sort() + } + + /// Tries to remove the entry at the given index. + /// + /// Returns an error if this would leave the object empty. + pub fn try_remove_at(&mut self, index: usize) -> Result, EmptyObject> { + if index == 0 && self.0.len() == 1 { + Err(EmptyObject) + } else { + Ok(self.0.remove_at(index)) + } + } + + /// Tries to remove all entries associated to the given key. + /// + /// Returns an error if this would leave the object empty. + /// + /// Runs in `O(n)` time (average). + pub fn try_remove<'q, Q>( + &mut self, + key: &'q Q, + ) -> Result, EmptyObject> + where + Q: ?Sized + Hash + Equivalent, + { + if self.iter().all(|e| key.equivalent(&e.key)) { + Err(EmptyObject) + } else { + Ok(self.0.remove(key)) + } + } + + /// Tries to remove the unique entry associated to the given key. + /// + /// Returns an error if multiple entries match the key, or if the object + /// would be left empty. + /// + /// Runs in `O(n)` time (average). + pub fn try_remove_unique(&mut self, key: &Q) -> Result, RemoveUniqueError> + where + Q: ?Sized + Hash + Equivalent, + { + if self.iter().all(|e| key.equivalent(&e.key)) { + Err(RemoveUniqueError::EmptyObject) + } else { + self.0.remove_unique(key).map_err(Into::into) + } + } +} + +#[derive(Debug, thiserror::Error)] +#[error("empty object")] +pub struct EmptyObject; + +#[derive(Debug, thiserror::Error)] +pub enum RemoveUniqueError { + #[error(transparent)] + DuplicateEntry(#[from] Duplicate), + + #[error("empty object")] + EmptyObject, +} + +impl Deref for NonEmptyObject { + type Target = Object; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl Borrow for NonEmptyObject { + fn borrow(&self) -> &Object { + self.as_object() + } +} + +impl AsRef for NonEmptyObject { + fn as_ref(&self) -> &Object { + self.as_object() + } +} + +impl From for Object { + fn from(value: NonEmptyObject) -> Self { + value.into_object() + } +} + +impl TryFrom for NonEmptyObject { + type Error = EmptyObject; + + fn try_from(value: Object) -> Result { + Self::try_from_object(value) + } +} + +impl<'de> serde::Deserialize<'de> for NonEmptyObject { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + Object::deserialize(deserializer)? + .try_into() + .map_err(serde::de::Error::custom) + } +} diff --git a/crates/claims/crates/vc/src/v1/syntax/credential.rs b/crates/claims/crates/vc/src/v1/syntax/credential.rs index 7607d68cc..5c3c70a76 100644 --- a/crates/claims/crates/vc/src/v1/syntax/credential.rs +++ b/crates/claims/crates/vc/src/v1/syntax/credential.rs @@ -34,7 +34,7 @@ pub type JsonCredentialTypes = Types; /// /// If you care about required context and/or type, use the /// [`SpecializedJsonCredential`] type directly. -pub type JsonCredential = SpecializedJsonCredential; +pub type JsonCredential = SpecializedJsonCredential; /// Specialized JSON Credential with custom required context and type. /// @@ -45,7 +45,7 @@ pub type JsonCredential = SpecializedJsonCredential; serialize = "S: Serialize", deserialize = "S: Deserialize<'de>, C: RequiredContextList, T: RequiredTypeSet" ))] -pub struct SpecializedJsonCredential { +pub struct SpecializedJsonCredential { /// JSON-LD context. #[serde(rename = "@context")] pub context: Context, diff --git a/crates/claims/crates/vc/src/v2/syntax/credential.rs b/crates/claims/crates/vc/src/v2/syntax/credential.rs index 0aa2b19ed..b821fba57 100644 --- a/crates/claims/crates/vc/src/v2/syntax/credential.rs +++ b/crates/claims/crates/vc/src/v2/syntax/credential.rs @@ -3,7 +3,7 @@ use std::{borrow::Cow, collections::BTreeMap, hash::Hash}; use super::{Context, InternationalString, RelatedResource}; use crate::syntax::{ not_null, value_or_array, IdOr, IdentifiedObject, IdentifiedTypedObject, - MaybeIdentifiedTypedObject, RequiredContextList, RequiredTypeSet, TypedObject, + MaybeIdentifiedTypedObject, NonEmptyObject, RequiredContextList, RequiredTypeSet, TypedObject, }; use iref::{Uri, UriBuf}; use rdf_types::VocabularyMut; @@ -19,7 +19,7 @@ pub use crate::v1::syntax::{CredentialType, JsonCredentialTypes, VERIFIABLE_CRED /// /// If you care about required context and/or type, use the /// [`SpecializedJsonCredential`] type directly. -pub type JsonCredential = SpecializedJsonCredential; +pub type JsonCredential = SpecializedJsonCredential; /// Specialized JSON Credential with custom required context and type. /// @@ -30,7 +30,7 @@ pub type JsonCredential = SpecializedJsonCredential; serialize = "S: Serialize", deserialize = "S: Deserialize<'de>, C: RequiredContextList, T: RequiredTypeSet" ))] -pub struct SpecializedJsonCredential { +pub struct SpecializedJsonCredential { /// JSON-LD context. #[serde(rename = "@context")] pub context: Context,