Skip to main content

idprova_core/aid/
builder.rs

1use chrono::Utc;
2
3use super::document::*;
4use crate::crypto::KeyPair;
5use crate::{IdprovaError, Result};
6
7/// Fluent builder for constructing AID documents.
8pub struct AidBuilder {
9    id: Option<String>,
10    controller: Option<String>,
11    name: Option<String>,
12    description: Option<String>,
13    model: Option<String>,
14    runtime: Option<String>,
15    config_attestation: Option<String>,
16    verification_methods: Vec<VerificationMethod>,
17    trust_level: Option<String>,
18}
19
20impl AidBuilder {
21    pub fn new() -> Self {
22        Self {
23            id: None,
24            controller: None,
25            name: None,
26            description: None,
27            model: None,
28            runtime: None,
29            config_attestation: None,
30            verification_methods: Vec::new(),
31            trust_level: None,
32        }
33    }
34
35    /// Set the DID identifier (e.g., "did:idprova:example.com:my-agent").
36    pub fn id(mut self, id: impl Into<String>) -> Self {
37        self.id = Some(id.into());
38        self
39    }
40
41    /// Set the controller DID (e.g., "did:idprova:example.com:alice").
42    pub fn controller(mut self, controller: impl Into<String>) -> Self {
43        self.controller = Some(controller.into());
44        self
45    }
46
47    /// Set the agent's human-readable name.
48    pub fn name(mut self, name: impl Into<String>) -> Self {
49        self.name = Some(name.into());
50        self
51    }
52
53    /// Set an optional description.
54    pub fn description(mut self, description: impl Into<String>) -> Self {
55        self.description = Some(description.into());
56        self
57    }
58
59    /// Set the AI model identifier.
60    pub fn model(mut self, model: impl Into<String>) -> Self {
61        self.model = Some(model.into());
62        self
63    }
64
65    /// Set the runtime environment.
66    pub fn runtime(mut self, runtime: impl Into<String>) -> Self {
67        self.runtime = Some(runtime.into());
68        self
69    }
70
71    /// Set config attestation hash.
72    pub fn config_attestation(mut self, hash: impl Into<String>) -> Self {
73        self.config_attestation = Some(hash.into());
74        self
75    }
76
77    /// Add an Ed25519 verification method from a keypair.
78    pub fn add_ed25519_key(mut self, keypair: &KeyPair) -> Self {
79        let did = self.id.clone().unwrap_or_default();
80        self.verification_methods.push(VerificationMethod {
81            id: "#key-ed25519".to_string(),
82            key_type: "Ed25519VerificationKey2020".to_string(),
83            controller: self.controller.clone().unwrap_or(did),
84            public_key_multibase: keypair.public_key_multibase(),
85        });
86        self
87    }
88
89    /// Set the trust level.
90    pub fn trust_level(mut self, level: impl Into<String>) -> Self {
91        self.trust_level = Some(level.into());
92        self
93    }
94
95    /// Build the AID document.
96    pub fn build(self) -> Result<AidDocument> {
97        let id = self
98            .id
99            .ok_or_else(|| IdprovaError::AidValidation("id is required".into()))?;
100        let controller = self
101            .controller
102            .ok_or_else(|| IdprovaError::AidValidation("controller is required".into()))?;
103        let name = self
104            .name
105            .ok_or_else(|| IdprovaError::AidValidation("name is required".into()))?;
106
107        if self.verification_methods.is_empty() {
108            return Err(IdprovaError::AidValidation(
109                "at least one verification method required".into(),
110            ));
111        }
112
113        let auth_refs: Vec<String> = self
114            .verification_methods
115            .iter()
116            .map(|vm| vm.id.clone())
117            .collect();
118
119        let metadata = AgentMetadata {
120            name,
121            description: self.description,
122            model: self.model,
123            runtime: self.runtime,
124            config_attestation: self.config_attestation,
125        };
126
127        let service = vec![AidService {
128            id: "#idprova-metadata".to_string(),
129            service_type: "IdprovaAgentMetadata".to_string(),
130            service_endpoint: serde_json::to_value(&metadata)?,
131        }];
132
133        let now = Utc::now();
134
135        let doc = AidDocument {
136            context: vec![
137                "https://www.w3.org/ns/did/v1".to_string(),
138                "https://idprova.dev/v1".to_string(),
139            ],
140            id,
141            controller,
142            verification_method: self.verification_methods,
143            authentication: auth_refs,
144            service: Some(service),
145            trust_level: self.trust_level,
146            version: Some(1),
147            created: Some(now),
148            updated: Some(now),
149            proof: None,
150        };
151
152        doc.validate()?;
153        Ok(doc)
154    }
155}
156
157impl Default for AidBuilder {
158    fn default() -> Self {
159        Self::new()
160    }
161}
162
163#[cfg(test)]
164mod tests {
165    use super::*;
166    use crate::crypto::KeyPair;
167
168    #[test]
169    fn test_build_minimal_aid() {
170        let kp = KeyPair::generate();
171        let doc = AidBuilder::new()
172            .id("did:idprova:example.com:test-agent")
173            .controller("did:idprova:example.com:alice")
174            .name("Test Agent")
175            .add_ed25519_key(&kp)
176            .build()
177            .unwrap();
178
179        assert_eq!(doc.id, "did:idprova:example.com:test-agent");
180        assert_eq!(doc.controller, "did:idprova:example.com:alice");
181        assert_eq!(doc.verification_method.len(), 1);
182        assert!(doc.proof.is_none());
183    }
184
185    #[test]
186    fn test_build_full_aid() {
187        let kp = KeyPair::generate();
188        let doc = AidBuilder::new()
189            .id("did:idprova:techblaze.com.au:kai")
190            .controller("did:idprova:techblaze.com.au:pratyush")
191            .name("Kai Lead Agent")
192            .description("Primary orchestration agent")
193            .model("acme-ai/agent-v2")
194            .runtime("openclaw/v2.1")
195            .config_attestation("blake3:abcdef1234567890")
196            .trust_level("L1")
197            .add_ed25519_key(&kp)
198            .build()
199            .unwrap();
200
201        assert_eq!(doc.trust_level.as_deref(), Some("L1"));
202        assert!(doc.service.is_some());
203    }
204
205    #[test]
206    fn test_build_missing_id_fails() {
207        let kp = KeyPair::generate();
208        let result = AidBuilder::new()
209            .controller("did:idprova:example.com:alice")
210            .name("Test")
211            .add_ed25519_key(&kp)
212            .build();
213        assert!(result.is_err());
214    }
215
216    #[test]
217    fn test_build_no_keys_fails() {
218        let result = AidBuilder::new()
219            .id("did:idprova:example.com:agent")
220            .controller("did:idprova:example.com:alice")
221            .name("Test")
222            .build();
223        assert!(result.is_err());
224    }
225
226    #[test]
227    fn test_serialization_roundtrip() {
228        let kp = KeyPair::generate();
229        let doc = AidBuilder::new()
230            .id("did:idprova:example.com:agent")
231            .controller("did:idprova:example.com:alice")
232            .name("Test Agent")
233            .add_ed25519_key(&kp)
234            .build()
235            .unwrap();
236
237        let json = serde_json::to_string_pretty(&doc).unwrap();
238        let parsed: AidDocument = serde_json::from_str(&json).unwrap();
239        assert_eq!(parsed.id, doc.id);
240        assert_eq!(parsed.controller, doc.controller);
241    }
242}