Skip to main content

qualia_core_db/identity/
vault_manifest.rs

1//! Vault Manifest CBOR-LD Projection
2//!
3//! This module provides CBOR-LD serialization and deserialization for Qualia vault manifests,
4//! enabling compact binary transfer while maintaining semantic interoperability through Q42 lexicon.
5
6use 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/// Vault manifest structure with CBOR-LD support
17#[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/// Vocabulary namespace for vault manifests
36#[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/// Term definition in vocabulary
47#[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/// Collection definition in vault manifest
59#[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/// Capability definition in vault manifest
77#[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/// CBOR-LD vault manifest processor
95#[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    /// Create new processor from Q42 volume
104    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    /// Convert vault manifest to CBOR-LD binary format
117    pub fn to_cbor_ld(&self, manifest: &VaultManifest) -> Result<Vec<u8>, CborLdError> {
118        // Ensure manifest has proper Q42 context
119        let mut enhanced_manifest = manifest.clone();
120        enhanced_manifest.context = "https://webizen.org/ld/vault/v1".to_string();
121
122        // Serialize to CBOR-LD
123        let mut buffer = Vec::new();
124        ciborium::into_writer(&enhanced_manifest, &mut buffer)
125            .map_err(|_| CborLdError::InvalidValueType)?;
126        Ok(buffer)
127    }
128
129    /// Convert CBOR-LD binary to vault manifest
130    pub fn from_cbor_ld(&self, cbor_bytes: &[u8]) -> Result<VaultManifest, CborLdError> {
131        // Deserialize from CBOR-LD
132        let manifest: VaultManifest =
133            ciborium::from_reader(cbor_bytes).map_err(|_| CborLdError::InvalidValueType)?;
134
135        // Validate context
136        if manifest.context != "https://webizen.org/ld/vault/v1" {
137            return Err(CborLdError::InvalidUtf8);
138        }
139
140        Ok(manifest)
141    }
142
143    /// Create compact CBOR-LD projection for transfer
144    pub fn to_compact_cbor_ld(&self, manifest: &VaultManifest) -> Result<Vec<u8>, CborLdError> {
145        // Create compact version with only essential fields
146        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    /// Convert from compact CBOR-LD projection
155    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    /// Validate manifest against Q42 lexicon
163    pub fn validate_manifest(&self, manifest: &VaultManifest) -> Result<(), CborLdError> {
164        // Check if all terms exist in Q42 lexicon
165        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    /// Get Q42 context reference
192    pub fn q42_context(&self) -> &Arc<Q42Context> {
193        &self.q42_context
194    }
195}
196
197/// Compact vault manifest for efficient transfer
198#[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    // Compact collections (only essential fields)
212    pub collections: Vec<CompactCollectionLD>,
213    // Compact capabilities (only essential fields)
214    pub capabilities: Vec<CompactCapabilityLD>,
215}
216
217/// Compact collection definition
218#[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/// Compact capability definition
229#[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    /// Convert from full vault manifest
241    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    /// Convert to full vault manifest
275    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    /// Create new vault manifest
325    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    /// Add collection to manifest
364    pub fn add_collection(&mut self, collection: CollectionLD) {
365        self.collections.push(collection);
366        self.modified = chrono::Utc::now().to_rfc3339();
367    }
368
369    /// Add capability to manifest
370    pub fn add_capability(&mut self, capability: CapabilityLD) {
371        self.capabilities.push(capability);
372        self.modified = chrono::Utc::now().to_rfc3339();
373    }
374
375    /// Set DID Q42 identifier
376    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    /// Set semantic context
382    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    /// Validate manifest structure
388    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        // Validate collections
402        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        // Validate capabilities
412        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        // Add test collection
450        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        // Add test capability
462        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        // Convert to compact
475        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        // Convert back to full
484        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        // Valid manifest
498        assert!(manifest.validate().is_ok());
499
500        // Invalid manifest - empty ID
501        manifest.id = "".to_string();
502        assert!(manifest.validate().is_err());
503
504        // Fix ID
505        manifest.id = "test-vault-123".to_string();
506
507        // Add invalid collection
508        manifest.add_collection(CollectionLD {
509            context: "https://webizen.org/ld/vault/v1".to_string(),
510            collection_type: "Collection".to_string(),
511            id: "".to_string(), // Empty ID
512            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}