Skip to main content

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}