oas-document 1.1.1

OAS Identity Document types and validation for the Open Agent Specification
Documentation
// OAS Spec §5.16 — Relationships & Affiliations
//! Relationships and affiliations section for OAS Identity Documents.
//!
//! Describes the entity's organizational context: parent organization,
//! team memberships, partnerships, and blocked DIDs.
//! Note: lineage is a separate required section (§8) — relationships
//! captures voluntary affiliations beyond the derivation chain.

use serde::{Deserialize, Serialize};

/// A relationship to another OAS entity.
///
/// # Examples
///
/// ```
/// use oas_document::relationships::Affiliation;
///
/// let aff = Affiliation {
///     did: "did:oas:prod:ao:research-team".to_string(),
///     relationship: "member".to_string(),
///     label: Some("Research Team".to_string()),
/// };
/// ```
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Affiliation {
    /// DID of the related entity.
    pub did: String,

    /// Relationship type: `"member"`, `"partner"`, `"subsidiary"`,
    /// `"affiliate"`, `"delegate"`, `"advisor"`.
    pub relationship: String,

    /// Human-readable label for this affiliation.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub label: Option<String>,
}

/// Relationships and affiliations section of an OAS Identity Document.
///
/// # Examples
///
/// ```
/// use oas_document::relationships::{RelationshipsSection, Affiliation};
///
/// let rels = RelationshipsSection {
///     parent_organization: Some("did:oas:prod:enr:acme-corp".to_string()),
///     affiliations: vec![
///         Affiliation {
///             did: "did:oas:prod:ao:ai-lab".to_string(),
///             relationship: "member".to_string(),
///             label: Some("AI Research Lab".to_string()),
///         },
///     ],
///     blocked_dids: vec![],
/// };
/// assert!(rels.parent_organization.is_some());
/// ```
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct RelationshipsSection {
    /// DID of the parent organization (ENR or AO).
    /// Distinct from lineage `creator_did` — this is an organizational
    /// relationship, not a derivation chain link.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub parent_organization: Option<String>,

    /// Voluntary affiliations (teams, partnerships, memberships).
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub affiliations: Vec<Affiliation>,

    /// DIDs that this entity refuses to interact with.
    /// Stored as public assertions to enable pre-flight checks.
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub blocked_dids: Vec<String>,
}

/// Standard relationship types.
pub mod relationship_types {
    pub const MEMBER: &str = "member";
    pub const PARTNER: &str = "partner";
    pub const SUBSIDIARY: &str = "subsidiary";
    pub const AFFILIATE: &str = "affiliate";
    pub const DELEGATE: &str = "delegate";
    pub const ADVISOR: &str = "advisor";
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn serde_round_trip() {
        let rels = RelationshipsSection {
            parent_organization: Some("did:oas:prod:enr:acme".to_string()),
            affiliations: vec![Affiliation {
                did: "did:oas:prod:ao:team-alpha".to_string(),
                relationship: relationship_types::MEMBER.to_string(),
                label: Some("Team Alpha".to_string()),
            }],
            blocked_dids: vec!["did:oas:prod:agent:rogue-42".to_string()],
        };

        let json = serde_json::to_string(&rels).unwrap_or_default();
        let parsed: RelationshipsSection =
            serde_json::from_str(&json).unwrap_or_else(|_| rels.clone());
        assert_eq!(rels, parsed);
    }

    #[test]
    fn empty_section_deserializes() {
        let json = r#"{}"#;
        let rels: Result<RelationshipsSection, _> = serde_json::from_str(json);
        assert!(rels.is_ok());
    }

    #[test]
    fn optional_fields_omitted() {
        let rels = RelationshipsSection {
            parent_organization: None,
            affiliations: vec![],
            blocked_dids: vec![],
        };
        let json = serde_json::to_string(&rels).unwrap_or_default();
        assert_eq!(json, "{}");
    }
}