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(¤t), 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}