version_migrate/lib.rs
1//! # version-migrate
2//!
3//! A library for explicit, type-safe schema versioning and migration.
4//!
5//! ## Example
6//!
7//! ```ignore
8//! use version_migrate::{Versioned, MigratesTo, IntoDomain, Migrator};
9//! use serde::{Serialize, Deserialize};
10//!
11//! // Version 1.0.0
12//! #[derive(Serialize, Deserialize, Versioned)]
13//! #[versioned(version = "1.0.0")]
14//! struct TaskV1_0_0 {
15//! id: String,
16//! title: String,
17//! }
18//!
19//! // Version 1.1.0
20//! #[derive(Serialize, Deserialize, Versioned)]
21//! #[versioned(version = "1.1.0")]
22//! struct TaskV1_1_0 {
23//! id: String,
24//! title: String,
25//! description: Option<String>,
26//! }
27//!
28//! // Domain model
29//! struct TaskEntity {
30//! id: String,
31//! title: String,
32//! description: Option<String>,
33//! }
34//!
35//! impl MigratesTo<TaskV1_1_0> for TaskV1_0_0 {
36//! fn migrate(self) -> TaskV1_1_0 {
37//! TaskV1_1_0 {
38//! id: self.id,
39//! title: self.title,
40//! description: None,
41//! }
42//! }
43//! }
44//!
45//! impl IntoDomain<TaskEntity> for TaskV1_1_0 {
46//! fn into_domain(self) -> TaskEntity {
47//! TaskEntity {
48//! id: self.id,
49//! title: self.title,
50//! description: self.description,
51//! }
52//! }
53//! }
54//! ```
55
56use serde::{Deserialize, Serialize};
57
58pub mod errors;
59mod migrator;
60
61// Re-export the derive macro
62pub use version_migrate_macro::Versioned;
63
64// Re-export error types
65pub use errors::MigrationError;
66
67// Re-export migrator types
68pub use migrator::{MigrationPath, Migrator};
69
70// Re-export async-trait for user convenience
71pub use async_trait::async_trait;
72
73/// A trait for versioned data schemas.
74///
75/// This trait marks a type as representing a specific version of a data schema.
76/// It should be derived using `#[derive(Versioned)]` along with the `#[versioned(version = "x.y.z")]` attribute.
77pub trait Versioned {
78 /// The semantic version of this schema.
79 const VERSION: &'static str;
80}
81
82/// Defines explicit migration logic from one version to another.
83///
84/// Implementing this trait establishes a migration path from `Self` (the source version)
85/// to `T` (the target version).
86pub trait MigratesTo<T: Versioned>: Versioned {
87 /// Migrates from the current version to the target version.
88 fn migrate(self) -> T;
89}
90
91/// Converts a versioned DTO into the application's domain model.
92///
93/// This trait should be implemented on the latest version of a DTO to convert
94/// it into the clean, version-agnostic domain model.
95pub trait IntoDomain<D>: Versioned {
96 /// Converts this versioned data into the domain model.
97 fn into_domain(self) -> D;
98}
99
100/// Async version of `MigratesTo` for migrations requiring I/O operations.
101///
102/// Use this trait when migrations need to perform asynchronous operations
103/// such as database queries or API calls.
104#[async_trait::async_trait]
105pub trait AsyncMigratesTo<T: Versioned>: Versioned + Send {
106 /// Asynchronously migrates from the current version to the target version.
107 ///
108 /// # Errors
109 ///
110 /// Returns `MigrationError` if the migration fails.
111 async fn migrate(self) -> Result<T, MigrationError>;
112}
113
114/// Async version of `IntoDomain` for domain conversions requiring I/O operations.
115///
116/// Use this trait when converting to the domain model requires asynchronous
117/// operations such as fetching additional data from external sources.
118#[async_trait::async_trait]
119pub trait AsyncIntoDomain<D>: Versioned + Send {
120 /// Asynchronously converts this versioned data into the domain model.
121 ///
122 /// # Errors
123 ///
124 /// Returns `MigrationError` if the conversion fails.
125 async fn into_domain(self) -> Result<D, MigrationError>;
126}
127
128/// A wrapper for serialized data that includes explicit version information.
129///
130/// This struct is used for persistence to ensure that the version of the data
131/// is always stored alongside the data itself.
132#[derive(Serialize, Deserialize, Debug, Clone)]
133pub struct VersionedWrapper<T> {
134 /// The semantic version of the data.
135 pub version: String,
136 /// The actual data.
137 pub data: T,
138}
139
140impl<T> VersionedWrapper<T> {
141 /// Creates a new versioned wrapper with the specified version and data.
142 pub fn new(version: String, data: T) -> Self {
143 Self { version, data }
144 }
145}
146
147impl<T: Versioned> VersionedWrapper<T> {
148 /// Creates a wrapper from a versioned value, automatically extracting its version.
149 pub fn from_versioned(data: T) -> Self {
150 Self {
151 version: T::VERSION.to_string(),
152 data,
153 }
154 }
155}
156
157#[cfg(test)]
158mod tests {
159 use super::*;
160
161 #[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]
162 struct TestData {
163 value: String,
164 }
165
166 impl Versioned for TestData {
167 const VERSION: &'static str = "1.0.0";
168 }
169
170 #[test]
171 fn test_versioned_wrapper_from_versioned() {
172 let data = TestData {
173 value: "test".to_string(),
174 };
175 let wrapper = VersionedWrapper::from_versioned(data);
176
177 assert_eq!(wrapper.version, "1.0.0");
178 assert_eq!(wrapper.data.value, "test");
179 }
180
181 #[test]
182 fn test_versioned_wrapper_new() {
183 let data = TestData {
184 value: "manual".to_string(),
185 };
186 let wrapper = VersionedWrapper::new("2.0.0".to_string(), data);
187
188 assert_eq!(wrapper.version, "2.0.0");
189 assert_eq!(wrapper.data.value, "manual");
190 }
191
192 #[test]
193 fn test_versioned_wrapper_serialization() {
194 let data = TestData {
195 value: "serialize_test".to_string(),
196 };
197 let wrapper = VersionedWrapper::from_versioned(data);
198
199 // Serialize
200 let json = serde_json::to_string(&wrapper).expect("Serialization failed");
201
202 // Deserialize
203 let deserialized: VersionedWrapper<TestData> =
204 serde_json::from_str(&json).expect("Deserialization failed");
205
206 assert_eq!(deserialized.version, "1.0.0");
207 assert_eq!(deserialized.data.value, "serialize_test");
208 }
209
210 #[test]
211 fn test_versioned_wrapper_with_complex_data() {
212 #[derive(Serialize, Deserialize, Debug, PartialEq)]
213 struct ComplexData {
214 id: u64,
215 name: String,
216 tags: Vec<String>,
217 metadata: Option<String>,
218 }
219
220 impl Versioned for ComplexData {
221 const VERSION: &'static str = "3.2.1";
222 }
223
224 let data = ComplexData {
225 id: 42,
226 name: "complex".to_string(),
227 tags: vec!["tag1".to_string(), "tag2".to_string()],
228 metadata: Some("meta".to_string()),
229 };
230
231 let wrapper = VersionedWrapper::from_versioned(data);
232 assert_eq!(wrapper.version, "3.2.1");
233 assert_eq!(wrapper.data.id, 42);
234 assert_eq!(wrapper.data.tags.len(), 2);
235 }
236
237 #[test]
238 fn test_versioned_wrapper_clone() {
239 let data = TestData {
240 value: "clone_test".to_string(),
241 };
242 let wrapper = VersionedWrapper::from_versioned(data);
243 let cloned = wrapper.clone();
244
245 assert_eq!(cloned.version, wrapper.version);
246 assert_eq!(cloned.data.value, wrapper.data.value);
247 }
248
249 #[test]
250 fn test_versioned_wrapper_debug() {
251 let data = TestData {
252 value: "debug".to_string(),
253 };
254 let wrapper = VersionedWrapper::from_versioned(data);
255 let debug_str = format!("{:?}", wrapper);
256
257 assert!(debug_str.contains("1.0.0"));
258 assert!(debug_str.contains("debug"));
259 }
260}