Skip to main content

oas_document/
compliance.rs

1// OAS Spec §5.15 — Compliance & Jurisdiction
2//! Compliance and jurisdiction section for OAS Identity Documents.
3//!
4//! Describes the legal and regulatory context in which an entity operates:
5//! jurisdictions, certifications, and policy URLs.
6
7use serde::{Deserialize, Serialize};
8
9/// A compliance certification held by the entity.
10///
11/// # Examples
12///
13/// ```
14/// use oas_document::compliance::Certification;
15///
16/// let cert = Certification {
17///     standard: "SOC2 Type II".to_string(),
18///     issuer: Some("Vanta".to_string()),
19///     valid_until: Some("2027-06-30".to_string()),
20///     url: Some("https://acme.com/compliance/soc2".to_string()),
21/// };
22/// ```
23#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
24#[serde(rename_all = "camelCase")]
25pub struct Certification {
26    /// Standard name: `"SOC2 Type II"`, `"GDPR"`, `"HIPAA"`, `"ISO 27001"`.
27    pub standard: String,
28
29    /// Issuing body or audit firm.
30    #[serde(skip_serializing_if = "Option::is_none")]
31    pub issuer: Option<String>,
32
33    /// Expiry date (ISO 8601 date or datetime).
34    #[serde(skip_serializing_if = "Option::is_none")]
35    pub valid_until: Option<String>,
36
37    /// URL to certification evidence or public report.
38    #[serde(skip_serializing_if = "Option::is_none")]
39    pub url: Option<String>,
40}
41
42/// Compliance and jurisdiction section of an OAS Identity Document.
43///
44/// # Examples
45///
46/// ```
47/// use oas_document::compliance::{ComplianceSection, Certification};
48///
49/// let compliance = ComplianceSection {
50///     jurisdictions: vec!["US".to_string(), "EU".to_string()],
51///     certifications: vec![Certification {
52///         standard: "GDPR".to_string(),
53///         issuer: None,
54///         valid_until: None,
55///         url: None,
56///     }],
57///     terms_of_service_url: Some("https://acme.com/tos".to_string()),
58///     privacy_policy_url: Some("https://acme.com/privacy".to_string()),
59///     data_handling_policy_url: Some("https://acme.com/data-policy".to_string()),
60///     data_residency: Some("EU".to_string()),
61/// };
62/// assert_eq!(compliance.jurisdictions.len(), 2);
63/// ```
64#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
65#[serde(rename_all = "camelCase")]
66pub struct ComplianceSection {
67    /// Operating jurisdictions (ISO 3166-1 alpha-2 country codes).
68    #[serde(default, skip_serializing_if = "Vec::is_empty")]
69    pub jurisdictions: Vec<String>,
70
71    /// Compliance certifications.
72    #[serde(default, skip_serializing_if = "Vec::is_empty")]
73    pub certifications: Vec<Certification>,
74
75    /// Terms of service URL.
76    #[serde(skip_serializing_if = "Option::is_none")]
77    pub terms_of_service_url: Option<String>,
78
79    /// Privacy policy URL.
80    #[serde(skip_serializing_if = "Option::is_none")]
81    pub privacy_policy_url: Option<String>,
82
83    /// Data handling / data processing policy URL.
84    #[serde(skip_serializing_if = "Option::is_none")]
85    pub data_handling_policy_url: Option<String>,
86
87    /// Data residency region (ISO 3166-1 alpha-2 or region name).
88    #[serde(skip_serializing_if = "Option::is_none")]
89    pub data_residency: Option<String>,
90}
91
92#[cfg(test)]
93mod tests {
94    use super::*;
95
96    #[test]
97    fn serde_round_trip() {
98        let comp = ComplianceSection {
99            jurisdictions: vec!["US".to_string()],
100            certifications: vec![Certification {
101                standard: "SOC2".to_string(),
102                issuer: Some("Vanta".to_string()),
103                valid_until: Some("2027-12-31".to_string()),
104                url: None,
105            }],
106            terms_of_service_url: Some("https://example.com/tos".to_string()),
107            privacy_policy_url: None,
108            data_handling_policy_url: None,
109            data_residency: Some("US".to_string()),
110        };
111
112        let json = serde_json::to_string(&comp).unwrap_or_default();
113        let parsed: ComplianceSection =
114            serde_json::from_str(&json).unwrap_or_else(|_| comp.clone());
115        assert_eq!(comp, parsed);
116    }
117
118    #[test]
119    fn empty_section_deserializes() {
120        let json = r#"{}"#;
121        let comp: Result<ComplianceSection, _> = serde_json::from_str(json);
122        assert!(comp.is_ok());
123    }
124}