drasi_lib/indexes/
config.rs1use serde::{Deserialize, Serialize};
16use std::path::Path;
17
18#[derive(Debug, Clone, Serialize, Deserialize)]
20pub struct StorageBackendConfig {
21 pub id: String,
23 #[serde(flatten)]
25 pub spec: StorageBackendSpec,
26}
27
28#[derive(Debug, Clone, Serialize, Deserialize)]
30#[serde(tag = "backend_type")]
31pub enum StorageBackendSpec {
32 #[serde(rename = "memory")]
40 Memory {
41 #[serde(default)]
43 enable_archive: bool,
44 },
45
46 #[serde(rename = "rocksdb")]
56 RocksDb {
57 path: String,
59 #[serde(default)]
61 enable_archive: bool,
62 #[serde(default)]
64 direct_io: bool,
65 },
66
67 #[serde(rename = "redis")]
76 Redis {
77 connection_string: String,
79 #[serde(skip_serializing_if = "Option::is_none")]
81 cache_size: Option<usize>,
82 },
83}
84
85#[derive(Debug, Clone, Serialize, Deserialize)]
87#[serde(untagged)]
88pub enum StorageBackendRef {
89 Named(String),
91 Inline(StorageBackendSpec),
93}
94
95impl StorageBackendSpec {
96 pub fn validate(&self) -> Result<(), String> {
98 match self {
99 StorageBackendSpec::Memory { .. } => Ok(()),
100 StorageBackendSpec::RocksDb { path, .. } => {
101 let path_obj = Path::new(path);
103 if !path_obj.is_absolute() {
104 return Err(format!("RocksDB path must be absolute, got: {path}"));
105 }
106 Ok(())
107 }
108 StorageBackendSpec::Redis {
109 connection_string,
110 cache_size,
111 } => {
112 if !connection_string.starts_with("redis://")
114 && !connection_string.starts_with("rediss://")
115 {
116 return Err(format!(
117 "Redis connection string must start with 'redis://' or 'rediss://', got: {connection_string}"
118 ));
119 }
120
121 if let Some(size) = cache_size {
123 if *size > 10_000_000 {
124 log::warn!(
125 "Redis cache_size is very large ({size}), this may consume significant memory"
126 );
127 }
128 }
129 Ok(())
130 }
131 }
132 }
133
134 pub fn is_volatile(&self) -> bool {
136 matches!(self, StorageBackendSpec::Memory { .. })
137 }
138}
139
140#[cfg(test)]
141mod tests {
142 use super::*;
143
144 #[test]
145 fn test_memory_serde() {
146 let yaml = r#"
147backend_type: memory
148enable_archive: true
149"#;
150 let spec: StorageBackendSpec = serde_yaml::from_str(yaml).unwrap();
151 match spec {
152 StorageBackendSpec::Memory { enable_archive } => {
153 assert!(enable_archive);
154 }
155 _ => panic!("Expected Memory variant"),
156 }
157
158 let serialized = serde_yaml::to_string(&spec).unwrap();
160 let deserialized: StorageBackendSpec = serde_yaml::from_str(&serialized).unwrap();
161 match deserialized {
162 StorageBackendSpec::Memory { enable_archive } => {
163 assert!(enable_archive);
164 }
165 _ => panic!("Expected Memory variant after round-trip"),
166 }
167 }
168
169 #[test]
170 fn test_rocksdb_serde() {
171 let yaml = r#"
172backend_type: rocksdb
173path: /data/drasi
174enable_archive: true
175direct_io: false
176"#;
177 let spec: StorageBackendSpec = serde_yaml::from_str(yaml).unwrap();
178 match spec {
179 StorageBackendSpec::RocksDb {
180 path,
181 enable_archive,
182 direct_io,
183 } => {
184 assert_eq!(path, "/data/drasi");
185 assert!(enable_archive);
186 assert!(!direct_io);
187 }
188 _ => panic!("Expected RocksDb variant"),
189 }
190 }
191
192 #[test]
193 fn test_redis_serde() {
194 let yaml = r#"
195backend_type: redis
196connection_string: "redis://localhost:6379"
197cache_size: 10000
198"#;
199 let spec: StorageBackendSpec = serde_yaml::from_str(yaml).unwrap();
200 match spec {
201 StorageBackendSpec::Redis {
202 connection_string,
203 cache_size,
204 } => {
205 assert_eq!(connection_string, "redis://localhost:6379");
206 assert_eq!(cache_size, Some(10000));
207 }
208 _ => panic!("Expected Redis variant"),
209 }
210 }
211
212 #[test]
213 fn test_storage_backend_config_serde() {
214 let yaml = r#"
215id: rocks_persistent
216backend_type: rocksdb
217path: /data/drasi
218enable_archive: true
219"#;
220 let config: StorageBackendConfig = serde_yaml::from_str(yaml).unwrap();
221 assert_eq!(config.id, "rocks_persistent");
222 match config.spec {
223 StorageBackendSpec::RocksDb {
224 path,
225 enable_archive,
226 ..
227 } => {
228 assert_eq!(path, "/data/drasi");
229 assert!(enable_archive);
230 }
231 _ => panic!("Expected RocksDb variant"),
232 }
233 }
234
235 #[test]
236 fn test_storage_backend_ref_named() {
237 let yaml = r#""rocks_persistent""#;
238 let ref_val: StorageBackendRef = serde_yaml::from_str(yaml).unwrap();
239 match ref_val {
240 StorageBackendRef::Named(name) => {
241 assert_eq!(name, "rocks_persistent");
242 }
243 _ => panic!("Expected Named variant"),
244 }
245 }
246
247 #[test]
248 fn test_storage_backend_ref_inline() {
249 let yaml = r#"
250backend_type: memory
251enable_archive: false
252"#;
253 let ref_val: StorageBackendRef = serde_yaml::from_str(yaml).unwrap();
254 match ref_val {
255 StorageBackendRef::Inline(spec) => match spec {
256 StorageBackendSpec::Memory { enable_archive } => {
257 assert!(!enable_archive);
258 }
259 _ => panic!("Expected Memory variant"),
260 },
261 _ => panic!("Expected Inline variant"),
262 }
263 }
264
265 #[test]
266 fn test_validate_memory() {
267 let spec = StorageBackendSpec::Memory {
268 enable_archive: true,
269 };
270 assert!(spec.validate().is_ok());
271 }
272
273 #[test]
274 fn test_validate_rocksdb_absolute_path() {
275 let spec = StorageBackendSpec::RocksDb {
276 path: "/data/drasi".to_string(),
277 enable_archive: true,
278 direct_io: false,
279 };
280 assert!(spec.validate().is_ok());
281 }
282
283 #[test]
284 fn test_validate_rocksdb_relative_path() {
285 let spec = StorageBackendSpec::RocksDb {
286 path: "data/drasi".to_string(),
287 enable_archive: true,
288 direct_io: false,
289 };
290 assert!(spec.validate().is_err());
291 let err = spec.validate().unwrap_err();
292 assert!(err.contains("must be absolute"));
293 }
294
295 #[test]
296 fn test_validate_redis_valid_url() {
297 let spec = StorageBackendSpec::Redis {
298 connection_string: "redis://localhost:6379".to_string(),
299 cache_size: Some(1000),
300 };
301 assert!(spec.validate().is_ok());
302 }
303
304 #[test]
305 fn test_validate_redis_invalid_url() {
306 let spec = StorageBackendSpec::Redis {
307 connection_string: "localhost:6379".to_string(),
308 cache_size: Some(1000),
309 };
310 assert!(spec.validate().is_err());
311 let err = spec.validate().unwrap_err();
312 assert!(err.contains("must start with"));
313 }
314
315 #[test]
316 fn test_is_volatile() {
317 let memory_spec = StorageBackendSpec::Memory {
318 enable_archive: false,
319 };
320 assert!(memory_spec.is_volatile());
321
322 let rocks_spec = StorageBackendSpec::RocksDb {
323 path: "/data/drasi".to_string(),
324 enable_archive: false,
325 direct_io: false,
326 };
327 assert!(!rocks_spec.is_volatile());
328
329 let redis_spec = StorageBackendSpec::Redis {
330 connection_string: "redis://localhost:6379".to_string(),
331 cache_size: None,
332 };
333 assert!(!redis_spec.is_volatile());
334 }
335}