1use crate::{ConfigMigrator, MigrationError, Migrator, Queryable};
7use local_store::{FileStorageStrategy, FormatStrategy, LoadBehavior};
8use serde_json::Value as JsonValue;
9use std::path::{Path, PathBuf};
10
11pub struct FileStorage {
22 inner: local_store::FileStorage,
24 config: ConfigMigrator,
26 strategy: FileStorageStrategy,
28}
29
30impl FileStorage {
31 pub fn new(
48 path: PathBuf,
49 migrator: Migrator,
50 strategy: FileStorageStrategy,
51 ) -> Result<Self, MigrationError> {
52 let file_was_missing = !path.exists();
54
55 let inner_strategy = FileStorageStrategy {
58 load_behavior: LoadBehavior::CreateIfMissing,
59 ..strategy.clone()
60 };
61 let inner = local_store::FileStorage::new(path.clone(), inner_strategy)
62 .map_err(MigrationError::Store)?;
63
64 let json_string = if !file_was_missing {
66 let raw = inner.read_string().map_err(MigrationError::Store)?;
68 if raw.trim().is_empty() {
69 "{}".to_string()
70 } else {
71 match strategy.format {
72 FormatStrategy::Toml => {
73 let tv: toml::Value = toml::from_str(&raw)
74 .map_err(|e| MigrationError::TomlParseError(e.to_string()))?;
75 let jv = toml_to_json(tv)?;
76 serde_json::to_string(&jv)
77 .map_err(|e| MigrationError::SerializationError(e.to_string()))?
78 }
79 FormatStrategy::Json => raw,
80 }
81 }
82 } else {
83 match strategy.load_behavior {
85 LoadBehavior::ErrorIfMissing => {
86 return Err(MigrationError::Store(local_store::StoreError::IoError {
87 operation: local_store::IoOperationKind::Read,
88 path: path.display().to_string(),
89 context: None,
90 error: "File not found".to_string(),
91 }));
92 }
93 LoadBehavior::CreateIfMissing | LoadBehavior::SaveIfMissing => {
94 if let Some(ref default_value) = strategy.default_value {
95 serde_json::to_string(default_value)
96 .map_err(|e| MigrationError::SerializationError(e.to_string()))?
97 } else {
98 "{}".to_string()
99 }
100 }
101 }
102 };
103
104 let config = ConfigMigrator::from(&json_string, migrator)?;
105 let storage = Self {
106 inner,
107 config,
108 strategy,
109 };
110
111 if file_was_missing && storage.strategy.load_behavior == LoadBehavior::SaveIfMissing {
113 storage.save()?;
114 }
115
116 Ok(storage)
117 }
118
119 pub fn save(&self) -> Result<(), MigrationError> {
125 let json_value = self.config.as_value();
126
127 let content = match self.strategy.format {
128 FormatStrategy::Toml => {
129 let tv = local_store::json_to_toml(json_value).map_err(|e| {
130 MigrationError::Store(local_store::StoreError::FormatConvert(e))
131 })?;
132 toml::to_string_pretty(&tv)
133 .map_err(|e| MigrationError::TomlSerializeError(e.to_string()))?
134 }
135 FormatStrategy::Json => serde_json::to_string_pretty(json_value)
136 .map_err(|e| MigrationError::SerializationError(e.to_string()))?,
137 };
138
139 self.inner
140 .write_string(&content)
141 .map_err(MigrationError::Store)
142 }
143
144 pub fn config(&self) -> &ConfigMigrator {
146 &self.config
147 }
148
149 pub fn config_mut(&mut self) -> &mut ConfigMigrator {
151 &mut self.config
152 }
153
154 pub fn query<T>(&self, key: &str) -> Result<Vec<T>, MigrationError>
158 where
159 T: Queryable + for<'de> serde::Deserialize<'de>,
160 {
161 self.config.query(key)
162 }
163
164 pub fn update<T>(&mut self, key: &str, value: Vec<T>) -> Result<(), MigrationError>
168 where
169 T: Queryable + serde::Serialize,
170 {
171 self.config.update(key, value)
172 }
173
174 pub fn update_and_save<T>(&mut self, key: &str, value: Vec<T>) -> Result<(), MigrationError>
176 where
177 T: Queryable + serde::Serialize,
178 {
179 self.update(key, value)?;
180 self.save()
181 }
182
183 pub fn path(&self) -> &Path {
189 self.inner.path()
190 }
191}
192
193fn toml_to_json(toml_value: toml::Value) -> Result<JsonValue, MigrationError> {
199 let json_str = serde_json::to_string(&toml_value)
200 .map_err(|e| MigrationError::SerializationError(e.to_string()))?;
201 let json_value: JsonValue = serde_json::from_str(&json_str)
202 .map_err(|e| MigrationError::DeserializationError(e.to_string()))?;
203 Ok(json_value)
204}
205
206#[cfg(test)]
207mod tests {
208 use super::*;
209 use crate::{IntoDomain, MigratesTo, Versioned};
210 use serde::{Deserialize, Serialize};
211 use tempfile::TempDir;
212
213 #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
214 struct TestEntity {
215 name: String,
216 count: u32,
217 }
218
219 impl Queryable for TestEntity {
220 const ENTITY_NAME: &'static str = "test";
221 }
222
223 #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
224 struct TestV1 {
225 name: String,
226 }
227
228 impl Versioned for TestV1 {
229 const VERSION: &'static str = "1.0.0";
230 }
231
232 impl MigratesTo<TestV2> for TestV1 {
233 fn migrate(self) -> TestV2 {
234 TestV2 {
235 name: self.name,
236 count: 0,
237 }
238 }
239 }
240
241 #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
242 struct TestV2 {
243 name: String,
244 count: u32,
245 }
246
247 impl Versioned for TestV2 {
248 const VERSION: &'static str = "2.0.0";
249 }
250
251 impl IntoDomain<TestEntity> for TestV2 {
252 fn into_domain(self) -> TestEntity {
253 TestEntity {
254 name: self.name,
255 count: self.count,
256 }
257 }
258 }
259
260 fn setup_migrator() -> Migrator {
261 let path = Migrator::define("test")
262 .from::<TestV1>()
263 .step::<TestV2>()
264 .into::<TestEntity>();
265
266 let mut migrator = Migrator::new();
267 migrator.register(path).unwrap();
268 migrator
269 }
270
271 #[test]
272 fn test_file_storage_strategy_builder() {
273 let strategy = FileStorageStrategy::new()
274 .with_format(FormatStrategy::Json)
275 .with_retry_count(5)
276 .with_cleanup(false)
277 .with_load_behavior(LoadBehavior::ErrorIfMissing);
278
279 assert_eq!(strategy.format, FormatStrategy::Json);
280 assert_eq!(strategy.atomic_write.retry_count, 5);
281 assert!(!strategy.atomic_write.cleanup_tmp_files);
282 assert_eq!(strategy.load_behavior, LoadBehavior::ErrorIfMissing);
283 }
284
285 #[test]
286 fn test_save_and_load_toml() {
287 let temp_dir = TempDir::new().unwrap();
288 let file_path = temp_dir.path().join("test.toml");
289 let migrator = setup_migrator();
290 let strategy = FileStorageStrategy::default(); let mut storage = FileStorage::new(file_path.clone(), migrator, strategy).unwrap();
293
294 let entities = vec![TestEntity {
296 name: "test".to_string(),
297 count: 42,
298 }];
299 storage.update_and_save("test", entities).unwrap();
300
301 let migrator2 = setup_migrator();
303 let storage2 =
304 FileStorage::new(file_path, migrator2, FileStorageStrategy::default()).unwrap();
305
306 let loaded: Vec<TestEntity> = storage2.query("test").unwrap();
308 assert_eq!(loaded.len(), 1);
309 assert_eq!(loaded[0].name, "test");
310 assert_eq!(loaded[0].count, 42);
311 }
312
313 #[test]
314 fn test_save_and_load_json() {
315 let temp_dir = TempDir::new().unwrap();
316 let file_path = temp_dir.path().join("test.json");
317 let migrator = setup_migrator();
318 let strategy = FileStorageStrategy::new().with_format(FormatStrategy::Json);
319
320 let mut storage = FileStorage::new(file_path.clone(), migrator, strategy).unwrap();
321
322 let entities = vec![TestEntity {
324 name: "json_test".to_string(),
325 count: 100,
326 }];
327 storage.update_and_save("test", entities).unwrap();
328
329 let migrator2 = setup_migrator();
331 let strategy2 = FileStorageStrategy::new().with_format(FormatStrategy::Json);
332 let storage2 = FileStorage::new(file_path, migrator2, strategy2).unwrap();
333
334 let loaded: Vec<TestEntity> = storage2.query("test").unwrap();
336 assert_eq!(loaded.len(), 1);
337 assert_eq!(loaded[0].name, "json_test");
338 assert_eq!(loaded[0].count, 100);
339 }
340
341 #[test]
342 fn test_load_behavior_create_if_missing() {
343 let temp_dir = TempDir::new().unwrap();
344 let file_path = temp_dir.path().join("nonexistent.toml");
345 let migrator = setup_migrator();
346 let strategy = FileStorageStrategy::new().with_load_behavior(LoadBehavior::CreateIfMissing);
347
348 let result = FileStorage::new(file_path, migrator, strategy);
349
350 assert!(result.is_ok()); }
352
353 #[test]
354 fn test_load_behavior_error_if_missing() {
355 let temp_dir = TempDir::new().unwrap();
356 let file_path = temp_dir.path().join("nonexistent.toml");
357 let migrator = setup_migrator();
358 let strategy = FileStorageStrategy::new().with_load_behavior(LoadBehavior::ErrorIfMissing);
359
360 let result = FileStorage::new(file_path, migrator, strategy);
361
362 assert!(result.is_err()); assert!(matches!(
364 result,
365 Err(MigrationError::Store(
366 local_store::StoreError::IoError { .. }
367 ))
368 ));
369 }
370
371 #[test]
372 fn test_load_behavior_save_if_missing() {
373 let temp_dir = TempDir::new().unwrap();
374 let file_path = temp_dir.path().join("save_if_missing.toml");
375 let migrator = setup_migrator();
376 let strategy = FileStorageStrategy::new().with_load_behavior(LoadBehavior::SaveIfMissing);
377
378 assert!(!file_path.exists());
380
381 let result = FileStorage::new(file_path.clone(), migrator, strategy.clone());
382
383 assert!(result.is_ok());
385 assert!(file_path.exists());
386
387 let _storage = result.unwrap();
389 let reloaded = FileStorage::new(file_path.clone(), setup_migrator(), strategy);
390 assert!(reloaded.is_ok());
391 }
392
393 #[test]
394 fn test_save_if_missing_with_default_value() {
395 let temp_dir = TempDir::new().unwrap();
396 let file_path = temp_dir.path().join("default_value.toml");
397 let migrator = setup_migrator();
398
399 let default_value = serde_json::json!({
401 "test": [
402 {
403 "version": "2.0.0",
404 "name": "default_user",
405 "count": 99
406 }
407 ]
408 });
409
410 let strategy = FileStorageStrategy::new()
411 .with_load_behavior(LoadBehavior::SaveIfMissing)
412 .with_default_value(default_value);
413
414 assert!(!file_path.exists());
416
417 let storage = FileStorage::new(file_path.clone(), migrator, strategy.clone()).unwrap();
418
419 assert!(file_path.exists());
421
422 let loaded: Vec<TestEntity> = storage.query("test").unwrap();
424 assert_eq!(loaded.len(), 1);
425 assert_eq!(loaded[0].name, "default_user");
426 assert_eq!(loaded[0].count, 99);
427
428 let reloaded = FileStorage::new(file_path.clone(), setup_migrator(), strategy).unwrap();
430 let reloaded_entities: Vec<TestEntity> = reloaded.query("test").unwrap();
431 assert_eq!(reloaded_entities.len(), 1);
432 assert_eq!(reloaded_entities[0].name, "default_user");
433 assert_eq!(reloaded_entities[0].count, 99);
434 }
435
436 #[test]
437 fn test_create_if_missing_with_default_value() {
438 let temp_dir = TempDir::new().unwrap();
439 let file_path = temp_dir.path().join("create_default.toml");
440 let migrator = setup_migrator();
441
442 let default_value = serde_json::json!({
443 "test": [{
444 "version": "2.0.0",
445 "name": "created",
446 "count": 42
447 }]
448 });
449
450 let strategy = FileStorageStrategy::new()
451 .with_load_behavior(LoadBehavior::CreateIfMissing)
452 .with_default_value(default_value);
453
454 let storage = FileStorage::new(file_path.clone(), migrator, strategy).unwrap();
456
457 let loaded: Vec<TestEntity> = storage.query("test").unwrap();
459 assert_eq!(loaded.len(), 1);
460 assert_eq!(loaded[0].name, "created");
461 assert_eq!(loaded[0].count, 42);
462 }
463
464 #[test]
465 fn test_atomic_write_no_tmp_file_left() {
466 let temp_dir = TempDir::new().unwrap();
467 let file_path = temp_dir.path().join("atomic.toml");
468 let migrator = setup_migrator();
469 let strategy = FileStorageStrategy::default();
470
471 let mut storage = FileStorage::new(file_path.clone(), migrator, strategy).unwrap();
472
473 let entities = vec![TestEntity {
474 name: "atomic".to_string(),
475 count: 1,
476 }];
477 storage.update_and_save("test", entities).unwrap();
478
479 let entries: Vec<_> = std::fs::read_dir(temp_dir.path())
481 .unwrap()
482 .filter_map(|e| e.ok())
483 .collect();
484
485 let tmp_files: Vec<_> = entries
486 .iter()
487 .filter(|e| {
488 e.file_name()
489 .to_string_lossy()
490 .starts_with(".atomic.toml.tmp")
491 })
492 .collect();
493
494 assert_eq!(tmp_files.len(), 0, "Temporary files should be cleaned up");
495 }
496
497 #[test]
498 fn test_file_storage_path() {
499 let temp_dir = TempDir::new().unwrap();
500 let file_path = temp_dir.path().join("test_config.toml");
501 let migrator = setup_migrator();
502 let strategy = FileStorageStrategy::default();
503
504 let storage = FileStorage::new(file_path.clone(), migrator, strategy).unwrap();
505
506 let returned_path = storage.path();
508 assert_eq!(returned_path, file_path.as_path());
509 }
510
511 #[test]
512 fn test_load_empty_file() {
513 let temp_dir = TempDir::new().unwrap();
514 let file_path = temp_dir.path().join("empty.toml");
515
516 std::fs::write(&file_path, "").unwrap();
518
519 let migrator = setup_migrator();
520 let strategy = FileStorageStrategy::default();
521
522 let result = FileStorage::new(file_path, migrator, strategy);
524 assert!(result.is_ok());
525 }
526
527 #[test]
528 fn test_load_whitespace_only_file() {
529 let temp_dir = TempDir::new().unwrap();
530 let file_path = temp_dir.path().join("whitespace.toml");
531
532 std::fs::write(&file_path, " \n\t\n ").unwrap();
534
535 let migrator = setup_migrator();
536 let strategy = FileStorageStrategy::default();
537
538 let result = FileStorage::new(file_path, migrator, strategy);
540 assert!(result.is_ok());
541 }
542
543 #[test]
544 fn test_config_accessors() {
545 let temp_dir = TempDir::new().unwrap();
546 let file_path = temp_dir.path().join("config_access.toml");
547 let migrator = setup_migrator();
548 let strategy = FileStorageStrategy::default();
549
550 let mut storage = FileStorage::new(file_path, migrator, strategy).unwrap();
551
552 let _config = storage.config();
554
555 let _config_mut = storage.config_mut();
557 }
558
559 #[test]
560 fn test_save_creates_parent_directory() {
561 let temp_dir = TempDir::new().unwrap();
562 let file_path = temp_dir
564 .path()
565 .join("subdir")
566 .join("nested")
567 .join("config.toml");
568 let migrator = setup_migrator();
569 let strategy = FileStorageStrategy::new().with_load_behavior(LoadBehavior::CreateIfMissing);
570
571 let storage = FileStorage::new(file_path.clone(), migrator, strategy).unwrap();
572
573 storage.save().unwrap();
575
576 assert!(file_path.exists());
577 assert!(file_path.parent().unwrap().exists());
578 }
579
580 #[test]
581 fn test_cleanup_with_multiple_temp_files() {
582 let temp_dir = TempDir::new().unwrap();
583 let file_path = temp_dir.path().join("cleanup_test.toml");
584 let migrator = setup_migrator();
585 let strategy = FileStorageStrategy::default();
586
587 let fake_tmp1 = temp_dir.path().join(".cleanup_test.toml.tmp.99999");
589 let fake_tmp2 = temp_dir.path().join(".cleanup_test.toml.tmp.88888");
590 std::fs::write(&fake_tmp1, "old temp 1").unwrap();
591 std::fs::write(&fake_tmp2, "old temp 2").unwrap();
592
593 let mut storage = FileStorage::new(file_path.clone(), migrator, strategy).unwrap();
594
595 let entities = vec![TestEntity {
597 name: "cleanup".to_string(),
598 count: 1,
599 }];
600 storage.update_and_save("test", entities).unwrap();
601
602 assert!(!fake_tmp1.exists());
604 assert!(!fake_tmp2.exists());
605 }
606
607 #[test]
608 fn test_save_without_cleanup() {
609 let temp_dir = TempDir::new().unwrap();
610 let file_path = temp_dir.path().join("no_cleanup.toml");
611 let migrator = setup_migrator();
612 let strategy = FileStorageStrategy::new().with_cleanup(false);
613
614 let fake_tmp = temp_dir.path().join(".no_cleanup.toml.tmp.99999");
616 std::fs::write(&fake_tmp, "old temp").unwrap();
617
618 let mut storage = FileStorage::new(file_path.clone(), migrator, strategy).unwrap();
619
620 let entities = vec![TestEntity {
621 name: "no_cleanup".to_string(),
622 count: 1,
623 }];
624 storage.update_and_save("test", entities).unwrap();
625
626 assert!(fake_tmp.exists());
628 }
629
630 #[test]
631 fn test_update_without_save() {
632 let temp_dir = TempDir::new().unwrap();
633 let file_path = temp_dir.path().join("update_no_save.toml");
634 let migrator = setup_migrator();
635 let strategy = FileStorageStrategy::default();
636
637 let mut storage = FileStorage::new(file_path.clone(), migrator, strategy).unwrap();
638
639 let entities = vec![TestEntity {
641 name: "memory_only".to_string(),
642 count: 42,
643 }];
644 storage.update("test", entities).unwrap();
645
646 let loaded: Vec<TestEntity> = storage.query("test").unwrap();
648 assert_eq!(loaded.len(), 1);
649 assert_eq!(loaded[0].name, "memory_only");
650
651 assert!(!file_path.exists());
653 }
654
655 #[test]
656 fn test_atomic_write_config_default() {
657 let config = local_store::AtomicWriteConfig::default();
658 assert_eq!(config.retry_count, 3);
659 assert!(config.cleanup_tmp_files);
660 }
661
662 #[test]
663 fn test_format_strategy_equality() {
664 assert_eq!(FormatStrategy::Toml, FormatStrategy::Toml);
665 assert_eq!(FormatStrategy::Json, FormatStrategy::Json);
666 assert_ne!(FormatStrategy::Toml, FormatStrategy::Json);
667 }
668
669 #[test]
670 fn test_load_behavior_equality() {
671 assert_eq!(LoadBehavior::CreateIfMissing, LoadBehavior::CreateIfMissing);
672 assert_eq!(LoadBehavior::SaveIfMissing, LoadBehavior::SaveIfMissing);
673 assert_eq!(LoadBehavior::ErrorIfMissing, LoadBehavior::ErrorIfMissing);
674 assert_ne!(LoadBehavior::CreateIfMissing, LoadBehavior::ErrorIfMissing);
675 }
676}