From a6e68cb0bae5c766ae8c2ed0e4e9e8937f3f4ddd Mon Sep 17 00:00:00 2001 From: Jiwon Kwon Date: Wed, 10 Jun 2026 14:46:07 +0900 Subject: [PATCH] Refine multi-value vocab references --- crates/feder-vocab/src/lib.rs | 141 +++++++++++++++++++--- crates/feder-vocab/tests/phase1_shapes.rs | 40 +++++- 2 files changed, 156 insertions(+), 25 deletions(-) diff --git a/crates/feder-vocab/src/lib.rs b/crates/feder-vocab/src/lib.rs index 64915cf..0d7a516 100644 --- a/crates/feder-vocab/src/lib.rs +++ b/crates/feder-vocab/src/lib.rs @@ -9,7 +9,7 @@ extern crate alloc; use alloc::{boxed::Box, string::String, vec::Vec}; use iri_string::types::IriString; -use serde::{Deserialize, Serialize}; +use serde::{Deserialize, Deserializer, Serialize, Serializer, ser::SerializeSeq}; /// The canonical Activity Streams JSON-LD context URL. pub const ACTIVITYSTREAMS_CONTEXT: &str = "https://www.w3.org/ns/activitystreams"; @@ -40,26 +40,105 @@ impl Reference { } } -/// A property value that can appear either once or multiple times. +/// Zero or more ActivityStreams property values. /// -/// ActivityStreams commonly allows fields to be absent, scalar, or arrays. -/// Absence is represented by `Option>` on the containing type. -#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] -#[serde(untagged)] -pub enum OneOrMany { - One(T), - Many(Vec), +/// Use this with `#[serde(default, skip_serializing_if = "References::is_empty")]` +/// on containing fields. Empty values then serialize as absent, one value +/// serializes as a scalar, and multiple values serialize as an array. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct References { + values: Vec, +} + +impl Default for References { + fn default() -> Self { + Self::new() + } } -impl OneOrMany { +impl References { + #[must_use] + pub fn new() -> Self { + Self { values: Vec::new() } + } + #[must_use] pub fn one(value: T) -> Self { - Self::One(value) + Self { + values: Vec::from([value]), + } } #[must_use] pub fn many(values: impl Into>) -> Self { - Self::Many(values.into()) + Self { + values: values.into(), + } + } + + #[must_use] + pub fn is_empty(&self) -> bool { + self.values.is_empty() + } + + #[must_use] + pub fn len(&self) -> usize { + self.values.len() + } + + pub fn iter(&self) -> core::slice::Iter<'_, T> { + self.values.iter() + } + + pub fn into_vec(self) -> Vec { + self.values + } +} + +impl From> for References { + fn from(values: Vec) -> Self { + Self::many(values) + } +} + +#[derive(Deserialize)] +#[serde(untagged)] +enum OneOrMany { + One(T), + Many(Vec), +} + +impl Serialize for References +where + T: Serialize, +{ + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + match self.values.as_slice() { + [] => { + let sequence = serializer.serialize_seq(Some(0))?; + sequence.end() + } + [value] => value.serialize(serializer), + values => values.serialize(serializer), + } + } +} + +impl<'de, T> Deserialize<'de> for References +where + T: Deserialize<'de>, +{ + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + match OneOrMany::deserialize(deserializer)? { + OneOrMany::One(value) => Ok(References::one(value)), + OneOrMany::Many(values) => Ok(References::many(values)), + } } } @@ -359,21 +438,45 @@ mod tests { } #[test] - fn one_or_many_deserializes_scalar_and_array() { - let one: OneOrMany = serde_json::from_value(json!("https://example.com/users/alice")) - .expect("deserialize scalar one-or-many value"); - let many: OneOrMany = serde_json::from_value(json!([ + fn references_deserializes_scalar_and_array() { + let one: References = serde_json::from_value(json!("https://example.com/users/alice")) + .expect("deserialize scalar references value"); + let many: References = serde_json::from_value(json!([ "https://example.com/users/alice", "https://example.com/users/bob" ])) - .expect("deserialize array one-or-many value"); + .expect("deserialize array references value"); - assert_eq!(one, OneOrMany::one(iri("https://example.com/users/alice"))); + assert_eq!(one, References::one(iri("https://example.com/users/alice"))); assert_eq!( many, - OneOrMany::many([ + References::many([ + iri("https://example.com/users/alice"), + iri("https://example.com/users/bob") + ]) + ); + } + + #[test] + fn references_serializes_empty_one_and_many() { + assert_eq!( + serde_json::to_value(References::::new()).expect("serialize empty references"), + json!([]) + ); + assert_eq!( + serde_json::to_value(References::one(iri("https://example.com/users/alice"))) + .expect("serialize one reference"), + json!("https://example.com/users/alice") + ); + assert_eq!( + serde_json::to_value(References::many([ iri("https://example.com/users/alice"), iri("https://example.com/users/bob") + ])) + .expect("serialize many references"), + json!([ + "https://example.com/users/alice", + "https://example.com/users/bob" ]) ); } diff --git a/crates/feder-vocab/tests/phase1_shapes.rs b/crates/feder-vocab/tests/phase1_shapes.rs index 2d572d9..371e682 100644 --- a/crates/feder-vocab/tests/phase1_shapes.rs +++ b/crates/feder-vocab/tests/phase1_shapes.rs @@ -1,4 +1,6 @@ -use feder_vocab::{ACTIVITYSTREAMS_CONTEXT, Accept, Create, Follow, Iri, Note, Reference}; +use feder_vocab::{ + ACTIVITYSTREAMS_CONTEXT, Accept, Create, Follow, Iri, Note, Reference, References, +}; use serde_json::{Value, json}; fn serialize(value: impl serde::Serialize) -> Value { @@ -124,11 +126,11 @@ fn reference_keeps_id_and_embedded_object_shapes_distinct() { } #[test] -fn one_or_many_can_represent_common_recipient_shapes() { - let single: feder_vocab::OneOrMany = +fn references_can_represent_common_recipient_shapes() { + let single: References = serde_json::from_value(json!("https://www.w3.org/ns/activitystreams#Public")) .expect("deserialize single recipient"); - let multiple: feder_vocab::OneOrMany = serde_json::from_value(json!([ + let multiple: References = serde_json::from_value(json!([ "https://www.w3.org/ns/activitystreams#Public", "https://example.com/users/alice/followers" ])) @@ -136,13 +138,39 @@ fn one_or_many_can_represent_common_recipient_shapes() { assert_eq!( single, - feder_vocab::OneOrMany::one(iri("https://www.w3.org/ns/activitystreams#Public")) + References::one(iri("https://www.w3.org/ns/activitystreams#Public")) ); assert_eq!( multiple, - feder_vocab::OneOrMany::many([ + References::many([ iri("https://www.w3.org/ns/activitystreams#Public"), iri("https://example.com/users/alice/followers") ]) ); } + +#[test] +fn references_treat_absent_and_empty_array_as_empty() { + #[derive(Debug, PartialEq, serde::Deserialize, serde::Serialize)] + struct Recipients { + #[serde(default, skip_serializing_if = "References::is_empty")] + to: References, + } + + let absent: Recipients = serde_json::from_value(json!({})).expect("deserialize absent field"); + let empty: Recipients = + serde_json::from_value(json!({ "to": [] })).expect("deserialize empty field"); + let one = Recipients { + to: References::one(iri("https://www.w3.org/ns/activitystreams#Public")), + }; + + assert_eq!(absent, empty); + assert_eq!( + serde_json::to_value(absent).expect("serialize absent"), + json!({}) + ); + assert_eq!( + serde_json::to_value(one).expect("serialize one recipient"), + json!({ "to": "https://www.w3.org/ns/activitystreams#Public" }) + ); +}