1use async_trait::async_trait;
4use serde_json::Value;
5
6use super::storage::StorageError;
7use crate::contract::scope::{ScopeId, scoped_key};
8use std::sync::Arc;
9
10#[async_trait]
12pub trait ConfigStore: Send + Sync {
13 async fn get(&self, namespace: &str, id: &str) -> Result<Option<Value>, StorageError>;
15
16 async fn list(
18 &self,
19 namespace: &str,
20 offset: usize,
21 limit: usize,
22 ) -> Result<Vec<(String, Value)>, StorageError>;
23
24 async fn put(&self, namespace: &str, id: &str, value: &Value) -> Result<(), StorageError>;
26
27 async fn delete(&self, namespace: &str, id: &str) -> Result<(), StorageError>;
29
30 async fn put_if_absent(
36 &self,
37 namespace: &str,
38 id: &str,
39 value: &Value,
40 ) -> Result<(), StorageError> {
41 if self.exists(namespace, id).await? {
42 return Err(StorageError::AlreadyExists(format!("{namespace}/{id}")));
43 }
44 self.put(namespace, id, value).await
45 }
46
47 async fn exists(&self, namespace: &str, id: &str) -> Result<bool, StorageError> {
49 Ok(self.get(namespace, id).await?.is_some())
50 }
51
52 async fn put_if_revision(
76 &self,
77 namespace: &str,
78 id: &str,
79 value: &Value,
80 expected_revision: u64,
81 ) -> Result<(), StorageError> {
82 let actual = self
83 .get(namespace, id)
84 .await?
85 .as_ref()
86 .and_then(extract_meta_revision)
87 .unwrap_or(0);
88 if actual != expected_revision {
89 return Err(StorageError::VersionConflict {
90 expected: expected_revision,
91 actual,
92 });
93 }
94 self.put(namespace, id, value).await
95 }
96
97 async fn delete_if_revision(
104 &self,
105 namespace: &str,
106 id: &str,
107 expected_revision: u64,
108 ) -> Result<(), StorageError> {
109 let actual = self
110 .get(namespace, id)
111 .await?
112 .as_ref()
113 .and_then(extract_meta_revision)
114 .unwrap_or(0);
115 if actual != expected_revision {
116 return Err(StorageError::VersionConflict {
117 expected: expected_revision,
118 actual,
119 });
120 }
121 self.delete(namespace, id).await
122 }
123}
124
125pub fn extract_meta_revision(value: &Value) -> Option<u64> {
130 value
131 .get("meta")
132 .and_then(|m| m.get("revision"))
133 .and_then(|r| r.as_u64())
134}
135
136#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
138#[serde(rename_all = "snake_case")]
139pub enum ConfigChangeKind {
140 Put,
141 Delete,
142}
143
144#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
146pub struct ConfigChangeEvent {
147 pub namespace: String,
148 pub id: String,
149 pub kind: ConfigChangeKind,
150}
151
152#[async_trait]
154pub trait ConfigChangeSubscriber: Send {
155 async fn next(&mut self) -> Result<ConfigChangeEvent, StorageError>;
156}
157
158#[async_trait]
164pub trait ConfigChangeNotifier: Send + Sync {
165 async fn subscribe(&self) -> Result<Box<dyn ConfigChangeSubscriber>, StorageError>;
166}
167
168#[derive(Clone)]
169pub struct ScopedConfigStore {
170 inner: Arc<dyn ConfigStore>,
171 scope_id: ScopeId,
172}
173
174impl ScopedConfigStore {
175 pub fn new(inner: Arc<dyn ConfigStore>, scope_id: ScopeId) -> Self {
176 Self { inner, scope_id }
177 }
178
179 pub fn scope_id(&self) -> &ScopeId {
180 &self.scope_id
181 }
182
183 pub fn inner(&self) -> &dyn ConfigStore {
184 self.inner.as_ref()
185 }
186
187 fn scoped_namespace(&self, namespace: &str) -> String {
188 scoped_key(&self.scope_id, namespace)
189 }
190}
191
192#[async_trait]
193impl ConfigStore for ScopedConfigStore {
194 async fn get(&self, namespace: &str, id: &str) -> Result<Option<Value>, StorageError> {
195 self.inner.get(&self.scoped_namespace(namespace), id).await
196 }
197
198 async fn list(
199 &self,
200 namespace: &str,
201 offset: usize,
202 limit: usize,
203 ) -> Result<Vec<(String, Value)>, StorageError> {
204 self.inner
205 .list(&self.scoped_namespace(namespace), offset, limit)
206 .await
207 }
208
209 async fn put(&self, namespace: &str, id: &str, value: &Value) -> Result<(), StorageError> {
210 self.inner
211 .put(&self.scoped_namespace(namespace), id, value)
212 .await
213 }
214
215 async fn delete(&self, namespace: &str, id: &str) -> Result<(), StorageError> {
216 self.inner
217 .delete(&self.scoped_namespace(namespace), id)
218 .await
219 }
220
221 async fn put_if_absent(
222 &self,
223 namespace: &str,
224 id: &str,
225 value: &Value,
226 ) -> Result<(), StorageError> {
227 self.inner
228 .put_if_absent(&self.scoped_namespace(namespace), id, value)
229 .await
230 }
231
232 async fn exists(&self, namespace: &str, id: &str) -> Result<bool, StorageError> {
233 self.inner
234 .exists(&self.scoped_namespace(namespace), id)
235 .await
236 }
237
238 async fn put_if_revision(
239 &self,
240 namespace: &str,
241 id: &str,
242 value: &Value,
243 expected_revision: u64,
244 ) -> Result<(), StorageError> {
245 self.inner
246 .put_if_revision(
247 &self.scoped_namespace(namespace),
248 id,
249 value,
250 expected_revision,
251 )
252 .await
253 }
254
255 async fn delete_if_revision(
256 &self,
257 namespace: &str,
258 id: &str,
259 expected_revision: u64,
260 ) -> Result<(), StorageError> {
261 self.inner
262 .delete_if_revision(&self.scoped_namespace(namespace), id, expected_revision)
263 .await
264 }
265}
266
267#[cfg(test)]
268mod tests {
269 use std::collections::HashMap;
270 use std::sync::Arc;
271
272 use tokio::sync::RwLock;
273
274 use super::*;
275
276 #[test]
279 fn extract_meta_revision_returns_some_for_valid_path() {
280 let value = serde_json::json!({"meta": {"revision": 42}});
281 assert_eq!(extract_meta_revision(&value), Some(42));
282 }
283
284 #[test]
285 fn extract_meta_revision_returns_none_for_missing_meta() {
286 let value = serde_json::json!({"spec": {"id": "foo"}});
287 assert_eq!(extract_meta_revision(&value), None);
288 }
289
290 #[test]
291 fn extract_meta_revision_returns_none_for_missing_revision_key() {
292 let value = serde_json::json!({"meta": {"source": {"kind": "user"}}});
293 assert_eq!(extract_meta_revision(&value), None);
294 }
295
296 #[test]
297 fn extract_meta_revision_returns_none_for_wrong_type() {
298 let value = serde_json::json!({"meta": {"revision": "not-a-number"}});
299 assert_eq!(extract_meta_revision(&value), None);
300 }
301
302 #[test]
303 fn extract_meta_revision_returns_none_for_bare_spec() {
304 let value = serde_json::json!({"id": "agent-1", "name": "Alice"});
306 assert_eq!(extract_meta_revision(&value), None);
307 }
308
309 #[derive(Debug, Default)]
310 struct MemoryConfigStore {
311 data: RwLock<HashMap<String, HashMap<String, Value>>>,
312 }
313
314 #[async_trait]
315 impl ConfigStore for MemoryConfigStore {
316 async fn get(&self, namespace: &str, id: &str) -> Result<Option<Value>, StorageError> {
317 let data = self.data.read().await;
318 Ok(data.get(namespace).and_then(|ns| ns.get(id)).cloned())
319 }
320
321 async fn list(
322 &self,
323 namespace: &str,
324 offset: usize,
325 limit: usize,
326 ) -> Result<Vec<(String, Value)>, StorageError> {
327 let data = self.data.read().await;
328 let Some(namespace_data) = data.get(namespace) else {
329 return Ok(Vec::new());
330 };
331 let mut items: Vec<_> = namespace_data
332 .iter()
333 .map(|(id, value)| (id.clone(), value.clone()))
334 .collect();
335 items.sort_by(|left, right| left.0.cmp(&right.0));
336 Ok(items.into_iter().skip(offset).take(limit).collect())
337 }
338
339 async fn put(&self, namespace: &str, id: &str, value: &Value) -> Result<(), StorageError> {
340 let mut data = self.data.write().await;
341 data.entry(namespace.to_string())
342 .or_default()
343 .insert(id.to_string(), value.clone());
344 Ok(())
345 }
346
347 async fn delete(&self, namespace: &str, id: &str) -> Result<(), StorageError> {
348 let mut data = self.data.write().await;
349 if let Some(namespace_data) = data.get_mut(namespace) {
350 namespace_data.remove(id);
351 }
352 Ok(())
353 }
354 }
355
356 #[tokio::test]
357 async fn config_store_round_trip() {
358 let store: Arc<dyn ConfigStore> = Arc::new(MemoryConfigStore::default());
359 let value = serde_json::json!({"id": "alpha", "label": "first"});
360
361 store.put("tests", "alpha", &value).await.unwrap();
362
363 assert_eq!(store.get("tests", "alpha").await.unwrap(), Some(value));
364 }
365
366 #[tokio::test]
367 async fn config_store_lists_sorted_entries() {
368 let store: Arc<dyn ConfigStore> = Arc::new(MemoryConfigStore::default());
369
370 store
371 .put(
372 "tests",
373 "bravo",
374 &serde_json::json!({"id": "bravo", "label": "second"}),
375 )
376 .await
377 .unwrap();
378 store
379 .put(
380 "tests",
381 "alpha",
382 &serde_json::json!({"id": "alpha", "label": "first"}),
383 )
384 .await
385 .unwrap();
386
387 let items = store.list("tests", 0, 10).await.unwrap();
388 assert_eq!(items[0].0, "alpha");
389 assert_eq!(items[1].0, "bravo");
390 }
391}