use std::marker::PhantomData;
use serde::de;
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use super::{Identity, Links, Meta, ResourceIdentifier};
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq)]
pub enum RelationshipData {
ToOne(Option<ResourceIdentifier>),
ToMany(Vec<ResourceIdentifier>),
}
impl Serialize for RelationshipData {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
match self {
RelationshipData::ToOne(None) => serializer.serialize_none(),
RelationshipData::ToOne(Some(rid)) => rid.serialize(serializer),
RelationshipData::ToMany(rids) => rids.serialize(serializer),
}
}
}
impl<'de> Deserialize<'de> for RelationshipData {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
let value = serde_json::Value::deserialize(deserializer)?;
match value {
serde_json::Value::Null => Ok(RelationshipData::ToOne(None)),
serde_json::Value::Array(arr) => {
let rids: Vec<ResourceIdentifier> = arr
.into_iter()
.map(serde_json::from_value)
.collect::<Result<_, _>>()
.map_err(de::Error::custom)?;
Ok(RelationshipData::ToMany(rids))
}
serde_json::Value::Object(_) => {
let rid: ResourceIdentifier =
serde_json::from_value(value).map_err(de::Error::custom)?;
Ok(RelationshipData::ToOne(Some(rid)))
}
_ => Err(de::Error::custom(
"relationship data must be null, object, or array",
)),
}
}
}
#[derive(Debug, Clone, PartialEq, Default)]
#[non_exhaustive]
pub struct ResourceRelationship {
pub data: Option<RelationshipData>,
pub links: Option<Links>,
pub meta: Option<Meta>,
}
impl From<Links> for ResourceRelationship {
fn from(links: Links) -> Self {
Self::from_links(links)
}
}
impl From<Meta> for ResourceRelationship {
fn from(meta: Meta) -> Self {
Self::from_meta(meta)
}
}
impl ResourceRelationship {
#[must_use]
pub fn new(data: RelationshipData) -> Self {
Self {
data: Some(data),
links: None,
meta: None,
}
}
#[must_use]
pub fn from_links(links: Links) -> Self {
Self {
data: None,
links: Some(links),
meta: None,
}
}
#[must_use]
pub fn from_meta(meta: Meta) -> Self {
Self {
data: None,
links: None,
meta: Some(meta),
}
}
#[must_use]
pub fn with_data(mut self, data: RelationshipData) -> Self {
self.data = Some(data);
self
}
#[must_use]
pub fn with_links(mut self, links: Links) -> Self {
self.links = Some(links);
self
}
#[must_use]
pub fn with_meta(mut self, meta: Meta) -> Self {
self.meta = Some(meta);
self
}
#[must_use]
pub fn identifiers(&self) -> &[ResourceIdentifier] {
match &self.data {
Some(RelationshipData::ToOne(Some(rid))) => std::slice::from_ref(rid),
Some(RelationshipData::ToMany(rids)) => rids.as_slice(),
_ => &[],
}
}
#[must_use]
pub fn to_one_identity(&self) -> Option<&Identity> {
match &self.data {
Some(RelationshipData::ToOne(Some(rid))) => Some(&rid.identity),
_ => None,
}
}
#[must_use]
pub fn first_id(&self) -> Option<&str> {
self.identifiers()
.iter()
.find_map(|rid| rid.identity.as_id())
}
#[must_use]
pub fn first_id_or_lid(&self) -> Option<&str> {
self.identifiers()
.first()
.and_then(|rid| rid.identity.as_id().or_else(|| rid.identity.as_lid()))
}
pub(crate) fn from_json_object(
obj: &serde_json::Map<String, serde_json::Value>,
) -> Result<Self, String> {
let data: Option<RelationshipData> = obj
.get("data")
.map(Deserialize::deserialize)
.transpose()
.map_err(|e: serde_json::Error| e.to_string())?;
let links: Option<Links> = obj
.get("links")
.map(Deserialize::deserialize)
.transpose()
.map_err(|e: serde_json::Error| e.to_string())?;
let meta: Option<Meta> = obj
.get("meta")
.map(Deserialize::deserialize)
.transpose()
.map_err(|e: serde_json::Error| e.to_string())?;
if data.is_none() && links.is_none() && meta.is_none() {
return Err(
"relationship object must contain at least one of `data`, `links`, or `meta`"
.to_string(),
);
}
Ok(ResourceRelationship { data, links, meta })
}
pub(crate) fn to_json_object(
&self,
) -> Result<serde_json::Map<String, serde_json::Value>, serde_json::Error> {
let mut obj = serde_json::Map::new();
if let Some(ref data) = self.data {
obj.insert("data".to_string(), serde_json::to_value(data)?);
}
if let Some(ref links) = self.links {
obj.insert("links".to_string(), serde_json::to_value(links)?);
}
if let Some(ref meta) = self.meta {
obj.insert("meta".to_string(), serde_json::to_value(meta)?);
}
Ok(obj)
}
}
impl Serialize for ResourceRelationship {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
let obj = self.to_json_object().map_err(serde::ser::Error::custom)?;
serde_json::Value::Object(obj).serialize(serializer)
}
}
impl<'de> Deserialize<'de> for ResourceRelationship {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
let value = serde_json::Value::deserialize(deserializer)?;
let obj = value
.as_object()
.ok_or_else(|| de::Error::custom("relationship object must be a JSON object"))?;
ResourceRelationship::from_json_object(obj).map_err(de::Error::custom)
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Relationship<T> {
pub data: RelationshipData,
#[serde(skip_serializing_if = "Option::is_none")]
pub links: Option<Links>,
#[serde(skip_serializing_if = "Option::is_none")]
pub meta: Option<Meta>,
#[serde(skip)]
_phantom: PhantomData<T>,
}
impl<T> Relationship<T> {
#[must_use]
pub fn new(data: RelationshipData) -> Self {
Self {
data,
links: None,
meta: None,
_phantom: PhantomData,
}
}
#[must_use]
pub fn to_one(rid: ResourceIdentifier) -> Self {
Self::new(RelationshipData::ToOne(Some(rid)))
}
#[must_use]
pub fn to_one_null() -> Self {
Self::new(RelationshipData::ToOne(None))
}
#[must_use]
pub fn to_one_id(r#type: impl Into<String>, id: impl Into<String>) -> Self {
Self::to_one(ResourceIdentifier::new(r#type, id))
}
#[must_use]
pub fn to_many(rids: impl IntoIterator<Item = ResourceIdentifier>) -> Self {
Self::new(RelationshipData::ToMany(rids.into_iter().collect()))
}
#[must_use]
pub fn identifiers(&self) -> &[ResourceIdentifier] {
match &self.data {
RelationshipData::ToOne(None) => &[],
RelationshipData::ToOne(Some(rid)) => std::slice::from_ref(rid),
RelationshipData::ToMany(rids) => rids.as_slice(),
}
}
pub fn ids(&self) -> impl Iterator<Item = &str> + '_ {
self.identifiers()
.iter()
.filter_map(|rid| rid.identity.as_id())
}
#[must_use]
pub fn first_id(&self) -> Option<&str> {
self.ids().next()
}
#[must_use]
pub fn first_id_or_lid(&self) -> Option<&str> {
self.identifiers()
.first()
.and_then(|rid| rid.identity.as_id().or_else(|| rid.identity.as_lid()))
}
pub fn single_id(&self) -> crate::Result<&str> {
match &self.data {
RelationshipData::ToOne(Some(rid)) => rid.require_id(),
RelationshipData::ToOne(None) => Err(crate::Error::NullRelationship),
RelationshipData::ToMany(_) => Err(crate::Error::RelationshipCardinalityMismatch {
expected: crate::Cardinality::ToOne,
}),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::model::Identity;
#[test]
fn resource_relationship_serialize_omits_none_fields() {
let rel = ResourceRelationship::new(RelationshipData::ToOne(Some(rid("people", "9"))));
let v = serde_json::to_value(&rel).unwrap();
assert_eq!(v["data"]["id"], "9");
assert!(v.get("links").is_none());
assert!(v.get("meta").is_none());
}
#[test]
fn resource_relationship_serialize_null_to_one_emits_data_null() {
let rel = ResourceRelationship::new(RelationshipData::ToOne(None));
let v = serde_json::to_value(&rel).unwrap();
assert!(v.get("data").is_some());
assert!(v["data"].is_null());
}
#[test]
fn resource_relationship_deserialize_preserves_links_and_meta() {
let json =
r#"{"data":{"type":"people","id":"9"},"links":{"related":"/a/1"},"meta":{"c":1}}"#;
let rel: ResourceRelationship = serde_json::from_str(json).unwrap();
assert!(matches!(rel.data, Some(RelationshipData::ToOne(Some(_)))));
assert!(rel.links.is_some());
assert!(rel.meta.is_some());
}
#[test]
fn resource_relationship_deserialize_links_only_has_no_data() {
let json = r#"{"links":{"related":"/a/1"}}"#;
let rel: ResourceRelationship = serde_json::from_str(json).unwrap();
assert!(rel.data.is_none());
assert!(rel.links.is_some());
}
#[test]
fn resource_relationship_deserialize_rejects_empty_object() {
let err = serde_json::from_str::<ResourceRelationship>("{}").unwrap_err();
assert!(err.to_string().contains("at least one of"), "got: {err}");
}
#[test]
fn resource_relationship_round_trip_is_stable() {
let json = r#"{"data":null,"meta":{"k":"v"}}"#;
let rel: ResourceRelationship = serde_json::from_str(json).unwrap();
let out = serde_json::to_value(&rel).unwrap();
assert!(out["data"].is_null());
assert_eq!(out["meta"]["k"], "v");
}
#[test]
fn resource_relationship_from_links_is_links_only() {
let mut links = Links::new();
links.insert("related", Some(crate::Link::String("/a/1".into())));
let rel = ResourceRelationship::from_links(links);
assert!(rel.data.is_none());
assert!(rel.meta.is_none());
let v = serde_json::to_value(&rel).unwrap();
assert!(v.get("data").is_none());
assert_eq!(v["links"]["related"], "/a/1");
}
#[test]
fn resource_relationship_builder_chaining_composes_members() {
let mut meta = serde_json::Map::new();
meta.insert("count".into(), serde_json::json!(2));
let rel = ResourceRelationship::new(RelationshipData::ToOne(Some(rid("people", "9"))))
.with_meta(meta);
assert!(matches!(rel.data, Some(RelationshipData::ToOne(Some(_)))));
assert!(rel.meta.is_some());
assert!(rel.links.is_none());
}
#[test]
fn resource_relationship_to_one_identity_and_ids() {
let rel = ResourceRelationship::new(RelationshipData::ToOne(Some(rid("people", "9"))));
assert_eq!(rel.to_one_identity(), Some(&Identity::Id("9".into())));
assert_eq!(rel.first_id(), Some("9"));
assert_eq!(rel.first_id_or_lid(), Some("9"));
}
#[test]
fn resource_relationship_lid_only_to_one() {
let rel = ResourceRelationship::new(RelationshipData::ToOne(Some(lid_rid("tags", "t1"))));
assert_eq!(rel.first_id(), None);
assert_eq!(rel.first_id_or_lid(), Some("t1"));
assert!(matches!(rel.to_one_identity(), Some(Identity::Lid(_))));
}
#[test]
fn resource_relationship_no_data_and_null_to_one_have_no_identity() {
let links_only = ResourceRelationship::from_meta(serde_json::Map::new());
assert_eq!(links_only.to_one_identity(), None);
assert_eq!(links_only.first_id_or_lid(), None);
assert!(links_only.identifiers().is_empty());
let null_to_one = ResourceRelationship::new(RelationshipData::ToOne(None));
assert_eq!(null_to_one.to_one_identity(), None);
assert!(null_to_one.identifiers().is_empty());
}
#[test]
fn resource_relationship_to_many_identifiers_and_first() {
let rel = ResourceRelationship::new(RelationshipData::ToMany(vec![
rid("tags", "1"),
rid("tags", "2"),
]));
assert_eq!(rel.identifiers().len(), 2);
assert_eq!(rel.first_id(), Some("1"));
assert_eq!(rel.to_one_identity(), None);
}
#[test]
fn test_relationship_data_to_one() {
let json = r#"{"type":"people","id":"9"}"#;
let data: RelationshipData = serde_json::from_str(json).unwrap();
match &data {
RelationshipData::ToOne(Some(rid)) => {
assert_eq!(rid.r#type, "people");
assert_eq!(rid.identity, Identity::Id("9".into()));
}
_ => panic!("expected ToOne(Some(...))"),
}
assert_eq!(serde_json::to_string(&data).unwrap(), json);
}
#[test]
fn test_relationship_data_to_one_null() {
let json = "null";
let data: RelationshipData = serde_json::from_str(json).unwrap();
assert!(matches!(data, RelationshipData::ToOne(None)));
assert_eq!(serde_json::to_string(&data).unwrap(), json);
}
#[test]
fn test_relationship_data_to_many() {
let json = r#"[{"type":"tags","id":"1"},{"type":"tags","id":"2"}]"#;
let data: RelationshipData = serde_json::from_str(json).unwrap();
match &data {
RelationshipData::ToMany(rids) => assert_eq!(rids.len(), 2),
_ => panic!("expected ToMany"),
}
assert_eq!(serde_json::to_string(&data).unwrap(), json);
}
#[test]
fn test_relationship_data_to_many_empty() {
let json = "[]";
let data: RelationshipData = serde_json::from_str(json).unwrap();
assert!(matches!(data, RelationshipData::ToMany(ref v) if v.is_empty()));
}
fn rid(type_: &str, id: &str) -> ResourceIdentifier {
ResourceIdentifier {
r#type: type_.into(),
identity: Identity::Id(id.into()),
meta: None,
}
}
fn lid_rid(type_: &str, lid: &str) -> ResourceIdentifier {
ResourceIdentifier {
r#type: type_.into(),
identity: Identity::Lid(lid.into()),
meta: None,
}
}
struct Target;
#[test]
fn relationship_to_one_wraps_single_identifier() {
let rel: Relationship<Target> = Relationship::to_one(rid("people", "9"));
assert_eq!(rel.data, RelationshipData::ToOne(Some(rid("people", "9"))));
}
#[test]
fn relationship_to_one_null_is_empty_to_one() {
let rel: Relationship<Target> = Relationship::to_one_null();
assert_eq!(rel.data, RelationshipData::ToOne(None));
}
#[test]
fn relationship_to_one_id_builds_identifier() {
let rel: Relationship<Target> = Relationship::to_one_id("people", "9");
assert_eq!(rel.data, RelationshipData::ToOne(Some(rid("people", "9"))));
assert_eq!(rel.first_id(), Some("9"));
}
#[test]
fn relationship_to_many_collects_iterator() {
let rel: Relationship<Target> =
Relationship::to_many([rid("people", "1"), rid("people", "2")]);
assert_eq!(
rel.data,
RelationshipData::ToMany(vec![rid("people", "1"), rid("people", "2")])
);
}
#[test]
fn relationship_ids_skips_null_to_one() {
let rel: Relationship<Target> = Relationship::new(RelationshipData::ToOne(None));
let collected: Vec<&str> = rel.ids().collect();
assert!(collected.is_empty());
}
#[test]
fn relationship_ids_returns_single_id_for_to_one() {
let rel: Relationship<Target> =
Relationship::new(RelationshipData::ToOne(Some(rid("people", "9"))));
let collected: Vec<&str> = rel.ids().collect();
assert_eq!(collected, vec!["9"]);
}
#[test]
fn relationship_ids_returns_all_ids_for_to_many() {
let rel: Relationship<Target> = Relationship::new(RelationshipData::ToMany(vec![
rid("tags", "1"),
rid("tags", "2"),
rid("tags", "3"),
]));
let collected: Vec<&str> = rel.ids().collect();
assert_eq!(collected, vec!["1", "2", "3"]);
}
#[test]
fn relationship_ids_skips_lid_entries() {
let rel: Relationship<Target> = Relationship::new(RelationshipData::ToMany(vec![
rid("tags", "1"),
lid_rid("tags", "local-a"),
rid("tags", "3"),
]));
let collected: Vec<&str> = rel.ids().collect();
assert_eq!(collected, vec!["1", "3"]);
}
#[test]
fn relationship_first_id_for_to_one() {
let rel: Relationship<Target> =
Relationship::new(RelationshipData::ToOne(Some(rid("people", "9"))));
assert_eq!(rel.first_id(), Some("9"));
}
#[test]
fn relationship_first_id_for_null_to_one_is_none() {
let rel: Relationship<Target> = Relationship::new(RelationshipData::ToOne(None));
assert_eq!(rel.first_id(), None);
}
#[test]
fn relationship_first_id_for_to_many_returns_first() {
let rel: Relationship<Target> = Relationship::new(RelationshipData::ToMany(vec![
rid("tags", "first"),
rid("tags", "second"),
]));
assert_eq!(rel.first_id(), Some("first"));
}
#[test]
fn relationship_first_id_for_empty_to_many_is_none() {
let rel: Relationship<Target> = Relationship::new(RelationshipData::ToMany(vec![]));
assert_eq!(rel.first_id(), None);
}
#[test]
fn relationship_first_id_skips_lid_only_to_one() {
let rel: Relationship<Target> =
Relationship::new(RelationshipData::ToOne(Some(lid_rid("tags", "local"))));
assert_eq!(rel.first_id(), None);
}
#[test]
fn relationship_identifiers_for_null_to_one_is_empty() {
let rel: Relationship<Target> = Relationship::new(RelationshipData::ToOne(None));
assert!(rel.identifiers().is_empty());
}
#[test]
fn relationship_identifiers_for_to_one_has_one_element() {
let rel: Relationship<Target> =
Relationship::new(RelationshipData::ToOne(Some(rid("people", "9"))));
let slice = rel.identifiers();
assert_eq!(slice.len(), 1);
assert_eq!(slice[0].r#type, "people");
}
#[test]
fn relationship_identifiers_for_to_many_returns_all() {
let rel: Relationship<Target> = Relationship::new(RelationshipData::ToMany(vec![
rid("tags", "1"),
lid_rid("tags", "local"),
]));
assert_eq!(rel.identifiers().len(), 2);
}
#[test]
fn relationship_first_id_or_lid_returns_id() {
let rel: Relationship<Target> =
Relationship::new(RelationshipData::ToOne(Some(rid("tags", "42"))));
assert_eq!(rel.first_id_or_lid(), Some("42"));
}
#[test]
fn relationship_first_id_or_lid_returns_lid_when_that_is_all_there_is() {
let rel: Relationship<Target> =
Relationship::new(RelationshipData::ToOne(Some(lid_rid("tags", "local-a"))));
assert_eq!(rel.first_id_or_lid(), Some("local-a"));
}
#[test]
fn relationship_first_id_or_lid_returns_first_of_to_many() {
let rel: Relationship<Target> = Relationship::new(RelationshipData::ToMany(vec![
lid_rid("tags", "local"),
rid("tags", "server-id"),
]));
assert_eq!(rel.first_id_or_lid(), Some("local"));
}
#[test]
fn single_id_returns_to_one_id() {
let rel: Relationship<Target> =
Relationship::new(RelationshipData::ToOne(Some(rid("people", "9"))));
assert_eq!(rel.single_id().unwrap(), "9");
}
#[test]
fn single_id_errors_for_null_to_one() {
let rel: Relationship<Target> = Relationship::new(RelationshipData::ToOne(None));
let err = rel.single_id().unwrap_err();
assert!(matches!(err, crate::Error::NullRelationship));
}
#[test]
fn single_id_errors_for_lid_only_to_one() {
let rel: Relationship<Target> =
Relationship::new(RelationshipData::ToOne(Some(lid_rid("tags", "local-a"))));
let err = rel.single_id().unwrap_err();
match err {
crate::Error::LidNotAllowed { r#type, lid } => {
assert_eq!(r#type, "tags");
assert_eq!(lid, "local-a");
}
other => panic!("expected LidNotAllowed, got {other:?}"),
}
}
#[test]
fn single_id_errors_for_to_many() {
let rel: Relationship<Target> = Relationship::new(RelationshipData::ToMany(vec![
rid("tags", "1"),
rid("tags", "2"),
]));
let err = rel.single_id().unwrap_err();
assert!(matches!(
err,
crate::Error::RelationshipCardinalityMismatch {
expected: crate::Cardinality::ToOne
}
));
}
#[test]
fn single_id_errors_for_empty_to_many() {
let rel: Relationship<Target> = Relationship::new(RelationshipData::ToMany(vec![]));
let err = rel.single_id().unwrap_err();
assert!(matches!(
err,
crate::Error::RelationshipCardinalityMismatch {
expected: crate::Cardinality::ToOne
}
));
}
}