Skip to main content

behest_store/memory/
artifact.rs

1//! In-memory artifact store backed by a `HashMap` protected by `RwLock`.
2
3use std::collections::HashMap;
4
5use async_trait::async_trait;
6use tokio::sync::RwLock;
7use uuid::Uuid;
8
9use crate::{Artifact, ArtifactStore, StoreResult};
10
11/// In-memory artifact store for testing, development, and prototyping.
12///
13/// Data is stored in a `HashMap<Uuid, Artifact>` protected by `RwLock`
14/// and is lost when the process exits. Implements [`ArtifactStore`].
15#[derive(Default)]
16pub struct MemoryArtifactStore {
17    artifacts: RwLock<HashMap<Uuid, Artifact>>,
18}
19
20impl MemoryArtifactStore {
21    /// Creates an empty in-memory artifact store.
22    #[must_use]
23    pub fn new() -> Self {
24        Self::default()
25    }
26}
27
28#[async_trait]
29impl ArtifactStore for MemoryArtifactStore {
30    async fn put(&self, artifact: Artifact) -> StoreResult<Artifact> {
31        let mut artifacts = self.artifacts.write().await;
32        let id = artifact.id;
33        artifacts.insert(id, artifact.clone());
34        Ok(artifact)
35    }
36
37    async fn get(&self, id: &Uuid) -> StoreResult<Option<Artifact>> {
38        let artifacts = self.artifacts.read().await;
39        Ok(artifacts.get(id).cloned())
40    }
41
42    async fn delete(&self, id: &Uuid) -> StoreResult<()> {
43        self.artifacts.write().await.remove(id);
44        Ok(())
45    }
46
47    async fn list_by_session(&self, session_id: &Uuid) -> StoreResult<Vec<Artifact>> {
48        let artifacts = self.artifacts.read().await;
49        Ok(artifacts
50            .values()
51            .filter(|a| a.session_id == Some(*session_id))
52            .cloned()
53            .collect())
54    }
55}
56
57#[cfg(test)]
58#[allow(clippy::unwrap_used)]
59mod tests {
60    use super::*;
61    use serde_json::json;
62
63    fn test_artifact() -> Artifact {
64        Artifact::new("test.txt", "text/plain", b"hello world".to_vec())
65    }
66
67    #[tokio::test]
68    async fn memory_artifact_store_should_put_and_get() {
69        let store = MemoryArtifactStore::new();
70        let artifact = test_artifact();
71        let id = artifact.id;
72
73        store.put(artifact).await.unwrap();
74        let loaded = store.get(&id).await.unwrap();
75
76        assert!(loaded.is_some());
77        let artifact = loaded.as_ref().unwrap();
78        assert_eq!(artifact.name, "test.txt");
79        assert_eq!(artifact.data, b"hello world");
80    }
81
82    #[tokio::test]
83    async fn memory_artifact_store_should_delete() {
84        let store = MemoryArtifactStore::new();
85        let artifact = test_artifact();
86        let id = artifact.id;
87
88        store.put(artifact).await.unwrap();
89        store.delete(&id).await.unwrap();
90
91        assert!(store.get(&id).await.unwrap().is_none());
92    }
93
94    #[tokio::test]
95    async fn memory_artifact_store_should_list_by_session() {
96        let store = MemoryArtifactStore::new();
97        let session_id = Uuid::now_v7();
98
99        store
100            .put(test_artifact().with_session(session_id))
101            .await
102            .unwrap();
103        store
104            .put(test_artifact().with_session(session_id))
105            .await
106            .unwrap();
107        store.put(test_artifact()).await.unwrap();
108
109        let results = store.list_by_session(&session_id).await.unwrap();
110        assert_eq!(results.len(), 2);
111    }
112
113    #[tokio::test]
114    async fn memory_artifact_store_should_return_none_for_unknown() {
115        let store = MemoryArtifactStore::new();
116        assert!(store.get(&Uuid::now_v7()).await.unwrap().is_none());
117    }
118
119    #[tokio::test]
120    async fn memory_artifact_store_should_preserve_binary_data() {
121        let store = MemoryArtifactStore::new();
122        let data: Vec<u8> = (0..=255).collect();
123        let artifact = Artifact::new("binary.bin", "application/octet-stream", data.clone());
124        let id = artifact.id;
125
126        store.put(artifact).await.unwrap();
127        let loaded = store.get(&id).await.unwrap().unwrap();
128
129        assert_eq!(loaded.data, data);
130        assert_eq!(loaded.content_type, "application/octet-stream");
131    }
132
133    #[tokio::test]
134    async fn memory_artifact_store_should_support_metadata() {
135        let store = MemoryArtifactStore::new();
136        let artifact = test_artifact().with_metadata(json!({"source": "upload"}));
137        let id = artifact.id;
138
139        store.put(artifact).await.unwrap();
140        let loaded = store.get(&id).await.unwrap().unwrap();
141
142        assert_eq!(loaded.metadata, json!({"source": "upload"}));
143    }
144}