1use serde::{Deserialize, Deserializer, Serialize};
4
5#[derive(Debug, Clone, Serialize, Default)]
7#[serde(tag = "type")]
8pub enum StorageConfig {
9 #[default]
10 #[serde(rename = "none")]
11 None,
12
13 #[serde(rename = "file")]
14 File(FileStorageConfig),
15
16 #[serde(rename = "sqlite")]
17 Sqlite(SqliteStorageConfig),
18
19 #[serde(rename = "redis")]
20 Redis(RedisStorageConfig),
21}
22
23#[derive(Deserialize)]
24#[serde(tag = "type", rename_all = "lowercase", deny_unknown_fields)]
25enum StorageConfigWire {
26 None {},
27 File {
28 path: String,
29 },
30 Sqlite {
31 path: String,
32 #[serde(default)]
33 table: Option<String>,
34 },
35 Redis {
36 url: String,
37 #[serde(default)]
38 prefix: Option<String>,
39 #[serde(default)]
40 ttl_seconds: Option<u64>,
41 },
42}
43
44impl<'de> Deserialize<'de> for StorageConfig {
45 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
46 where
47 D: Deserializer<'de>,
48 {
49 Ok(match StorageConfigWire::deserialize(deserializer)? {
50 StorageConfigWire::None {} => Self::None,
51 StorageConfigWire::File { path } => Self::File(FileStorageConfig { path }),
52 StorageConfigWire::Sqlite { path, table } => {
53 Self::Sqlite(SqliteStorageConfig { path, table })
54 }
55 StorageConfigWire::Redis {
56 url,
57 prefix,
58 ttl_seconds,
59 } => Self::Redis(RedisStorageConfig {
60 url,
61 prefix,
62 ttl_seconds,
63 }),
64 })
65 }
66}
67
68#[derive(Debug, Clone, Serialize, Deserialize)]
69pub struct FileStorageConfig {
70 pub path: String,
71}
72
73#[derive(Debug, Clone, Serialize, Deserialize)]
74pub struct SqliteStorageConfig {
75 pub path: String,
76
77 #[serde(default)]
78 pub table: Option<String>,
79}
80
81#[derive(Debug, Clone, Serialize, Deserialize)]
82pub struct RedisStorageConfig {
83 pub url: String,
84
85 #[serde(default)]
86 pub prefix: Option<String>,
87
88 #[serde(default)]
89 pub ttl_seconds: Option<u64>,
90}
91
92impl StorageConfig {
93 pub fn none() -> Self {
94 StorageConfig::None
95 }
96
97 pub fn file(path: impl Into<String>) -> Self {
98 StorageConfig::File(FileStorageConfig { path: path.into() })
99 }
100
101 pub fn sqlite(path: impl Into<String>) -> Self {
102 StorageConfig::Sqlite(SqliteStorageConfig {
103 path: path.into(),
104 table: None,
105 })
106 }
107
108 pub fn redis(url: impl Into<String>) -> Self {
109 StorageConfig::Redis(RedisStorageConfig {
110 url: url.into(),
111 prefix: None,
112 ttl_seconds: None,
113 })
114 }
115
116 pub fn is_none(&self) -> bool {
117 matches!(self, StorageConfig::None)
118 }
119
120 pub fn is_file(&self) -> bool {
121 matches!(self, StorageConfig::File(_))
122 }
123
124 pub fn is_sqlite(&self) -> bool {
125 matches!(self, StorageConfig::Sqlite(_))
126 }
127
128 pub fn is_redis(&self) -> bool {
129 matches!(self, StorageConfig::Redis(_))
130 }
131
132 pub fn storage_type(&self) -> &'static str {
133 match self {
134 StorageConfig::None => "none",
135 StorageConfig::File(_) => "file",
136 StorageConfig::Sqlite(_) => "sqlite",
137 StorageConfig::Redis(_) => "redis",
138 }
139 }
140
141 pub fn get_path(&self) -> Option<&str> {
142 match self {
143 StorageConfig::File(c) => Some(&c.path),
144 StorageConfig::Sqlite(c) => Some(&c.path),
145 _ => None,
146 }
147 }
148
149 pub fn get_url(&self) -> Option<&str> {
150 match self {
151 StorageConfig::Redis(c) => Some(&c.url),
152 _ => None,
153 }
154 }
155
156 pub fn get_prefix(&self) -> &str {
157 match self {
158 StorageConfig::Redis(c) => c.prefix.as_deref().unwrap_or("agent:"),
159 _ => "agent:",
160 }
161 }
162
163 pub fn get_ttl(&self) -> Option<u64> {
164 match self {
165 StorageConfig::Redis(c) => c.ttl_seconds,
166 _ => None,
167 }
168 }
169
170 pub fn get_table(&self) -> Option<&str> {
171 match self {
172 StorageConfig::Sqlite(c) => c.table.as_deref(),
173 _ => None,
174 }
175 }
176
177 pub fn as_file(&self) -> Option<&FileStorageConfig> {
178 match self {
179 StorageConfig::File(c) => Some(c),
180 _ => None,
181 }
182 }
183
184 pub fn as_sqlite(&self) -> Option<&SqliteStorageConfig> {
185 match self {
186 StorageConfig::Sqlite(c) => Some(c),
187 _ => None,
188 }
189 }
190
191 pub fn as_redis(&self) -> Option<&RedisStorageConfig> {
192 match self {
193 StorageConfig::Redis(c) => Some(c),
194 _ => None,
195 }
196 }
197}
198
199impl RedisStorageConfig {
200 pub fn with_prefix(mut self, prefix: impl Into<String>) -> Self {
201 self.prefix = Some(prefix.into());
202 self
203 }
204
205 pub fn with_ttl(mut self, ttl_seconds: u64) -> Self {
206 self.ttl_seconds = Some(ttl_seconds);
207 self
208 }
209}
210
211impl SqliteStorageConfig {
212 pub fn with_table(mut self, table: impl Into<String>) -> Self {
213 self.table = Some(table.into());
214 self
215 }
216}
217
218use ai_agents_storage::StorageConfig as StorageStorageConfig;
219
220pub fn to_storage_config(config: &StorageConfig) -> StorageStorageConfig {
222 match config {
223 StorageConfig::None => StorageStorageConfig::None,
224 StorageConfig::File(fc) => StorageStorageConfig::File {
225 path: fc.path.clone(),
226 },
227 StorageConfig::Sqlite(sc) => StorageStorageConfig::Sqlite {
228 path: sc.path.clone(),
229 },
230 StorageConfig::Redis(rc) => StorageStorageConfig::Redis {
231 url: rc.url.clone(),
232 prefix: rc.prefix.clone(),
233 ttl_seconds: rc.ttl_seconds,
234 },
235 }
236}
237
238#[cfg(test)]
239mod tests {
240 use super::*;
241
242 #[test]
243 fn test_storage_config_default() {
244 let config = StorageConfig::default();
245 assert!(config.is_none());
246 assert!(!config.is_file());
247 assert!(!config.is_sqlite());
248 assert!(!config.is_redis());
249 assert_eq!(config.storage_type(), "none");
250 }
251
252 #[test]
253 fn test_storage_config_none_yaml() {
254 let yaml = "type: none\n";
255 let config: StorageConfig = serde_yaml::from_str(yaml).unwrap();
256 assert!(config.is_none());
257 }
258
259 fn assert_unknown_field(yaml: &str, field: &str) {
260 let error = serde_yaml::from_str::<StorageConfig>(yaml)
261 .unwrap_err()
262 .to_string();
263 assert!(
264 error.contains(&format!("unknown field `{field}`")),
265 "{error}"
266 );
267 }
268
269 #[test]
270 fn test_storage_config_rejects_unknown_none_field() {
271 assert_unknown_field("type: none\nunexpected: true\n", "unexpected");
272 }
273
274 #[test]
275 fn test_storage_config_rejects_unknown_file_field() {
276 assert_unknown_field("type: file\npath: ./data\nread_only: true\n", "read_only");
277 }
278
279 #[test]
280 fn test_storage_config_rejects_unknown_sqlite_field() {
281 assert_unknown_field("type: sqlite\npath: ./data.db\ntabl: sessions\n", "tabl");
282 }
283
284 #[test]
285 fn test_storage_config_rejects_unknown_redis_field() {
286 assert_unknown_field(
287 "type: redis\nurl: redis://localhost:6379\nttl_second: 60\n",
288 "ttl_second",
289 );
290 }
291
292 #[test]
293 fn test_storage_config_file() {
294 let yaml = r#"
295type: file
296path: "./data/sessions"
297"#;
298 let config: StorageConfig = serde_yaml::from_str(yaml).unwrap();
299 assert!(config.is_file());
300 assert_eq!(config.get_path(), Some("./data/sessions"));
301 assert_eq!(config.storage_type(), "file");
302 }
303
304 #[test]
305 fn test_storage_config_file_builder() {
306 let config = StorageConfig::file("./data/sessions");
307 assert!(config.is_file());
308 assert_eq!(config.get_path(), Some("./data/sessions"));
309 }
310
311 #[test]
312 fn test_storage_config_sqlite() {
313 let yaml = r#"
314type: sqlite
315path: "./data/sessions.db"
316"#;
317 let config: StorageConfig = serde_yaml::from_str(yaml).unwrap();
318 assert!(config.is_sqlite());
319 assert_eq!(config.get_path(), Some("./data/sessions.db"));
320 assert_eq!(config.storage_type(), "sqlite");
321 }
322
323 #[test]
324 fn test_storage_config_sqlite_with_table() {
325 let yaml = r#"
326type: sqlite
327path: "./data/sessions.db"
328table: "custom_sessions"
329"#;
330 let config: StorageConfig = serde_yaml::from_str(yaml).unwrap();
331 assert!(config.is_sqlite());
332 assert_eq!(config.get_table(), Some("custom_sessions"));
333 }
334
335 #[test]
336 fn test_storage_config_sqlite_builder() {
337 let config = StorageConfig::sqlite("./data/sessions.db");
338 assert!(config.is_sqlite());
339 assert_eq!(config.get_path(), Some("./data/sessions.db"));
340 }
341
342 #[test]
343 fn test_storage_config_redis() {
344 let yaml = r#"
345type: redis
346url: "redis://localhost:6379"
347prefix: "myagent:"
348ttl_seconds: 86400
349"#;
350 let config: StorageConfig = serde_yaml::from_str(yaml).unwrap();
351 assert!(config.is_redis());
352 assert_eq!(config.get_url(), Some("redis://localhost:6379"));
353 assert_eq!(config.get_prefix(), "myagent:");
354 assert_eq!(config.get_ttl(), Some(86400));
355 assert_eq!(config.storage_type(), "redis");
356 }
357
358 #[test]
359 fn test_storage_config_redis_builder() {
360 let config = StorageConfig::redis("redis://localhost:6379");
361 assert!(config.is_redis());
362 assert_eq!(config.get_url(), Some("redis://localhost:6379"));
363 assert_eq!(config.get_prefix(), "agent:");
364 assert_eq!(config.get_ttl(), None);
365 }
366
367 #[test]
368 fn test_storage_config_default_prefix() {
369 let config = StorageConfig::default();
370 assert_eq!(config.get_prefix(), "agent:");
371
372 let config = StorageConfig::file("./data");
373 assert_eq!(config.get_prefix(), "agent:");
374
375 let yaml = r#"
376type: redis
377url: "redis://localhost:6379"
378"#;
379 let config: StorageConfig = serde_yaml::from_str(yaml).unwrap();
380 assert_eq!(config.get_prefix(), "agent:");
381 }
382
383 #[test]
384 fn test_storage_config_accessors() {
385 let file_config = StorageConfig::file("./data");
386 assert!(file_config.as_file().is_some());
387 assert!(file_config.as_sqlite().is_none());
388 assert!(file_config.as_redis().is_none());
389
390 let sqlite_config = StorageConfig::sqlite("./data.db");
391 assert!(sqlite_config.as_file().is_none());
392 assert!(sqlite_config.as_sqlite().is_some());
393 assert!(sqlite_config.as_redis().is_none());
394
395 let redis_config = StorageConfig::redis("redis://localhost");
396 assert!(redis_config.as_file().is_none());
397 assert!(redis_config.as_sqlite().is_none());
398 assert!(redis_config.as_redis().is_some());
399 }
400
401 #[test]
402 fn test_redis_config_builder_methods() {
403 let config = RedisStorageConfig {
404 url: "redis://localhost:6379".to_string(),
405 prefix: None,
406 ttl_seconds: None,
407 }
408 .with_prefix("test:")
409 .with_ttl(3600);
410
411 assert_eq!(config.prefix, Some("test:".to_string()));
412 assert_eq!(config.ttl_seconds, Some(3600));
413 }
414
415 #[test]
416 fn test_sqlite_config_builder_methods() {
417 let config = SqliteStorageConfig {
418 path: "./data.db".to_string(),
419 table: None,
420 }
421 .with_table("custom_table");
422
423 assert_eq!(config.table, Some("custom_table".to_string()));
424 }
425
426 #[test]
427 fn test_storage_config_serialization() {
428 let config = StorageConfig::redis("redis://localhost:6379");
429 let yaml = serde_yaml::to_string(&config).unwrap();
430 assert!(yaml.contains("type: redis"));
431 assert!(yaml.contains("url: redis://localhost:6379"));
432 }
433
434 #[test]
435 fn test_storage_config_valid_round_trips() {
436 let configs = [
437 StorageConfig::none(),
438 StorageConfig::file("./data/sessions"),
439 StorageConfig::sqlite("./data/sessions.db"),
440 StorageConfig::Sqlite(
441 SqliteStorageConfig {
442 path: "./data/custom.db".to_string(),
443 table: None,
444 }
445 .with_table("custom_sessions"),
446 ),
447 StorageConfig::Redis(
448 RedisStorageConfig {
449 url: "redis://localhost:6379".to_string(),
450 prefix: None,
451 ttl_seconds: None,
452 }
453 .with_prefix("custom:")
454 .with_ttl(3600),
455 ),
456 ];
457
458 for config in configs {
459 let yaml = serde_yaml::to_string(&config).unwrap();
460 let restored: StorageConfig = serde_yaml::from_str(&yaml).unwrap();
461 assert_eq!(
462 serde_yaml::to_value(restored).unwrap(),
463 serde_yaml::to_value(config).unwrap()
464 );
465 }
466 }
467
468 #[test]
469 fn test_to_storage_config_none() {
470 use ai_agents_storage::StorageConfig as SC;
471 let result = to_storage_config(&StorageConfig::None);
472 assert!(matches!(result, SC::None));
473 }
474
475 #[test]
476 fn test_to_storage_config_file() {
477 use ai_agents_storage::StorageConfig as SC;
478 let config = StorageConfig::file("./data/sessions");
479 let result = to_storage_config(&config);
480 match result {
481 SC::File { path } => assert_eq!(path, "./data/sessions"),
482 other => panic!("expected File, got {:?}", other),
483 }
484 }
485
486 #[test]
487 fn test_to_storage_config_sqlite() {
488 use ai_agents_storage::StorageConfig as SC;
489 let config = StorageConfig::sqlite("./data/db.sqlite");
490 let result = to_storage_config(&config);
491 match result {
492 SC::Sqlite { path } => assert_eq!(path, "./data/db.sqlite"),
493 other => panic!("expected Sqlite, got {:?}", other),
494 }
495 }
496}