1use serde::{Deserialize, Serialize};
4
5#[derive(Debug, Clone, Serialize, Deserialize)]
7#[serde(deny_unknown_fields)]
8pub struct FileStorageConfig {
9 #[serde(default = "default_file_storage_type")]
11 pub storage_type: String,
12 #[serde(default)]
14 pub local_path: Option<String>,
15 #[serde(default)]
17 pub s3: Option<S3Config>,
18}
19
20impl Default for FileStorageConfig {
21 fn default() -> Self {
22 Self {
23 storage_type: "local".to_string(),
24 local_path: None,
25 s3: None,
26 }
27 }
28}
29
30fn default_file_storage_type() -> String {
31 "local".to_string()
32}
33
34impl FileStorageConfig {
35 pub fn merge(mut self, other: Self) -> Self {
37 let default = Self::default();
38
39 if !other.storage_type.is_empty() && other.storage_type != default.storage_type {
40 self.storage_type = other.storage_type;
41 }
42 if other.local_path.is_some() {
43 self.local_path = other.local_path;
44 }
45 if other.s3.is_some() {
46 self.s3 = other.s3;
47 }
48
49 self
50 }
51}
52
53#[derive(Clone, Serialize, Deserialize)]
55#[serde(deny_unknown_fields)]
56pub struct S3Config {
57 pub bucket: String,
59 pub region: String,
61 pub access_key_id: String,
63 pub secret_access_key: String,
65 pub endpoint: Option<String>,
67}
68
69impl std::fmt::Debug for S3Config {
70 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
71 f.debug_struct("S3Config")
72 .field("bucket", &self.bucket)
73 .field("region", &self.region)
74 .field("access_key_id", &"***REDACTED***")
75 .field("secret_access_key", &"***REDACTED***")
76 .field("endpoint", &self.endpoint)
77 .finish()
78 }
79}
80
81#[derive(Clone, Serialize, Deserialize)]
83#[serde(deny_unknown_fields)]
84pub struct VectorDbConfig {
85 pub db_type: String,
87 pub url: String,
89 pub api_key: String,
91 pub index_name: String,
93 #[serde(default)]
100 pub allow_degraded: bool,
101}
102
103impl std::fmt::Debug for VectorDbConfig {
104 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
105 f.debug_struct("VectorDbConfig")
106 .field("db_type", &self.db_type)
107 .field("url", &self.url)
108 .field("api_key", &"***REDACTED***")
109 .field("index_name", &self.index_name)
110 .field("allow_degraded", &self.allow_degraded)
111 .finish()
112 }
113}
114
115impl Default for VectorDbConfig {
116 fn default() -> Self {
117 Self {
118 db_type: "pinecone".to_string(),
119 url: String::new(),
120 api_key: String::new(),
121 index_name: "default".to_string(),
122 allow_degraded: false,
123 }
124 }
125}
126
127#[derive(Clone, Serialize, Deserialize, Default)]
129#[serde(deny_unknown_fields)]
130pub struct AlertingConfig {
131 #[serde(default)]
133 pub enabled: bool,
134 pub slack_webhook: Option<String>,
136 pub email: Option<EmailConfig>,
138}
139
140impl std::fmt::Debug for AlertingConfig {
141 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
142 f.debug_struct("AlertingConfig")
143 .field("enabled", &self.enabled)
144 .field(
145 "slack_webhook",
146 &self.slack_webhook.as_ref().map(|_| "***REDACTED***"),
147 )
148 .field("email", &self.email)
149 .finish()
150 }
151}
152
153#[derive(Clone, Serialize, Deserialize)]
155#[serde(deny_unknown_fields)]
156pub struct EmailConfig {
157 pub smtp_server: String,
159 pub smtp_port: u16,
161 pub username: String,
163 pub password: String,
165 pub from_address: String,
167 pub to_addresses: Vec<String>,
169}
170
171impl std::fmt::Debug for EmailConfig {
172 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
173 f.debug_struct("EmailConfig")
174 .field("smtp_server", &self.smtp_server)
175 .field("smtp_port", &self.smtp_port)
176 .field("username", &self.username)
177 .field("password", &"***REDACTED***")
178 .field("from_address", &self.from_address)
179 .field("to_addresses", &self.to_addresses)
180 .finish()
181 }
182}
183
184#[cfg(test)]
185mod tests {
186 use super::*;
187
188 #[test]
191 fn test_file_storage_config_default() {
192 let config = FileStorageConfig::default();
193 assert_eq!(config.storage_type, "local");
194 assert!(config.local_path.is_none());
196 assert!(config.s3.is_none());
197 }
198
199 #[test]
200 fn test_file_storage_config_local() {
201 let config = FileStorageConfig {
202 storage_type: "local".to_string(),
203 local_path: Some("/var/data".to_string()),
204 s3: None,
205 };
206 assert_eq!(config.storage_type, "local");
207 assert_eq!(config.local_path, Some("/var/data".to_string()));
208 }
209
210 #[test]
211 fn test_file_storage_config_s3() {
212 let s3 = S3Config {
213 bucket: "my-bucket".to_string(),
214 region: "us-east-1".to_string(),
215 access_key_id: "AKIAIOSFODNN7EXAMPLE".to_string(),
216 secret_access_key: "secret".to_string(),
217 endpoint: None,
218 };
219
220 let config = FileStorageConfig {
221 storage_type: "s3".to_string(),
222 local_path: None,
223 s3: Some(s3),
224 };
225 assert_eq!(config.storage_type, "s3");
226 assert!(config.s3.is_some());
227 }
228
229 #[test]
230 fn test_file_storage_config_serialization() {
231 let config = FileStorageConfig::default();
232 let json = serde_json::to_value(&config).unwrap();
233 assert_eq!(json["storage_type"], "local");
234 assert!(json["local_path"].is_null());
235 }
236
237 #[test]
238 fn test_file_storage_config_deserializes_partial_local_config() {
239 let json = r#"{"local_path": "/configured/files"}"#;
240 let config: FileStorageConfig = serde_json::from_str(json).unwrap();
241
242 assert_eq!(config.storage_type, "local");
243 assert_eq!(config.local_path, Some("/configured/files".to_string()));
244 assert!(config.s3.is_none());
245 }
246
247 #[test]
248 fn test_file_storage_config_clone() {
249 let config = FileStorageConfig::default();
250 let cloned = config.clone();
251 assert_eq!(config.storage_type, cloned.storage_type);
252 }
253
254 #[test]
255 fn test_file_storage_config_merge_local_path() {
256 let base = FileStorageConfig::default();
257 let other = FileStorageConfig {
258 storage_type: "local".to_string(),
259 local_path: Some("/configured/files".to_string()),
260 s3: None,
261 };
262
263 let merged = base.merge(other);
264
265 assert_eq!(merged.storage_type, "local");
266 assert_eq!(merged.local_path, Some("/configured/files".to_string()));
267 }
268
269 #[test]
272 fn test_s3_config_structure() {
273 let config = S3Config {
274 bucket: "test-bucket".to_string(),
275 region: "eu-west-1".to_string(),
276 access_key_id: "access".to_string(),
277 secret_access_key: "secret".to_string(),
278 endpoint: Some("https://s3.custom.com".to_string()),
279 };
280 assert_eq!(config.bucket, "test-bucket");
281 assert_eq!(config.region, "eu-west-1");
282 assert!(config.endpoint.is_some());
283 }
284
285 #[test]
286 fn test_s3_config_serialization() {
287 let config = S3Config {
288 bucket: "bucket".to_string(),
289 region: "us-west-2".to_string(),
290 access_key_id: "key".to_string(),
291 secret_access_key: "secret".to_string(),
292 endpoint: None,
293 };
294 let json = serde_json::to_value(&config).unwrap();
295 assert_eq!(json["bucket"], "bucket");
296 assert_eq!(json["region"], "us-west-2");
297 }
298
299 #[test]
300 fn test_s3_config_deserialization() {
301 let json = r#"{
302 "bucket": "my-bucket",
303 "region": "ap-southeast-1",
304 "access_key_id": "AKIA...",
305 "secret_access_key": "secret123"
306 }"#;
307 let config: S3Config = serde_json::from_str(json).unwrap();
308 assert_eq!(config.bucket, "my-bucket");
309 assert!(config.endpoint.is_none());
310 }
311
312 #[test]
313 fn test_s3_config_clone() {
314 let config = S3Config {
315 bucket: "clone-test".to_string(),
316 region: "us-east-1".to_string(),
317 access_key_id: "key".to_string(),
318 secret_access_key: "secret".to_string(),
319 endpoint: None,
320 };
321 let cloned = config.clone();
322 assert_eq!(config.bucket, cloned.bucket);
323 }
324
325 #[test]
328 fn test_vector_db_config_default() {
329 let config = VectorDbConfig::default();
330 assert_eq!(config.db_type, "pinecone");
331 assert!(config.url.is_empty());
332 assert!(config.api_key.is_empty());
333 assert_eq!(config.index_name, "default");
334 }
335
336 #[test]
337 fn test_vector_db_config_structure() {
338 let config = VectorDbConfig {
339 db_type: "weaviate".to_string(),
340 url: "http://weaviate:8080".to_string(),
341 api_key: "weaviate-key".to_string(),
342 index_name: "embeddings".to_string(),
343 allow_degraded: false,
344 };
345 assert_eq!(config.db_type, "weaviate");
346 assert_eq!(config.index_name, "embeddings");
347 }
348
349 #[test]
350 fn test_vector_db_config_serialization() {
351 let config = VectorDbConfig {
352 db_type: "qdrant".to_string(),
353 url: "http://qdrant:6333".to_string(),
354 api_key: "qdrant-key".to_string(),
355 index_name: "vectors".to_string(),
356 allow_degraded: false,
357 };
358 let json = serde_json::to_value(&config).unwrap();
359 assert_eq!(json["db_type"], "qdrant");
360 assert_eq!(json["url"], "http://qdrant:6333");
361 }
362
363 #[test]
364 fn test_vector_db_config_allow_degraded_default_false() {
365 let config = VectorDbConfig::default();
366 assert!(
367 !config.allow_degraded,
368 "allow_degraded must default to false so explicit failures are surfaced"
369 );
370 }
371
372 #[test]
373 fn test_vector_db_config_clone() {
374 let config = VectorDbConfig::default();
375 let cloned = config.clone();
376 assert_eq!(config.db_type, cloned.db_type);
377 }
378
379 #[test]
382 fn test_alerting_config_default() {
383 let config = AlertingConfig::default();
384 assert!(!config.enabled);
385 assert!(config.slack_webhook.is_none());
386 assert!(config.email.is_none());
387 }
388
389 #[test]
390 fn test_alerting_config_with_slack() {
391 let config = AlertingConfig {
392 enabled: true,
393 slack_webhook: Some("https://hooks.slack.com/xxx".to_string()),
394 email: None,
395 };
396 assert!(config.enabled);
397 assert!(config.slack_webhook.is_some());
398 }
399
400 #[test]
401 fn test_alerting_config_serialization() {
402 let config = AlertingConfig {
403 enabled: true,
404 slack_webhook: Some("https://slack.webhook".to_string()),
405 email: None,
406 };
407 let json = serde_json::to_value(&config).unwrap();
408 assert_eq!(json["enabled"], true);
409 }
410
411 #[test]
412 fn test_alerting_config_clone() {
413 let config = AlertingConfig::default();
414 let cloned = config.clone();
415 assert_eq!(config.enabled, cloned.enabled);
416 }
417
418 #[test]
421 fn test_email_config_structure() {
422 let config = EmailConfig {
423 smtp_server: "smtp.example.com".to_string(),
424 smtp_port: 587,
425 username: "user@example.com".to_string(),
426 password: "password".to_string(),
427 from_address: "alerts@example.com".to_string(),
428 to_addresses: vec!["admin@example.com".to_string()],
429 };
430 assert_eq!(config.smtp_server, "smtp.example.com");
431 assert_eq!(config.smtp_port, 587);
432 assert_eq!(config.to_addresses.len(), 1);
433 }
434
435 #[test]
436 fn test_email_config_serialization() {
437 let config = EmailConfig {
438 smtp_server: "mail.test.com".to_string(),
439 smtp_port: 465,
440 username: "sender".to_string(),
441 password: "pass".to_string(),
442 from_address: "from@test.com".to_string(),
443 to_addresses: vec!["to1@test.com".to_string(), "to2@test.com".to_string()],
444 };
445 let json = serde_json::to_value(&config).unwrap();
446 assert_eq!(json["smtp_port"], 465);
447 assert!(json["to_addresses"].is_array());
448 }
449
450 #[test]
451 fn test_email_config_deserialization() {
452 let json = r#"{
453 "smtp_server": "smtp.gmail.com",
454 "smtp_port": 587,
455 "username": "user",
456 "password": "pass",
457 "from_address": "from@gmail.com",
458 "to_addresses": ["to@gmail.com"]
459 }"#;
460 let config: EmailConfig = serde_json::from_str(json).unwrap();
461 assert_eq!(config.smtp_server, "smtp.gmail.com");
462 assert_eq!(config.smtp_port, 587);
463 }
464
465 #[test]
466 fn test_email_config_clone() {
467 let config = EmailConfig {
468 smtp_server: "smtp.clone.com".to_string(),
469 smtp_port: 25,
470 username: "clone".to_string(),
471 password: "pass".to_string(),
472 from_address: "clone@test.com".to_string(),
473 to_addresses: vec![],
474 };
475 let cloned = config.clone();
476 assert_eq!(config.smtp_server, cloned.smtp_server);
477 }
478}