use std::fmt;
use serde::de::{MapAccess, Visitor, value::MapAccessDeserializer};
use serde::{Deserialize, Deserializer, Serialize};
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct Act {
pub sub: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub act: Option<Box<Act>>,
}
#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
struct ActFields {
sub: String,
#[serde(default)]
act: Option<Box<Act>>,
}
impl<'de> Deserialize<'de> for Act {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
struct MapOnly;
impl<'de> Visitor<'de> for MapOnly {
type Value = Act;
fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("an RFC 8693 `act` object")
}
fn visit_map<A: MapAccess<'de>>(self, map: A) -> Result<Act, A::Error> {
let fields = ActFields::deserialize(MapAccessDeserializer::new(map))?;
Ok(Act {
sub: fields.sub,
act: fields.act,
})
}
}
deserializer.deserialize_map(MapOnly)
}
}
impl Act {
#[must_use]
pub fn new(sub: impl Into<String>) -> Self {
Self {
sub: sub.into(),
act: None,
}
}
#[must_use]
pub fn depth(&self) -> usize {
let mut depth = 1;
let mut link = self.act.as_deref();
while let Some(next) = link {
depth += 1;
link = next.act.as_deref();
}
depth
}
}
#[cfg(test)]
mod tests {
#![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
use super::*;
#[test]
fn single_actor_is_depth_one() {
assert_eq!(Act::new("01HSAB00000000000000000000").depth(), 1);
}
#[test]
fn depth_counts_every_nested_link() {
let chain = Act {
sub: "a".into(),
act: Some(Box::new(Act {
sub: "b".into(),
act: Some(Box::new(Act::new("c"))),
})),
};
assert_eq!(chain.depth(), 3);
}
#[test]
fn single_hop_serializes_to_the_rfc_shape() {
let json = serde_json::to_value(Act::new("actor")).expect("serialize");
assert_eq!(json, serde_json::json!({"sub": "actor"}));
}
#[test]
fn nested_shape_round_trips() {
let chain = Act {
sub: "outer".into(),
act: Some(Box::new(Act::new("inner"))),
};
let json = serde_json::to_value(&chain).expect("serialize");
assert_eq!(
json,
serde_json::json!({"sub":"outer","act":{"sub":"inner"}})
);
assert_eq!(serde_json::from_value::<Act>(json).expect("parse"), chain);
}
#[test]
fn interior_pii_is_rejected_at_every_level() {
for smuggled in [
serde_json::json!({"sub": "actor", "email": "a@b.c"}),
serde_json::json!({"sub": "outer", "act": {"sub": "inner", "email": "a@b.c"}}),
] {
assert!(
serde_json::from_value::<Act>(smuggled.clone()).is_err(),
"{smuggled} smuggles a claim past M45's top-level-only scan",
);
}
}
#[test]
fn sub_is_mandatory() {
assert!(serde_json::from_value::<Act>(serde_json::json!({})).is_err());
}
#[test]
fn sequence_form_is_not_an_actor_object() {
for seq in [
serde_json::json!(["actor"]),
serde_json::json!(["actor", null, "smuggled"]),
serde_json::json!({"sub": "outer", "act": ["inner", null, "smuggled"]}),
] {
assert!(
serde_json::from_value::<Act>(seq.clone()).is_err(),
"{seq} is not the RFC 8693 object form",
);
}
}
}