Skip to main content

jsonapi_core/model/
relationship.rs

1use std::marker::PhantomData;
2
3use serde::de;
4use serde::{Deserialize, Deserializer, Serialize, Serializer};
5
6use super::{Links, Meta, ResourceIdentifier};
7
8/// Resource linkage inside a relationship.
9#[non_exhaustive]
10#[derive(Debug, Clone, PartialEq)]
11pub enum RelationshipData {
12    /// To-one: `null` (empty) or a single resource identifier.
13    ToOne(Option<ResourceIdentifier>),
14    /// To-many: an array of resource identifiers (may be empty).
15    ToMany(Vec<ResourceIdentifier>),
16}
17
18impl Serialize for RelationshipData {
19    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
20        match self {
21            RelationshipData::ToOne(None) => serializer.serialize_none(),
22            RelationshipData::ToOne(Some(rid)) => rid.serialize(serializer),
23            RelationshipData::ToMany(rids) => rids.serialize(serializer),
24        }
25    }
26}
27
28impl<'de> Deserialize<'de> for RelationshipData {
29    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
30        let value = serde_json::Value::deserialize(deserializer)?;
31        match value {
32            serde_json::Value::Null => Ok(RelationshipData::ToOne(None)),
33            serde_json::Value::Array(arr) => {
34                let rids: Vec<ResourceIdentifier> = arr
35                    .into_iter()
36                    .map(serde_json::from_value)
37                    .collect::<Result<_, _>>()
38                    .map_err(de::Error::custom)?;
39                Ok(RelationshipData::ToMany(rids))
40            }
41            serde_json::Value::Object(_) => {
42                let rid: ResourceIdentifier =
43                    serde_json::from_value(value).map_err(de::Error::custom)?;
44                Ok(RelationshipData::ToOne(Some(rid)))
45            }
46            _ => Err(de::Error::custom(
47                "relationship data must be null, object, or array",
48            )),
49        }
50    }
51}
52
53/// Typed relationship reference. Carries the target type as a phantom
54/// for type-safe registry lookups.
55#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
56pub struct Relationship<T> {
57    /// The relationship linkage data.
58    pub data: RelationshipData,
59    /// Relationship-level links.
60    #[serde(skip_serializing_if = "Option::is_none")]
61    pub links: Option<Links>,
62    /// Relationship-level meta information.
63    #[serde(skip_serializing_if = "Option::is_none")]
64    pub meta: Option<Meta>,
65    #[serde(skip)]
66    _phantom: PhantomData<T>,
67}
68
69impl<T> Relationship<T> {
70    /// Create a new relationship with the given linkage data.
71    pub fn new(data: RelationshipData) -> Self {
72        Self {
73            data,
74            links: None,
75            meta: None,
76            _phantom: PhantomData,
77        }
78    }
79
80    /// Unified slice view of every identifier inside the relationship,
81    /// regardless of cardinality.
82    ///
83    /// - `ToOne(None)` → empty slice.
84    /// - `ToOne(Some(rid))` → one-element slice.
85    /// - `ToMany(vec)` → the full vec as a slice.
86    #[must_use]
87    pub fn identifiers(&self) -> &[ResourceIdentifier] {
88        match &self.data {
89            RelationshipData::ToOne(None) => &[],
90            RelationshipData::ToOne(Some(rid)) => std::slice::from_ref(rid),
91            RelationshipData::ToMany(rids) => rids.as_slice(),
92        }
93    }
94
95    /// Iterator over server-assigned IDs, regardless of cardinality.
96    /// Skips `Lid` identifiers and null to-one relationships.
97    pub fn ids(&self) -> impl Iterator<Item = &str> + '_ {
98        self.identifiers()
99            .iter()
100            .filter_map(|rid| rid.identity.as_id())
101    }
102
103    /// The first server-assigned ID in the relationship, or `None` if the
104    /// relationship is null-to-one, empty-to-many, or contains only local
105    /// identifiers.
106    #[must_use]
107    pub fn first_id(&self) -> Option<&str> {
108        self.ids().next()
109    }
110
111    /// The first identifier — server-assigned `id` or client-local `lid` —
112    /// as a `&str`. Returns `None` for null-to-one or empty-to-many.
113    /// Useful when you need *some* identifier without caring about kind
114    /// (e.g. when assembling an atomic-operation ref).
115    #[must_use]
116    pub fn first_id_or_lid(&self) -> Option<&str> {
117        self.identifiers()
118            .first()
119            .and_then(|rid| rid.identity.as_id().or_else(|| rid.identity.as_lid()))
120    }
121}
122
123#[cfg(test)]
124mod tests {
125    use super::*;
126    use crate::model::Identity;
127
128    #[test]
129    fn test_relationship_data_to_one() {
130        let json = r#"{"type":"people","id":"9"}"#;
131        let data: RelationshipData = serde_json::from_str(json).unwrap();
132        match &data {
133            RelationshipData::ToOne(Some(rid)) => {
134                assert_eq!(rid.type_, "people");
135                assert_eq!(rid.identity, Identity::Id("9".into()));
136            }
137            _ => panic!("expected ToOne(Some(...))"),
138        }
139        assert_eq!(serde_json::to_string(&data).unwrap(), json);
140    }
141
142    #[test]
143    fn test_relationship_data_to_one_null() {
144        let json = "null";
145        let data: RelationshipData = serde_json::from_str(json).unwrap();
146        assert!(matches!(data, RelationshipData::ToOne(None)));
147        assert_eq!(serde_json::to_string(&data).unwrap(), json);
148    }
149
150    #[test]
151    fn test_relationship_data_to_many() {
152        let json = r#"[{"type":"tags","id":"1"},{"type":"tags","id":"2"}]"#;
153        let data: RelationshipData = serde_json::from_str(json).unwrap();
154        match &data {
155            RelationshipData::ToMany(rids) => assert_eq!(rids.len(), 2),
156            _ => panic!("expected ToMany"),
157        }
158        assert_eq!(serde_json::to_string(&data).unwrap(), json);
159    }
160
161    #[test]
162    fn test_relationship_data_to_many_empty() {
163        let json = "[]";
164        let data: RelationshipData = serde_json::from_str(json).unwrap();
165        assert!(matches!(data, RelationshipData::ToMany(ref v) if v.is_empty()));
166    }
167
168    // ----- Relationship helpers (improvement #3) -----
169
170    fn rid(type_: &str, id: &str) -> ResourceIdentifier {
171        ResourceIdentifier {
172            type_: type_.into(),
173            identity: Identity::Id(id.into()),
174            meta: None,
175        }
176    }
177
178    fn lid_rid(type_: &str, lid: &str) -> ResourceIdentifier {
179        ResourceIdentifier {
180            type_: type_.into(),
181            identity: Identity::Lid(lid.into()),
182            meta: None,
183        }
184    }
185
186    // Phantom target; `Relationship::<T>` only uses T for type-safe registry
187    // lookups at the call site, so this is a fine stand-in for unit tests.
188    struct Target;
189
190    #[test]
191    fn relationship_ids_skips_null_to_one() {
192        let rel: Relationship<Target> = Relationship::new(RelationshipData::ToOne(None));
193        let collected: Vec<&str> = rel.ids().collect();
194        assert!(collected.is_empty());
195    }
196
197    #[test]
198    fn relationship_ids_returns_single_id_for_to_one() {
199        let rel: Relationship<Target> =
200            Relationship::new(RelationshipData::ToOne(Some(rid("people", "9"))));
201        let collected: Vec<&str> = rel.ids().collect();
202        assert_eq!(collected, vec!["9"]);
203    }
204
205    #[test]
206    fn relationship_ids_returns_all_ids_for_to_many() {
207        let rel: Relationship<Target> = Relationship::new(RelationshipData::ToMany(vec![
208            rid("tags", "1"),
209            rid("tags", "2"),
210            rid("tags", "3"),
211        ]));
212        let collected: Vec<&str> = rel.ids().collect();
213        assert_eq!(collected, vec!["1", "2", "3"]);
214    }
215
216    #[test]
217    fn relationship_ids_skips_lid_entries() {
218        let rel: Relationship<Target> = Relationship::new(RelationshipData::ToMany(vec![
219            rid("tags", "1"),
220            lid_rid("tags", "local-a"),
221            rid("tags", "3"),
222        ]));
223        let collected: Vec<&str> = rel.ids().collect();
224        assert_eq!(collected, vec!["1", "3"]);
225    }
226
227    #[test]
228    fn relationship_first_id_for_to_one() {
229        let rel: Relationship<Target> =
230            Relationship::new(RelationshipData::ToOne(Some(rid("people", "9"))));
231        assert_eq!(rel.first_id(), Some("9"));
232    }
233
234    #[test]
235    fn relationship_first_id_for_null_to_one_is_none() {
236        let rel: Relationship<Target> = Relationship::new(RelationshipData::ToOne(None));
237        assert_eq!(rel.first_id(), None);
238    }
239
240    #[test]
241    fn relationship_first_id_for_to_many_returns_first() {
242        let rel: Relationship<Target> = Relationship::new(RelationshipData::ToMany(vec![
243            rid("tags", "first"),
244            rid("tags", "second"),
245        ]));
246        assert_eq!(rel.first_id(), Some("first"));
247    }
248
249    #[test]
250    fn relationship_first_id_for_empty_to_many_is_none() {
251        let rel: Relationship<Target> = Relationship::new(RelationshipData::ToMany(vec![]));
252        assert_eq!(rel.first_id(), None);
253    }
254
255    #[test]
256    fn relationship_first_id_skips_lid_only_to_one() {
257        let rel: Relationship<Target> =
258            Relationship::new(RelationshipData::ToOne(Some(lid_rid("tags", "local"))));
259        assert_eq!(rel.first_id(), None);
260    }
261
262    #[test]
263    fn relationship_identifiers_for_null_to_one_is_empty() {
264        let rel: Relationship<Target> = Relationship::new(RelationshipData::ToOne(None));
265        assert!(rel.identifiers().is_empty());
266    }
267
268    #[test]
269    fn relationship_identifiers_for_to_one_has_one_element() {
270        let rel: Relationship<Target> =
271            Relationship::new(RelationshipData::ToOne(Some(rid("people", "9"))));
272        let slice = rel.identifiers();
273        assert_eq!(slice.len(), 1);
274        assert_eq!(slice[0].type_, "people");
275    }
276
277    #[test]
278    fn relationship_identifiers_for_to_many_returns_all() {
279        let rel: Relationship<Target> = Relationship::new(RelationshipData::ToMany(vec![
280            rid("tags", "1"),
281            lid_rid("tags", "local"),
282        ]));
283        assert_eq!(rel.identifiers().len(), 2);
284    }
285
286    #[test]
287    fn relationship_first_id_or_lid_returns_id() {
288        let rel: Relationship<Target> =
289            Relationship::new(RelationshipData::ToOne(Some(rid("tags", "42"))));
290        assert_eq!(rel.first_id_or_lid(), Some("42"));
291    }
292
293    #[test]
294    fn relationship_first_id_or_lid_returns_lid_when_that_is_all_there_is() {
295        let rel: Relationship<Target> =
296            Relationship::new(RelationshipData::ToOne(Some(lid_rid("tags", "local-a"))));
297        assert_eq!(rel.first_id_or_lid(), Some("local-a"));
298    }
299
300    #[test]
301    fn relationship_first_id_or_lid_returns_first_of_to_many() {
302        let rel: Relationship<Target> = Relationship::new(RelationshipData::ToMany(vec![
303            lid_rid("tags", "local"),
304            rid("tags", "server-id"),
305        ]));
306        assert_eq!(rel.first_id_or_lid(), Some("local"));
307    }
308}