rustrails_storage/service/
memory.rs1use std::{sync::Arc, time::Duration};
4
5use async_trait::async_trait;
6use bytes::Bytes;
7use dashmap::DashMap;
8use url::Url;
9
10use super::{StorageError, StorageService, checked_key};
11
12#[derive(Debug, Clone)]
14pub struct MemoryService {
15 name: String,
16 objects: Arc<DashMap<String, Bytes>>,
17 base_url: Url,
18}
19
20impl MemoryService {
21 pub fn new(name: impl Into<String>) -> Result<Self, StorageError> {
27 let base_url = Url::parse("memory://storage/")
28 .map_err(|error| StorageError::InvalidUrl(error.to_string()))?;
29 Ok(Self {
30 name: name.into(),
31 objects: Arc::new(DashMap::new()),
32 base_url,
33 })
34 }
35
36 #[must_use]
38 pub fn with_base_url(mut self, base_url: Url) -> Self {
39 self.base_url = base_url;
40 self
41 }
42
43 #[must_use]
45 pub fn len(&self) -> usize {
46 self.objects.len()
47 }
48
49 #[must_use]
51 pub fn is_empty(&self) -> bool {
52 self.objects.is_empty()
53 }
54}
55
56#[async_trait]
57impl StorageService for MemoryService {
58 fn name(&self) -> &str {
59 &self.name
60 }
61
62 async fn upload(&self, key: &str, data: Bytes) -> Result<(), StorageError> {
63 let key = checked_key(key)?.to_owned();
64 if self.objects.contains_key(&key) {
65 return Err(StorageError::DuplicateKey(key));
66 }
67 self.objects.insert(key, data);
68 Ok(())
69 }
70
71 async fn download(&self, key: &str) -> Result<Bytes, StorageError> {
72 let key = checked_key(key)?;
73 self.objects
74 .get(key)
75 .map(|entry| entry.value().clone())
76 .ok_or_else(|| StorageError::NotFound(key.to_owned()))
77 }
78
79 async fn delete(&self, key: &str) -> Result<(), StorageError> {
80 let key = checked_key(key)?;
81 let _ = self.objects.remove(key);
82 Ok(())
83 }
84
85 async fn exists(&self, key: &str) -> Result<bool, StorageError> {
86 let key = checked_key(key)?;
87 Ok(self.objects.contains_key(key))
88 }
89
90 async fn url(&self, key: &str, expires_in: Duration) -> Result<Url, StorageError> {
91 let key = checked_key(key)?;
92 let mut url = self.base_url.clone();
93 url.set_path(key);
94 url.query_pairs_mut()
95 .append_pair("service", &self.name)
96 .append_pair("expires_in", &expires_in.as_secs().to_string());
97 Ok(url)
98 }
99}
100
101#[cfg(test)]
102mod tests {
103 use super::*;
104
105 fn service() -> MemoryService {
106 MemoryService::new("memory").expect("service should build")
107 }
108
109 #[tokio::test]
110 async fn test_upload_and_download_round_trip() {
111 let service = service();
112 service
113 .upload("a.txt", Bytes::from_static(b"hello"))
114 .await
115 .expect("upload should succeed");
116 assert_eq!(
117 service
118 .download("a.txt")
119 .await
120 .expect("download should succeed"),
121 Bytes::from_static(b"hello")
122 );
123 }
124
125 #[tokio::test]
126 async fn test_upload_rejects_duplicate_key() {
127 let service = service();
128 service
129 .upload("a.txt", Bytes::from_static(b"one"))
130 .await
131 .expect("upload should succeed");
132 let error = service
133 .upload("a.txt", Bytes::from_static(b"two"))
134 .await
135 .expect_err("duplicate should fail");
136 assert!(matches!(error, StorageError::DuplicateKey(key) if key == "a.txt"));
137 }
138
139 #[tokio::test]
140 async fn test_download_missing_key_returns_not_found() {
141 let service = service();
142 let error = service
143 .download("missing")
144 .await
145 .expect_err("download should fail");
146 assert!(matches!(error, StorageError::NotFound(key) if key == "missing"));
147 }
148
149 #[tokio::test]
150 async fn test_delete_removes_value() {
151 let service = service();
152 service
153 .upload("a.txt", Bytes::from_static(b"hello"))
154 .await
155 .expect("upload should succeed");
156 service
157 .delete("a.txt")
158 .await
159 .expect("delete should succeed");
160 assert!(
161 !service
162 .exists("a.txt")
163 .await
164 .expect("exists should succeed")
165 );
166 }
167
168 #[tokio::test]
169 async fn test_delete_missing_key_is_a_noop() {
170 let service = service();
171 service
172 .delete("missing")
173 .await
174 .expect("delete should succeed");
175 assert!(service.is_empty());
176 }
177
178 #[tokio::test]
179 async fn test_exists_tracks_presence() {
180 let service = service();
181 assert!(
182 !service
183 .exists("a.txt")
184 .await
185 .expect("exists should succeed")
186 );
187 service
188 .upload("a.txt", Bytes::from_static(b"hello"))
189 .await
190 .expect("upload should succeed");
191 assert!(
192 service
193 .exists("a.txt")
194 .await
195 .expect("exists should succeed")
196 );
197 }
198
199 #[tokio::test]
200 async fn test_url_includes_expiry_and_service_name() {
201 let service = service();
202 let url = service
203 .url("a.txt", Duration::from_secs(30))
204 .await
205 .expect("url should build");
206 assert_eq!(
207 url.as_str(),
208 "memory://storage/a.txt?service=memory&expires_in=30"
209 );
210 }
211
212 #[tokio::test]
213 async fn test_zero_byte_upload_is_supported() {
214 let service = service();
215 service
216 .upload("empty", Bytes::new())
217 .await
218 .expect("upload should succeed");
219 assert_eq!(
220 service
221 .download("empty")
222 .await
223 .expect("download should succeed"),
224 Bytes::new()
225 );
226 }
227
228 #[tokio::test]
229 async fn test_len_tracks_number_of_entries() {
230 let service = service();
231 service
232 .upload("a", Bytes::from_static(b"a"))
233 .await
234 .expect("upload should succeed");
235 service
236 .upload("b", Bytes::from_static(b"b"))
237 .await
238 .expect("upload should succeed");
239 assert_eq!(service.len(), 2);
240 }
241
242 #[tokio::test]
243 async fn test_custom_base_url_is_respected() {
244 let service = MemoryService::new("memory")
245 .expect("service should build")
246 .with_base_url(Url::parse("https://memory.example/").expect("url should parse"));
247 let url = service
248 .url("a.txt", Duration::from_secs(5))
249 .await
250 .expect("url should build");
251 assert_eq!(
252 url.as_str(),
253 "https://memory.example/a.txt?service=memory&expires_in=5"
254 );
255 }
256
257 #[tokio::test]
258 async fn test_invalid_empty_key_rejected() {
259 let service = service();
260 let error = service
261 .upload("", Bytes::from_static(b"hello"))
262 .await
263 .expect_err("upload should fail");
264 assert!(matches!(error, StorageError::InvalidUrl(_)));
265 }
266}