1use crate::{AppPaths, MigrationError, Migrator};
9use local_store::{DirStorageStrategy, FormatStrategy};
10use std::path::Path;
11
12pub struct VersionedDirStorage {
30 inner: local_store::DirStorage,
32 migrator: Migrator,
34 strategy: DirStorageStrategy,
36}
37
38impl VersionedDirStorage {
39 pub fn new(
55 paths: AppPaths,
56 category: impl Into<String>,
57 migrator: Migrator,
58 strategy: DirStorageStrategy,
59 ) -> Result<Self, MigrationError> {
60 let inner = local_store::DirStorage::new(paths, category, strategy.clone())
61 .map_err(MigrationError::Store)?;
62 Ok(Self {
63 inner,
64 migrator,
65 strategy,
66 })
67 }
68
69 pub fn save<T>(
80 &self,
81 entity_name: impl Into<String>,
82 id: impl Into<String>,
83 entity: T,
84 ) -> Result<(), MigrationError>
85 where
86 T: serde::Serialize,
87 {
88 let entity_name = entity_name.into();
89 let id = id.into();
90
91 let json_string = self.migrator.save_domain_flat(&entity_name, entity)?;
92
93 let versioned_value: serde_json::Value = serde_json::from_str(&json_string)
94 .map_err(|e| MigrationError::DeserializationError(e.to_string()))?;
95
96 let content = self.serialize_content(&versioned_value)?;
97
98 self.inner
99 .save_raw_string(&entity_name, &id, &content)
100 .map_err(store_err_to_migration)
101 }
102
103 pub fn load<D>(
114 &self,
115 entity_name: impl Into<String>,
116 id: impl Into<String>,
117 ) -> Result<D, MigrationError>
118 where
119 D: serde::de::DeserializeOwned,
120 {
121 let entity_name = entity_name.into();
122 let id = id.into();
123
124 let content = self
125 .inner
126 .load_raw_string(&id)
127 .map_err(store_err_to_migration)?;
128
129 let value = self.deserialize_content(&content)?;
130 self.migrator.load_flat_from(&entity_name, value)
131 }
132
133 pub fn list_ids(&self) -> Result<Vec<String>, MigrationError> {
142 self.inner.list_ids().map_err(store_err_to_migration)
143 }
144
145 pub fn load_all<D>(
154 &self,
155 entity_name: impl Into<String>,
156 ) -> Result<Vec<(String, D)>, MigrationError>
157 where
158 D: serde::de::DeserializeOwned,
159 {
160 let entity_name = entity_name.into();
161 let ids = self.list_ids()?;
162 let mut results = Vec::new();
163 for id in ids {
164 let entity = self.load(&entity_name, &id)?;
165 results.push((id, entity));
166 }
167 Ok(results)
168 }
169
170 pub fn exists(&self, id: impl Into<String>) -> Result<bool, MigrationError> {
176 self.inner.exists(id).map_err(store_err_to_migration)
177 }
178
179 pub fn delete(&self, id: impl Into<String>) -> Result<(), MigrationError> {
187 self.inner.delete(id).map_err(store_err_to_migration)
188 }
189
190 pub fn base_path(&self) -> &Path {
192 self.inner.base_path()
193 }
194
195 fn serialize_content(&self, value: &serde_json::Value) -> Result<String, MigrationError> {
200 match self.strategy.format {
201 FormatStrategy::Json => serde_json::to_string_pretty(value)
202 .map_err(|e| MigrationError::SerializationError(e.to_string())),
203 FormatStrategy::Toml => {
204 let tv = local_store::format_convert::json_to_toml(value).map_err(|e| {
205 MigrationError::Store(local_store::StoreError::FormatConvert(e))
206 })?;
207 toml::to_string_pretty(&tv)
208 .map_err(|e| MigrationError::TomlSerializeError(e.to_string()))
209 }
210 }
211 }
212
213 fn deserialize_content(&self, content: &str) -> Result<serde_json::Value, MigrationError> {
214 match self.strategy.format {
215 FormatStrategy::Json => serde_json::from_str(content)
216 .map_err(|e| MigrationError::DeserializationError(e.to_string())),
217 FormatStrategy::Toml => {
218 let tv: toml::Value = toml::from_str(content)
219 .map_err(|e| MigrationError::TomlParseError(e.to_string()))?;
220 toml_to_json(tv)
221 }
222 }
223 }
224}
225
226fn toml_to_json(toml_value: toml::Value) -> Result<serde_json::Value, MigrationError> {
231 let json_str = serde_json::to_string(&toml_value)
232 .map_err(|e| MigrationError::SerializationError(e.to_string()))?;
233 let json_value: serde_json::Value = serde_json::from_str(&json_str)
234 .map_err(|e| MigrationError::DeserializationError(e.to_string()))?;
235 Ok(json_value)
236}
237
238fn store_err_to_migration(e: local_store::StoreError) -> MigrationError {
242 match e {
243 local_store::StoreError::FilenameEncoding { id, reason } => {
244 MigrationError::FilenameEncoding { id, reason }
245 }
246 other => MigrationError::Store(other),
247 }
248}
249
250#[cfg(feature = "async")]
255pub use async_impl::VersionedAsyncDirStorage;
256
257#[cfg(feature = "async")]
258mod async_impl {
259 use crate::{AppPaths, MigrationError, Migrator};
260 use local_store::DirStorageStrategy;
261 use std::path::Path;
262
263 use super::{store_err_to_migration, toml_to_json, FormatStrategy};
264
265 pub struct VersionedAsyncDirStorage {
276 inner: local_store::AsyncDirStorage,
278 migrator: Migrator,
280 strategy: DirStorageStrategy,
282 }
283
284 impl VersionedAsyncDirStorage {
285 pub async fn new(
295 paths: AppPaths,
296 category: impl Into<String>,
297 migrator: Migrator,
298 strategy: DirStorageStrategy,
299 ) -> Result<Self, MigrationError> {
300 let inner = local_store::AsyncDirStorage::new(paths, category, strategy.clone())
301 .await
302 .map_err(MigrationError::Store)?;
303 Ok(Self {
304 inner,
305 migrator,
306 strategy,
307 })
308 }
309
310 pub async fn save<T>(
320 &self,
321 entity_name: impl Into<String>,
322 id: impl Into<String>,
323 entity: T,
324 ) -> Result<(), MigrationError>
325 where
326 T: serde::Serialize,
327 {
328 let entity_name = entity_name.into();
329 let id = id.into();
330
331 let json_string = self.migrator.save_domain_flat(&entity_name, entity)?;
332
333 let versioned_value: serde_json::Value = serde_json::from_str(&json_string)
334 .map_err(|e| MigrationError::DeserializationError(e.to_string()))?;
335
336 let content = self.serialize_content(&versioned_value)?;
337
338 self.inner
339 .save_raw_string(&entity_name, &id, &content)
340 .await
341 .map_err(store_err_to_migration)
342 }
343
344 pub async fn load<D>(
354 &self,
355 entity_name: impl Into<String>,
356 id: impl Into<String>,
357 ) -> Result<D, MigrationError>
358 where
359 D: serde::de::DeserializeOwned,
360 {
361 let entity_name = entity_name.into();
362 let id = id.into();
363
364 let content = self
365 .inner
366 .load_raw_string(&id)
367 .await
368 .map_err(store_err_to_migration)?;
369
370 let value = self.deserialize_content(&content)?;
371 self.migrator.load_flat_from(&entity_name, value)
372 }
373
374 pub async fn list_ids(&self) -> Result<Vec<String>, MigrationError> {
380 self.inner.list_ids().await.map_err(store_err_to_migration)
381 }
382
383 pub async fn load_all<D>(
389 &self,
390 entity_name: impl Into<String>,
391 ) -> Result<Vec<(String, D)>, MigrationError>
392 where
393 D: serde::de::DeserializeOwned,
394 {
395 let entity_name = entity_name.into();
396 let ids = self.list_ids().await?;
397 let mut results = Vec::new();
398 for id in ids {
399 let entity = self.load(&entity_name, &id).await?;
400 results.push((id, entity));
401 }
402 Ok(results)
403 }
404
405 pub async fn exists(&self, id: impl Into<String>) -> Result<bool, MigrationError> {
411 self.inner.exists(id).await.map_err(store_err_to_migration)
412 }
413
414 pub async fn delete(&self, id: impl Into<String>) -> Result<(), MigrationError> {
422 self.inner.delete(id).await.map_err(store_err_to_migration)
423 }
424
425 pub fn base_path(&self) -> &Path {
427 self.inner.base_path()
428 }
429
430 fn serialize_content(&self, value: &serde_json::Value) -> Result<String, MigrationError> {
435 match self.strategy.format {
436 FormatStrategy::Json => serde_json::to_string_pretty(value)
437 .map_err(|e| MigrationError::SerializationError(e.to_string())),
438 FormatStrategy::Toml => {
439 let tv = local_store::format_convert::json_to_toml(value).map_err(|e| {
440 MigrationError::Store(local_store::StoreError::FormatConvert(e))
441 })?;
442 toml::to_string_pretty(&tv)
443 .map_err(|e| MigrationError::TomlSerializeError(e.to_string()))
444 }
445 }
446 }
447
448 fn deserialize_content(&self, content: &str) -> Result<serde_json::Value, MigrationError> {
449 match self.strategy.format {
450 FormatStrategy::Json => serde_json::from_str(content)
451 .map_err(|e| MigrationError::DeserializationError(e.to_string())),
452 FormatStrategy::Toml => {
453 let tv: toml::Value = toml::from_str(content)
454 .map_err(|e| MigrationError::TomlParseError(e.to_string()))?;
455 toml_to_json(tv)
456 }
457 }
458 }
459 }
460}
461
462#[cfg(test)]
467mod tests {
468 use super::*;
469 use crate::{AppPaths, FromDomain, IntoDomain, MigratesTo, PathStrategy, Versioned};
470 use serde::{Deserialize, Serialize};
471 use tempfile::TempDir;
472
473 #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
474 struct SessionV1_0_0 {
475 id: String,
476 user_id: String,
477 }
478
479 impl Versioned for SessionV1_0_0 {
480 const VERSION: &'static str = "1.0.0";
481 }
482
483 #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
484 struct SessionV1_1_0 {
485 id: String,
486 user_id: String,
487 created_at: Option<String>,
488 }
489
490 impl Versioned for SessionV1_1_0 {
491 const VERSION: &'static str = "1.1.0";
492 }
493
494 impl MigratesTo<SessionV1_1_0> for SessionV1_0_0 {
495 fn migrate(self) -> SessionV1_1_0 {
496 SessionV1_1_0 {
497 id: self.id,
498 user_id: self.user_id,
499 created_at: None,
500 }
501 }
502 }
503
504 #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
505 struct SessionEntity {
506 id: String,
507 user_id: String,
508 created_at: Option<String>,
509 }
510
511 impl IntoDomain<SessionEntity> for SessionV1_1_0 {
512 fn into_domain(self) -> SessionEntity {
513 SessionEntity {
514 id: self.id,
515 user_id: self.user_id,
516 created_at: self.created_at,
517 }
518 }
519 }
520
521 impl FromDomain<SessionEntity> for SessionV1_1_0 {
522 fn from_domain(domain: SessionEntity) -> Self {
523 SessionV1_1_0 {
524 id: domain.id,
525 user_id: domain.user_id,
526 created_at: domain.created_at,
527 }
528 }
529 }
530
531 fn setup_session_migrator() -> Migrator {
532 let path = Migrator::define("session")
533 .from::<SessionV1_0_0>()
534 .step::<SessionV1_1_0>()
535 .into_with_save::<SessionEntity>();
536
537 let mut migrator = Migrator::new();
538 migrator.register(path).unwrap();
540 migrator
541 }
542
543 #[test]
544 fn test_versioned_dir_storage_new_creates_directory() {
545 let temp_dir = TempDir::new().unwrap();
546 let paths = AppPaths::new("testapp")
547 .data_strategy(PathStrategy::CustomBase(temp_dir.path().to_path_buf()));
548
549 let migrator = Migrator::new();
550 let strategy = DirStorageStrategy::default();
551
552 let storage = VersionedDirStorage::new(paths, "sessions", migrator, strategy).unwrap();
553
554 assert!(storage.base_path().exists());
555 assert!(storage.base_path().is_dir());
556 assert!(storage.base_path().ends_with("data/testapp/sessions"));
557 }
558
559 #[test]
560 fn test_versioned_dir_storage_category_into_string() {
561 let temp_dir = TempDir::new().unwrap();
563 let paths = AppPaths::new("testapp")
564 .data_strategy(PathStrategy::CustomBase(temp_dir.path().to_path_buf()));
565
566 let migrator = Migrator::new();
567 let strategy = DirStorageStrategy::default();
568
569 let result = VersionedDirStorage::new(paths.clone(), "sessions", migrator, strategy);
571 assert!(result.is_ok());
572
573 let migrator2 = Migrator::new();
574 let strategy2 = DirStorageStrategy::default();
575 let result2 =
577 VersionedDirStorage::new(paths, String::from("sessions2"), migrator2, strategy2);
578 assert!(result2.is_ok());
579 }
580
581 #[test]
582 fn test_versioned_dir_storage_save_and_load() {
583 let temp_dir = TempDir::new().unwrap();
584 let paths = AppPaths::new("testapp")
585 .data_strategy(PathStrategy::CustomBase(temp_dir.path().to_path_buf()));
586
587 let migrator = setup_session_migrator();
588 let strategy = DirStorageStrategy::default();
589 let storage = VersionedDirStorage::new(paths, "sessions", migrator, strategy).unwrap();
590
591 let session = SessionEntity {
592 id: "session-1".to_string(),
593 user_id: "user-1".to_string(),
594 created_at: Some("2024-01-01".to_string()),
595 };
596
597 storage
598 .save("session", &session.id, session.clone())
599 .unwrap();
600
601 let loaded: SessionEntity = storage.load("session", "session-1").unwrap();
602 assert_eq!(loaded.id, session.id);
603 assert_eq!(loaded.user_id, session.user_id);
604 assert_eq!(loaded.created_at, session.created_at);
605 }
606
607 #[test]
608 fn test_versioned_dir_storage_list_ids() {
609 let temp_dir = TempDir::new().unwrap();
610 let paths = AppPaths::new("testapp")
611 .data_strategy(PathStrategy::CustomBase(temp_dir.path().to_path_buf()));
612
613 let migrator = setup_session_migrator();
614 let strategy = DirStorageStrategy::default();
615 let storage = VersionedDirStorage::new(paths, "sessions", migrator, strategy).unwrap();
616
617 let ids = ["session-c", "session-a", "session-b"];
618 for id in &ids {
619 let session = SessionEntity {
620 id: id.to_string(),
621 user_id: "user".to_string(),
622 created_at: None,
623 };
624 storage.save("session", *id, session).unwrap();
625 }
626
627 let listed = storage.list_ids().unwrap();
628 assert_eq!(listed.len(), 3);
629 assert_eq!(listed, vec!["session-a", "session-b", "session-c"]);
630 }
631
632 #[test]
633 fn test_versioned_dir_storage_load_all() {
634 let temp_dir = TempDir::new().unwrap();
635 let paths = AppPaths::new("testapp")
636 .data_strategy(PathStrategy::CustomBase(temp_dir.path().to_path_buf()));
637
638 let migrator = setup_session_migrator();
639 let strategy = DirStorageStrategy::default();
640 let storage = VersionedDirStorage::new(paths, "sessions", migrator, strategy).unwrap();
641
642 let sessions = vec![
643 SessionEntity {
644 id: "s1".to_string(),
645 user_id: "u1".to_string(),
646 created_at: None,
647 },
648 SessionEntity {
649 id: "s2".to_string(),
650 user_id: "u2".to_string(),
651 created_at: Some("2024-01-01".to_string()),
652 },
653 ];
654
655 for s in &sessions {
656 storage.save("session", &s.id, s.clone()).unwrap();
657 }
658
659 let results: Vec<(String, SessionEntity)> = storage.load_all("session").unwrap();
660 assert_eq!(results.len(), 2);
661 }
662
663 #[test]
664 fn test_versioned_dir_storage_exists_and_delete() {
665 let temp_dir = TempDir::new().unwrap();
666 let paths = AppPaths::new("testapp")
667 .data_strategy(PathStrategy::CustomBase(temp_dir.path().to_path_buf()));
668
669 let migrator = setup_session_migrator();
670 let strategy = DirStorageStrategy::default();
671 let storage = VersionedDirStorage::new(paths, "sessions", migrator, strategy).unwrap();
672
673 let session = SessionEntity {
674 id: "del-session".to_string(),
675 user_id: "u".to_string(),
676 created_at: None,
677 };
678 storage.save("session", "del-session", session).unwrap();
679
680 assert!(storage.exists("del-session").unwrap());
681 storage.delete("del-session").unwrap();
682 assert!(!storage.exists("del-session").unwrap());
683 }
684
685 #[test]
686 fn test_versioned_dir_storage_base_path() {
687 let temp_dir = TempDir::new().unwrap();
688 let paths = AppPaths::new("myapp")
689 .data_strategy(PathStrategy::CustomBase(temp_dir.path().to_path_buf()));
690
691 let migrator = Migrator::new();
692 let strategy = DirStorageStrategy::default();
693 let storage = VersionedDirStorage::new(paths, "entities", migrator, strategy).unwrap();
694
695 assert!(storage.base_path().ends_with("entities"));
696 }
697}