Skip to main content

adk_ui/
persistence.rs

1use async_trait::async_trait;
2use serde::{Deserialize, Serialize};
3use serde_json::Value;
4use std::collections::HashMap;
5use std::path::{Path, PathBuf};
6use std::sync::{Mutex, RwLock};
7
8#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
9pub struct SavedSurface {
10    pub id: String,
11    pub name: String,
12    pub owner: String,
13    pub version: u64,
14    pub created_at: String,
15    pub updated_at: String,
16    pub payload: Value,
17}
18
19#[derive(Debug, thiserror::Error)]
20pub enum SurfaceStoreError {
21    #[error("surface not found: {owner}/{id}")]
22    NotFound { owner: String, id: String },
23    #[error("surface version conflict: expected {expected}, found {actual}")]
24    VersionConflict { expected: u64, actual: u64 },
25    #[error("invalid surface identifier: {0}")]
26    InvalidId(String),
27    #[error("surface store IO error: {0}")]
28    Io(String),
29    #[error("surface store JSON error: {0}")]
30    Json(String),
31}
32
33#[async_trait]
34pub trait SurfaceStore: Send + Sync {
35    async fn save(
36        &self,
37        owner: &str,
38        id: &str,
39        name: &str,
40        payload: Value,
41        expected_version: Option<u64>,
42    ) -> Result<SavedSurface, SurfaceStoreError>;
43
44    async fn load(&self, owner: &str, id: &str) -> Result<SavedSurface, SurfaceStoreError>;
45    async fn list(&self, owner: &str) -> Result<Vec<SavedSurface>, SurfaceStoreError>;
46    async fn delete(
47        &self,
48        owner: &str,
49        id: &str,
50        expected_version: Option<u64>,
51    ) -> Result<bool, SurfaceStoreError>;
52}
53
54fn now() -> String {
55    chrono::Utc::now().to_rfc3339()
56}
57
58fn validate_segment(value: &str) -> Result<(), SurfaceStoreError> {
59    if value.trim().is_empty() || value.len() > 256 || value.contains('\0') {
60        return Err(SurfaceStoreError::InvalidId(value.to_string()));
61    }
62    Ok(())
63}
64
65fn build_saved(
66    previous: Option<&SavedSurface>,
67    owner: &str,
68    id: &str,
69    name: &str,
70    payload: Value,
71) -> SavedSurface {
72    let timestamp = now();
73    SavedSurface {
74        id: id.to_string(),
75        name: name.to_string(),
76        owner: owner.to_string(),
77        version: previous.map_or(1, |surface| surface.version + 1),
78        created_at: previous
79            .map_or_else(|| timestamp.clone(), |surface| surface.created_at.clone()),
80        updated_at: timestamp,
81        payload,
82    }
83}
84
85fn check_version(
86    previous: Option<&SavedSurface>,
87    expected: Option<u64>,
88) -> Result<(), SurfaceStoreError> {
89    if let Some(expected) = expected {
90        let actual = previous.map_or(0, |surface| surface.version);
91        if actual != expected {
92            return Err(SurfaceStoreError::VersionConflict { expected, actual });
93        }
94    }
95    Ok(())
96}
97
98#[derive(Default)]
99pub struct InMemorySurfaceStore {
100    surfaces: RwLock<HashMap<(String, String), SavedSurface>>,
101}
102
103impl InMemorySurfaceStore {
104    pub fn new() -> Self {
105        Self::default()
106    }
107}
108
109#[async_trait]
110impl SurfaceStore for InMemorySurfaceStore {
111    async fn save(
112        &self,
113        owner: &str,
114        id: &str,
115        name: &str,
116        payload: Value,
117        expected_version: Option<u64>,
118    ) -> Result<SavedSurface, SurfaceStoreError> {
119        validate_segment(owner)?;
120        validate_segment(id)?;
121        let key = (owner.to_string(), id.to_string());
122        let mut surfaces = self
123            .surfaces
124            .write()
125            .unwrap_or_else(|poisoned| poisoned.into_inner());
126        check_version(surfaces.get(&key), expected_version)?;
127        let saved = build_saved(surfaces.get(&key), owner, id, name, payload);
128        surfaces.insert(key, saved.clone());
129        Ok(saved)
130    }
131
132    async fn load(&self, owner: &str, id: &str) -> Result<SavedSurface, SurfaceStoreError> {
133        let surfaces = self
134            .surfaces
135            .read()
136            .unwrap_or_else(|poisoned| poisoned.into_inner());
137        surfaces
138            .get(&(owner.to_string(), id.to_string()))
139            .cloned()
140            .ok_or_else(|| SurfaceStoreError::NotFound {
141                owner: owner.to_string(),
142                id: id.to_string(),
143            })
144    }
145
146    async fn list(&self, owner: &str) -> Result<Vec<SavedSurface>, SurfaceStoreError> {
147        let surfaces = self
148            .surfaces
149            .read()
150            .unwrap_or_else(|poisoned| poisoned.into_inner());
151        let mut values = surfaces
152            .values()
153            .filter(|surface| surface.owner == owner)
154            .cloned()
155            .collect::<Vec<_>>();
156        values.sort_by(|left, right| {
157            left.name
158                .cmp(&right.name)
159                .then_with(|| left.id.cmp(&right.id))
160        });
161        Ok(values)
162    }
163
164    async fn delete(
165        &self,
166        owner: &str,
167        id: &str,
168        expected_version: Option<u64>,
169    ) -> Result<bool, SurfaceStoreError> {
170        let key = (owner.to_string(), id.to_string());
171        let mut surfaces = self
172            .surfaces
173            .write()
174            .unwrap_or_else(|poisoned| poisoned.into_inner());
175        check_version(surfaces.get(&key), expected_version)?;
176        Ok(surfaces.remove(&key).is_some())
177    }
178}
179
180pub struct FsSurfaceStore {
181    root: PathBuf,
182    lock: Mutex<()>,
183}
184
185impl FsSurfaceStore {
186    pub fn new(root: impl Into<PathBuf>) -> Result<Self, SurfaceStoreError> {
187        let root = root.into();
188        std::fs::create_dir_all(&root).map_err(|error| SurfaceStoreError::Io(error.to_string()))?;
189        Ok(Self {
190            root,
191            lock: Mutex::new(()),
192        })
193    }
194
195    pub fn root(&self) -> &Path {
196        &self.root
197    }
198
199    fn encode(value: &str) -> String {
200        value
201            .as_bytes()
202            .iter()
203            .map(|byte| format!("{byte:02x}"))
204            .collect()
205    }
206
207    fn owner_dir(&self, owner: &str) -> PathBuf {
208        self.root.join(Self::encode(owner))
209    }
210    fn surface_path(&self, owner: &str, id: &str) -> PathBuf {
211        self.owner_dir(owner)
212            .join(format!("{}.json", Self::encode(id)))
213    }
214
215    fn read_path(path: &Path, owner: &str, id: &str) -> Result<SavedSurface, SurfaceStoreError> {
216        let raw = std::fs::read_to_string(path).map_err(|error| {
217            if error.kind() == std::io::ErrorKind::NotFound {
218                SurfaceStoreError::NotFound {
219                    owner: owner.to_string(),
220                    id: id.to_string(),
221                }
222            } else {
223                SurfaceStoreError::Io(error.to_string())
224            }
225        })?;
226        serde_json::from_str(&raw).map_err(|error| SurfaceStoreError::Json(error.to_string()))
227    }
228}
229
230#[async_trait]
231impl SurfaceStore for FsSurfaceStore {
232    async fn save(
233        &self,
234        owner: &str,
235        id: &str,
236        name: &str,
237        payload: Value,
238        expected_version: Option<u64>,
239    ) -> Result<SavedSurface, SurfaceStoreError> {
240        validate_segment(owner)?;
241        validate_segment(id)?;
242        let _guard = self
243            .lock
244            .lock()
245            .unwrap_or_else(|poisoned| poisoned.into_inner());
246        let path = self.surface_path(owner, id);
247        let previous = if path.exists() {
248            Some(Self::read_path(&path, owner, id)?)
249        } else {
250            None
251        };
252        check_version(previous.as_ref(), expected_version)?;
253        let saved = build_saved(previous.as_ref(), owner, id, name, payload);
254        let owner_dir = self.owner_dir(owner);
255        std::fs::create_dir_all(&owner_dir)
256            .map_err(|error| SurfaceStoreError::Io(error.to_string()))?;
257        let temp = owner_dir.join(format!(".{}.{}.tmp", Self::encode(id), std::process::id()));
258        let bytes = serde_json::to_vec_pretty(&saved)
259            .map_err(|error| SurfaceStoreError::Json(error.to_string()))?;
260        std::fs::write(&temp, bytes).map_err(|error| SurfaceStoreError::Io(error.to_string()))?;
261        std::fs::rename(&temp, &path).map_err(|error| SurfaceStoreError::Io(error.to_string()))?;
262        Ok(saved)
263    }
264
265    async fn load(&self, owner: &str, id: &str) -> Result<SavedSurface, SurfaceStoreError> {
266        validate_segment(owner)?;
267        validate_segment(id)?;
268        let _guard = self
269            .lock
270            .lock()
271            .unwrap_or_else(|poisoned| poisoned.into_inner());
272        Self::read_path(&self.surface_path(owner, id), owner, id)
273    }
274
275    async fn list(&self, owner: &str) -> Result<Vec<SavedSurface>, SurfaceStoreError> {
276        validate_segment(owner)?;
277        let _guard = self
278            .lock
279            .lock()
280            .unwrap_or_else(|poisoned| poisoned.into_inner());
281        let directory = self.owner_dir(owner);
282        if !directory.exists() {
283            return Ok(vec![]);
284        }
285        let mut surfaces: Vec<SavedSurface> = Vec::new();
286        for entry in std::fs::read_dir(directory)
287            .map_err(|error| SurfaceStoreError::Io(error.to_string()))?
288        {
289            let path = entry
290                .map_err(|error| SurfaceStoreError::Io(error.to_string()))?
291                .path();
292            if path.extension().and_then(|value| value.to_str()) != Some("json") {
293                continue;
294            }
295            let raw = std::fs::read_to_string(path)
296                .map_err(|error| SurfaceStoreError::Io(error.to_string()))?;
297            surfaces.push(
298                serde_json::from_str(&raw)
299                    .map_err(|error| SurfaceStoreError::Json(error.to_string()))?,
300            );
301        }
302        surfaces.sort_by(|left, right| {
303            left.name
304                .cmp(&right.name)
305                .then_with(|| left.id.cmp(&right.id))
306        });
307        Ok(surfaces)
308    }
309
310    async fn delete(
311        &self,
312        owner: &str,
313        id: &str,
314        expected_version: Option<u64>,
315    ) -> Result<bool, SurfaceStoreError> {
316        validate_segment(owner)?;
317        validate_segment(id)?;
318        let _guard = self
319            .lock
320            .lock()
321            .unwrap_or_else(|poisoned| poisoned.into_inner());
322        let path = self.surface_path(owner, id);
323        if !path.exists() {
324            check_version(None, expected_version)?;
325            return Ok(false);
326        }
327        let current = Self::read_path(&path, owner, id)?;
328        check_version(Some(&current), expected_version)?;
329        std::fs::remove_file(path).map_err(|error| SurfaceStoreError::Io(error.to_string()))?;
330        Ok(true)
331    }
332}
333
334#[cfg(test)]
335mod tests {
336    use super::*;
337    use serde_json::json;
338
339    async fn exercise_store(store: &dyn SurfaceStore) {
340        let first = store
341            .save("agent", "main", "Main", json!({"value": 1}), Some(0))
342            .await
343            .unwrap();
344        assert_eq!(first.version, 1);
345        let second = store
346            .save("agent", "main", "Main", json!({"value": 2}), Some(1))
347            .await
348            .unwrap();
349        assert_eq!(second.version, 2);
350        assert_eq!(second.created_at, first.created_at);
351        assert!(matches!(
352            store
353                .save("agent", "main", "Main", json!({}), Some(1))
354                .await,
355            Err(SurfaceStoreError::VersionConflict { actual: 2, .. })
356        ));
357        assert_eq!(store.list("agent").await.unwrap().len(), 1);
358        assert!(store.list("other").await.unwrap().is_empty());
359        assert!(store.delete("agent", "main", Some(2)).await.unwrap());
360    }
361
362    #[tokio::test]
363    async fn in_memory_store_versions_and_scopes_surfaces() {
364        exercise_store(&InMemorySurfaceStore::new()).await;
365    }
366
367    #[tokio::test]
368    async fn filesystem_store_versions_and_scopes_surfaces() {
369        let directory = tempfile::tempdir().unwrap();
370        let store = FsSurfaceStore::new(directory.path()).unwrap();
371        exercise_store(&store).await;
372    }
373}