use serde::{Deserialize, Deserializer, Serialize, Serializer};
use super::Meta;
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum Identity {
Id(String),
Lid(String),
}
impl Identity {
#[must_use]
pub fn as_id(&self) -> Option<&str> {
match self {
Identity::Id(id) => Some(id),
_ => None,
}
}
#[must_use]
pub fn as_lid(&self) -> Option<&str> {
match self {
Identity::Lid(lid) => Some(lid),
_ => None,
}
}
}
impl Default for Identity {
fn default() -> Self {
Identity::Id(String::new())
}
}
#[derive(Debug, Clone, PartialEq, Default)]
pub struct ResourceIdentifier {
pub r#type: String,
pub identity: Identity,
pub meta: Option<Meta>,
}
impl ResourceIdentifier {
#[must_use]
pub fn new(r#type: impl Into<String>, id: impl Into<String>) -> Self {
Self {
r#type: r#type.into(),
identity: Identity::Id(id.into()),
meta: None,
}
}
#[must_use]
pub fn with_lid(r#type: impl Into<String>, lid: impl Into<String>) -> Self {
Self {
r#type: r#type.into(),
identity: Identity::Lid(lid.into()),
meta: None,
}
}
#[must_use]
pub fn many(
r#type: impl Into<String>,
ids: impl IntoIterator<Item = impl Into<String>>,
) -> Vec<Self> {
let r#type = r#type.into();
ids.into_iter()
.map(|id| Self {
r#type: r#type.clone(),
identity: Identity::Id(id.into()),
meta: None,
})
.collect()
}
pub fn require_id(&self) -> crate::Result<&str> {
match self.identity.as_id() {
Some(id) => Ok(id),
None => Err(crate::Error::LidNotAllowed {
r#type: self.r#type.clone(),
lid: self.identity.as_lid().unwrap_or_default().to_string(),
}),
}
}
}
#[derive(Serialize)]
struct ResourceIdentifierSerRepr<'a> {
#[serde(rename = "type")]
type_: &'a str,
#[serde(skip_serializing_if = "Option::is_none")]
id: Option<&'a str>,
#[serde(skip_serializing_if = "Option::is_none")]
lid: Option<&'a str>,
#[serde(skip_serializing_if = "Option::is_none")]
meta: Option<&'a Meta>,
}
#[derive(Deserialize)]
struct ResourceIdentifierDeRepr {
#[serde(rename = "type")]
type_: String,
#[serde(default)]
id: Option<String>,
#[serde(default)]
lid: Option<String>,
#[serde(default)]
meta: Option<Meta>,
}
impl Serialize for ResourceIdentifier {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
let (id, lid) = match &self.identity {
Identity::Id(id) => (Some(id.as_str()), None),
Identity::Lid(lid) => (None, Some(lid.as_str())),
};
ResourceIdentifierSerRepr {
type_: &self.r#type,
id,
lid,
meta: self.meta.as_ref(),
}
.serialize(serializer)
}
}
impl<'de> Deserialize<'de> for ResourceIdentifier {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
let repr = ResourceIdentifierDeRepr::deserialize(deserializer)?;
let identity = match (repr.id, repr.lid) {
(Some(_), Some(_)) => {
return Err(serde::de::Error::custom(
"resource identifier must not have both `id` and `lid`",
));
}
(Some(id), None) => Identity::Id(id),
(None, Some(lid)) => Identity::Lid(lid),
(None, None) => {
return Err(serde::de::Error::custom(
"resource identifier must have `id` or `lid`",
));
}
};
Ok(ResourceIdentifier {
r#type: repr.type_,
identity,
meta: repr.meta,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_identity_as_id_variant() {
let id = Identity::Id("42".into());
assert_eq!(id.as_id(), Some("42"));
assert_eq!(id.as_lid(), None);
}
#[test]
fn test_identity_as_lid_variant() {
let lid = Identity::Lid("local-1".into());
assert_eq!(lid.as_id(), None);
assert_eq!(lid.as_lid(), Some("local-1"));
}
#[test]
fn test_resource_identifier_new_builds_id() {
let rid = ResourceIdentifier::new("people", "1");
assert_eq!(rid.r#type, "people");
assert_eq!(rid.identity, Identity::Id("1".into()));
assert_eq!(rid.meta, None);
assert_eq!(
serde_json::to_string(&rid).unwrap(),
r#"{"type":"people","id":"1"}"#
);
}
#[test]
fn test_resource_identifier_with_lid_builds_lid() {
let rid = ResourceIdentifier::with_lid("people", "local-1");
assert_eq!(rid.r#type, "people");
assert_eq!(rid.identity, Identity::Lid("local-1".into()));
assert_eq!(rid.meta, None);
assert_eq!(
serde_json::to_string(&rid).unwrap(),
r#"{"type":"people","lid":"local-1"}"#
);
}
#[test]
fn test_resource_identifier_many_builds_shared_type_ids() {
let rids = ResourceIdentifier::many("tags", ["1", "2", "3"]);
assert_eq!(rids.len(), 3);
assert!(rids.iter().all(|r| r.r#type == "tags"));
assert_eq!(rids[2].identity, Identity::Id("3".into()));
}
#[test]
fn test_resource_identifier_require_id_ok_for_id() {
let rid = ResourceIdentifier::new("people", "9");
assert_eq!(rid.require_id().unwrap(), "9");
}
#[test]
fn test_resource_identifier_require_id_errors_for_lid() {
let rid = ResourceIdentifier::with_lid("people", "local-1");
match rid.require_id() {
Err(crate::Error::LidNotAllowed { r#type, lid }) => {
assert_eq!(r#type, "people");
assert_eq!(lid, "local-1");
}
other => panic!("expected LidNotAllowed, got {other:?}"),
}
}
#[test]
fn test_resource_identifier_with_id() {
let json = r#"{"type":"people","id":"1"}"#;
let rid: ResourceIdentifier = serde_json::from_str(json).unwrap();
assert_eq!(rid.r#type, "people");
assert_eq!(rid.identity, Identity::Id("1".into()));
assert_eq!(rid.meta, None);
let serialized = serde_json::to_string(&rid).unwrap();
assert_eq!(serialized, json);
}
#[test]
fn test_resource_identifier_with_lid() {
let json = r#"{"type":"people","lid":"local-1"}"#;
let rid: ResourceIdentifier = serde_json::from_str(json).unwrap();
assert_eq!(rid.identity, Identity::Lid("local-1".into()));
let serialized = serde_json::to_string(&rid).unwrap();
assert_eq!(serialized, json);
}
#[test]
fn test_resource_identifier_with_meta() {
let json = r#"{"type":"articles","id":"5","meta":{"created":true}}"#;
let rid: ResourceIdentifier = serde_json::from_str(json).unwrap();
assert_eq!(rid.r#type, "articles");
assert!(rid.meta.is_some());
assert_eq!(
rid.meta.as_ref().unwrap()["created"],
serde_json::json!(true)
);
}
#[test]
fn test_resource_identifier_missing_identity() {
let json = r#"{"type":"people"}"#;
let result: std::result::Result<ResourceIdentifier, _> = serde_json::from_str(json);
assert!(result.is_err());
}
#[test]
fn test_resource_identifier_rejects_both_id_and_lid() {
let json = r#"{"type":"people","id":"1","lid":"local-1"}"#;
let result: std::result::Result<ResourceIdentifier, _> = serde_json::from_str(json);
assert!(result.is_err());
}
#[test]
fn test_resource_identifier_empty_id() {
let json = r#"{"type":"people","id":""}"#;
let rid: ResourceIdentifier = serde_json::from_str(json).unwrap();
assert_eq!(rid.identity, Identity::Id("".into()));
}
}