Skip to main content

actpub_activitystreams/
value.rs

1//! Polymorphic value wrappers used throughout Activity Streams 2.0.
2//!
3//! Activity Streams 2.0 is notoriously loose about the shape of its values:
4//! most "array-typed" properties may appear as a bare single value in JSON,
5//! and object-typed properties frequently appear as plain URL strings that
6//! reference a remote resource. These wrappers preserve type safety on the
7//! Rust side while accepting the full range of legal JSON shapes.
8
9use serde::{Deserialize, Deserializer, Serialize, Serializer};
10use url::Url;
11
12/// A value that may serialize as either a single `T` or as an ordered array
13/// of `T`.
14///
15/// On the wire an empty [`OneOrMany`] is emitted as an empty array, a single
16/// entry as a bare value, and multiple entries as a JSON array.
17///
18/// # Examples
19///
20/// ```
21/// # use actpub_activitystreams::OneOrMany;
22/// let one: OneOrMany<String> =
23///     serde_json::from_str(r#""Hello""#).unwrap();
24/// assert_eq!(one.as_slice(), &["Hello".to_owned()]);
25///
26/// let many: OneOrMany<String> =
27///     serde_json::from_str(r#"["A", "B"]"#).unwrap();
28/// assert_eq!(many.len(), 2);
29/// ```
30#[derive(Debug, Clone, PartialEq, Eq, Hash)]
31pub struct OneOrMany<T>(Vec<T>);
32
33impl<T> OneOrMany<T> {
34    /// Creates an empty [`OneOrMany`].
35    #[must_use]
36    pub const fn new() -> Self {
37        Self(Vec::new())
38    }
39
40    /// Creates a [`OneOrMany`] containing a single value.
41    pub fn one(value: T) -> Self {
42        Self(vec![value])
43    }
44
45    /// Creates a [`OneOrMany`] from a pre-existing [`Vec`].
46    #[must_use]
47    pub const fn many(values: Vec<T>) -> Self {
48        Self(values)
49    }
50
51    /// Returns a slice view of the contained values.
52    #[must_use]
53    pub fn as_slice(&self) -> &[T] {
54        &self.0
55    }
56
57    /// Returns a mutable slice view of the contained values.
58    pub fn as_mut_slice(&mut self) -> &mut [T] {
59        &mut self.0
60    }
61
62    /// Returns the underlying [`Vec`], consuming `self`.
63    #[must_use]
64    pub fn into_vec(self) -> Vec<T> {
65        self.0
66    }
67
68    /// Appends a value.
69    pub fn push(&mut self, value: T) {
70        self.0.push(value);
71    }
72
73    /// Returns `true` if empty.
74    #[must_use]
75    pub const fn is_empty(&self) -> bool {
76        self.0.is_empty()
77    }
78
79    /// Returns the number of contained values.
80    #[must_use]
81    pub const fn len(&self) -> usize {
82        self.0.len()
83    }
84
85    /// Returns an iterator over the contained values.
86    pub fn iter(&self) -> core::slice::Iter<'_, T> {
87        self.0.iter()
88    }
89
90    /// Returns a mutable iterator over the contained values.
91    pub fn iter_mut(&mut self) -> core::slice::IterMut<'_, T> {
92        self.0.iter_mut()
93    }
94
95    /// Returns a reference to the first value, if any.
96    #[must_use]
97    pub fn first(&self) -> Option<&T> {
98        self.0.first()
99    }
100
101    /// Returns a reference to the last value, if any.
102    #[must_use]
103    pub fn last(&self) -> Option<&T> {
104        self.0.last()
105    }
106}
107
108impl<T> Default for OneOrMany<T> {
109    fn default() -> Self {
110        Self::new()
111    }
112}
113
114impl<T> IntoIterator for OneOrMany<T> {
115    type Item = T;
116    type IntoIter = std::vec::IntoIter<T>;
117
118    fn into_iter(self) -> Self::IntoIter {
119        self.0.into_iter()
120    }
121}
122
123impl<'a, T> IntoIterator for &'a OneOrMany<T> {
124    type Item = &'a T;
125    type IntoIter = core::slice::Iter<'a, T>;
126
127    fn into_iter(self) -> Self::IntoIter {
128        self.0.iter()
129    }
130}
131
132impl<'a, T> IntoIterator for &'a mut OneOrMany<T> {
133    type Item = &'a mut T;
134    type IntoIter = core::slice::IterMut<'a, T>;
135
136    fn into_iter(self) -> Self::IntoIter {
137        self.0.iter_mut()
138    }
139}
140
141impl<T> From<T> for OneOrMany<T> {
142    fn from(value: T) -> Self {
143        Self::one(value)
144    }
145}
146
147impl<T> From<Vec<T>> for OneOrMany<T> {
148    fn from(values: Vec<T>) -> Self {
149        Self::many(values)
150    }
151}
152
153impl<T> FromIterator<T> for OneOrMany<T> {
154    fn from_iter<I: IntoIterator<Item = T>>(iter: I) -> Self {
155        Self(iter.into_iter().collect())
156    }
157}
158
159impl<T: Serialize> Serialize for OneOrMany<T> {
160    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
161        match self.0.as_slice() {
162            [only] => only.serialize(serializer),
163            many => many.serialize(serializer),
164        }
165    }
166}
167
168impl<'de, T: Deserialize<'de>> Deserialize<'de> for OneOrMany<T> {
169    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
170        #[derive(Deserialize)]
171        #[serde(untagged)]
172        enum Either<T> {
173            Many(Vec<T>),
174            One(T),
175        }
176
177        // Accept JSON `null` and the two concrete shapes. Real Fediverse
178        // traffic frequently sends `null` for absent array-typed properties
179        // (e.g. Mastodon's `inReplyTo: null`); we treat those as empty.
180        match Option::<Either<T>>::deserialize(deserializer)? {
181            None => Ok(Self::new()),
182            Some(Either::One(value)) => Ok(Self::one(value)),
183            Some(Either::Many(values)) => Ok(Self::many(values)),
184        }
185    }
186}
187
188/// A value that may appear inlined as `T` or as a bare URL reference.
189///
190/// AS 2.0 object-valued properties frequently arrive either:
191///
192/// - embedded as a full object (`{ "id": "https://…", "type": "Note", … }`)
193/// - or as just the URL string (`"https://example.com/note/1"`)
194///
195/// [`UrlOr`] captures both variants.
196#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
197#[serde(untagged)]
198pub enum UrlOr<T> {
199    /// A bare URL reference to the remote resource.
200    Url(Url),
201    /// An inlined object of the target type.
202    Object(T),
203}
204
205impl<T> UrlOr<T> {
206    /// Returns the referenced URL, regardless of whether it is inlined or
207    /// bare, provided the inlined variant exposes an `id` via [`HasId`].
208    pub fn url(&self) -> Option<&Url>
209    where
210        T: HasId,
211    {
212        match self {
213            Self::Url(u) => Some(u),
214            Self::Object(o) => o.id(),
215        }
216    }
217
218    /// Returns the inlined object if it was inlined.
219    #[must_use]
220    pub const fn as_object(&self) -> Option<&T> {
221        match self {
222            Self::Object(o) => Some(o),
223            Self::Url(_) => None,
224        }
225    }
226}
227
228/// Types with an optional `id` URL, enabling uniform reference resolution.
229pub trait HasId {
230    /// Returns the `id` of this object, if set.
231    fn id(&self) -> Option<&Url>;
232}
233
234/// The `Public` pseudo-actor used for public audience targeting in
235/// `ActivityPub`.
236///
237/// The specification defines a single URI, but real Fediverse traffic
238/// contains three spellings. [`Public::is_public`] recognises all of them.
239#[derive(Copy, Clone, Debug, Eq, PartialEq)]
240pub struct Public;
241
242impl Public {
243    /// Full public URI per the `ActivityPub` specification.
244    pub const URI: &'static str = "https://www.w3.org/ns/activitystreams#Public";
245    /// CURIE form using the `as:` prefix defined by the AS 2.0 JSON-LD
246    /// context document.
247    pub const CURIE: &'static str = "as:Public";
248    /// Bare form `Public`. Legal only inside a JSON-LD `@context` that
249    /// defines the `as:` prefix. Accepted for interop but not emitted.
250    pub const BARE: &'static str = "Public";
251
252    /// Returns `true` if `value` is any of the three accepted spellings of
253    /// the AS 2.0 Public pseudo-actor.
254    #[must_use]
255    pub fn is_public(value: &str) -> bool {
256        matches!(value, Self::URI | Self::CURIE | Self::BARE)
257    }
258}
259
260#[cfg(test)]
261mod tests {
262    use pretty_assertions::assert_eq;
263    use serde_json::json;
264
265    use super::*;
266
267    #[test]
268    fn one_or_many_single_value_serialises_as_bare_value() {
269        let value = OneOrMany::one("hello".to_owned());
270        let json = serde_json::to_value(&value).expect("serialise");
271        assert_eq!(json, json!("hello"));
272
273        let back: OneOrMany<String> = serde_json::from_value(json).expect("deserialise");
274        assert_eq!(back, value);
275    }
276
277    #[test]
278    fn one_or_many_multi_value_serialises_as_array() {
279        let value = OneOrMany::many(vec![1_i32, 2, 3]);
280        let json = serde_json::to_value(&value).expect("serialise");
281        assert_eq!(json, json!([1, 2, 3]));
282
283        let back: OneOrMany<i32> = serde_json::from_value(json).expect("deserialise");
284        assert_eq!(back, value);
285    }
286
287    #[test]
288    fn one_or_many_empty_serialises_as_empty_array() {
289        let value: OneOrMany<String> = OneOrMany::new();
290        let json = serde_json::to_value(&value).expect("serialise");
291        assert_eq!(json, json!([]));
292    }
293
294    #[test]
295    fn one_or_many_deserialises_null_as_empty() {
296        // Mastodon and Misskey frequently emit `null` for absent
297        // array-typed properties (e.g. `inReplyTo: null`). Treating this
298        // as an empty collection preserves lossless roundtrip semantics
299        // for everything except the null literal itself.
300        let back: OneOrMany<String> =
301            serde_json::from_value(json!(null)).expect("null must deserialise");
302        assert!(back.is_empty(), "null must yield an empty collection");
303    }
304
305    #[test]
306    fn one_or_many_collects_from_iterator() {
307        let value: OneOrMany<i32> = [1, 2, 3].into_iter().collect();
308        assert_eq!(value.as_slice(), &[1, 2, 3]);
309    }
310
311    #[test]
312    fn url_or_deserializes_bare_url() {
313        #[derive(Deserialize, Serialize, Debug, PartialEq)]
314        struct Dummy {
315            id: String,
316        }
317        let value: UrlOr<Dummy> = serde_json::from_value(json!("https://example/1")).unwrap();
318        assert!(matches!(value, UrlOr::Url(_)));
319    }
320
321    #[test]
322    fn url_or_deserializes_object() {
323        #[derive(Deserialize, Serialize, Debug, PartialEq)]
324        struct Dummy {
325            id: String,
326        }
327        let value: UrlOr<Dummy> = serde_json::from_value(json!({ "id": "abc" })).unwrap();
328        assert!(matches!(value, UrlOr::Object(Dummy { .. })));
329    }
330
331    #[test]
332    fn public_is_recognised_in_all_spellings() {
333        assert!(Public::is_public(Public::URI));
334        assert!(Public::is_public(Public::CURIE));
335        assert!(Public::is_public(Public::BARE));
336        assert!(!Public::is_public("https://example/actor"));
337    }
338}