version_migrate/
lib.rs

1//! # version-migrate
2//!
3//! A library for explicit, type-safe schema versioning and migration.
4//!
5//! ## Features
6//!
7//! - **Type-safe migrations**: Define migrations between versions using traits
8//! - **Validation**: Automatic validation of migration paths (circular path detection, version ordering)
9//! - **Multi-format support**: Load from JSON, TOML, YAML, or any serde-compatible format
10//! - **Legacy data support**: Automatic fallback for data without version information
11//! - **Vec support**: Migrate collections of versioned entities
12//! - **Hierarchical structures**: Support for nested versioned entities
13//! - **Async migrations**: Optional async support for I/O-heavy migrations
14//!
15//! ## Basic Example
16//!
17//! ```ignore
18//! use version_migrate::{Versioned, MigratesTo, IntoDomain, Migrator};
19//! use serde::{Serialize, Deserialize};
20//!
21//! // Version 1.0.0
22//! #[derive(Serialize, Deserialize, Versioned)]
23//! #[versioned(version = "1.0.0")]
24//! struct TaskV1_0_0 {
25//!     id: String,
26//!     title: String,
27//! }
28//!
29//! // Version 1.1.0
30//! #[derive(Serialize, Deserialize, Versioned)]
31//! #[versioned(version = "1.1.0")]
32//! struct TaskV1_1_0 {
33//!     id: String,
34//!     title: String,
35//!     description: Option<String>,
36//! }
37//!
38//! // Domain model
39//! struct TaskEntity {
40//!     id: String,
41//!     title: String,
42//!     description: Option<String>,
43//! }
44//!
45//! impl MigratesTo<TaskV1_1_0> for TaskV1_0_0 {
46//!     fn migrate(self) -> TaskV1_1_0 {
47//!         TaskV1_1_0 {
48//!             id: self.id,
49//!             title: self.title,
50//!             description: None,
51//!         }
52//!     }
53//! }
54//!
55//! impl IntoDomain<TaskEntity> for TaskV1_1_0 {
56//!     fn into_domain(self) -> TaskEntity {
57//!         TaskEntity {
58//!             id: self.id,
59//!             title: self.title,
60//!             description: self.description,
61//!         }
62//!     }
63//! }
64//! ```
65//!
66//! ## Working with Collections (Vec)
67//!
68//! ```ignore
69//! // Save multiple versioned entities
70//! let tasks = vec![
71//!     TaskV1_0_0 { id: "1".into(), title: "Task 1".into() },
72//!     TaskV1_0_0 { id: "2".into(), title: "Task 2".into() },
73//! ];
74//! let json = migrator.save_vec(tasks)?;
75//!
76//! // Load and migrate multiple entities
77//! let domains: Vec<TaskEntity> = migrator.load_vec("task", &json)?;
78//! ```
79//!
80//! ## Legacy Data Support
81//!
82//! Handle data that was created before versioning was introduced:
83//!
84//! ```ignore
85//! // Legacy data without version information
86//! let legacy_json = r#"{"id": "task-1", "title": "Legacy Task"}"#;
87//!
88//! // Automatically treats legacy data as the first version and migrates
89//! let domain: TaskEntity = migrator.load_with_fallback("task", legacy_json)?;
90//!
91//! // Also works with properly versioned data
92//! let versioned_json = r#"{"version":"1.0.0","data":{"id":"task-1","title":"My Task"}}"#;
93//! let domain: TaskEntity = migrator.load_with_fallback("task", versioned_json)?;
94//! ```
95//!
96//! ## Hierarchical Structures
97//!
98//! For complex configurations with nested versioned entities:
99//!
100//! ```ignore
101//! #[derive(Serialize, Deserialize, Versioned)]
102//! #[versioned(version = "1.0.0")]
103//! struct ConfigV1 {
104//!     setting: SettingV1,
105//!     items: Vec<ItemV1>,
106//! }
107//!
108//! #[derive(Serialize, Deserialize, Versioned)]
109//! #[versioned(version = "2.0.0")]
110//! struct ConfigV2 {
111//!     setting: SettingV2,
112//!     items: Vec<ItemV2>,
113//! }
114//!
115//! impl MigratesTo<ConfigV2> for ConfigV1 {
116//!     fn migrate(self) -> ConfigV2 {
117//!         ConfigV2 {
118//!             // Migrate nested entities
119//!             setting: self.setting.migrate(),
120//!             items: self.items.into_iter()
121//!                 .map(|item| item.migrate())
122//!                 .collect(),
123//!         }
124//!     }
125//! }
126//! ```
127//!
128//! ## Design Philosophy
129//!
130//! This library follows the **explicit versioning** approach:
131//!
132//! - Each version has its own type (V1, V2, V3, etc.)
133//! - Migration logic is explicit and testable
134//! - Version changes are tracked in code
135//! - Root-level versioning ensures consistency
136//!
137//! This differs from ProtoBuf's "append-only" approach but allows for:
138//! - Schema refactoring and cleanup
139//! - Type-safe migration paths
140//! - Clear version history in code
141
142use serde::{Deserialize, Serialize};
143
144pub mod dir_storage;
145pub mod errors;
146mod migrator;
147pub mod paths;
148pub mod storage;
149
150// Re-export the derive macros
151pub use version_migrate_macro::Versioned;
152
153// Re-export Queryable derive macro (same name as trait is OK in Rust)
154#[doc(inline)]
155pub use version_migrate_macro::Queryable as DeriveQueryable;
156
157// Re-export VersionMigrate derive macro
158#[doc(inline)]
159pub use version_migrate_macro::VersionMigrate;
160/// Creates a migration path with simplified syntax.
161///
162/// This macro provides a concise way to define migration paths between versioned types.
163///
164/// # Syntax
165///
166/// Basic usage:
167/// ```ignore
168/// migrator!("entity", [V1, V2, V3])
169/// ```
170///
171/// With custom version/data keys:
172/// ```ignore
173/// migrator!("entity", [V1, V2, V3], version_key = "v", data_key = "d")
174/// ```
175///
176/// # Arguments
177///
178/// * `entity` - The entity name as a string literal (e.g., `"user"`, `"task"`)
179/// * `versions` - A list of version types in migration order (e.g., `[V1, V2, V3]`)
180/// * `version_key` - (Optional) Custom key for the version field (default: `"version"`)
181/// * `data_key` - (Optional) Custom key for the data field (default: `"data"`)
182///
183/// # Examples
184///
185/// ```ignore
186/// use version_migrate::{migrator, Migrator};
187///
188/// // Simple two-step migration
189/// let path = migrator!("task", [TaskV1, TaskV2]);
190///
191/// // Multi-step migration
192/// let path = migrator!("task", [TaskV1, TaskV2, TaskV3]);
193///
194/// // Many versions (arbitrary length supported)
195/// let path = migrator!("task", [TaskV1, TaskV2, TaskV3, TaskV4, TaskV5, TaskV6]);
196///
197/// // With custom keys
198/// let path = migrator!("task", [TaskV1, TaskV2], version_key = "v", data_key = "d");
199///
200/// // Register with migrator
201/// let mut migrator = Migrator::new();
202/// migrator.register(path).unwrap();
203/// ```
204///
205/// # Generated Code
206///
207/// The macro expands to the equivalent builder pattern:
208/// ```ignore
209/// // migrator!("entity", [V1, V2])
210/// // expands to:
211/// Migrator::define("entity")
212///     .from::<V1>()
213///     .into::<V2>()
214/// ```
215#[macro_export]
216macro_rules! migrator {
217    // Basic: migrator!("entity", [V1, V2, V3, ...])
218    ($entity:expr, [$first:ty, $($rest:ty),+ $(,)?]) => {
219        $crate::migrator_vec_helper!($first; $($rest),+; $entity)
220    };
221
222    // With custom keys: migrator!("entity", [V1, V2, ...], version_key = "v", data_key = "d")
223    ($entity:expr, [$first:ty, $($rest:ty),+ $(,)?], version_key = $version_key:expr, data_key = $data_key:expr) => {
224        $crate::migrator_vec_helper_with_keys!($first; $($rest),+; $entity; $version_key; $data_key)
225    };
226}
227
228/// Helper macro for Vec notation without custom keys
229#[doc(hidden)]
230#[macro_export]
231macro_rules! migrator_vec_helper {
232    // Base case: two versions left
233    ($first:ty; $last:ty; $entity:expr) => {
234        $crate::Migrator::define($entity)
235            .from::<$first>()
236            .into::<$last>()
237    };
238
239    // Recursive case: more than two versions
240    ($first:ty; $second:ty, $($rest:ty),+; $entity:expr) => {
241        $crate::migrator_vec_build_steps!($first; $($rest),+; $entity; {
242            $crate::Migrator::define($entity).from::<$first>().step::<$second>()
243        })
244    };
245}
246
247/// Helper for building all steps, then applying final .into()
248#[doc(hidden)]
249#[macro_export]
250macro_rules! migrator_vec_build_steps {
251    // Final case: last version, call .into()
252    ($first:ty; $last:ty; $entity:expr; { $builder:expr }) => {
253        $builder.into::<$last>()
254    };
255
256    // Recursive case: add .step() and continue
257    ($first:ty; $current:ty, $($rest:ty),+; $entity:expr; { $builder:expr }) => {
258        $crate::migrator_vec_build_steps!($first; $($rest),+; $entity; {
259            $builder.step::<$current>()
260        })
261    };
262}
263
264/// Helper macro for Vec notation with custom keys
265#[doc(hidden)]
266#[macro_export]
267macro_rules! migrator_vec_helper_with_keys {
268    // Base case: two versions left
269    ($first:ty; $last:ty; $entity:expr; $version_key:expr; $data_key:expr) => {
270        $crate::Migrator::define($entity)
271            .with_keys($version_key, $data_key)
272            .from::<$first>()
273            .into::<$last>()
274    };
275
276    // Recursive case: more than two versions
277    ($first:ty; $second:ty, $($rest:ty),+; $entity:expr; $version_key:expr; $data_key:expr) => {
278        $crate::migrator_vec_build_steps_with_keys!($first; $($rest),+; $entity; $version_key; $data_key; {
279            $crate::Migrator::define($entity).with_keys($version_key, $data_key).from::<$first>().step::<$second>()
280        })
281    };
282}
283
284/// Helper for building all steps with custom keys, then applying final .into()
285#[doc(hidden)]
286#[macro_export]
287macro_rules! migrator_vec_build_steps_with_keys {
288    // Final case: last version, call .into()
289    ($first:ty; $last:ty; $entity:expr; $version_key:expr; $data_key:expr; { $builder:expr }) => {
290        $builder.into::<$last>()
291    };
292
293    // Recursive case: add .step() and continue
294    ($first:ty; $current:ty, $($rest:ty),+; $entity:expr; $version_key:expr; $data_key:expr; { $builder:expr }) => {
295        $crate::migrator_vec_build_steps_with_keys!($first; $($rest),+; $entity; $version_key; $data_key; {
296            $builder.step::<$current>()
297        })
298    };
299}
300
301// Re-export error types
302pub use errors::{IoOperationKind, MigrationError};
303
304// Re-export migrator types
305pub use migrator::{ConfigMigrator, MigrationPath, Migrator};
306
307// Re-export storage types
308pub use storage::{
309    AtomicWriteConfig, FileStorage, FileStorageStrategy, FormatStrategy, LoadBehavior,
310};
311
312// Re-export dir_storage types
313pub use dir_storage::{DirStorage, DirStorageStrategy, FilenameEncoding};
314
315#[cfg(feature = "async")]
316pub use dir_storage::AsyncDirStorage;
317
318// Re-export paths types
319pub use paths::{AppPaths, PathStrategy, PrefPath};
320
321// Re-export async-trait for user convenience
322#[cfg(feature = "async")]
323pub use async_trait::async_trait;
324
325/// A trait for versioned data schemas.
326///
327/// This trait marks a type as representing a specific version of a data schema.
328/// It should be derived using `#[derive(Versioned)]` along with the `#[versioned(version = "x.y.z")]` attribute.
329///
330/// # Custom Keys
331///
332/// You can customize the serialization keys:
333///
334/// ```ignore
335/// #[derive(Versioned)]
336/// #[versioned(
337///     version = "1.0.0",
338///     version_key = "schema_version",
339///     data_key = "payload"
340/// )]
341/// struct Task { ... }
342/// // Serializes to: {"schema_version":"1.0.0","payload":{...}}
343/// ```
344pub trait Versioned {
345    /// The semantic version of this schema.
346    const VERSION: &'static str;
347
348    /// The key name for the version field in serialized data.
349    /// Defaults to "version".
350    const VERSION_KEY: &'static str = "version";
351
352    /// The key name for the data field in serialized data.
353    /// Defaults to "data".
354    const DATA_KEY: &'static str = "data";
355}
356
357/// Defines explicit migration logic from one version to another.
358///
359/// Implementing this trait establishes a migration path from `Self` (the source version)
360/// to `T` (the target version).
361pub trait MigratesTo<T: Versioned>: Versioned {
362    /// Migrates from the current version to the target version.
363    fn migrate(self) -> T;
364}
365
366/// Converts a versioned DTO into the application's domain model.
367///
368/// This trait should be implemented on the latest version of a DTO to convert
369/// it into the clean, version-agnostic domain model.
370pub trait IntoDomain<D>: Versioned {
371    /// Converts this versioned data into the domain model.
372    fn into_domain(self) -> D;
373}
374
375/// Converts a domain model back into a versioned DTO.
376///
377/// This trait should be implemented on versioned DTOs to enable conversion
378/// from the domain model back to the versioned format for serialization.
379///
380/// # Example
381///
382/// ```ignore
383/// impl FromDomain<TaskEntity> for TaskV1_1_0 {
384///     fn from_domain(domain: TaskEntity) -> Self {
385///         TaskV1_1_0 {
386///             id: domain.id,
387///             title: domain.title,
388///             description: domain.description,
389///         }
390///     }
391/// }
392/// ```
393pub trait FromDomain<D>: Versioned + Serialize {
394    /// Converts a domain model into this versioned format.
395    fn from_domain(domain: D) -> Self;
396}
397
398/// Associates a domain entity with its latest versioned representation.
399///
400/// This trait enables automatic saving of domain entities using their latest version.
401/// It should typically be derived using the `#[version_migrate]` attribute macro.
402///
403/// # Example
404///
405/// ```ignore
406/// #[derive(Serialize, Deserialize)]
407/// #[version_migrate(entity = "task", latest = TaskV1_1_0)]
408/// struct TaskEntity {
409///     id: String,
410///     title: String,
411///     description: Option<String>,
412/// }
413///
414/// // Now you can save entities directly
415/// let entity = TaskEntity { ... };
416/// let json = migrator.save_entity(entity)?;
417/// ```
418pub trait LatestVersioned: Sized {
419    /// The latest versioned type for this entity.
420    type Latest: Versioned + Serialize + FromDomain<Self>;
421
422    /// The entity name used for migration paths.
423    const ENTITY_NAME: &'static str;
424
425    /// Converts this domain entity into its latest versioned format.
426    fn to_latest(self) -> Self::Latest {
427        Self::Latest::from_domain(self)
428    }
429}
430
431/// Marks a domain type as queryable, associating it with an entity name.
432///
433/// This trait enables `ConfigMigrator` to automatically determine which entity
434/// path to use when querying or updating data.
435///
436/// # Example
437///
438/// ```ignore
439/// impl Queryable for TaskEntity {
440///     const ENTITY_NAME: &'static str = "task";
441/// }
442///
443/// let tasks: Vec<TaskEntity> = config.query("tasks")?;
444/// ```
445pub trait Queryable {
446    /// The entity name used to look up migration paths in the `Migrator`.
447    const ENTITY_NAME: &'static str;
448}
449
450/// Async version of `MigratesTo` for migrations requiring I/O operations.
451///
452/// Use this trait when migrations need to perform asynchronous operations
453/// such as database queries or API calls.
454#[cfg(feature = "async")]
455#[async_trait::async_trait]
456pub trait AsyncMigratesTo<T: Versioned>: Versioned + Send {
457    /// Asynchronously migrates from the current version to the target version.
458    ///
459    /// # Errors
460    ///
461    /// Returns `MigrationError` if the migration fails.
462    async fn migrate(self) -> Result<T, MigrationError>;
463}
464
465/// Async version of `IntoDomain` for domain conversions requiring I/O operations.
466///
467/// Use this trait when converting to the domain model requires asynchronous
468/// operations such as fetching additional data from external sources.
469#[cfg(feature = "async")]
470#[async_trait::async_trait]
471pub trait AsyncIntoDomain<D>: Versioned + Send {
472    /// Asynchronously converts this versioned data into the domain model.
473    ///
474    /// # Errors
475    ///
476    /// Returns `MigrationError` if the conversion fails.
477    async fn into_domain(self) -> Result<D, MigrationError>;
478}
479
480/// A wrapper for serialized data that includes explicit version information.
481///
482/// This struct is used for persistence to ensure that the version of the data
483/// is always stored alongside the data itself.
484#[derive(Serialize, Deserialize, Debug, Clone)]
485pub struct VersionedWrapper<T> {
486    /// The semantic version of the data.
487    pub version: String,
488    /// The actual data.
489    pub data: T,
490}
491
492impl<T> VersionedWrapper<T> {
493    /// Creates a new versioned wrapper with the specified version and data.
494    pub fn new(version: String, data: T) -> Self {
495        Self { version, data }
496    }
497}
498
499impl<T: Versioned> VersionedWrapper<T> {
500    /// Creates a wrapper from a versioned value, automatically extracting its version.
501    pub fn from_versioned(data: T) -> Self {
502        Self {
503            version: T::VERSION.to_string(),
504            data,
505        }
506    }
507}
508
509#[cfg(test)]
510mod tests {
511    use super::*;
512
513    #[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]
514    struct TestData {
515        value: String,
516    }
517
518    impl Versioned for TestData {
519        const VERSION: &'static str = "1.0.0";
520    }
521
522    #[test]
523    fn test_versioned_wrapper_from_versioned() {
524        let data = TestData {
525            value: "test".to_string(),
526        };
527        let wrapper = VersionedWrapper::from_versioned(data);
528
529        assert_eq!(wrapper.version, "1.0.0");
530        assert_eq!(wrapper.data.value, "test");
531    }
532
533    #[test]
534    fn test_versioned_wrapper_new() {
535        let data = TestData {
536            value: "manual".to_string(),
537        };
538        let wrapper = VersionedWrapper::new("2.0.0".to_string(), data);
539
540        assert_eq!(wrapper.version, "2.0.0");
541        assert_eq!(wrapper.data.value, "manual");
542    }
543
544    #[test]
545    fn test_versioned_wrapper_serialization() {
546        let data = TestData {
547            value: "serialize_test".to_string(),
548        };
549        let wrapper = VersionedWrapper::from_versioned(data);
550
551        // Serialize
552        let json = serde_json::to_string(&wrapper).expect("Serialization failed");
553
554        // Deserialize
555        let deserialized: VersionedWrapper<TestData> =
556            serde_json::from_str(&json).expect("Deserialization failed");
557
558        assert_eq!(deserialized.version, "1.0.0");
559        assert_eq!(deserialized.data.value, "serialize_test");
560    }
561
562    #[test]
563    fn test_versioned_wrapper_with_complex_data() {
564        #[derive(Serialize, Deserialize, Debug, PartialEq)]
565        struct ComplexData {
566            id: u64,
567            name: String,
568            tags: Vec<String>,
569            metadata: Option<String>,
570        }
571
572        impl Versioned for ComplexData {
573            const VERSION: &'static str = "3.2.1";
574        }
575
576        let data = ComplexData {
577            id: 42,
578            name: "complex".to_string(),
579            tags: vec!["tag1".to_string(), "tag2".to_string()],
580            metadata: Some("meta".to_string()),
581        };
582
583        let wrapper = VersionedWrapper::from_versioned(data);
584        assert_eq!(wrapper.version, "3.2.1");
585        assert_eq!(wrapper.data.id, 42);
586        assert_eq!(wrapper.data.tags.len(), 2);
587    }
588
589    #[test]
590    fn test_versioned_wrapper_clone() {
591        let data = TestData {
592            value: "clone_test".to_string(),
593        };
594        let wrapper = VersionedWrapper::from_versioned(data);
595        let cloned = wrapper.clone();
596
597        assert_eq!(cloned.version, wrapper.version);
598        assert_eq!(cloned.data.value, wrapper.data.value);
599    }
600
601    #[test]
602    fn test_versioned_wrapper_debug() {
603        let data = TestData {
604            value: "debug".to_string(),
605        };
606        let wrapper = VersionedWrapper::from_versioned(data);
607        let debug_str = format!("{:?}", wrapper);
608
609        assert!(debug_str.contains("1.0.0"));
610        assert!(debug_str.contains("debug"));
611    }
612}