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    /// capabilityInvocation, and capabilityDelegation 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.clone());
132        self.capability_delegation.push(key_ref);
133        self
134    }
135
136    /// Sets the conformance level.
137    pub fn conformance_level(mut self, level: ConformanceLevel) -> Self {
138        self.conformance_level = level;
139        self
140    }
141
142    /// Sets the human-readable name.
143    pub fn name(mut self, name: &str) -> Self {
144        self.name = Some(name.to_string());
145        self
146    }
147
148    /// Sets the human-readable description.
149    pub fn description(mut self, description: &str) -> Self {
150        self.description = Some(description.to_string());
151        self
152    }
153
154    /// Sets the lineage section.
155    pub fn lineage(mut self, lineage: LineageSection) -> Self {
156        self.lineage = Some(lineage);
157        self
158    }
159
160    /// Sets the governance section (ENR entities only).
161    pub fn governance(mut self, governance: GovernanceSection) -> Self {
162        self.governance = Some(governance);
163        self
164    }
165
166    /// Sets the lifecycle status.
167    pub fn lifecycle_status(mut self, status: LifecycleStatus) -> Self {
168        self.lifecycle_status = Some(status);
169        self
170    }
171
172    /// Sets the sequence number.
173    pub fn sequence(mut self, seq: u64) -> Self {
174        self.sequence = seq;
175        self
176    }
177
178    /// Adds a service endpoint.
179    pub fn add_service(mut self, service: ServiceEndpoint) -> Self {
180        self.services.push(service);
181        self
182    }
183
184    /// Sets the profile section (§5.8).
185    pub fn profile(mut self, profile: ProfileSection) -> Self {
186        self.profile = Some(profile);
187        self
188    }
189
190    /// Sets the contact directory (§5.9).
191    pub fn contact(mut self, contact: ContactSection) -> Self {
192        self.contact = Some(contact);
193        self
194    }
195
196    /// Sets the calendar & scheduling section (§5.10).
197    pub fn calendar(mut self, calendar: CalendarSection) -> Self {
198        self.calendar = Some(calendar);
199        self
200    }
201
202    /// Sets the operational specification (§5.11).
203    pub fn operational(mut self, operational: OperationalSection) -> Self {
204        self.operational = Some(operational);
205        self
206    }
207
208    /// Sets the pricing & economics section (§5.12).
209    pub fn pricing(mut self, pricing: PricingSection) -> Self {
210        self.pricing = Some(pricing);
211        self
212    }
213
214    /// Sets the interoperability section (§5.13).
215    pub fn interoperability(mut self, interop: InteroperabilitySection) -> Self {
216        self.interoperability = Some(interop);
217        self
218    }
219
220    /// Sets the reputation summary (§5.14).
221    pub fn reputation(mut self, reputation: ReputationSection) -> Self {
222        self.reputation = Some(reputation);
223        self
224    }
225
226    /// Sets the compliance & jurisdiction section (§5.15).
227    pub fn compliance(mut self, compliance: ComplianceSection) -> Self {
228        self.compliance = Some(compliance);
229        self
230    }
231
232    /// Sets the relationships & affiliations section (§5.16).
233    pub fn relationships(mut self, relationships: RelationshipsSection) -> Self {
234        self.relationships = Some(relationships);
235        self
236    }
237
238    /// Builds the document and signs it.
239    ///
240    /// # Arguments
241    ///
242    /// * `signing_keypair` - The keypair to sign the document with.
243    /// * `created` - ISO 8601 timestamp for both document creation and proof.
244    ///
245    /// # Returns
246    ///
247    /// A complete, signed [`OasDocument`].
248    ///
249    /// # Errors
250    ///
251    /// Returns [`DocumentError`] if signing fails.
252    pub fn build_and_sign(
253        self,
254        signing_keypair: &OasKeyPair,
255        created: &str,
256    ) -> Result<OasDocument, DocumentError> {
257        let controller = self.controller.unwrap_or_else(|| self.id.clone());
258
259        let mut doc = OasDocument {
260            context: OasDocument::default_context(),
261            id: self.id,
262            controller,
263            verification_method: self.verification_methods,
264            authentication: self.authentication,
265            assertion_method: self.assertion_method,
266            capability_invocation: self.capability_invocation,
267            capability_delegation: self.capability_delegation,
268            service: self.services,
269            oas_version: crate::document::OAS_VERSION.to_string(),
270            kind: self.kind,
271            name: self.name,
272            description: self.description,
273            lineage: self.lineage,
274            governance: self.governance,
275            conformance_level: self.conformance_level,
276            lifecycle_status: self.lifecycle_status,
277            sequence: self.sequence,
278            metadata: DocumentMetadata {
279                created: created.to_string(),
280                updated: None,
281                version: None,
282            },
283            proof: None,
284            revoked: None,
285            revoked_at: None,
286            profile: self.profile,
287            contact: self.contact,
288            calendar: self.calendar,
289            operational: self.operational,
290            pricing: self.pricing,
291            interoperability: self.interoperability,
292            reputation: self.reputation,
293            compliance: self.compliance,
294            relationships: self.relationships,
295        };
296
297        // Generate proof: serialize without proof, sign, attach
298        let doc_json =
299            serde_json::to_value(&doc).map_err(|e| DocumentError::ProofGenerationFailed {
300                reason: format!("failed to serialize document: {e}"),
301            })?;
302
303        let vm_id = doc
304            .verification_method
305            .first()
306            .map(|vm| vm.id.as_str())
307            .ok_or_else(|| DocumentError::ProofGenerationFailed {
308                reason: "no verification method available for signing".to_string(),
309            })?
310            .to_string();
311
312        let proof = DocumentProof::generate(&doc_json, signing_keypair, &vm_id, created)?;
313        doc.proof = Some(proof);
314
315        Ok(doc)
316    }
317}
318
319#[cfg(test)]
320mod tests {
321    use super::*;
322
323    #[test]
324    fn build_hmr_document() {
325        let keypair = OasKeyPair::generate();
326        let doc = DocumentBuilder::new("did:oas:test:hmr:alice", "hmr")
327            .controller("did:oas:test:hmr:alice")
328            .conformance_level(ConformanceLevel::L1)
329            .add_verification_method(&keypair)
330            .name("Alice")
331            .build_and_sign(&keypair, "2026-01-15T00:00:00Z");
332
333        assert!(doc.is_ok());
334        let doc = doc.unwrap_or_else(|_| unreachable_doc());
335        assert_eq!(doc.kind, "hmr");
336        assert!(doc.proof.is_some());
337        assert_eq!(doc.sequence, 1);
338        assert!(doc.is_root());
339    }
340
341    #[test]
342    fn build_agent_document_with_lineage() {
343        let keypair = OasKeyPair::generate();
344        let lineage = LineageSection {
345            human_root_did: "did:oas:test:hmr:alice".to_string(),
346            creator_did: "did:oas:test:hmr:alice".to_string(),
347            generation: 0,
348            derivation_proof: None,
349            human_root_chain: vec![
350                "did:oas:test:agent:bot".to_string(),
351                "did:oas:test:hmr:alice".to_string(),
352            ],
353            org_inclusion_proof: None,
354            anchor_refs: None,
355        };
356
357        let doc = DocumentBuilder::new("did:oas:test:agent:bot", "agent")
358            .controller("did:oas:test:hmr:alice")
359            .conformance_level(ConformanceLevel::L1)
360            .add_verification_method(&keypair)
361            .lineage(lineage)
362            .lifecycle_status(LifecycleStatus::Active)
363            .build_and_sign(&keypair, "2026-01-15T00:00:00Z");
364
365        assert!(doc.is_ok());
366        let doc = doc.unwrap_or_else(|_| unreachable_doc());
367        assert!(doc.lineage.is_some());
368        assert!(!doc.is_root());
369    }
370
371    fn unreachable_doc() -> OasDocument {
372        OasDocument {
373            context: vec![],
374            id: String::new(),
375            controller: String::new(),
376            verification_method: vec![],
377            authentication: vec![],
378            assertion_method: vec![],
379            capability_invocation: vec![],
380            capability_delegation: vec![],
381            service: vec![],
382            oas_version: String::new(),
383            kind: String::new(),
384            name: None,
385            description: None,
386            lineage: None,
387            governance: None,
388            conformance_level: ConformanceLevel::L0,
389            lifecycle_status: None,
390            sequence: 0,
391            metadata: DocumentMetadata {
392                created: String::new(),
393                updated: None,
394                version: None,
395            },
396            proof: None,
397            revoked: None,
398            revoked_at: None,
399            profile: None,
400            contact: None,
401            calendar: None,
402            operational: None,
403            pricing: None,
404            interoperability: None,
405            reputation: None,
406            compliance: None,
407            relationships: None,
408        }
409    }
410}