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:
Daiki Mizukami 2024-03-08 00:45:15 +09:00 committed by GitHub
parent a919bdf7d1
commit 3879c49f8b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
23 changed files with 1736 additions and 94 deletions

10
Cargo.lock generated
View File

@ -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"

View File

@ -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(),
};

View File

@ -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 {

View File

@ -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),
}

View File

@ -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,

View File

@ -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,

View File

@ -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

View File

@ -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,
}

View File

@ -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,

View File

@ -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,

View File

@ -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];

View File

@ -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>,
}

View File

@ -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,
}

View File

@ -1,3 +1,5 @@
pub(crate) mod serde;
pub trait RdfNode {
fn id(&self) -> Option<&str>;
}

View File

@ -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)
);
}
}

View File

@ -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,
],
);
}
}

View File

@ -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>)
);
}
}

View File

@ -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,
],
);
}
}

View File

@ -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,
],
);
}
}

View File

@ -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));
}
}

View File

@ -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)
}
}

View File

@ -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())
);
}
}

View File

@ -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())