1use serde::{Deserialize, Serialize};
7use std::collections::HashMap;
8#[cfg(not(target_arch = "wasm32"))]
9use std::sync::Arc;
10
11#[cfg(not(target_arch = "wasm32"))]
12use crate::q42_lexicon::{CborLdError, Q42CborLdParser, Q42Context};
13#[cfg(not(target_arch = "wasm32"))]
14use crate::q42_volume::Q42Volume;
15
16#[derive(Debug, Clone, Serialize, Deserialize)]
18pub struct VaultManifest {
19 #[serde(rename = "@context")]
20 pub context: String,
21 #[serde(rename = "type")]
22 pub manifest_type: String,
23 pub id: String,
24 pub created: String,
25 pub modified: String,
26 pub vocabulary: VocabularyLD,
27 pub collections: Vec<CollectionLD>,
28 pub capabilities: Vec<CapabilityLD>,
29 #[serde(rename = "did_q42")]
30 pub did_q42: Option<String>,
31 #[serde(rename = "semantic_context")]
32 pub semantic_context: Option<u64>,
33}
34
35#[derive(Debug, Clone, Serialize, Deserialize)]
37pub struct VocabularyLD {
38 #[serde(rename = "@context")]
39 pub context: String,
40 #[serde(rename = "base_uri")]
41 pub base_uri: String,
42 pub prefixes: HashMap<String, String>,
43 pub terms: HashMap<String, TermDefinition>,
44}
45
46#[derive(Debug, Clone, Serialize, Deserialize)]
48pub struct TermDefinition {
49 #[serde(rename = "@id")]
50 pub id: String,
51 #[serde(rename = "@type")]
52 pub term_type: Option<String>,
53 pub description: Option<String>,
54 pub range: Option<String>,
55 pub domain: Option<String>,
56}
57
58#[derive(Debug, Clone, Serialize, Deserialize)]
60pub struct CollectionLD {
61 #[serde(rename = "@context")]
62 pub context: String,
63 #[serde(rename = "@type")]
64 pub collection_type: String,
65 pub id: String,
66 pub name: String,
67 pub description: Option<String>,
68 #[serde(rename = "target_shapes")]
69 pub target_shapes: Vec<String>,
70 #[serde(rename = "access_mode")]
71 pub access_mode: String,
72 #[serde(rename = "routing_constraints")]
73 pub routing_constraints: Option<u8>,
74}
75
76#[derive(Debug, Clone, Serialize, Deserialize)]
78pub struct CapabilityLD {
79 #[serde(rename = "@context")]
80 pub context: String,
81 #[serde(rename = "@type")]
82 pub capability_type: String,
83 pub id: String,
84 pub name: String,
85 pub description: Option<String>,
86 pub actions: Vec<String>,
87 pub target: String,
88 #[serde(rename = "routing_constraints")]
89 pub routing_constraints: Option<u8>,
90 #[serde(rename = "expires")]
91 pub expires: Option<String>,
92}
93
94#[cfg(not(target_arch = "wasm32"))]
96pub struct VaultManifestProcessor {
97 q42_context: Arc<Q42Context>,
98 cbor_ld_parser: Arc<Q42CborLdParser>,
99}
100
101#[cfg(not(target_arch = "wasm32"))]
102impl VaultManifestProcessor {
103 pub fn from_volume(volume: &Q42Volume) -> Result<Self, CborLdError> {
105 let context =
106 Arc::new(Q42Context::from_volume(volume).map_err(|_| CborLdError::InvalidOffset)?);
107 let parser =
108 Arc::new(Q42CborLdParser::from_volume(volume).map_err(|_| CborLdError::InvalidOffset)?);
109
110 Ok(Self {
111 q42_context: context,
112 cbor_ld_parser: parser,
113 })
114 }
115
116 pub fn to_cbor_ld(&self, manifest: &VaultManifest) -> Result<Vec<u8>, CborLdError> {
118 let mut enhanced_manifest = manifest.clone();
120 enhanced_manifest.context = "https://webizen.org/ld/vault/v1".to_string();
121
122 let mut buffer = Vec::new();
124 ciborium::into_writer(&enhanced_manifest, &mut buffer)
125 .map_err(|_| CborLdError::InvalidValueType)?;
126 Ok(buffer)
127 }
128
129 pub fn from_cbor_ld(&self, cbor_bytes: &[u8]) -> Result<VaultManifest, CborLdError> {
131 let manifest: VaultManifest =
133 ciborium::from_reader(cbor_bytes).map_err(|_| CborLdError::InvalidValueType)?;
134
135 if manifest.context != "https://webizen.org/ld/vault/v1" {
137 return Err(CborLdError::InvalidUtf8);
138 }
139
140 Ok(manifest)
141 }
142
143 pub fn to_compact_cbor_ld(&self, manifest: &VaultManifest) -> Result<Vec<u8>, CborLdError> {
145 let compact_manifest = CompactVaultManifest::from_full(manifest);
147
148 let mut buffer = Vec::new();
149 ciborium::into_writer(&compact_manifest, &mut buffer)
150 .map_err(|_| CborLdError::InvalidValueType)?;
151 Ok(buffer)
152 }
153
154 pub fn from_compact_cbor_ld(&self, cbor_bytes: &[u8]) -> Result<VaultManifest, CborLdError> {
156 let compact: CompactVaultManifest =
157 ciborium::from_reader(cbor_bytes).map_err(|_| CborLdError::InvalidValueType)?;
158
159 Ok(compact.to_full())
160 }
161
162 pub fn validate_manifest(&self, manifest: &VaultManifest) -> Result<(), CborLdError> {
164 let lexicon = self.cbor_ld_parser.lexicon();
166 for collection in &manifest.collections {
167 if lexicon.resolve_term(&collection.collection_type).is_none()
168 && self
169 .q42_context
170 .resolve_semantic_term(&collection.collection_type)
171 .is_none()
172 {
173 return Err(CborLdError::InvalidUtf8);
174 }
175 }
176
177 for capability in &manifest.capabilities {
178 if lexicon.resolve_term(&capability.capability_type).is_none()
179 && self
180 .q42_context
181 .resolve_semantic_term(&capability.capability_type)
182 .is_none()
183 {
184 return Err(CborLdError::InvalidUtf8);
185 }
186 }
187
188 Ok(())
189 }
190
191 pub fn q42_context(&self) -> &Arc<Q42Context> {
193 &self.q42_context
194 }
195}
196
197#[derive(Debug, Clone, Serialize, Deserialize)]
199pub struct CompactVaultManifest {
200 #[serde(rename = "@context")]
201 pub context: String,
202 #[serde(rename = "@type")]
203 pub manifest_type: String,
204 pub id: String,
205 pub created: String,
206 pub modified: String,
207 #[serde(rename = "did_q42")]
208 pub did_q42: Option<String>,
209 #[serde(rename = "semantic_context")]
210 pub semantic_context: Option<u64>,
211 pub collections: Vec<CompactCollectionLD>,
213 pub capabilities: Vec<CompactCapabilityLD>,
215}
216
217#[derive(Debug, Clone, Serialize, Deserialize)]
219pub struct CompactCollectionLD {
220 pub id: String,
221 pub name: String,
222 #[serde(rename = "target_shapes")]
223 pub target_shapes: Vec<String>,
224 #[serde(rename = "routing_constraints")]
225 pub routing_constraints: Option<u8>,
226}
227
228#[derive(Debug, Clone, Serialize, Deserialize)]
230pub struct CompactCapabilityLD {
231 pub id: String,
232 pub name: String,
233 pub actions: Vec<String>,
234 pub target: String,
235 #[serde(rename = "routing_constraints")]
236 pub routing_constraints: Option<u8>,
237}
238
239impl CompactVaultManifest {
240 pub fn from_full(full: &VaultManifest) -> Self {
242 Self {
243 context: full.context.clone(),
244 manifest_type: full.manifest_type.clone(),
245 id: full.id.clone(),
246 created: full.created.clone(),
247 modified: full.modified.clone(),
248 did_q42: full.did_q42.clone(),
249 semantic_context: full.semantic_context,
250 collections: full
251 .collections
252 .iter()
253 .map(|c| CompactCollectionLD {
254 id: c.id.clone(),
255 name: c.name.clone(),
256 target_shapes: c.target_shapes.clone(),
257 routing_constraints: c.routing_constraints,
258 })
259 .collect(),
260 capabilities: full
261 .capabilities
262 .iter()
263 .map(|c| CompactCapabilityLD {
264 id: c.id.clone(),
265 name: c.name.clone(),
266 actions: c.actions.clone(),
267 target: c.target.clone(),
268 routing_constraints: c.routing_constraints,
269 })
270 .collect(),
271 }
272 }
273
274 pub fn to_full(&self) -> VaultManifest {
276 VaultManifest {
277 context: self.context.clone(),
278 manifest_type: self.manifest_type.clone(),
279 id: self.id.clone(),
280 created: self.created.clone(),
281 modified: self.modified.clone(),
282 vocabulary: VocabularyLD {
283 context: "https://webizen.org/ld/vocab/".to_string(),
284 base_uri: "https://webizen.org/ld/vocab/".to_string(),
285 prefixes: HashMap::new(),
286 terms: HashMap::new(),
287 },
288 collections: self
289 .collections
290 .iter()
291 .map(|c| CollectionLD {
292 context: "https://webizen.org/ld/vault/v1".to_string(),
293 collection_type: "Collection".to_string(),
294 id: c.id.clone(),
295 name: c.name.clone(),
296 description: None,
297 target_shapes: c.target_shapes.clone(),
298 access_mode: "read".to_string(),
299 routing_constraints: c.routing_constraints,
300 })
301 .collect(),
302 capabilities: self
303 .capabilities
304 .iter()
305 .map(|c| CapabilityLD {
306 context: "https://webizen.org/ld/vault/v1".to_string(),
307 capability_type: "Capability".to_string(),
308 id: c.id.clone(),
309 name: c.name.clone(),
310 description: None,
311 actions: c.actions.clone(),
312 target: c.target.clone(),
313 routing_constraints: c.routing_constraints,
314 expires: None,
315 })
316 .collect(),
317 did_q42: self.did_q42.clone(),
318 semantic_context: self.semantic_context,
319 }
320 }
321}
322
323impl VaultManifest {
324 pub fn new(id: String) -> Self {
326 let now = chrono::Utc::now().to_rfc3339();
327
328 Self {
329 context: "https://webizen.org/ld/vault/v1".to_string(),
330 manifest_type: "VaultManifest".to_string(),
331 id,
332 created: now.clone(),
333 modified: now,
334 vocabulary: VocabularyLD {
335 context: "https://webizen.org/ld/vocab/".to_string(),
336 base_uri: "https://webizen.org/ld/vocab/".to_string(),
337 prefixes: {
338 let mut prefixes = HashMap::new();
339 prefixes.insert(
340 "qualia".to_string(),
341 "https://webizen.org/ld/vocab/".to_string(),
342 );
343 prefixes.insert(
344 "did".to_string(),
345 "https://www.w3.org/TR/did-core/".to_string(),
346 );
347 prefixes.insert("sec".to_string(), "https://w3id.org/security/".to_string());
348 prefixes.insert(
349 "xsd".to_string(),
350 "http://www.w3.org/2001/XMLSchema#".to_string(),
351 );
352 prefixes
353 },
354 terms: HashMap::new(),
355 },
356 collections: Vec::new(),
357 capabilities: Vec::new(),
358 did_q42: None,
359 semantic_context: None,
360 }
361 }
362
363 pub fn add_collection(&mut self, collection: CollectionLD) {
365 self.collections.push(collection);
366 self.modified = chrono::Utc::now().to_rfc3339();
367 }
368
369 pub fn add_capability(&mut self, capability: CapabilityLD) {
371 self.capabilities.push(capability);
372 self.modified = chrono::Utc::now().to_rfc3339();
373 }
374
375 pub fn set_did_q42(&mut self, did_q42: String) {
377 self.did_q42 = Some(did_q42);
378 self.modified = chrono::Utc::now().to_rfc3339();
379 }
380
381 pub fn set_semantic_context(&mut self, semantic_context: u64) {
383 self.semantic_context = Some(semantic_context);
384 self.modified = chrono::Utc::now().to_rfc3339();
385 }
386
387 pub fn validate(&self) -> Result<(), String> {
389 if self.id.is_empty() {
390 return Err("Manifest ID cannot be empty".to_string());
391 }
392
393 if self.created.is_empty() {
394 return Err("Created timestamp cannot be empty".to_string());
395 }
396
397 if self.modified.is_empty() {
398 return Err("Modified timestamp cannot be empty".to_string());
399 }
400
401 for (i, collection) in self.collections.iter().enumerate() {
403 if collection.id.is_empty() {
404 return Err(format!("Collection {} has empty ID", i));
405 }
406 if collection.name.is_empty() {
407 return Err(format!("Collection {} has empty name", i));
408 }
409 }
410
411 for (i, capability) in self.capabilities.iter().enumerate() {
413 if capability.id.is_empty() {
414 return Err(format!("Capability {} has empty ID", i));
415 }
416 if capability.name.is_empty() {
417 return Err(format!("Capability {} has empty name", i));
418 }
419 if capability.target.is_empty() {
420 return Err(format!("Capability {} has empty target", i));
421 }
422 }
423
424 Ok(())
425 }
426}
427
428#[cfg(test)]
429mod tests {
430 use super::*;
431
432 #[test]
433 fn test_vault_manifest_creation() {
434 let manifest = VaultManifest::new("test-vault-123".to_string());
435
436 assert_eq!(manifest.id, "test-vault-123");
437 assert_eq!(manifest.manifest_type, "VaultManifest");
438 assert_eq!(manifest.context, "https://webizen.org/ld/vault/v1");
439 assert!(!manifest.created.is_empty());
440 assert!(!manifest.modified.is_empty());
441 assert!(manifest.collections.is_empty());
442 assert!(manifest.capabilities.is_empty());
443 }
444
445 #[test]
446 fn test_compact_manifest_conversion() {
447 let mut full_manifest = VaultManifest::new("test-vault-123".to_string());
448
449 full_manifest.add_collection(CollectionLD {
451 context: "https://webizen.org/ld/vault/v1".to_string(),
452 collection_type: "Collection".to_string(),
453 id: "collection-1".to_string(),
454 name: "Test Collection".to_string(),
455 description: Some("A test collection".to_string()),
456 target_shapes: vec!["foaf:Person".to_string()],
457 access_mode: "read".to_string(),
458 routing_constraints: Some(0b01),
459 });
460
461 full_manifest.add_capability(CapabilityLD {
463 context: "https://webizen.org/ld/vault/v1".to_string(),
464 capability_type: "Capability".to_string(),
465 id: "capability-1".to_string(),
466 name: "Test Capability".to_string(),
467 description: Some("A test capability".to_string()),
468 actions: vec!["read".to_string(), "write".to_string()],
469 target: "collection-1".to_string(),
470 routing_constraints: Some(0b01),
471 expires: None,
472 });
473
474 let compact = CompactVaultManifest::from_full(&full_manifest);
476
477 assert_eq!(compact.id, full_manifest.id);
478 assert_eq!(compact.collections.len(), 1);
479 assert_eq!(compact.capabilities.len(), 1);
480 assert_eq!(compact.collections[0].name, "Test Collection");
481 assert_eq!(compact.capabilities[0].name, "Test Capability");
482
483 let restored = compact.to_full();
485
486 assert_eq!(restored.id, full_manifest.id);
487 assert_eq!(restored.collections.len(), 1);
488 assert_eq!(restored.capabilities.len(), 1);
489 assert_eq!(restored.collections[0].name, "Test Collection");
490 assert_eq!(restored.capabilities[0].name, "Test Capability");
491 }
492
493 #[test]
494 fn test_manifest_validation() {
495 let mut manifest = VaultManifest::new("test-vault-123".to_string());
496
497 assert!(manifest.validate().is_ok());
499
500 manifest.id = "".to_string();
502 assert!(manifest.validate().is_err());
503
504 manifest.id = "test-vault-123".to_string();
506
507 manifest.add_collection(CollectionLD {
509 context: "https://webizen.org/ld/vault/v1".to_string(),
510 collection_type: "Collection".to_string(),
511 id: "".to_string(), name: "Test Collection".to_string(),
513 description: None,
514 target_shapes: vec![],
515 access_mode: "read".to_string(),
516 routing_constraints: None,
517 });
518
519 assert!(manifest.validate().is_err());
520 }
521}