diff --git a/Cargo.lock b/Cargo.lock index 37b342f9e..003ccb152 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3686,6 +3686,7 @@ dependencies = [ "iso8601-timestamp", "pretty_assertions", "serde", + "serde_test", "simd-json", "smol_str", "speedy-uuid", @@ -6247,6 +6248,15 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_test" +version = "1.0.176" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a2f49ace1498612d14f7e0b8245519584db8299541dfe31a06374a828d620ab" +dependencies = [ + "serde", +] + [[package]] name = "serde_urlencoded" version = "0.7.1" diff --git a/crates/kitsune-activitypub/src/deliverer/mod.rs b/crates/kitsune-activitypub/src/deliverer/mod.rs index 7d780a3cc..770df0a2b 100644 --- a/crates/kitsune-activitypub/src/deliverer/mod.rs +++ b/crates/kitsune-activitypub/src/deliverer/mod.rs @@ -21,7 +21,7 @@ use kitsune_db::{ PgPool, }; use kitsune_service::attachment::AttachmentService; -use kitsune_type::ap::{ap_context, helper::StringOrObject, Activity, ActivityType, ObjectField}; +use kitsune_type::ap::{ap_context, Activity, ActivityType, ObjectField}; use kitsune_url::UrlService; use kitsune_util::try_join; use scoped_futures::ScopedFutureExt; @@ -93,7 +93,7 @@ impl Deliverer { context: ap_context(), id: format!("{}#accept", follow.url), r#type: ActivityType::Accept, - actor: StringOrObject::String(followed_account_url), + actor: followed_account_url, object: ObjectField::Url(follow.url), published: Timestamp::now_utc(), }; @@ -280,7 +280,7 @@ impl Deliverer { context: ap_context(), id: format!("{}#reject", follow.url), r#type: ActivityType::Reject, - actor: StringOrObject::String(followed_account_url), + actor: followed_account_url, object: ObjectField::Url(follow.url), published: Timestamp::now_utc(), }; diff --git a/crates/kitsune-activitypub/src/lib.rs b/crates/kitsune-activitypub/src/lib.rs index 28358d173..8222ddcbc 100644 --- a/crates/kitsune-activitypub/src/lib.rs +++ b/crates/kitsune-activitypub/src/lib.rs @@ -201,16 +201,17 @@ async fn preprocess_object( language_detection_config, }: ProcessNewObject<'_>, ) -> Result> { - let attributed_to = object.attributed_to().ok_or(Error::InvalidDocument)?; let user = if let Some(author) = author { CowBox::borrowed(author) } else { - if Uri::try_from(attributed_to)?.authority() != Uri::try_from(&object.id)?.authority() { + if Uri::try_from(&object.attributed_to)?.authority() + != Uri::try_from(&object.id)?.authority() + { return Err(Error::InvalidDocument); } let Some(author) = fetcher - .fetch_account(attributed_to.into()) + .fetch_account(object.attributed_to.as_str().into()) .await .map_err(Error::FetchAccount)? else { diff --git a/crates/kitsune-activitypub/src/mapping/activity.rs b/crates/kitsune-activitypub/src/mapping/activity.rs index a2dd31e3a..aae38deca 100644 --- a/crates/kitsune-activitypub/src/mapping/activity.rs +++ b/crates/kitsune-activitypub/src/mapping/activity.rs @@ -7,7 +7,7 @@ use kitsune_db::{ model::{account::Account, favourite::Favourite, follower::Follow, post::Post}, schema::{accounts, posts}, }; -use kitsune_type::ap::{ap_context, helper::StringOrObject, Activity, ActivityType, ObjectField}; +use kitsune_type::ap::{ap_context, Activity, ActivityType, ObjectField}; use kitsune_util::try_join; use scoped_futures::ScopedFutureExt; use std::future::Future; @@ -34,7 +34,7 @@ impl IntoActivity for Account { context: ap_context(), id: format!("{account_url}#update"), r#type: ActivityType::Update, - actor: StringOrObject::String(account_url), + actor: account_url, object: ObjectField::Actor(self.into_object(state).await?), published: Timestamp::now_utc(), }) @@ -74,7 +74,7 @@ impl IntoActivity for Favourite { context: ap_context(), id: self.url, r#type: ActivityType::Like, - actor: StringOrObject::String(account_url), + actor: account_url, object: ObjectField::Url(post_url), published: self.created_at, }) @@ -96,7 +96,7 @@ impl IntoActivity for Favourite { context: ap_context(), id: format!("{}#undo", self.url), r#type: ActivityType::Undo, - actor: StringOrObject::String(account_url.clone()), + actor: account_url.clone(), object: ObjectField::Activity(self.into_activity(state).await?.into()), published: Timestamp::now_utc(), }) @@ -131,7 +131,7 @@ impl IntoActivity for Follow { Ok(Activity { context: ap_context(), id: self.url, - actor: StringOrObject::String(attributed_to), + actor: attributed_to, r#type: ActivityType::Follow, object: ObjectField::Url(object), published: self.created_at, @@ -154,7 +154,7 @@ impl IntoActivity for Follow { context: ap_context(), id: format!("{}#undo", self.url), r#type: ActivityType::Undo, - actor: StringOrObject::String(attributed_to), + actor: attributed_to, published: self.created_at, object: ObjectField::Activity(self.into_activity(state).await?.into()), }) @@ -184,7 +184,7 @@ impl IntoActivity for Post { context: ap_context(), id: format!("{}/activity", self.url), r#type: ActivityType::Announce, - actor: StringOrObject::String(account_url), + actor: account_url, object: ObjectField::Url(reposted_post_url), published: self.created_at, }) @@ -196,7 +196,7 @@ impl IntoActivity for Post { context: ap_context(), id: format!("{}/activity", object.id), r#type: ActivityType::Create, - actor: StringOrObject::String(account_url), + actor: account_url, published: created_at, object: ObjectField::Object(object), }) @@ -211,7 +211,7 @@ impl IntoActivity for Post { context: ap_context(), id: format!("{}#undo", self.url), r#type: ActivityType::Undo, - actor: StringOrObject::String(account_url), + actor: account_url, object: ObjectField::Url(self.url), published: Timestamp::now_utc(), } @@ -222,7 +222,7 @@ impl IntoActivity for Post { context: ap_context(), id: format!("{}#delete", object.id), r#type: ActivityType::Delete, - actor: StringOrObject::String(account_url), + actor: account_url, published: Timestamp::now_utc(), object: ObjectField::Object(object), } diff --git a/crates/kitsune-activitypub/src/mapping/object.rs b/crates/kitsune-activitypub/src/mapping/object.rs index 969b583b7..37b9e3ff1 100644 --- a/crates/kitsune-activitypub/src/mapping/object.rs +++ b/crates/kitsune-activitypub/src/mapping/object.rs @@ -18,7 +18,7 @@ use kitsune_type::ap::{ ap_context, emoji::Emoji, object::{MediaAttachment, MediaAttachmentType}, - AttributedToField, Object, ObjectType, Tag, TagType, + Object, ObjectType, Tag, TagType, }; use kitsune_util::try_join; use mime::Mime; @@ -177,7 +177,7 @@ impl IntoObject for Post { context: ap_context(), id: self.url, r#type: ObjectType::Note, - attributed_to: AttributedToField::Url(account_url), + attributed_to: account_url, in_reply_to, sensitive: self.is_sensitive, name: None, diff --git a/crates/kitsune-activitypub/tests/fetcher/infinite.rs b/crates/kitsune-activitypub/tests/fetcher/infinite.rs index 769e9c478..108f77a80 100644 --- a/crates/kitsune-activitypub/tests/fetcher/infinite.rs +++ b/crates/kitsune-activitypub/tests/fetcher/infinite.rs @@ -11,7 +11,7 @@ use kitsune_search::NoopSearchService; use kitsune_test::{build_ap_response, database_test, language_detection_config}; use kitsune_type::ap::{ actor::{Actor, ActorType, PublicKey}, - ap_context, AttributedToField, Object, ObjectType, PUBLIC_IDENTIFIER, + ap_context, Object, ObjectType, PUBLIC_IDENTIFIER, }; use kitsune_webfinger::Webfinger; use std::{ @@ -65,7 +65,7 @@ async fn fetch_infinitely_long_reply_chain() { context: ap_context(), id: format!("https://example.com/notes/{note_id}"), r#type: ObjectType::Note, - attributed_to: AttributedToField::Url(author.id.clone()), + attributed_to: author.id.clone(), in_reply_to: Some(format!("https://example.com/notes/{}", note_id + 1)), name: None, summary: None, diff --git a/crates/kitsune-type/Cargo.toml b/crates/kitsune-type/Cargo.toml index ac48b592b..ec0373287 100644 --- a/crates/kitsune-type/Cargo.toml +++ b/crates/kitsune-type/Cargo.toml @@ -15,6 +15,7 @@ utoipa = { version = "4.2.0", features = ["chrono", "uuid"] } [dev-dependencies] pretty_assertions = "1.4.0" +serde_test = "1" [lints] workspace = true diff --git a/crates/kitsune-type/src/ap/actor.rs b/crates/kitsune-type/src/ap/actor.rs index 30c34c237..ad1d012b0 100644 --- a/crates/kitsune-type/src/ap/actor.rs +++ b/crates/kitsune-type/src/ap/actor.rs @@ -1,5 +1,5 @@ use super::object::MediaAttachment; -use crate::jsonld::RdfNode; +use crate::jsonld::{self, RdfNode}; use iso8601_timestamp::Timestamp; use serde::{Deserialize, Serialize}; use simd_json::OwnedValue; @@ -29,20 +29,51 @@ pub struct Actor { #[serde(default, rename = "@context")] pub context: OwnedValue, pub id: String, + #[serde(deserialize_with = "jsonld::serde::FirstOk::deserialize")] pub r#type: ActorType, + #[serde(default)] + #[serde(deserialize_with = "jsonld::serde::Optional::>::deserialize")] pub name: Option, + #[serde(deserialize_with = "jsonld::serde::First::deserialize")] pub preferred_username: String, + #[serde(default)] + #[serde(deserialize_with = "jsonld::serde::Optional::>::deserialize")] pub subject: Option, + #[serde(default)] + #[serde(deserialize_with = "jsonld::serde::Optional::>::deserialize")] pub icon: Option, + #[serde(default)] + #[serde(deserialize_with = "jsonld::serde::Optional::>::deserialize")] pub image: Option, #[serde(default)] + #[serde(deserialize_with = "jsonld::serde::First::deserialize")] pub manually_approves_followers: bool, + #[serde(deserialize_with = "jsonld::serde::First::deserialize")] pub public_key: PublicKey, + #[serde(default)] + #[serde(deserialize_with = "jsonld::serde::Optional::>::deserialize")] pub endpoints: Option, + #[serde(default)] + #[serde( + deserialize_with = "jsonld::serde::Optional::>::deserialize" + )] pub featured: Option, + #[serde(deserialize_with = "jsonld::serde::FirstId::deserialize")] pub inbox: String, + #[serde(default)] + #[serde( + deserialize_with = "jsonld::serde::Optional::>::deserialize" + )] pub outbox: Option, + #[serde(default)] + #[serde( + deserialize_with = "jsonld::serde::Optional::>::deserialize" + )] pub followers: Option, + #[serde(default)] + #[serde( + deserialize_with = "jsonld::serde::Optional::>::deserialize" + )] pub following: Option, #[serde(default = "Timestamp::now_utc")] pub published: Timestamp, @@ -57,6 +88,10 @@ impl RdfNode for Actor { #[derive(Clone, Debug, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct Endpoints { + #[serde(default)] + #[serde( + deserialize_with = "jsonld::serde::Optional::>::deserialize" + )] pub shared_inbox: Option, } @@ -64,6 +99,8 @@ pub struct Endpoints { #[serde(rename_all = "camelCase")] pub struct PublicKey { pub id: String, + #[serde(deserialize_with = "jsonld::serde::FirstId::deserialize")] pub owner: String, + #[serde(deserialize_with = "jsonld::serde::First::deserialize")] pub public_key_pem: String, } diff --git a/crates/kitsune-type/src/ap/collection.rs b/crates/kitsune-type/src/ap/collection.rs index 6fc11731b..a1d71362c 100644 --- a/crates/kitsune-type/src/ap/collection.rs +++ b/crates/kitsune-type/src/ap/collection.rs @@ -1,3 +1,4 @@ +use crate::jsonld; use serde::{Deserialize, Serialize}; use simd_json::OwnedValue; @@ -12,6 +13,7 @@ pub struct Collection { #[serde(default, rename = "@context")] pub context: OwnedValue, pub id: String, + #[serde(deserialize_with = "jsonld::serde::FirstOk::deserialize")] pub r#type: CollectionType, pub total_items: u64, pub first: Option, @@ -29,6 +31,7 @@ pub struct CollectionPage { #[serde(default, rename = "@context")] pub context: OwnedValue, pub id: String, + #[serde(deserialize_with = "jsonld::serde::FirstOk::deserialize")] pub r#type: PageType, pub next: String, pub prev: String, diff --git a/crates/kitsune-type/src/ap/emoji.rs b/crates/kitsune-type/src/ap/emoji.rs index a5b3df61b..36b08c3ea 100644 --- a/crates/kitsune-type/src/ap/emoji.rs +++ b/crates/kitsune-type/src/ap/emoji.rs @@ -1,5 +1,5 @@ use super::object::MediaAttachment; -use crate::jsonld::RdfNode; +use crate::jsonld::{self, RdfNode}; use iso8601_timestamp::Timestamp; use serde::{Deserialize, Serialize}; use simd_json::OwnedValue; @@ -10,8 +10,11 @@ pub struct Emoji { #[serde(default, rename = "@context")] pub context: OwnedValue, pub id: String, + #[serde(deserialize_with = "jsonld::serde::FirstOk::deserialize")] pub r#type: String, + #[serde(deserialize_with = "jsonld::serde::First::deserialize")] pub name: String, + #[serde(deserialize_with = "jsonld::serde::First::deserialize")] pub icon: MediaAttachment, #[serde(default = "Timestamp::now_utc")] pub updated: Timestamp, diff --git a/crates/kitsune-type/src/ap/helper.rs b/crates/kitsune-type/src/ap/helper.rs index 7fd666ec6..0c769287c 100644 --- a/crates/kitsune-type/src/ap/helper.rs +++ b/crates/kitsune-type/src/ap/helper.rs @@ -1,34 +1,4 @@ use super::{Object, PUBLIC_IDENTIFIER}; -use serde::{Deserialize, Serialize}; - -#[derive(Clone, Debug, Deserialize, Serialize)] -#[serde(untagged)] -pub enum StringOrObject { - String(String), - Object(T), -} - -impl StringOrObject { - pub fn into_string(self) -> Option { - match self { - Self::String(str) => Some(str), - Self::Object(..) => None, - } - } - - pub fn into_object(self) -> Option { - match self { - Self::String(..) => None, - Self::Object(obj) => Some(obj), - } - } -} - -impl Default for StringOrObject { - fn default() -> Self { - Self::String(String::new()) - } -} pub trait CcTo { fn cc(&self) -> &[String]; diff --git a/crates/kitsune-type/src/ap/mod.rs b/crates/kitsune-type/src/ap/mod.rs index 19b8a49dc..a42d9513e 100644 --- a/crates/kitsune-type/src/ap/mod.rs +++ b/crates/kitsune-type/src/ap/mod.rs @@ -1,9 +1,5 @@ -use self::{ - actor::{Actor, ActorType}, - helper::StringOrObject, - object::MediaAttachment, -}; -use crate::jsonld::RdfNode; +use self::{actor::Actor, object::MediaAttachment}; +use crate::jsonld::{self, RdfNode}; use iso8601_timestamp::Timestamp; use serde::{Deserialize, Serialize}; use simd_json::{json, OwnedValue}; @@ -51,20 +47,6 @@ pub enum ActivityType { Update, } -#[derive(Clone, Debug, Deserialize, Serialize)] -pub struct AttributedToListEntry { - pub r#type: ActorType, - pub id: String, -} - -#[derive(Clone, Debug, Deserialize, Serialize)] -#[serde(untagged)] -pub enum AttributedToField { - Actor(Actor), - Url(String), - List(Vec), -} - #[derive(Clone, Debug, Deserialize, Serialize)] #[serde(untagged)] pub enum ObjectField { @@ -124,22 +106,16 @@ pub struct Activity { #[serde(default, rename = "@context")] pub context: OwnedValue, pub id: String, + #[serde(deserialize_with = "jsonld::serde::FirstOk::deserialize")] pub r#type: ActivityType, - pub actor: StringOrObject, + #[serde(deserialize_with = "jsonld::serde::FirstId::deserialize")] + pub actor: String, pub object: ObjectField, #[serde(default = "Timestamp::now_utc")] pub published: Timestamp, } impl Activity { - #[must_use] - pub fn actor(&self) -> &str { - match self.actor { - StringOrObject::Object(ref obj) => &obj.id, - StringOrObject::String(ref url) => url, - } - } - #[must_use] pub fn object(&self) -> &str { match self.object { @@ -173,37 +149,42 @@ pub struct Object { #[serde(default, rename = "@context")] pub context: OwnedValue, pub id: String, + #[serde(deserialize_with = "jsonld::serde::FirstOk::deserialize")] pub r#type: ObjectType, - pub attributed_to: AttributedToField, + #[serde(deserialize_with = "jsonld::serde::FirstId::deserialize")] + pub attributed_to: String, + #[serde(default)] + #[serde( + deserialize_with = "jsonld::serde::Optional::>::deserialize" + )] pub in_reply_to: Option, + #[serde(default)] + #[serde(deserialize_with = "jsonld::serde::Optional::>::deserialize")] pub name: Option, + #[serde(default)] + #[serde(deserialize_with = "jsonld::serde::Optional::>::deserialize")] pub summary: Option, + #[serde(deserialize_with = "jsonld::serde::First::deserialize")] pub content: String, pub media_type: Option, #[serde(default)] + #[serde(deserialize_with = "jsonld::serde::Set::deserialize")] pub attachment: Vec, #[serde(default)] + #[serde(deserialize_with = "jsonld::serde::Set::deserialize")] pub tag: Vec, #[serde(default)] + #[serde(deserialize_with = "jsonld::serde::First::deserialize")] pub sensitive: bool, pub published: Timestamp, #[serde(default)] + #[serde(deserialize_with = "jsonld::serde::IdSet::deserialize")] pub to: Vec, #[serde(default)] + #[serde(deserialize_with = "jsonld::serde::IdSet::deserialize")] pub cc: Vec, } -impl Object { - #[must_use] - pub fn attributed_to(&self) -> Option<&str> { - match self.attributed_to { - AttributedToField::Actor(ref actor) => Some(&actor.id), - AttributedToField::Url(ref url) => Some(url), - AttributedToField::List(ref list) => list.iter().map(|item| item.id.as_str()).next(), - } - } -} - impl RdfNode for Object { fn id(&self) -> Option<&str> { Some(&self.id) @@ -220,8 +201,12 @@ pub enum TagType { #[derive(Clone, Debug, Deserialize, Serialize)] pub struct Tag { pub id: Option, + #[serde(deserialize_with = "jsonld::serde::FirstOk::deserialize")] pub r#type: TagType, + #[serde(deserialize_with = "jsonld::serde::First::deserialize")] pub name: String, pub href: Option, + #[serde(default)] + #[serde(deserialize_with = "jsonld::serde::Optional::>::deserialize")] pub icon: Option, } diff --git a/crates/kitsune-type/src/ap/object.rs b/crates/kitsune-type/src/ap/object.rs index fc996a793..b32089954 100644 --- a/crates/kitsune-type/src/ap/object.rs +++ b/crates/kitsune-type/src/ap/object.rs @@ -1,3 +1,4 @@ +use crate::jsonld; use serde::{Deserialize, Serialize}; #[derive(Clone, Debug, Deserialize, Serialize)] @@ -11,9 +12,15 @@ pub enum MediaAttachmentType { #[derive(Clone, Debug, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct MediaAttachment { + #[serde(deserialize_with = "jsonld::serde::FirstOk::deserialize")] pub r#type: MediaAttachmentType, + #[serde(default)] + #[serde(deserialize_with = "jsonld::serde::Optional::>::deserialize")] pub name: Option, pub media_type: Option, + #[serde(default)] + #[serde(deserialize_with = "jsonld::serde::Optional::>::deserialize")] pub blurhash: Option, + #[serde(deserialize_with = "jsonld::serde::First::deserialize")] pub url: String, } diff --git a/crates/kitsune-type/src/jsonld.rs b/crates/kitsune-type/src/jsonld/mod.rs similarity index 70% rename from crates/kitsune-type/src/jsonld.rs rename to crates/kitsune-type/src/jsonld/mod.rs index 5d569ac46..254293f61 100644 --- a/crates/kitsune-type/src/jsonld.rs +++ b/crates/kitsune-type/src/jsonld/mod.rs @@ -1,3 +1,5 @@ +pub(crate) mod serde; + pub trait RdfNode { fn id(&self) -> Option<&str>; } diff --git a/crates/kitsune-type/src/jsonld/serde/first.rs b/crates/kitsune-type/src/jsonld/serde/first.rs new file mode 100644 index 000000000..5f67821c3 --- /dev/null +++ b/crates/kitsune-type/src/jsonld/serde/first.rs @@ -0,0 +1,154 @@ +use super::OptionSeed; +use core::{ + fmt::{self, Formatter}, + marker::PhantomData, +}; +use serde::de::{ + self, Deserialize, DeserializeSeed, Deserializer, IgnoredAny, IntoDeserializer, SeqAccess, +}; + +/// Deserialises the first element of a JSON-LD set. +pub struct First { + seed: T, +} + +struct Visitor(T); + +impl<'de, T> First> +where + T: Deserialize<'de>, +{ + pub fn new() -> Self { + Self::with_seed(PhantomData) + } + + pub fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + Self::new().deserialize(deserializer) + } +} + +impl<'de, T> First +where + T: DeserializeSeed<'de>, +{ + pub fn with_seed(seed: T) -> Self { + Self { seed } + } +} + +// XXX: Intentionally limiting to `First>` rather than `First<_>` to help inference +// of the type parameter of `Optional::>::deserialize`. +impl<'de, T> Default for First> +where + T: Deserialize<'de>, +{ + fn default() -> Self { + Self::new() + } +} + +impl<'de, T> DeserializeSeed<'de> for First +where + T: DeserializeSeed<'de>, +{ + type Value = T::Value; + + fn deserialize(self, deserializer: D) -> Result + where + D: Deserializer<'de>, + { + deserializer.deserialize_any(Visitor(self.seed)) + } +} + +impl<'de, T> de::Visitor<'de> for Visitor +where + T: DeserializeSeed<'de>, +{ + type Value = T::Value; + + fn expecting(&self, f: &mut Formatter<'_>) -> fmt::Result { + f.write_str(super::EXPECTING_SET) + } + + fn visit_seq(self, mut seq: A) -> Result + where + A: SeqAccess<'de>, + { + let mut seed = OptionSeed(Some(self.0)); + let value = if let Some(value) = seq.next_element_seed(&mut seed)? { + // Unwrapping is fine here because the first call to `OptionSeed::deserialize` always + // returns a `Some` and `next_element_seed` can only call it at most once because its + // signature takes the seed by value. + let value = value.unwrap(); + while let Some(IgnoredAny) = seq.next_element()? {} + value + } else if let Some(seed) = seed.0 { + seed.deserialize(().into_deserializer())? + } else { + // Weirdly, the `SeqAccess` has consumed the seed yet it didn't return a value. + return Err(de::Error::invalid_length(0, &super::EXPECTING_SET)); + }; + + Ok(value) + } + + forward_to_into_deserializer! { + fn visit_bool(bool); + fn visit_i8(i8); + fn visit_i16(i16); + fn visit_i32(i32); + fn visit_i64(i64); + fn visit_i128(i128); + fn visit_u8(u8); + fn visit_u16(u16); + fn visit_u32(u32); + fn visit_u64(u64); + fn visit_u128(u128); + fn visit_f32(f32); + fn visit_f64(f64); + fn visit_char(char); + fn visit_str(&str); + fn visit_borrowed_str(&'de str); + fn visit_string(String); + fn visit_bytes(&[u8]); + fn visit_borrowed_bytes(&'de [u8]); + fn visit_byte_buf(Vec); + fn visit_none(); + fn visit_some(); + fn visit_unit(); + fn visit_newtype_struct(); + fn visit_map(); + fn visit_enum(); + } +} + +#[cfg(test)] +mod tests { + use super::{super::into_deserializer, First}; + use core::marker::PhantomData; + + #[test] + fn single() { + let data = 42; + assert_eq!(First::deserialize(into_deserializer(data)), Ok(data)); + } + + #[test] + fn seq() { + let data = vec![42, 21]; + assert_eq!(First::deserialize(into_deserializer(data)), Ok(42)); + } + + #[test] + fn empty() { + let data: Vec = Vec::new(); + assert_eq!( + First::>>::deserialize(into_deserializer(data)), + Ok(None) + ); + } +} diff --git a/crates/kitsune-type/src/jsonld/serde/first_id.rs b/crates/kitsune-type/src/jsonld/serde/first_id.rs new file mode 100644 index 000000000..ebde40ee5 --- /dev/null +++ b/crates/kitsune-type/src/jsonld/serde/first_id.rs @@ -0,0 +1,122 @@ +use super::{First, Id}; +use core::marker::PhantomData; +use serde::de::{Deserialize, DeserializeSeed, Deserializer}; + +/// Deserialises the node identifier string of the first element of a JSON-LD set. +pub struct FirstId { + seed: T, +} + +impl<'de, T> FirstId> +where + T: Deserialize<'de>, +{ + pub fn new() -> Self { + Self::with_seed(PhantomData) + } + + pub fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + Self::new().deserialize(deserializer) + } +} + +impl<'de, T> FirstId +where + T: DeserializeSeed<'de>, +{ + pub fn with_seed(seed: T) -> Self { + Self { seed } + } +} + +impl<'de, T> Default for FirstId> +where + T: Deserialize<'de>, +{ + fn default() -> Self { + Self::new() + } +} + +impl<'de, T> DeserializeSeed<'de> for FirstId +where + T: DeserializeSeed<'de>, +{ + type Value = T::Value; + + fn deserialize(self, deserializer: D) -> Result + where + D: Deserializer<'de>, + { + First::with_seed(Id::with_seed(self.seed)).deserialize(deserializer) + } +} + +#[cfg(test)] +mod tests { + use super::{super::into_deserializer, FirstId}; + use serde::Deserialize; + use serde_test::{assert_de_tokens, Token}; + use std::collections::HashMap; + + #[test] + fn single_string() { + let data = "http://example.com/"; + assert_eq!( + FirstId::deserialize(into_deserializer(data)), + Ok(data.to_owned()) + ); + } + + #[test] + fn single_embedded() { + let data: HashMap<_, _> = [("id", "http://example.com/")].into_iter().collect(); + assert_eq!( + FirstId::deserialize(into_deserializer(data)), + Ok("http://example.com/".to_owned()) + ); + } + + #[test] + fn seq() { + #[derive(Debug, Deserialize, PartialEq)] + #[serde(transparent)] + struct Test { + #[serde(deserialize_with = "FirstId::deserialize")] + term: String, + } + + assert_de_tokens( + &Test { + term: "http://example.com/1".to_owned(), + }, + &[ + Token::Seq { len: Some(2) }, + Token::Str("http://example.com/1"), + Token::Map { len: None }, + Token::Str("id"), + Token::Str("http://example.com/2"), + Token::MapEnd, + Token::SeqEnd, + ], + ); + + assert_de_tokens( + &Test { + term: "http://example.com/1".to_owned(), + }, + &[ + Token::Seq { len: Some(2) }, + Token::Map { len: None }, + Token::Str("id"), + Token::Str("http://example.com/1"), + Token::MapEnd, + Token::Str("http://example.com/2"), + Token::SeqEnd, + ], + ); + } +} diff --git a/crates/kitsune-type/src/jsonld/serde/first_ok.rs b/crates/kitsune-type/src/jsonld/serde/first_ok.rs new file mode 100644 index 000000000..46bd6f3ba --- /dev/null +++ b/crates/kitsune-type/src/jsonld/serde/first_ok.rs @@ -0,0 +1,263 @@ +use super::CatchError; +use core::{ + fmt::{self, Formatter}, + marker::PhantomData, +}; +use serde::de::{ + self, + value::{EnumAccessDeserializer, MapAccessDeserializer}, + Deserialize, DeserializeSeed, Deserializer, EnumAccess, IgnoredAny, IntoDeserializer, + MapAccess, SeqAccess, +}; + +// XXX: Conceptually, we could decompose it into `First` and a helper type that filters successfully +// deserialised elements in a JSON-LD set. In practice, however, the latter type cannot be +// implemented (at least straightforwardly) because it would need to hook the +// `SeqAccess::next_element_seed` method, where we cannot clone the generic seed value like we're +// doing in `Visitor::visit_seq` below. + +/// Deserialises a single element from a JSON-LD set. +/// +/// It tries to deserialise each of the elements in the set and returns the first one successfully +/// deserialised. +/// +/// The detection of recoverable errors is a "best effort" check and won't work for maps for +/// example, although it works for strings. It's suitable for tag-like fields like `"type"`. +pub struct FirstOk { + seed: T, +} + +struct Visitor(T); + +impl<'de, T> FirstOk> +where + T: Deserialize<'de>, +{ + pub fn new() -> Self { + Self::with_seed(PhantomData) + } + + pub fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + Self::new().deserialize(deserializer) + } +} + +impl<'de, T> FirstOk +where + T: DeserializeSeed<'de> + Clone, +{ + pub fn with_seed(seed: T) -> Self { + Self { seed } + } +} + +impl<'de, T> Default for FirstOk> +where + T: Deserialize<'de>, +{ + fn default() -> Self { + Self::new() + } +} + +impl<'de, T> DeserializeSeed<'de> for FirstOk +where + T: DeserializeSeed<'de> + Clone, +{ + type Value = T::Value; + + fn deserialize(self, deserializer: D) -> Result + where + D: Deserializer<'de>, + { + deserializer.deserialize_any(Visitor(self.seed)) + } +} + +macro_rules! forward_to_into_deserializer { + ($(fn $name:ident($T:ty);)*) => {$( + fn $name(self, v: $T) -> Result + where + E: de::Error, + { + self.0 + .clone() + .deserialize(serde::de::IntoDeserializer::into_deserializer(v)) + // No (deserialisable) element in the (single-value) set. + // Interpret it as equivalent to `null` according to the JSON-LD data model. + .or_else(|_: E| self.0.deserialize(serde::de::IntoDeserializer::into_deserializer(()))) + } + )*}; +} + +impl<'de, T> de::Visitor<'de> for Visitor +where + T: DeserializeSeed<'de> + Clone, +{ + type Value = T::Value; + + fn expecting(&self, f: &mut Formatter<'_>) -> fmt::Result { + f.write_str(super::EXPECTING_SET) + } + + fn visit_seq(self, mut seq: A) -> Result + where + A: SeqAccess<'de>, + { + loop { + match seq.next_element_seed(CatchError::<_, A::Error>::new(self.0.clone()))? { + Some(Ok(value)) => { + while let Some(IgnoredAny) = seq.next_element()? {} + return Ok(value); + } + Some(Err(_)) => {} + None => return self.0.deserialize(().into_deserializer()), + } + } + } + + fn visit_borrowed_str(self, v: &'de str) -> Result + where + E: de::Error, + { + self.0 + .clone() + .deserialize(de::value::BorrowedStrDeserializer::new(v)) + .or_else(|_: E| self.0.deserialize(().into_deserializer())) + } + + fn visit_borrowed_bytes(self, v: &'de [u8]) -> Result + where + E: de::Error, + { + self.0 + .clone() + .deserialize(de::value::BorrowedBytesDeserializer::new(v)) + .or_else(|_: E| self.0.deserialize(().into_deserializer())) + } + + fn visit_none(self) -> Result + where + E: de::Error, + { + self.0.deserialize(().into_deserializer()) + } + + fn visit_some(self, deserializer: D) -> Result + where + D: Deserializer<'de>, + { + FirstOk::with_seed(self.0).deserialize(deserializer) + } + + fn visit_unit(self) -> Result + where + E: de::Error, + { + self.0.deserialize(().into_deserializer()) + } + + fn visit_newtype_struct(self, deserializer: D) -> Result + where + D: Deserializer<'de>, + { + FirstOk::with_seed(self.0).deserialize(deserializer) + } + + fn visit_map(self, map: A) -> Result + where + A: MapAccess<'de>, + { + self.0.deserialize(MapAccessDeserializer::new(map)) + } + + fn visit_enum(self, data: A) -> Result + where + A: EnumAccess<'de>, + { + self.0.deserialize(EnumAccessDeserializer::new(data)) + } + + forward_to_into_deserializer! { + fn visit_bool(bool); + fn visit_i8(i8); + fn visit_i16(i16); + fn visit_i32(i32); + fn visit_i64(i64); + fn visit_i128(i128); + fn visit_u8(u8); + fn visit_u16(u16); + fn visit_u32(u32); + fn visit_u64(u64); + fn visit_u128(u128); + fn visit_f32(f32); + fn visit_f64(f64); + fn visit_char(char); + fn visit_str(&str); + fn visit_string(String); + fn visit_bytes(&[u8]); + fn visit_byte_buf(Vec); + } +} + +#[cfg(test)] +mod tests { + use serde::Deserialize; + + use super::super::into_deserializer; + use super::FirstOk; + + #[test] + fn simple() { + #[derive(Debug, Deserialize, PartialEq)] + enum Test { + A, + } + let data = "A"; + assert_eq!(FirstOk::deserialize(into_deserializer(data)), Ok(Test::A)); + } + + #[test] + fn simple_fail() { + #[derive(Debug, Deserialize, PartialEq)] + enum Test { + A, + } + + let data = "B"; + assert_eq!( + FirstOk::deserialize(into_deserializer(data)), + Ok(None::) + ); + } + + #[test] + fn seq() { + #[derive(Debug, Deserialize, PartialEq)] + enum Test { + A, + B, + } + + let data = vec!["C", "B", "A"]; + assert_eq!(FirstOk::deserialize(into_deserializer(data)), Ok(Test::B)); + } + + #[test] + fn seq_fail() { + #[derive(Debug, Deserialize, PartialEq)] + enum Test { + A, + B, + } + + let data = vec!["C", "D"]; + assert_eq!( + FirstOk::deserialize(into_deserializer(data)), + Ok(None::) + ); + } +} diff --git a/crates/kitsune-type/src/jsonld/serde/id.rs b/crates/kitsune-type/src/jsonld/serde/id.rs new file mode 100644 index 000000000..fce185f73 --- /dev/null +++ b/crates/kitsune-type/src/jsonld/serde/id.rs @@ -0,0 +1,205 @@ +use core::{ + fmt::{self, Formatter}, + marker::PhantomData, +}; +use serde::{ + de::{ + self, value::SeqAccessDeserializer, DeserializeSeed, Deserializer, IgnoredAny, MapAccess, + SeqAccess, + }, + Deserialize, +}; + +/// Deserialises a single node identifier string or a set of node identifier strings. +pub struct Id { + seed: T, +} + +struct Visitor(T); + +#[cfg_attr(not(test), allow(dead_code))] +impl<'de, T> Id> +where + T: Deserialize<'de>, +{ + pub fn new() -> Self { + Self::with_seed(PhantomData) + } + + pub fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + Self::new().deserialize(deserializer) + } +} + +impl<'de, T> Id +where + T: DeserializeSeed<'de>, +{ + pub fn with_seed(seed: T) -> Self { + Self { seed } + } +} + +impl<'de, T> Default for Id> +where + T: Deserialize<'de>, +{ + fn default() -> Self { + Self::new() + } +} + +impl<'de, T> DeserializeSeed<'de> for Id +where + T: DeserializeSeed<'de>, +{ + type Value = T::Value; + + fn deserialize(self, deserializer: D) -> Result + where + D: Deserializer<'de>, + { + deserializer.deserialize_any(Visitor(self.seed)) + } +} + +impl<'de, T> de::Visitor<'de> for Visitor +where + T: DeserializeSeed<'de>, +{ + type Value = T::Value; + + fn expecting(&self, f: &mut Formatter<'_>) -> fmt::Result { + f.write_str("a JSON-LD node") + } + + fn visit_map(self, mut map: A) -> Result + where + A: MapAccess<'de>, + { + #[derive(Deserialize)] + #[serde(field_identifier, rename_all = "camelCase")] + enum Key { + #[serde(alias = "@id")] + Id, + #[serde(other)] + Other, + } + + while let Some(key) = map.next_key()? { + match key { + Key::Id => { + let value = map.next_value_seed(self.0)?; + while let Some((IgnoredAny, IgnoredAny)) = map.next_entry()? {} + return Ok(value); + } + Key::Other => { + let IgnoredAny = map.next_value()?; + } + } + } + + Err(de::Error::missing_field("id")) + } + + fn visit_seq(self, seq: A) -> Result + where + A: SeqAccess<'de>, + { + struct SeqAccess(A); + + impl<'de, A> de::SeqAccess<'de> for SeqAccess + where + A: de::SeqAccess<'de>, + { + type Error = A::Error; + + fn next_element_seed(&mut self, seed: T) -> Result, Self::Error> + where + T: DeserializeSeed<'de>, + { + self.0.next_element_seed(Id::with_seed(seed)) + } + + fn size_hint(&self) -> Option { + self.0.size_hint() + } + } + + self.0 + .deserialize(SeqAccessDeserializer::new(SeqAccess(seq))) + } + + forward_to_into_deserializer! { + fn visit_str(&str); + fn visit_borrowed_str(&'de str); + fn visit_string(String); + fn visit_bytes(&[u8]); + fn visit_borrowed_bytes(&'de [u8]); + fn visit_byte_buf(Vec); + } +} + +#[cfg(test)] +mod tests { + use super::{super::into_deserializer, Id}; + use core::marker::PhantomData; + use serde::Deserialize; + use serde_test::{assert_de_tokens, Token}; + use std::collections::HashMap; + + #[test] + fn single() { + let data = "http://example.com/"; + assert_eq!( + Id::deserialize(into_deserializer(data)), + Ok(data.to_owned()) + ); + } + + #[test] + fn single_embedded() { + let data: HashMap<_, _> = [("id", "http://example.com/")].into_iter().collect(); + assert_eq!( + Id::deserialize(into_deserializer(data)), + Ok("http://example.com/".to_owned()) + ); + } + + #[test] + fn embedded_missing_id() { + let data: HashMap<_, _> = [("foo", "http://example.com/")].into_iter().collect(); + assert!(Id::>::deserialize(into_deserializer(data)).is_err()); + } + + #[test] + fn seq() { + #[derive(Debug, Deserialize, PartialEq)] + #[serde(transparent)] + struct Test { + #[serde(deserialize_with = "Id::deserialize")] + term: Vec, + } + + assert_de_tokens( + &Test { + term: vec![ + "http://example.com/1".to_owned(), + "http://example.com/2".to_owned(), + ], + }, + &[ + Token::Seq { len: Some(2) }, + Token::Str("http://example.com/1"), + Token::Map { len: None }, + Token::Str("id"), + Token::Str("http://example.com/2"), + Token::MapEnd, + Token::SeqEnd, + ], + ); + } +} diff --git a/crates/kitsune-type/src/jsonld/serde/id_set.rs b/crates/kitsune-type/src/jsonld/serde/id_set.rs new file mode 100644 index 000000000..d63fe2ebf --- /dev/null +++ b/crates/kitsune-type/src/jsonld/serde/id_set.rs @@ -0,0 +1,110 @@ +use super::{Id, Set}; +use core::marker::PhantomData; +use serde::de::{Deserialize, DeserializeSeed, Deserializer}; + +/// Deserialises a JSON-LD set of nodes as a sequence of node identifier strings. +pub struct IdSet { + seed: T, +} + +impl<'de, T> IdSet> +where + T: Deserialize<'de>, +{ + pub fn new() -> Self { + Self::with_seed(PhantomData) + } + + pub fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + Self::new().deserialize(deserializer) + } +} + +impl<'de, T> IdSet +where + T: DeserializeSeed<'de>, +{ + pub fn with_seed(seed: T) -> Self { + Self { seed } + } +} + +impl<'de, T> Default for IdSet> +where + T: Deserialize<'de>, +{ + fn default() -> Self { + Self::new() + } +} + +impl<'de, T> DeserializeSeed<'de> for IdSet +where + T: DeserializeSeed<'de>, +{ + type Value = T::Value; + + fn deserialize(self, deserializer: D) -> Result + where + D: Deserializer<'de>, + { + Set::with_seed(Id::with_seed(self.seed)).deserialize(deserializer) + } +} + +#[cfg(test)] +mod tests { + use super::{super::into_deserializer, IdSet}; + use serde::Deserialize; + use serde_test::{assert_de_tokens, Token}; + use std::collections::HashMap; + + #[test] + fn single_string() { + let data = "http://example.com/"; + assert_eq!( + IdSet::deserialize(into_deserializer(data)), + Ok(vec![data.to_owned()]) + ); + } + + #[test] + fn single_embedded() { + let data: HashMap<_, _> = [("id", "http://example.com/")].into_iter().collect(); + assert_eq!( + IdSet::deserialize(into_deserializer(data)), + Ok(vec!["http://example.com/".to_owned()]) + ); + } + + #[test] + fn seq() { + #[derive(Debug, Deserialize, PartialEq)] + #[serde(transparent)] + struct Test { + #[serde(deserialize_with = "IdSet::deserialize")] + term: Vec, + } + + assert_de_tokens( + &Test { + term: vec![ + "http://example.com/1".to_owned(), + "http://example.com/2".to_owned(), + ], + }, + &[ + Token::Seq { len: Some(2) }, + Token::Str("http://example.com/1"), + Token::Map { len: None }, + Token::Str("id"), + Token::Str("http://example.com/2"), + Token::MapEnd, + Token::SeqEnd, + ], + ); + } +} diff --git a/crates/kitsune-type/src/jsonld/serde/mod.rs b/crates/kitsune-type/src/jsonld/serde/mod.rs new file mode 100644 index 000000000..15380a423 --- /dev/null +++ b/crates/kitsune-type/src/jsonld/serde/mod.rs @@ -0,0 +1,468 @@ +//! Serde helpers to translate JSON-LD data structures. +//! +//! ## JSON-LD `@set` +//! +//! When a JSON-LD term's `@container` is unspecified or is set to `@set`, JSON entry values in the +//! following groups are semantically equivalent, respectively: +//! +//! - A non-array value (`"value"`) and a single-value array of the same value (`["value"]`) +//! - An empty array (`[]`), `null` and an absent entry +//! +//! The following helpers in the module deserialise a set as a sequence: +//! +//! - [`Set`] +//! - [`IdSet`] +//! +//! The following helpers deserialise a single value or `null` from a set: +//! +//! - [`First`] +//! - [`FirstId`] +//! - [`FirstOk`] +//! +//! ## JSON-LD `@id` +//! +//! When a JSON-LD term's `@type` is set to `@id`, a JSON entry value of a single (IRI) string +//! (`"http://example.com/"`) is a shorthand for a Linked Data node identified by that string +//! (`{"@id": "http://example.com/"}`. +//! +//! The following helpers deserialise the node identifier string(s): +//! +//! - [`Id`] +//! - [`FirstId`] +//! - [`IdSet`] + +macro_rules! forward_to_into_deserializer { + ( + fn visit_borrowed_str($T:ty); + $($rest:tt)* + ) => { + fn visit_borrowed_str(self, v: $T) -> Result + where + E: serde::de::Error, + { + self.0.deserialize(serde::de::value::BorrowedStrDeserializer::new(v)) + } + forward_to_into_deserializer! { $($rest)* } + }; + ( + fn visit_borrowed_bytes($T:ty); + $($rest:tt)* + ) => { + fn visit_borrowed_bytes(self, v: $T) -> Result + where + E: serde::de::Error, + { + self.0.deserialize(serde::de::value::BorrowedBytesDeserializer::new(v)) + } + forward_to_into_deserializer! { $($rest)* } + }; + ( + fn visit_none(); + $($rest:tt)* + ) => { + fn visit_none(self) -> Result + where + E: serde::de::Error, + { + self.0.deserialize(serde::de::IntoDeserializer::into_deserializer(())) + } + forward_to_into_deserializer! { $($rest)* } + }; + ( + fn visit_some(); + $($rest:tt)* + ) => { + fn visit_some(self, deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + self.0.deserialize(deserializer) + } + forward_to_into_deserializer! { $($rest)* } + }; + ( + fn visit_unit(); + $($rest:tt)* + ) => { + fn visit_unit(self) -> Result + where + E: serde::de::Error, + { + self.0.deserialize(serde::de::IntoDeserializer::into_deserializer(())) + } + forward_to_into_deserializer! { $($rest)* } + }; + ( + fn visit_newtype_struct(); + $($rest:tt)* + ) => { + fn visit_newtype_struct(self, deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + self.0.deserialize(deserializer) + } + forward_to_into_deserializer! { $($rest)* } + }; + ( + fn visit_seq(); + $($rest:tt)* + ) => { + fn visit_seq(self, seq: A) -> Result + where + A: serde::de::SeqAccess<'de>, + { + self.0.deserialize(serde::de::value::SeqAccessDeserializer::new(seq)) + } + forward_to_into_deserializer! { $($rest)* } + }; + ( + fn visit_map(); + $($rest:tt)* + ) => { + fn visit_map(self, map: A) -> Result + where + A: serde::de::MapAccess<'de>, + { + self.0.deserialize(serde::de::value::MapAccessDeserializer::new(map)) + } + forward_to_into_deserializer! { $($rest)* } + }; + ( + fn visit_enum(); + $($rest:tt)* + ) => { + fn visit_enum(self, data: A) -> Result + where + A: serde::de::EnumAccess<'de>, + { + self.0.deserialize(serde::de::value::EnumAccessDeserializer::new(data)) + } + forward_to_into_deserializer! { $($rest)* } + }; + ( + fn $name:ident($T:ty); + $($rest:tt)* + ) => { + fn $name(self, v: $T) -> Result + where + E: serde::de::Error, + { + self.0.deserialize(serde::de::IntoDeserializer::into_deserializer(v)) + } + forward_to_into_deserializer! { $($rest)* } + }; + () => {}; +} + +mod first; +mod first_id; +mod first_ok; +mod id; +mod id_set; +mod optional; +mod set; + +pub use self::first::First; +pub use self::first_id::FirstId; +pub use self::first_ok::FirstOk; +pub use self::id::Id; +pub use self::id_set::IdSet; +pub use self::optional::Optional; +pub use self::set::Set; + +use core::{ + fmt::{self, Formatter}, + marker::PhantomData, +}; +use serde::de::{ + self, + value::{EnumAccessDeserializer, MapAccessDeserializer, SeqAccessDeserializer}, + DeserializeSeed, Deserializer, EnumAccess, IntoDeserializer, MapAccess, SeqAccess, Visitor, +}; + +const EXPECTING_SET: &str = "a JSON-LD set"; + +/// A wrapper to implement `IntoDeserializer` for an `impl Deserializer`, because Serde somehow +/// doesn't provide a blanket impl. +struct DeserializerIntoDeserializer(D); + +/// A `DeserializeSeed` that catches a recoverable error and returns it as a successful value. +struct CatchError { + seed: T, + marker: PhantomData E>, +} + +struct OptionSeed(Option); + +impl<'de, D> IntoDeserializer<'de, D::Error> for DeserializerIntoDeserializer +where + D: Deserializer<'de>, +{ + type Deserializer = D; + + fn into_deserializer(self) -> Self::Deserializer { + self.0 + } +} + +impl<'de, T> DeserializeSeed<'de> for &mut OptionSeed +where + T: DeserializeSeed<'de>, +{ + type Value = Option; + + fn deserialize(self, deserializer: D) -> Result + where + D: Deserializer<'de>, + { + if let Some(seed) = self.0.take() { + seed.deserialize(deserializer).map(Some) + } else { + Ok(None) + } + } +} + +impl<'de, T, E> CatchError +where + T: DeserializeSeed<'de>, + E: de::Error, +{ + pub fn new(seed: T) -> Self { + Self { + seed, + marker: PhantomData, + } + } +} + +impl<'de, T, E> DeserializeSeed<'de> for CatchError +where + T: DeserializeSeed<'de>, + E: de::Error, +{ + type Value = Result; + + fn deserialize(self, deserializer: D) -> Result + where + D: Deserializer<'de>, + { + deserializer.deserialize_any(self) + } +} + +macro_rules! catch_error_forward_to_into_deserializer { + ($(fn $name:ident($T:ty);)*) => {$( + fn $name(self, v: $T) -> Result { + // We can tell that the error isn't fatal to the deserialiser because it's originated + // from the already deserialised value `$t` rather than the deserialiser. + Ok(self.seed.deserialize(serde::de::IntoDeserializer::into_deserializer(v))) + } + )*}; +} + +impl<'de, T, E> Visitor<'de> for CatchError +where + T: DeserializeSeed<'de>, + E: de::Error, +{ + type Value = Result; + + fn expecting(&self, f: &mut Formatter<'_>) -> fmt::Result { + f.write_str("a value") + } + + fn visit_borrowed_str(self, v: &'de str) -> Result + where + E: de::Error, + { + Ok(self + .seed + .deserialize(de::value::BorrowedStrDeserializer::new(v))) + } + + fn visit_borrowed_bytes(self, v: &'de [u8]) -> Result + where + E: de::Error, + { + Ok(self + .seed + .deserialize(de::value::BorrowedBytesDeserializer::new(v))) + } + + fn visit_none(self) -> Result { + Ok(self.seed.deserialize(().into_deserializer())) + } + + fn visit_some(self, deserializer: D) -> Result + where + D: Deserializer<'de>, + { + self.deserialize(deserializer) + } + + fn visit_unit(self) -> Result { + Ok(self.seed.deserialize(().into_deserializer())) + } + + fn visit_newtype_struct(self, deserializer: D) -> Result + where + D: Deserializer<'de>, + { + self.deserialize(deserializer) + } + + // XXX: The following methods cannot determine whether an error is recoverable. While we might + // be able to implement them _right_ way by hooking into the `*Access` trait implementations and + // recursively applying `CatchError` in the `*_seed` method calls, that wouldn't be worth the + // effort since we're currently not using `FirstOk` (the only user of `CatchError` as of now) + // for these types, and as for `visit_seq`, JSON-LD doesn't support nested sets anyway. + fn visit_seq(self, seq: A) -> Result + where + A: SeqAccess<'de>, + { + self.seed + .deserialize(SeqAccessDeserializer::new(seq)) + .map(Ok) + } + + fn visit_map(self, map: A) -> Result + where + A: MapAccess<'de>, + { + self.seed + .deserialize(MapAccessDeserializer::new(map)) + .map(Ok) + } + + fn visit_enum(self, data: A) -> Result + where + A: EnumAccess<'de>, + { + self.seed + .deserialize(EnumAccessDeserializer::new(data)) + .map(Ok) + } + + catch_error_forward_to_into_deserializer! { + fn visit_bool(bool); + fn visit_i8(i8); + fn visit_i16(i16); + fn visit_i32(i32); + fn visit_i64(i64); + fn visit_i128(i128); + fn visit_u8(u8); + fn visit_u16(u16); + fn visit_u32(u32); + fn visit_u64(u64); + fn visit_u128(u128); + fn visit_f32(f32); + fn visit_f64(f64); + fn visit_char(char); + fn visit_str(&str); + fn visit_string(String); + fn visit_bytes(&[u8]); + fn visit_byte_buf(Vec); + } +} + +#[cfg(test)] +fn into_deserializer<'de, T>(value: T) -> T::Deserializer +where + T: serde::de::IntoDeserializer<'de, serde::de::value::Error>, +{ + serde::de::IntoDeserializer::into_deserializer(value) +} + +#[cfg(test)] +mod tests { + use super::{First, FirstId, FirstOk, IdSet, Optional}; + use serde::Deserialize; + + /// Checks that the types work for some random real-world-ish use cases. + #[test] + fn integrate() { + #[derive(Debug, Deserialize, PartialEq)] + enum Type { + Note, + } + + #[derive(Debug, Deserialize, PartialEq)] + #[serde(rename_all = "camelCase")] + struct Object { + id: String, + #[serde(deserialize_with = "FirstOk::deserialize")] + r#type: Type, + #[serde(deserialize_with = "FirstId::deserialize")] + attributed_to: String, + #[serde(default)] + #[serde(deserialize_with = "Optional::>::deserialize")] + summary: Option, + #[serde(default)] + #[serde(deserialize_with = "Optional::>::deserialize")] + content: Option, + #[serde(default)] + #[serde(deserialize_with = "IdSet::deserialize")] + to: Vec, + #[serde(default)] + #[serde(deserialize_with = "IdSet::deserialize")] + cc: Vec, + } + + let object = br#" + { + "@context": "https://www.w3.org/ns/activitystreams", + "id": "https://example.com/notes/1", + "type": "Note", + "attributedTo": "https://example.com/actors/1", + "summary": "An ordinary Note", + "content": "Hello, world!", + "to": ["https://example.com/actors/2"], + "cc": ["https://www.w3.org/ns/activitystreams#Public"] + } + "#; + let expected = Object { + id: "https://example.com/notes/1".to_owned(), + r#type: Type::Note, + attributed_to: "https://example.com/actors/1".to_owned(), + summary: Some("An ordinary Note".to_owned()), + content: Some("Hello, world!".to_owned()), + to: vec!["https://example.com/actors/2".to_owned()], + cc: vec!["https://www.w3.org/ns/activitystreams#Public".to_owned()], + }; + assert_eq!(simd_json::from_slice(&mut object.to_owned()), Ok(expected)); + + let object = br#" + { + "@context": "https://www.w3.org/ns/activitystreams", + "id": "https://example.com/notes/1", + "type": ["http://schema.org/CreativeWork", "Note"], + "attributedTo": [ + { + "id": "https://example.com/actors/1", + "type": "Person" + }, + "https://example.com/actors/2" + ], + "summary": "A quirky Note", + "to": "https://example.com/actors/3" + } + "#; + let expected = Object { + id: "https://example.com/notes/1".to_owned(), + // Multiple `type`s including unknown ones: + r#type: Type::Note, + // Multiple `attributedTo`s and an embedded node: + attributed_to: "https://example.com/actors/1".to_owned(), + summary: Some("A quirky Note".to_owned()), + // Absent `Option` field: + content: None, + // Single-value set: + to: vec!["https://example.com/actors/3".to_owned()], + // Absent `serde(default)` field: + cc: vec![], + }; + assert_eq!(simd_json::from_slice(&mut object.to_owned()), Ok(expected)); + } +} diff --git a/crates/kitsune-type/src/jsonld/serde/optional.rs b/crates/kitsune-type/src/jsonld/serde/optional.rs new file mode 100644 index 000000000..42fd59455 --- /dev/null +++ b/crates/kitsune-type/src/jsonld/serde/optional.rs @@ -0,0 +1,79 @@ +use core::fmt::{self, Formatter}; +use serde::de::{self, DeserializeSeed, Deserializer}; + +/// Deserialises an `Option` value. +/// +/// Workaround until Serde introduces a native mechanism for applying the +/// `#[serde(deserialize_with)]` attribute to the type inside an `Option<_>`. +/// +/// cf. . +pub struct Optional { + seed: T, +} + +struct Visitor(T); + +impl<'de, T> Optional +where + T: DeserializeSeed<'de> + Default, +{ + pub fn new() -> Self { + Self::with_seed(T::default()) + } + + pub fn deserialize(deserializer: D) -> Result, D::Error> + where + D: Deserializer<'de>, + { + Self::new().deserialize(deserializer) + } +} + +impl<'de, T> Optional +where + T: DeserializeSeed<'de>, +{ + pub fn with_seed(seed: T) -> Self { + Self { seed } + } +} + +impl<'de, T> DeserializeSeed<'de> for Optional +where + T: DeserializeSeed<'de>, +{ + type Value = Option; + + fn deserialize(self, deserializer: D) -> Result + where + D: Deserializer<'de>, + { + deserializer.deserialize_option(Visitor(self.seed)) + } +} + +impl<'de, T> de::Visitor<'de> for Visitor +where + T: DeserializeSeed<'de>, +{ + type Value = Option; + + fn expecting(&self, f: &mut Formatter<'_>) -> fmt::Result { + f.write_str("option") + } + + fn visit_unit(self) -> Result { + Ok(None) + } + + fn visit_none(self) -> Result { + Ok(None) + } + + fn visit_some(self, deserializer: D) -> Result + where + D: Deserializer<'de>, + { + self.0.deserialize(deserializer).map(Some) + } +} diff --git a/crates/kitsune-type/src/jsonld/serde/set.rs b/crates/kitsune-type/src/jsonld/serde/set.rs new file mode 100644 index 000000000..4659d1836 --- /dev/null +++ b/crates/kitsune-type/src/jsonld/serde/set.rs @@ -0,0 +1,222 @@ +use super::DeserializerIntoDeserializer; +use core::{ + fmt::{self, Formatter}, + iter, + marker::PhantomData, +}; +use serde::de::{ + self, + value::{ + BorrowedBytesDeserializer, BorrowedStrDeserializer, EnumAccessDeserializer, + MapAccessDeserializer, SeqAccessDeserializer, SeqDeserializer, + }, + Deserialize, DeserializeSeed, Deserializer, EnumAccess, MapAccess, SeqAccess, +}; + +/// Deserialises a JSON-LD set as a sequence. +pub struct Set { + seed: T, +} + +struct Visitor(T); + +impl<'de, T> Set> +where + T: Deserialize<'de>, +{ + pub fn new() -> Self { + Self::with_seed(PhantomData) + } + + pub fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + Self::new().deserialize(deserializer) + } +} + +impl<'de, T> Set +where + T: DeserializeSeed<'de>, +{ + pub fn with_seed(seed: T) -> Self { + Self { seed } + } +} + +impl<'de, T> Default for Set> +where + T: Deserialize<'de>, +{ + fn default() -> Self { + Self::new() + } +} + +impl<'de, T> DeserializeSeed<'de> for Set +where + T: DeserializeSeed<'de>, +{ + type Value = T::Value; + + fn deserialize(self, deserializer: D) -> Result + where + D: Deserializer<'de>, + { + deserializer.deserialize_any(Visitor(self.seed)) + } +} + +macro_rules! forward_to_seq_deserializer { + ($(fn $name:ident($T:ty);)*) => {$( + fn $name(self, v: $T) -> Result + where + E: serde::de::Error, + { + self.0.deserialize(serde::de::value::SeqDeserializer::new(core::iter::once(v))) + } + )*}; +} + +impl<'de, T> de::Visitor<'de> for Visitor +where + T: DeserializeSeed<'de>, +{ + type Value = T::Value; + + fn expecting(&self, f: &mut Formatter<'_>) -> fmt::Result { + f.write_str(super::EXPECTING_SET) + } + + fn visit_borrowed_str(self, v: &'de str) -> Result + where + E: de::Error, + { + let iter = iter::once(DeserializerIntoDeserializer(BorrowedStrDeserializer::new( + v, + ))); + self.0.deserialize(SeqDeserializer::new(iter)) + } + + fn visit_borrowed_bytes(self, v: &'de [u8]) -> Result + where + E: de::Error, + { + let iter = iter::once(DeserializerIntoDeserializer( + BorrowedBytesDeserializer::new(v), + )); + self.0.deserialize(SeqDeserializer::new(iter)) + } + + fn visit_seq(self, seq: A) -> Result + where + A: SeqAccess<'de>, + { + self.0.deserialize(SeqAccessDeserializer::new(seq)) + } + + fn visit_none(self) -> Result + where + E: de::Error, + { + self.visit_unit() + } + + fn visit_some(self, deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let iter = iter::once(DeserializerIntoDeserializer(deserializer)); + self.0.deserialize(SeqDeserializer::new(iter)) + } + + fn visit_unit(self) -> Result + where + E: de::Error, + { + // TODO: Use `!` (`Infallible`) when it implements `IntoDeserializer`. + let iter = iter::empty::<()>(); + self.0.deserialize(SeqDeserializer::new(iter)) + } + + fn visit_newtype_struct(self, deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let iter = iter::once(DeserializerIntoDeserializer(deserializer)); + self.0.deserialize(SeqDeserializer::new(iter)) + } + + fn visit_map(self, map: A) -> Result + where + A: MapAccess<'de>, + { + let iter = iter::once(DeserializerIntoDeserializer(MapAccessDeserializer::new( + map, + ))); + self.0.deserialize(SeqDeserializer::new(iter)) + } + + fn visit_enum(self, data: A) -> Result + where + A: EnumAccess<'de>, + { + let iter = iter::once(DeserializerIntoDeserializer(EnumAccessDeserializer::new( + data, + ))); + self.0.deserialize(SeqDeserializer::new(iter)) + } + + forward_to_seq_deserializer! { + fn visit_bool(bool); + fn visit_i8(i8); + fn visit_i16(i16); + fn visit_i32(i32); + fn visit_i64(i64); + fn visit_i128(i128); + fn visit_u8(u8); + fn visit_u16(u16); + fn visit_u32(u32); + fn visit_u64(u64); + fn visit_u128(u128); + fn visit_f32(f32); + fn visit_f64(f64); + fn visit_char(char); + fn visit_str(&str); + fn visit_string(String); + fn visit_byte_buf(Vec); + } +} + +#[cfg(test)] +mod tests { + use super::{super::into_deserializer, Set}; + + #[test] + fn single() { + let data = 42; + assert_eq!(Set::deserialize(into_deserializer(data)), Ok(vec![data])); + } + + #[test] + fn seq() { + let data = vec![42, 21]; + assert_eq!(Set::deserialize(into_deserializer(data.clone())), Ok(data)); + } + + #[test] + fn empty() { + let data: Vec = Vec::new(); + assert_eq!(Set::deserialize(into_deserializer(data.clone())), Ok(data)); + } + + #[test] + fn unit() { + let data = (); + assert_eq!( + Set::deserialize(into_deserializer(data)), + Ok(Vec::::new()) + ); + } +} diff --git a/kitsune/src/http/extractor/signed_activity.rs b/kitsune/src/http/extractor/signed_activity.rs index 2f043953c..0e0230340 100644 --- a/kitsune/src/http/extractor/signed_activity.rs +++ b/kitsune/src/http/extractor/signed_activity.rs @@ -51,7 +51,7 @@ impl FromRequest for SignedActivity { } }; - let ap_id = activity.actor(); + let ap_id = activity.actor.as_str(); let Some(remote_user) = state .fetcher .fetch_account(ap_id.into())