Skip to main content

oas_document/
builder.rs

1// OAS Spec §5 — OAS Identity Document
2//! Builder pattern for constructing OAS Identity Documents.
3
4use oas_crypto::keypair::OasKeyPair;
5
6use crate::calendar::CalendarSection;
7use crate::compliance::ComplianceSection;
8use crate::conformance::ConformanceLevel;
9use crate::contact::ContactSection;
10use crate::document::{DocumentMetadata, OasDocument};
11use crate::error::DocumentError;
12use crate::governance::GovernanceSection;
13use crate::interop::InteroperabilitySection;
14use crate::lifecycle::LifecycleStatus;
15use crate::lineage_section::LineageSection;
16use crate::operational::OperationalSection;
17use crate::pricing::PricingSection;
18use crate::profile::ProfileSection;
19use crate::proof_format::DocumentProof;
20use crate::relationships::RelationshipsSection;
21use crate::reputation::ReputationSection;
22use crate::service::ServiceEndpoint;
23use crate::verification_method::VerificationMethod;
24
25/// Builder for constructing [`OasDocument`] instances.
26///
27/// Provides an ergonomic API for building OAS Identity Documents
28/// with proper defaults and validation.
29///
30/// # Examples
31///
32/// ```
33/// use oas_document::builder::DocumentBuilder;
34/// use oas_document::conformance::ConformanceLevel;
35/// use oas_crypto::keypair::OasKeyPair;
36///
37/// let keypair = OasKeyPair::generate();
38/// let doc = DocumentBuilder::new("did:oas:test:hmr:alice", "hmr")
39///     .controller("did:oas:test:hmr:alice")
40///     .conformance_level(ConformanceLevel::L1)
41///     .add_verification_method(&keypair)
42///     .name("Alice")
43///     .build_and_sign(&keypair, "2026-01-15T00:00:00Z")
44///     .unwrap();
45/// assert_eq!(doc.kind, "hmr");
46/// assert!(doc.proof.is_some());
47/// ```
48pub struct DocumentBuilder {
49    id: String,
50    kind: String,
51    controller: Option<String>,
52    verification_methods: Vec<VerificationMethod>,
53    authentication: Vec<String>,
54    assertion_method: Vec<String>,
55    capability_invocation: Vec<String>,
56    capability_delegation: Vec<String>,
57    services: Vec<ServiceEndpoint>,
58    name: Option<String>,
59    description: Option<String>,
60    lineage: Option<LineageSection>,
61    governance: Option<GovernanceSection>,
62    conformance_level: ConformanceLevel,
63    lifecycle_status: Option<LifecycleStatus>,
64    sequence: u64,
65    // Extended identity sections (§5.8–§5.16)
66    profile: Option<ProfileSection>,
67    contact: Option<ContactSection>,
68    calendar: Option<CalendarSection>,
69    operational: Option<OperationalSection>,
70    pricing: Option<PricingSection>,
71    interoperability: Option<InteroperabilitySection>,
72    reputation: Option<ReputationSection>,
73    compliance: Option<ComplianceSection>,
74    relationships: Option<RelationshipsSection>,
75}
76
77impl DocumentBuilder {
78    /// Creates a new builder with the given DID and kind.
79    ///
80    /// # Arguments
81    ///
82    /// * `id` - The `did:oas` identifier.
83    /// * `kind` - The entity kind string (e.g., `"hmr"`, `"agent"`).
84    pub fn new(id: &str, kind: &str) -> Self {
85        Self {
86            id: id.to_string(),
87            kind: kind.to_string(),
88            controller: None,
89            verification_methods: Vec::new(),
90            authentication: Vec::new(),
91            assertion_method: Vec::new(),
92            capability_invocation: Vec::new(),
93            capability_delegation: Vec::new(),
94            services: Vec::new(),
95            name: None,
96            description: None,
97            lineage: None,
98            governance: None,
99            conformance_level: ConformanceLevel::L0,
100            lifecycle_status: None,
101            sequence: 1,
102            profile: None,
103            contact: None,
104            calendar: None,
105            operational: None,
106            pricing: None,
107            interoperability: None,
108            reputation: None,
109            compliance: None,
110            relationships: None,
111        }
112    }
113
114    /// Sets the controller DID.
115    pub fn controller(mut self, controller: &str) -> Self {
116        self.controller = Some(controller.to_string());
117        self
118    }
119
120    /// Adds a verification method from a keypair.
121    ///
122    /// Automatically adds the key to authentication, assertionMethod,
123    /// and capabilityInvocation relationships.
124    pub fn add_verification_method(mut self, keypair: &OasKeyPair) -> Self {
125        let key_num = self.verification_methods.len() as u32 + 1;
126        let vm = VerificationMethod::new(&self.id, key_num, &keypair.public_key_multibase());
127        let key_ref = vm.id.clone();
128        self.verification_methods.push(vm);
129        self.authentication.push(key_ref.clone());
130        self.assertion_method.push(key_ref.clone());
131        self.capability_invocation.push(key_ref);
132        self
133    }
134
135    /// Sets the conformance level.
136    pub fn conformance_level(mut self, level: ConformanceLevel) -> Self {
137        self.conformance_level = level;
138        self
139    }
140
141    /// Sets the human-readable name.
142    pub fn name(mut self, name: &str) -> Self {
143        self.name = Some(name.to_string());
144        self
145    }
146
147    /// Sets the human-readable description.
148    pub fn description(mut self, description: &str) -> Self {
149        self.description = Some(description.to_string());
150        self
151    }
152
153    /// Sets the lineage section.
154    pub fn lineage(mut self, lineage: LineageSection) -> Self {
155        self.lineage = Some(lineage);
156        self
157    }
158
159    /// Sets the governance section (ENR entities only).
160    pub fn governance(mut self, governance: GovernanceSection) -> Self {
161        self.governance = Some(governance);
162        self
163    }
164
165    /// Sets the lifecycle status.
166    pub fn lifecycle_status(mut self, status: LifecycleStatus) -> Self {
167        self.lifecycle_status = Some(status);
168        self
169    }
170
171    /// Sets the sequence number.
172    pub fn sequence(mut self, seq: u64) -> Self {
173        self.sequence = seq;
174        self
175    }
176
177    /// Adds a service endpoint.
178    pub fn add_service(mut self, service: ServiceEndpoint) -> Self {
179        self.services.push(service);
180        self
181    }
182
183    /// Sets the profile section (§5.8).
184    pub fn profile(mut self, profile: ProfileSection) -> Self {
185        self.profile = Some(profile);
186        self
187    }
188
189    /// Sets the contact directory (§5.9).
190    pub fn contact(mut self, contact: ContactSection) -> Self {
191        self.contact = Some(contact);
192        self
193    }
194
195    /// Sets the calendar & scheduling section (§5.10).
196    pub fn calendar(mut self, calendar: CalendarSection) -> Self {
197        self.calendar = Some(calendar);
198        self
199    }
200
201    /// Sets the operational specification (§5.11).
202    pub fn operational(mut self, operational: OperationalSection) -> Self {
203        self.operational = Some(operational);
204        self
205    }
206
207    /// Sets the pricing & economics section (§5.12).
208    pub fn pricing(mut self, pricing: PricingSection) -> Self {
209        self.pricing = Some(pricing);
210        self
211    }
212
213    /// Sets the interoperability section (§5.13).
214    pub fn interoperability(mut self, interop: InteroperabilitySection) -> Self {
215        self.interoperability = Some(interop);
216        self
217    }
218
219    /// Sets the reputation summary (§5.14).
220    pub fn reputation(mut self, reputation: ReputationSection) -> Self {
221        self.reputation = Some(reputation);
222        self
223    }
224
225    /// Sets the compliance & jurisdiction section (§5.15).
226    pub fn compliance(mut self, compliance: ComplianceSection) -> Self {
227        self.compliance = Some(compliance);
228        self
229    }
230
231    /// Sets the relationships & affiliations section (§5.16).
232    pub fn relationships(mut self, relationships: RelationshipsSection) -> Self {
233        self.relationships = Some(relationships);
234        self
235    }
236
237    /// Builds the document and signs it.
238    ///
239    /// # Arguments
240    ///
241    /// * `signing_keypair` - The keypair to sign the document with.
242    /// * `created` - ISO 8601 timestamp for both document creation and proof.
243    ///
244    /// # Returns
245    ///
246    /// A complete, signed [`OasDocument`].
247    ///
248    /// # Errors
249    ///
250    /// Returns [`DocumentError`] if signing fails.
251    pub fn build_and_sign(
252        self,
253        signing_keypair: &OasKeyPair,
254        created: &str,
255    ) -> Result<OasDocument, DocumentError> {
256        let controller = self.controller.unwrap_or_else(|| self.id.clone());
257
258        let mut doc = OasDocument {
259            context: OasDocument::default_context(),
260            id: self.id,
261            controller,
262            verification_method: self.verification_methods,
263            authentication: self.authentication,
264            assertion_method: self.assertion_method,
265            capability_invocation: self.capability_invocation,
266            capability_delegation: self.capability_delegation,
267            service: self.services,
268            oas_version: crate::document::OAS_VERSION.to_string(),
269            kind: self.kind,
270            name: self.name,
271            description: self.description,
272            lineage: self.lineage,
273            governance: self.governance,
274            conformance_level: self.conformance_level,
275            lifecycle_status: self.lifecycle_status,
276            sequence: self.sequence,
277            metadata: DocumentMetadata {
278                created: created.to_string(),
279                updated: None,
280                version: None,
281            },
282            proof: None,
283            revoked: None,
284            revoked_at: None,
285            profile: self.profile,
286            contact: self.contact,
287            calendar: self.calendar,
288            operational: self.operational,
289            pricing: self.pricing,
290            interoperability: self.interoperability,
291            reputation: self.reputation,
292            compliance: self.compliance,
293            relationships: self.relationships,
294        };
295
296        // Generate proof: serialize without proof, sign, attach
297        let doc_json =
298            serde_json::to_value(&doc).map_err(|e| DocumentError::ProofGenerationFailed {
299                reason: format!("failed to serialize document: {e}"),
300            })?;
301
302        let vm_id = doc
303            .verification_method
304            .first()
305            .map(|vm| vm.id.as_str())
306            .ok_or_else(|| DocumentError::ProofGenerationFailed {
307                reason: "no verification method available for signing".to_string(),
308            })?
309            .to_string();
310
311        let proof = DocumentProof::generate(&doc_json, signing_keypair, &vm_id, created)?;
312        doc.proof = Some(proof);
313
314        Ok(doc)
315    }
316}
317
318#[cfg(test)]
319mod tests {
320    use super::*;
321
322    #[test]
323    fn build_hmr_document() {
324        let keypair = OasKeyPair::generate();
325        let doc = DocumentBuilder::new("did:oas:test:hmr:alice", "hmr")
326            .controller("did:oas:test:hmr:alice")
327            .conformance_level(ConformanceLevel::L1)
328            .add_verification_method(&keypair)
329            .name("Alice")
330            .build_and_sign(&keypair, "2026-01-15T00:00:00Z");
331
332        assert!(doc.is_ok());
333        let doc = doc.unwrap_or_else(|_| unreachable_doc());
334        assert_eq!(doc.kind, "hmr");
335        assert!(doc.proof.is_some());
336        assert_eq!(doc.sequence, 1);
337        assert!(doc.is_root());
338    }
339
340    #[test]
341    fn build_agent_document_with_lineage() {
342        let keypair = OasKeyPair::generate();
343        let lineage = LineageSection {
344            human_root_did: "did:oas:test:hmr:alice".to_string(),
345            creator_did: "did:oas:test:hmr:alice".to_string(),
346            generation: 0,
347            derivation_proof: None,
348            human_root_chain: vec![
349                "did:oas:test:agent:bot".to_string(),
350                "did:oas:test:hmr:alice".to_string(),
351            ],
352            org_inclusion_proof: None,
353            anchor_refs: None,
354        };
355
356        let doc = DocumentBuilder::new("did:oas:test:agent:bot", "agent")
357            .controller("did:oas:test:hmr:alice")
358            .conformance_level(ConformanceLevel::L1)
359            .add_verification_method(&keypair)
360            .lineage(lineage)
361            .lifecycle_status(LifecycleStatus::Active)
362            .build_and_sign(&keypair, "2026-01-15T00:00:00Z");
363
364        assert!(doc.is_ok());
365        let doc = doc.unwrap_or_else(|_| unreachable_doc());
366        assert!(doc.lineage.is_some());
367        assert!(!doc.is_root());
368    }
369
370    fn unreachable_doc() -> OasDocument {
371        OasDocument {
372            context: vec![],
373            id: String::new(),
374            controller: String::new(),
375            verification_method: vec![],
376            authentication: vec![],
377            assertion_method: vec![],
378            capability_invocation: vec![],
379            capability_delegation: vec![],
380            service: vec![],
381            oas_version: String::new(),
382            kind: String::new(),
383            name: None,
384            description: None,
385            lineage: None,
386            governance: None,
387            conformance_level: ConformanceLevel::L0,
388            lifecycle_status: None,
389            sequence: 0,
390            metadata: DocumentMetadata {
391                created: String::new(),
392                updated: None,
393                version: None,
394            },
395            proof: None,
396            revoked: None,
397            revoked_at: None,
398            profile: None,
399            contact: None,
400            calendar: None,
401            operational: None,
402            pricing: None,
403            interoperability: None,
404            reputation: None,
405            compliance: None,
406            relationships: None,
407        }
408    }
409}