oas_document/relationships.rs
1// OAS Spec §5.16 — Relationships & Affiliations
2//! Relationships and affiliations section for OAS Identity Documents.
3//!
4//! Describes the entity's organizational context: parent organization,
5//! team memberships, partnerships, and blocked DIDs.
6//! Note: lineage is a separate required section (§8) — relationships
7//! captures voluntary affiliations beyond the derivation chain.
8
9use serde::{Deserialize, Serialize};
10
11/// A relationship to another OAS entity.
12///
13/// # Examples
14///
15/// ```
16/// use oas_document::relationships::Affiliation;
17///
18/// let aff = Affiliation {
19/// did: "did:oas:prod:ao:research-team".to_string(),
20/// relationship: "member".to_string(),
21/// label: Some("Research Team".to_string()),
22/// };
23/// ```
24#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
25#[serde(rename_all = "camelCase")]
26pub struct Affiliation {
27 /// DID of the related entity.
28 pub did: String,
29
30 /// Relationship type: `"member"`, `"partner"`, `"subsidiary"`,
31 /// `"affiliate"`, `"delegate"`, `"advisor"`.
32 pub relationship: String,
33
34 /// Human-readable label for this affiliation.
35 #[serde(skip_serializing_if = "Option::is_none")]
36 pub label: Option<String>,
37}
38
39/// Relationships and affiliations section of an OAS Identity Document.
40///
41/// # Examples
42///
43/// ```
44/// use oas_document::relationships::{RelationshipsSection, Affiliation};
45///
46/// let rels = RelationshipsSection {
47/// parent_organization: Some("did:oas:prod:enr:acme-corp".to_string()),
48/// affiliations: vec![
49/// Affiliation {
50/// did: "did:oas:prod:ao:ai-lab".to_string(),
51/// relationship: "member".to_string(),
52/// label: Some("AI Research Lab".to_string()),
53/// },
54/// ],
55/// blocked_dids: vec![],
56/// };
57/// assert!(rels.parent_organization.is_some());
58/// ```
59#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
60#[serde(rename_all = "camelCase")]
61pub struct RelationshipsSection {
62 /// DID of the parent organization (ENR or AO).
63 /// Distinct from lineage `creator_did` — this is an organizational
64 /// relationship, not a derivation chain link.
65 #[serde(skip_serializing_if = "Option::is_none")]
66 pub parent_organization: Option<String>,
67
68 /// Voluntary affiliations (teams, partnerships, memberships).
69 #[serde(default, skip_serializing_if = "Vec::is_empty")]
70 pub affiliations: Vec<Affiliation>,
71
72 /// DIDs that this entity refuses to interact with.
73 /// Stored as public assertions to enable pre-flight checks.
74 #[serde(default, skip_serializing_if = "Vec::is_empty")]
75 pub blocked_dids: Vec<String>,
76}
77
78/// Standard relationship types.
79pub mod relationship_types {
80 pub const MEMBER: &str = "member";
81 pub const PARTNER: &str = "partner";
82 pub const SUBSIDIARY: &str = "subsidiary";
83 pub const AFFILIATE: &str = "affiliate";
84 pub const DELEGATE: &str = "delegate";
85 pub const ADVISOR: &str = "advisor";
86}
87
88#[cfg(test)]
89mod tests {
90 use super::*;
91
92 #[test]
93 fn serde_round_trip() {
94 let rels = RelationshipsSection {
95 parent_organization: Some("did:oas:prod:enr:acme".to_string()),
96 affiliations: vec![Affiliation {
97 did: "did:oas:prod:ao:team-alpha".to_string(),
98 relationship: relationship_types::MEMBER.to_string(),
99 label: Some("Team Alpha".to_string()),
100 }],
101 blocked_dids: vec!["did:oas:prod:agent:rogue-42".to_string()],
102 };
103
104 let json = serde_json::to_string(&rels).unwrap_or_default();
105 let parsed: RelationshipsSection =
106 serde_json::from_str(&json).unwrap_or_else(|_| rels.clone());
107 assert_eq!(rels, parsed);
108 }
109
110 #[test]
111 fn empty_section_deserializes() {
112 let json = r#"{}"#;
113 let rels: Result<RelationshipsSection, _> = serde_json::from_str(json);
114 assert!(rels.is_ok());
115 }
116
117 #[test]
118 fn optional_fields_omitted() {
119 let rels = RelationshipsSection {
120 parent_organization: None,
121 affiliations: vec![],
122 blocked_dids: vec![],
123 };
124 let json = serde_json::to_string(&rels).unwrap_or_default();
125 assert_eq!(json, "{}");
126 }
127}