mirror of https://github.com/kitsune-soc/kitsune
Add helpers to deserialise JSON-LD in a more robust way (#492)
* Add helpers to deserialise JSON-LD in a more robust way * Add brief introduction to JSON-LD data model * Fix deserialisation of `Option<_>` fields Workaround for <https://github.com/serde-rs/serde/issues/723>. * Remove `AttributedToListEntry` type * Minor cleanup * Replace `&x[..]` with `x.as_str()` Per review comment at: <https://github.com/kitsune-soc/kitsune/pull/492#discussion_r1502774399>. Co-Authored-By: Aumetra Weisman <aumetra@cryptolab.net> --------- Co-authored-by: Aumetra Weisman <aumetra@cryptolab.net>
This commit is contained in:
parent
a919bdf7d1
commit
3879c49f8b
|
@ -3687,6 +3687,7 @@ dependencies = [
|
|||
"iso8601-timestamp",
|
||||
"pretty_assertions",
|
||||
"serde",
|
||||
"serde_test",
|
||||
"simd-json",
|
||||
"smol_str",
|
||||
"speedy-uuid",
|
||||
|
@ -6248,6 +6249,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"
|
||||
|
|
|
@ -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(),
|
||||
};
|
||||
|
|
|
@ -201,16 +201,17 @@ async fn preprocess_object(
|
|||
language_detection_config,
|
||||
}: ProcessNewObject<'_>,
|
||||
) -> Result<PreprocessedObject<'_>> {
|
||||
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 {
|
||||
|
|
|
@ -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),
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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::<jsonld::serde::First<_>>::deserialize")]
|
||||
pub name: Option<String>,
|
||||
#[serde(deserialize_with = "jsonld::serde::First::deserialize")]
|
||||
pub preferred_username: String,
|
||||
#[serde(default)]
|
||||
#[serde(deserialize_with = "jsonld::serde::Optional::<jsonld::serde::First<_>>::deserialize")]
|
||||
pub subject: Option<String>,
|
||||
#[serde(default)]
|
||||
#[serde(deserialize_with = "jsonld::serde::Optional::<jsonld::serde::First<_>>::deserialize")]
|
||||
pub icon: Option<MediaAttachment>,
|
||||
#[serde(default)]
|
||||
#[serde(deserialize_with = "jsonld::serde::Optional::<jsonld::serde::First<_>>::deserialize")]
|
||||
pub image: Option<MediaAttachment>,
|
||||
#[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::<jsonld::serde::First<_>>::deserialize")]
|
||||
pub endpoints: Option<Endpoints>,
|
||||
#[serde(default)]
|
||||
#[serde(
|
||||
deserialize_with = "jsonld::serde::Optional::<jsonld::serde::FirstId<_>>::deserialize"
|
||||
)]
|
||||
pub featured: Option<String>,
|
||||
#[serde(deserialize_with = "jsonld::serde::FirstId::deserialize")]
|
||||
pub inbox: String,
|
||||
#[serde(default)]
|
||||
#[serde(
|
||||
deserialize_with = "jsonld::serde::Optional::<jsonld::serde::FirstId<_>>::deserialize"
|
||||
)]
|
||||
pub outbox: Option<String>,
|
||||
#[serde(default)]
|
||||
#[serde(
|
||||
deserialize_with = "jsonld::serde::Optional::<jsonld::serde::FirstId<_>>::deserialize"
|
||||
)]
|
||||
pub followers: Option<String>,
|
||||
#[serde(default)]
|
||||
#[serde(
|
||||
deserialize_with = "jsonld::serde::Optional::<jsonld::serde::FirstId<_>>::deserialize"
|
||||
)]
|
||||
pub following: Option<String>,
|
||||
#[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::<jsonld::serde::FirstId<_>>::deserialize"
|
||||
)]
|
||||
pub shared_inbox: Option<String>,
|
||||
}
|
||||
|
||||
|
@ -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,
|
||||
}
|
||||
|
|
|
@ -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<String>,
|
||||
|
@ -29,6 +31,7 @@ pub struct CollectionPage<T> {
|
|||
#[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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -1,34 +1,4 @@
|
|||
use super::{Object, PUBLIC_IDENTIFIER};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
#[serde(untagged)]
|
||||
pub enum StringOrObject<T> {
|
||||
String(String),
|
||||
Object(T),
|
||||
}
|
||||
|
||||
impl<T> StringOrObject<T> {
|
||||
pub fn into_string(self) -> Option<String> {
|
||||
match self {
|
||||
Self::String(str) => Some(str),
|
||||
Self::Object(..) => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn into_object(self) -> Option<T> {
|
||||
match self {
|
||||
Self::String(..) => None,
|
||||
Self::Object(obj) => Some(obj),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Default for StringOrObject<T> {
|
||||
fn default() -> Self {
|
||||
Self::String(String::new())
|
||||
}
|
||||
}
|
||||
|
||||
pub trait CcTo {
|
||||
fn cc(&self) -> &[String];
|
||||
|
|
|
@ -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<AttributedToListEntry>),
|
||||
}
|
||||
|
||||
#[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<Actor>,
|
||||
#[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::<jsonld::serde::FirstId<_>>::deserialize"
|
||||
)]
|
||||
pub in_reply_to: Option<String>,
|
||||
#[serde(default)]
|
||||
#[serde(deserialize_with = "jsonld::serde::Optional::<jsonld::serde::First<_>>::deserialize")]
|
||||
pub name: Option<String>,
|
||||
#[serde(default)]
|
||||
#[serde(deserialize_with = "jsonld::serde::Optional::<jsonld::serde::First<_>>::deserialize")]
|
||||
pub summary: Option<String>,
|
||||
#[serde(deserialize_with = "jsonld::serde::First::deserialize")]
|
||||
pub content: String,
|
||||
pub media_type: Option<String>,
|
||||
#[serde(default)]
|
||||
#[serde(deserialize_with = "jsonld::serde::Set::deserialize")]
|
||||
pub attachment: Vec<MediaAttachment>,
|
||||
#[serde(default)]
|
||||
#[serde(deserialize_with = "jsonld::serde::Set::deserialize")]
|
||||
pub tag: Vec<Tag>,
|
||||
#[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<String>,
|
||||
#[serde(default)]
|
||||
#[serde(deserialize_with = "jsonld::serde::IdSet::deserialize")]
|
||||
pub cc: Vec<String>,
|
||||
}
|
||||
|
||||
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<String>,
|
||||
#[serde(deserialize_with = "jsonld::serde::FirstOk::deserialize")]
|
||||
pub r#type: TagType,
|
||||
#[serde(deserialize_with = "jsonld::serde::First::deserialize")]
|
||||
pub name: String,
|
||||
pub href: Option<String>,
|
||||
#[serde(default)]
|
||||
#[serde(deserialize_with = "jsonld::serde::Optional::<jsonld::serde::First<_>>::deserialize")]
|
||||
pub icon: Option<MediaAttachment>,
|
||||
}
|
||||
|
|
|
@ -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::<jsonld::serde::First<_>>::deserialize")]
|
||||
pub name: Option<String>,
|
||||
pub media_type: Option<String>,
|
||||
#[serde(default)]
|
||||
#[serde(deserialize_with = "jsonld::serde::Optional::<jsonld::serde::First<_>>::deserialize")]
|
||||
pub blurhash: Option<String>,
|
||||
#[serde(deserialize_with = "jsonld::serde::First::deserialize")]
|
||||
pub url: String,
|
||||
}
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
pub(crate) mod serde;
|
||||
|
||||
pub trait RdfNode {
|
||||
fn id(&self) -> Option<&str>;
|
||||
}
|
|
@ -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<T> {
|
||||
seed: T,
|
||||
}
|
||||
|
||||
struct Visitor<T>(T);
|
||||
|
||||
impl<'de, T> First<PhantomData<T>>
|
||||
where
|
||||
T: Deserialize<'de>,
|
||||
{
|
||||
pub fn new() -> Self {
|
||||
Self::with_seed(PhantomData)
|
||||
}
|
||||
|
||||
pub fn deserialize<D>(deserializer: D) -> Result<T, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
Self::new().deserialize(deserializer)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de, T> First<T>
|
||||
where
|
||||
T: DeserializeSeed<'de>,
|
||||
{
|
||||
pub fn with_seed(seed: T) -> Self {
|
||||
Self { seed }
|
||||
}
|
||||
}
|
||||
|
||||
// XXX: Intentionally limiting to `First<PhantomData<_>>` rather than `First<_>` to help inference
|
||||
// of the type parameter of `Optional::<First<_>>::deserialize`.
|
||||
impl<'de, T> Default for First<PhantomData<T>>
|
||||
where
|
||||
T: Deserialize<'de>,
|
||||
{
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de, T> DeserializeSeed<'de> for First<T>
|
||||
where
|
||||
T: DeserializeSeed<'de>,
|
||||
{
|
||||
type Value = T::Value;
|
||||
|
||||
fn deserialize<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
deserializer.deserialize_any(Visitor(self.seed))
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de, T> de::Visitor<'de> for Visitor<T>
|
||||
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<A>(self, mut seq: A) -> Result<Self::Value, A::Error>
|
||||
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<u8>);
|
||||
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<u32> = Vec::new();
|
||||
assert_eq!(
|
||||
First::<PhantomData<Option<u32>>>::deserialize(into_deserializer(data)),
|
||||
Ok(None)
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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<T> {
|
||||
seed: T,
|
||||
}
|
||||
|
||||
impl<'de, T> FirstId<PhantomData<T>>
|
||||
where
|
||||
T: Deserialize<'de>,
|
||||
{
|
||||
pub fn new() -> Self {
|
||||
Self::with_seed(PhantomData)
|
||||
}
|
||||
|
||||
pub fn deserialize<D>(deserializer: D) -> Result<T, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
Self::new().deserialize(deserializer)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de, T> FirstId<T>
|
||||
where
|
||||
T: DeserializeSeed<'de>,
|
||||
{
|
||||
pub fn with_seed(seed: T) -> Self {
|
||||
Self { seed }
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de, T> Default for FirstId<PhantomData<T>>
|
||||
where
|
||||
T: Deserialize<'de>,
|
||||
{
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de, T> DeserializeSeed<'de> for FirstId<T>
|
||||
where
|
||||
T: DeserializeSeed<'de>,
|
||||
{
|
||||
type Value = T::Value;
|
||||
|
||||
fn deserialize<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
|
||||
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,
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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<T> {
|
||||
seed: T,
|
||||
}
|
||||
|
||||
struct Visitor<T>(T);
|
||||
|
||||
impl<'de, T> FirstOk<PhantomData<T>>
|
||||
where
|
||||
T: Deserialize<'de>,
|
||||
{
|
||||
pub fn new() -> Self {
|
||||
Self::with_seed(PhantomData)
|
||||
}
|
||||
|
||||
pub fn deserialize<D>(deserializer: D) -> Result<T, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
Self::new().deserialize(deserializer)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de, T> FirstOk<T>
|
||||
where
|
||||
T: DeserializeSeed<'de> + Clone,
|
||||
{
|
||||
pub fn with_seed(seed: T) -> Self {
|
||||
Self { seed }
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de, T> Default for FirstOk<PhantomData<T>>
|
||||
where
|
||||
T: Deserialize<'de>,
|
||||
{
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de, T> DeserializeSeed<'de> for FirstOk<T>
|
||||
where
|
||||
T: DeserializeSeed<'de> + Clone,
|
||||
{
|
||||
type Value = T::Value;
|
||||
|
||||
fn deserialize<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
deserializer.deserialize_any(Visitor(self.seed))
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! forward_to_into_deserializer {
|
||||
($(fn $name:ident($T:ty);)*) => {$(
|
||||
fn $name<E>(self, v: $T) -> Result<Self::Value, E>
|
||||
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<T>
|
||||
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<A>(self, mut seq: A) -> Result<Self::Value, A::Error>
|
||||
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<E>(self, v: &'de str) -> Result<Self::Value, E>
|
||||
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<E>(self, v: &'de [u8]) -> Result<Self::Value, E>
|
||||
where
|
||||
E: de::Error,
|
||||
{
|
||||
self.0
|
||||
.clone()
|
||||
.deserialize(de::value::BorrowedBytesDeserializer::new(v))
|
||||
.or_else(|_: E| self.0.deserialize(().into_deserializer()))
|
||||
}
|
||||
|
||||
fn visit_none<E>(self) -> Result<Self::Value, E>
|
||||
where
|
||||
E: de::Error,
|
||||
{
|
||||
self.0.deserialize(().into_deserializer())
|
||||
}
|
||||
|
||||
fn visit_some<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
FirstOk::with_seed(self.0).deserialize(deserializer)
|
||||
}
|
||||
|
||||
fn visit_unit<E>(self) -> Result<Self::Value, E>
|
||||
where
|
||||
E: de::Error,
|
||||
{
|
||||
self.0.deserialize(().into_deserializer())
|
||||
}
|
||||
|
||||
fn visit_newtype_struct<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
FirstOk::with_seed(self.0).deserialize(deserializer)
|
||||
}
|
||||
|
||||
fn visit_map<A>(self, map: A) -> Result<Self::Value, A::Error>
|
||||
where
|
||||
A: MapAccess<'de>,
|
||||
{
|
||||
self.0.deserialize(MapAccessDeserializer::new(map))
|
||||
}
|
||||
|
||||
fn visit_enum<A>(self, data: A) -> Result<Self::Value, A::Error>
|
||||
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<u8>);
|
||||
}
|
||||
}
|
||||
|
||||
#[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>)
|
||||
);
|
||||
}
|
||||
|
||||
#[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::<Test>)
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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<T> {
|
||||
seed: T,
|
||||
}
|
||||
|
||||
struct Visitor<T>(T);
|
||||
|
||||
#[cfg_attr(not(test), allow(dead_code))]
|
||||
impl<'de, T> Id<PhantomData<T>>
|
||||
where
|
||||
T: Deserialize<'de>,
|
||||
{
|
||||
pub fn new() -> Self {
|
||||
Self::with_seed(PhantomData)
|
||||
}
|
||||
|
||||
pub fn deserialize<D>(deserializer: D) -> Result<T, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
Self::new().deserialize(deserializer)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de, T> Id<T>
|
||||
where
|
||||
T: DeserializeSeed<'de>,
|
||||
{
|
||||
pub fn with_seed(seed: T) -> Self {
|
||||
Self { seed }
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de, T> Default for Id<PhantomData<T>>
|
||||
where
|
||||
T: Deserialize<'de>,
|
||||
{
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de, T> DeserializeSeed<'de> for Id<T>
|
||||
where
|
||||
T: DeserializeSeed<'de>,
|
||||
{
|
||||
type Value = T::Value;
|
||||
|
||||
fn deserialize<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
deserializer.deserialize_any(Visitor(self.seed))
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de, T> de::Visitor<'de> for Visitor<T>
|
||||
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<A>(self, mut map: A) -> Result<Self::Value, A::Error>
|
||||
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<A>(self, seq: A) -> Result<Self::Value, A::Error>
|
||||
where
|
||||
A: SeqAccess<'de>,
|
||||
{
|
||||
struct SeqAccess<A>(A);
|
||||
|
||||
impl<'de, A> de::SeqAccess<'de> for SeqAccess<A>
|
||||
where
|
||||
A: de::SeqAccess<'de>,
|
||||
{
|
||||
type Error = A::Error;
|
||||
|
||||
fn next_element_seed<T>(&mut self, seed: T) -> Result<Option<T::Value>, Self::Error>
|
||||
where
|
||||
T: DeserializeSeed<'de>,
|
||||
{
|
||||
self.0.next_element_seed(Id::with_seed(seed))
|
||||
}
|
||||
|
||||
fn size_hint(&self) -> Option<usize> {
|
||||
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<u8>);
|
||||
}
|
||||
}
|
||||
|
||||
#[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::<PhantomData<String>>::deserialize(into_deserializer(data)).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn seq() {
|
||||
#[derive(Debug, Deserialize, PartialEq)]
|
||||
#[serde(transparent)]
|
||||
struct Test {
|
||||
#[serde(deserialize_with = "Id::deserialize")]
|
||||
term: Vec<String>,
|
||||
}
|
||||
|
||||
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,
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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<T> {
|
||||
seed: T,
|
||||
}
|
||||
|
||||
impl<'de, T> IdSet<PhantomData<T>>
|
||||
where
|
||||
T: Deserialize<'de>,
|
||||
{
|
||||
pub fn new() -> Self {
|
||||
Self::with_seed(PhantomData)
|
||||
}
|
||||
|
||||
pub fn deserialize<D>(deserializer: D) -> Result<T, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
Self::new().deserialize(deserializer)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de, T> IdSet<T>
|
||||
where
|
||||
T: DeserializeSeed<'de>,
|
||||
{
|
||||
pub fn with_seed(seed: T) -> Self {
|
||||
Self { seed }
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de, T> Default for IdSet<PhantomData<T>>
|
||||
where
|
||||
T: Deserialize<'de>,
|
||||
{
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de, T> DeserializeSeed<'de> for IdSet<T>
|
||||
where
|
||||
T: DeserializeSeed<'de>,
|
||||
{
|
||||
type Value = T::Value;
|
||||
|
||||
fn deserialize<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
|
||||
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<String>,
|
||||
}
|
||||
|
||||
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,
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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<E>(self, v: $T) -> Result<Self::Value, E>
|
||||
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<E>(self, v: $T) -> Result<Self::Value, E>
|
||||
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<E>(self) -> Result<Self::Value, E>
|
||||
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<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
self.0.deserialize(deserializer)
|
||||
}
|
||||
forward_to_into_deserializer! { $($rest)* }
|
||||
};
|
||||
(
|
||||
fn visit_unit();
|
||||
$($rest:tt)*
|
||||
) => {
|
||||
fn visit_unit<E>(self) -> Result<Self::Value, E>
|
||||
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<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
self.0.deserialize(deserializer)
|
||||
}
|
||||
forward_to_into_deserializer! { $($rest)* }
|
||||
};
|
||||
(
|
||||
fn visit_seq();
|
||||
$($rest:tt)*
|
||||
) => {
|
||||
fn visit_seq<A>(self, seq: A) -> Result<Self::Value, A::Error>
|
||||
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<A>(self, map: A) -> Result<Self::Value, A::Error>
|
||||
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<A>(self, data: A) -> Result<Self::Value, A::Error>
|
||||
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<E>(self, v: $T) -> Result<Self::Value, E>
|
||||
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>(D);
|
||||
|
||||
/// A `DeserializeSeed` that catches a recoverable error and returns it as a successful value.
|
||||
struct CatchError<T, E> {
|
||||
seed: T,
|
||||
marker: PhantomData<fn() -> E>,
|
||||
}
|
||||
|
||||
struct OptionSeed<T>(Option<T>);
|
||||
|
||||
impl<'de, D> IntoDeserializer<'de, D::Error> for DeserializerIntoDeserializer<D>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
type Deserializer = D;
|
||||
|
||||
fn into_deserializer(self) -> Self::Deserializer {
|
||||
self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de, T> DeserializeSeed<'de> for &mut OptionSeed<T>
|
||||
where
|
||||
T: DeserializeSeed<'de>,
|
||||
{
|
||||
type Value = Option<T::Value>;
|
||||
|
||||
fn deserialize<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
if let Some(seed) = self.0.take() {
|
||||
seed.deserialize(deserializer).map(Some)
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de, T, E> CatchError<T, E>
|
||||
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<T, E>
|
||||
where
|
||||
T: DeserializeSeed<'de>,
|
||||
E: de::Error,
|
||||
{
|
||||
type Value = Result<T::Value, E>;
|
||||
|
||||
fn deserialize<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
deserializer.deserialize_any(self)
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! catch_error_forward_to_into_deserializer {
|
||||
($(fn $name:ident($T:ty);)*) => {$(
|
||||
fn $name<E2>(self, v: $T) -> Result<Self::Value, E2> {
|
||||
// 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<T, E>
|
||||
where
|
||||
T: DeserializeSeed<'de>,
|
||||
E: de::Error,
|
||||
{
|
||||
type Value = Result<T::Value, E>;
|
||||
|
||||
fn expecting(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
||||
f.write_str("a value")
|
||||
}
|
||||
|
||||
fn visit_borrowed_str<E2>(self, v: &'de str) -> Result<Self::Value, E2>
|
||||
where
|
||||
E: de::Error,
|
||||
{
|
||||
Ok(self
|
||||
.seed
|
||||
.deserialize(de::value::BorrowedStrDeserializer::new(v)))
|
||||
}
|
||||
|
||||
fn visit_borrowed_bytes<E2>(self, v: &'de [u8]) -> Result<Self::Value, E2>
|
||||
where
|
||||
E: de::Error,
|
||||
{
|
||||
Ok(self
|
||||
.seed
|
||||
.deserialize(de::value::BorrowedBytesDeserializer::new(v)))
|
||||
}
|
||||
|
||||
fn visit_none<E2>(self) -> Result<Self::Value, E2> {
|
||||
Ok(self.seed.deserialize(().into_deserializer()))
|
||||
}
|
||||
|
||||
fn visit_some<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
self.deserialize(deserializer)
|
||||
}
|
||||
|
||||
fn visit_unit<E2>(self) -> Result<Self::Value, E2> {
|
||||
Ok(self.seed.deserialize(().into_deserializer()))
|
||||
}
|
||||
|
||||
fn visit_newtype_struct<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
|
||||
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<A>(self, seq: A) -> Result<Self::Value, A::Error>
|
||||
where
|
||||
A: SeqAccess<'de>,
|
||||
{
|
||||
self.seed
|
||||
.deserialize(SeqAccessDeserializer::new(seq))
|
||||
.map(Ok)
|
||||
}
|
||||
|
||||
fn visit_map<A>(self, map: A) -> Result<Self::Value, A::Error>
|
||||
where
|
||||
A: MapAccess<'de>,
|
||||
{
|
||||
self.seed
|
||||
.deserialize(MapAccessDeserializer::new(map))
|
||||
.map(Ok)
|
||||
}
|
||||
|
||||
fn visit_enum<A>(self, data: A) -> Result<Self::Value, A::Error>
|
||||
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<u8>);
|
||||
}
|
||||
}
|
||||
|
||||
#[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::<First<_>>::deserialize")]
|
||||
summary: Option<String>,
|
||||
#[serde(default)]
|
||||
#[serde(deserialize_with = "Optional::<First<_>>::deserialize")]
|
||||
content: Option<String>,
|
||||
#[serde(default)]
|
||||
#[serde(deserialize_with = "IdSet::deserialize")]
|
||||
to: Vec<String>,
|
||||
#[serde(default)]
|
||||
#[serde(deserialize_with = "IdSet::deserialize")]
|
||||
cc: Vec<String>,
|
||||
}
|
||||
|
||||
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));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,79 @@
|
|||
use core::fmt::{self, Formatter};
|
||||
use serde::de::{self, DeserializeSeed, Deserializer};
|
||||
|
||||
/// Deserialises an `Option<T::Value>` value.
|
||||
///
|
||||
/// Workaround until Serde introduces a native mechanism for applying the
|
||||
/// `#[serde(deserialize_with)]` attribute to the type inside an `Option<_>`.
|
||||
///
|
||||
/// cf. <https://github.com/serde-rs/serde/issues/723>.
|
||||
pub struct Optional<T> {
|
||||
seed: T,
|
||||
}
|
||||
|
||||
struct Visitor<T>(T);
|
||||
|
||||
impl<'de, T> Optional<T>
|
||||
where
|
||||
T: DeserializeSeed<'de> + Default,
|
||||
{
|
||||
pub fn new() -> Self {
|
||||
Self::with_seed(T::default())
|
||||
}
|
||||
|
||||
pub fn deserialize<D>(deserializer: D) -> Result<Option<T::Value>, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
Self::new().deserialize(deserializer)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de, T> Optional<T>
|
||||
where
|
||||
T: DeserializeSeed<'de>,
|
||||
{
|
||||
pub fn with_seed(seed: T) -> Self {
|
||||
Self { seed }
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de, T> DeserializeSeed<'de> for Optional<T>
|
||||
where
|
||||
T: DeserializeSeed<'de>,
|
||||
{
|
||||
type Value = Option<T::Value>;
|
||||
|
||||
fn deserialize<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
deserializer.deserialize_option(Visitor(self.seed))
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de, T> de::Visitor<'de> for Visitor<T>
|
||||
where
|
||||
T: DeserializeSeed<'de>,
|
||||
{
|
||||
type Value = Option<T::Value>;
|
||||
|
||||
fn expecting(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
||||
f.write_str("option")
|
||||
}
|
||||
|
||||
fn visit_unit<E>(self) -> Result<Self::Value, E> {
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
fn visit_none<E>(self) -> Result<Self::Value, E> {
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
fn visit_some<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
self.0.deserialize(deserializer).map(Some)
|
||||
}
|
||||
}
|
|
@ -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<T> {
|
||||
seed: T,
|
||||
}
|
||||
|
||||
struct Visitor<T>(T);
|
||||
|
||||
impl<'de, T> Set<PhantomData<T>>
|
||||
where
|
||||
T: Deserialize<'de>,
|
||||
{
|
||||
pub fn new() -> Self {
|
||||
Self::with_seed(PhantomData)
|
||||
}
|
||||
|
||||
pub fn deserialize<D>(deserializer: D) -> Result<T, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
Self::new().deserialize(deserializer)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de, T> Set<T>
|
||||
where
|
||||
T: DeserializeSeed<'de>,
|
||||
{
|
||||
pub fn with_seed(seed: T) -> Self {
|
||||
Self { seed }
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de, T> Default for Set<PhantomData<T>>
|
||||
where
|
||||
T: Deserialize<'de>,
|
||||
{
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de, T> DeserializeSeed<'de> for Set<T>
|
||||
where
|
||||
T: DeserializeSeed<'de>,
|
||||
{
|
||||
type Value = T::Value;
|
||||
|
||||
fn deserialize<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
deserializer.deserialize_any(Visitor(self.seed))
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! forward_to_seq_deserializer {
|
||||
($(fn $name:ident($T:ty);)*) => {$(
|
||||
fn $name<E>(self, v: $T) -> Result<Self::Value, E>
|
||||
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<T>
|
||||
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<E>(self, v: &'de str) -> Result<Self::Value, E>
|
||||
where
|
||||
E: de::Error,
|
||||
{
|
||||
let iter = iter::once(DeserializerIntoDeserializer(BorrowedStrDeserializer::new(
|
||||
v,
|
||||
)));
|
||||
self.0.deserialize(SeqDeserializer::new(iter))
|
||||
}
|
||||
|
||||
fn visit_borrowed_bytes<E>(self, v: &'de [u8]) -> Result<Self::Value, E>
|
||||
where
|
||||
E: de::Error,
|
||||
{
|
||||
let iter = iter::once(DeserializerIntoDeserializer(
|
||||
BorrowedBytesDeserializer::new(v),
|
||||
));
|
||||
self.0.deserialize(SeqDeserializer::new(iter))
|
||||
}
|
||||
|
||||
fn visit_seq<A>(self, seq: A) -> Result<Self::Value, A::Error>
|
||||
where
|
||||
A: SeqAccess<'de>,
|
||||
{
|
||||
self.0.deserialize(SeqAccessDeserializer::new(seq))
|
||||
}
|
||||
|
||||
fn visit_none<E>(self) -> Result<Self::Value, E>
|
||||
where
|
||||
E: de::Error,
|
||||
{
|
||||
self.visit_unit()
|
||||
}
|
||||
|
||||
fn visit_some<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
let iter = iter::once(DeserializerIntoDeserializer(deserializer));
|
||||
self.0.deserialize(SeqDeserializer::new(iter))
|
||||
}
|
||||
|
||||
fn visit_unit<E>(self) -> Result<Self::Value, E>
|
||||
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<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
let iter = iter::once(DeserializerIntoDeserializer(deserializer));
|
||||
self.0.deserialize(SeqDeserializer::new(iter))
|
||||
}
|
||||
|
||||
fn visit_map<A>(self, map: A) -> Result<Self::Value, A::Error>
|
||||
where
|
||||
A: MapAccess<'de>,
|
||||
{
|
||||
let iter = iter::once(DeserializerIntoDeserializer(MapAccessDeserializer::new(
|
||||
map,
|
||||
)));
|
||||
self.0.deserialize(SeqDeserializer::new(iter))
|
||||
}
|
||||
|
||||
fn visit_enum<A>(self, data: A) -> Result<Self::Value, A::Error>
|
||||
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<u8>);
|
||||
}
|
||||
}
|
||||
|
||||
#[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<u32> = 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::<u32>::new())
|
||||
);
|
||||
}
|
||||
}
|
|
@ -51,7 +51,7 @@ impl FromRequest<Zustand, Body> 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())
|
||||
|
|
Loading…
Reference in New Issue