baseid_wallet_core/
store.rs1use std::collections::BTreeMap;
4use std::sync::atomic::{AtomicU64, Ordering};
5use std::sync::Mutex;
6
7use baseid_core::credential::CredentialSummary;
8use baseid_core::types::CredentialId;
9
10use crate::error::WalletError;
11use crate::holder::WalletStore;
12use crate::{AnyCredential, CredentialFilter, CredentialRecord};
13
14pub struct InMemoryStore {
19 credentials: Mutex<BTreeMap<String, CredentialRecord>>,
20 next_id: AtomicU64,
21}
22
23impl InMemoryStore {
24 pub fn new() -> Self {
26 Self {
27 credentials: Mutex::new(BTreeMap::new()),
28 next_id: AtomicU64::new(1),
29 }
30 }
31
32 pub fn len(&self) -> usize {
34 self.credentials.lock().unwrap().len()
35 }
36
37 pub fn is_empty(&self) -> bool {
39 self.len() == 0
40 }
41}
42
43impl Default for InMemoryStore {
44 fn default() -> Self {
45 Self::new()
46 }
47}
48
49impl WalletStore for InMemoryStore {
50 async fn store_credential(&self, cred: AnyCredential) -> baseid_core::Result<CredentialId> {
51 let id_num = self.next_id.fetch_add(1, Ordering::SeqCst);
52 let id = CredentialId(format!("cred-{id_num}"));
53
54 let record = CredentialRecord {
55 id: id.clone(),
56 credential: cred,
57 raw: None,
58 };
59
60 self.credentials
61 .lock()
62 .unwrap()
63 .insert(id.0.clone(), record);
64
65 Ok(id)
66 }
67
68 async fn get_credential(&self, id: &CredentialId) -> baseid_core::Result<AnyCredential> {
69 let store = self.credentials.lock().unwrap();
70 let record = store
71 .get(&id.0)
72 .ok_or(WalletError::CredentialNotFound(id.clone()))?;
73 Ok(record.credential.clone())
74 }
75
76 async fn list_credentials(
77 &self,
78 filter: CredentialFilter,
79 ) -> baseid_core::Result<Vec<CredentialSummary>> {
80 let store = self.credentials.lock().unwrap();
81 let summaries = store
82 .values()
83 .filter(|record| {
84 if let Some(ref fmt) = filter.format {
85 if record.credential.credential_format() != *fmt {
86 return false;
87 }
88 }
89 if let Some(ref issuer) = filter.issuer {
90 if record.credential.issuer_id() != *issuer {
91 return false;
92 }
93 }
94 true
95 })
96 .map(|record| record.summary())
97 .collect();
98 Ok(summaries)
99 }
100
101 async fn delete_credential(&self, id: &CredentialId) -> baseid_core::Result<()> {
102 let mut store = self.credentials.lock().unwrap();
103 store
104 .remove(&id.0)
105 .ok_or(WalletError::CredentialNotFound(id.clone()))?;
106 Ok(())
107 }
108}
109
110#[cfg(test)]
111mod tests {
112 use super::*;
113 use baseid_core::types::CredentialFormat;
114 use baseid_vc::credential::Issuer;
115 use baseid_vc::VerifiableCredential;
116
117 fn sample_vc(type_name: &str, issuer: &str) -> AnyCredential {
118 AnyCredential::W3cVc(VerifiableCredential {
119 context: vec!["https://www.w3.org/ns/credentials/v2".to_string()],
120 id: None,
121 r#type: vec!["VerifiableCredential".to_string(), type_name.to_string()],
122 issuer: Issuer::Uri(issuer.to_string()),
123 valid_from: Some("2024-01-01T00:00:00Z".to_string()),
124 valid_until: None,
125 credential_subject: serde_json::json!({}),
126 credential_status: None,
127 proof: None,
128 })
129 }
130
131 #[tokio::test]
132 async fn store_and_get_roundtrip() {
133 let store = InMemoryStore::new();
134 let cred = sample_vc("UniversityDegree", "did:key:issuer1");
135 let id = store.store_credential(cred).await.unwrap();
136
137 let retrieved = store.get_credential(&id).await.unwrap();
138 assert_eq!(retrieved.credential_format(), CredentialFormat::W3cVc);
139 assert_eq!(retrieved.issuer_id(), "did:key:issuer1");
140 }
141
142 #[tokio::test]
143 async fn store_generates_unique_ids() {
144 let store = InMemoryStore::new();
145 let id1 = store
146 .store_credential(sample_vc("A", "issuer"))
147 .await
148 .unwrap();
149 let id2 = store
150 .store_credential(sample_vc("B", "issuer"))
151 .await
152 .unwrap();
153 assert_ne!(id1, id2);
154 assert_eq!(store.len(), 2);
155 }
156
157 #[tokio::test]
158 async fn list_all() {
159 let store = InMemoryStore::new();
160 store
161 .store_credential(sample_vc("A", "issuer-a"))
162 .await
163 .unwrap();
164 store
165 .store_credential(sample_vc("B", "issuer-b"))
166 .await
167 .unwrap();
168
169 let all = store
170 .list_credentials(CredentialFilter::default())
171 .await
172 .unwrap();
173 assert_eq!(all.len(), 2);
174 }
175
176 #[tokio::test]
177 async fn list_filter_by_issuer() {
178 let store = InMemoryStore::new();
179 store
180 .store_credential(sample_vc("A", "issuer-a"))
181 .await
182 .unwrap();
183 store
184 .store_credential(sample_vc("B", "issuer-b"))
185 .await
186 .unwrap();
187
188 let filtered = store
189 .list_credentials(CredentialFilter {
190 format: None,
191 issuer: Some("issuer-a".to_string()),
192 })
193 .await
194 .unwrap();
195 assert_eq!(filtered.len(), 1);
196 assert_eq!(filtered[0].metadata.issuer, "issuer-a");
197 }
198
199 #[tokio::test]
200 async fn delete_credential() {
201 let store = InMemoryStore::new();
202 let id = store
203 .store_credential(sample_vc("A", "issuer"))
204 .await
205 .unwrap();
206 assert_eq!(store.len(), 1);
207
208 store.delete_credential(&id).await.unwrap();
209 assert!(store.is_empty());
210 }
211
212 #[tokio::test]
213 async fn get_not_found() {
214 let store = InMemoryStore::new();
215 let result = store
216 .get_credential(&CredentialId("nonexistent".to_string()))
217 .await;
218 assert!(result.is_err());
219 }
220
221 #[tokio::test]
222 async fn delete_not_found() {
223 let store = InMemoryStore::new();
224 let result = store
225 .delete_credential(&CredentialId("nonexistent".to_string()))
226 .await;
227 assert!(result.is_err());
228 }
229}