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/// Use this when you need just the path without creating a Migrator instance.
164///
165/// # Syntax
166///
167/// Basic usage:
168/// ```ignore
169/// migrate_path!("entity", [V1, V2, V3])
170/// ```
171///
172/// With custom version/data keys:
173/// ```ignore
174/// migrate_path!("entity", [V1, V2, V3], version_key = "v", data_key = "d")
175/// ```
176///
177/// # Arguments
178///
179/// * `entity` - The entity name as a string literal (e.g., `"user"`, `"task"`)
180/// * `versions` - A list of version types in migration order (e.g., `[V1, V2, V3]`)
181/// * `version_key` - (Optional) Custom key for the version field (default: `"version"`)
182/// * `data_key` - (Optional) Custom key for the data field (default: `"data"`)
183///
184/// # Examples
185///
186/// ```ignore
187/// use version_migrate::{migrate_path, Migrator};
188///
189/// // Simple two-step migration
190/// let path = migrate_path!("task", [TaskV1, TaskV2]);
191///
192/// // Multi-step migration
193/// let path = migrate_path!("task", [TaskV1, TaskV2, TaskV3]);
194///
195/// // Many versions (arbitrary length supported)
196/// let path = migrate_path!("task", [TaskV1, TaskV2, TaskV3, TaskV4, TaskV5, TaskV6]);
197///
198/// // With custom keys
199/// let path = migrate_path!("task", [TaskV1, TaskV2], version_key = "v", data_key = "d");
200///
201/// // Register with migrator
202/// let mut migrator = Migrator::new();
203/// migrator.register(path).unwrap();
204/// ```
205///
206/// # Generated Code
207///
208/// The macro expands to the equivalent builder pattern:
209/// ```ignore
210/// // migrate_path!("entity", [V1, V2])
211/// // expands to:
212/// Migrator::define("entity")
213/// .from::<V1>()
214/// .into::<V2>()
215/// ```
216#[macro_export]
217macro_rules! migrate_path {
218 // Basic: migrate_path!("entity", [V1, V2, V3, ...])
219 ($entity:expr, [$first:ty, $($rest:ty),+ $(,)?]) => {
220 $crate::migrator_vec_helper!($first; $($rest),+; $entity)
221 };
222
223 // With custom keys: migrate_path!("entity", [V1, V2, ...], version_key = "v", data_key = "d")
224 ($entity:expr, [$first:ty, $($rest:ty),+ $(,)?], version_key = $version_key:expr, data_key = $data_key:expr) => {
225 $crate::migrator_vec_helper_with_keys!($first; $($rest),+; $entity; $version_key; $data_key)
226 };
227}
228
229/// Helper macro for Vec notation without custom keys
230#[doc(hidden)]
231#[macro_export]
232macro_rules! migrator_vec_helper {
233 // Base case: two versions left
234 ($first:ty; $last:ty; $entity:expr) => {
235 $crate::Migrator::define($entity)
236 .from::<$first>()
237 .into::<$last>()
238 };
239
240 // Recursive case: more than two versions
241 ($first:ty; $second:ty, $($rest:ty),+; $entity:expr) => {
242 $crate::migrator_vec_build_steps!($first; $($rest),+; $entity; {
243 $crate::Migrator::define($entity).from::<$first>().step::<$second>()
244 })
245 };
246}
247
248/// Helper for building all steps, then applying final .into()
249#[doc(hidden)]
250#[macro_export]
251macro_rules! migrator_vec_build_steps {
252 // Final case: last version, call .into()
253 ($first:ty; $last:ty; $entity:expr; { $builder:expr }) => {
254 $builder.into::<$last>()
255 };
256
257 // Recursive case: add .step() and continue
258 ($first:ty; $current:ty, $($rest:ty),+; $entity:expr; { $builder:expr }) => {
259 $crate::migrator_vec_build_steps!($first; $($rest),+; $entity; {
260 $builder.step::<$current>()
261 })
262 };
263}
264
265/// Helper macro for Vec notation with custom keys
266#[doc(hidden)]
267#[macro_export]
268macro_rules! migrator_vec_helper_with_keys {
269 // Base case: two versions left
270 ($first:ty; $last:ty; $entity:expr; $version_key:expr; $data_key:expr) => {
271 $crate::Migrator::define($entity)
272 .with_keys($version_key, $data_key)
273 .from::<$first>()
274 .into::<$last>()
275 };
276
277 // Recursive case: more than two versions
278 ($first:ty; $second:ty, $($rest:ty),+; $entity:expr; $version_key:expr; $data_key:expr) => {
279 $crate::migrator_vec_build_steps_with_keys!($first; $($rest),+; $entity; $version_key; $data_key; {
280 $crate::Migrator::define($entity).with_keys($version_key, $data_key).from::<$first>().step::<$second>()
281 })
282 };
283}
284
285/// Helper for building all steps with custom keys, then applying final .into()
286#[doc(hidden)]
287#[macro_export]
288macro_rules! migrator_vec_build_steps_with_keys {
289 // Final case: last version, call .into()
290 ($first:ty; $last:ty; $entity:expr; $version_key:expr; $data_key:expr; { $builder:expr }) => {
291 $builder.into::<$last>()
292 };
293
294 // Recursive case: add .step() and continue
295 ($first:ty; $current:ty, $($rest:ty),+; $entity:expr; $version_key:expr; $data_key:expr; { $builder:expr }) => {
296 $crate::migrator_vec_build_steps_with_keys!($first; $($rest),+; $entity; $version_key; $data_key; {
297 $builder.step::<$current>()
298 })
299 };
300}
301
302/// Creates a fully initialized `Migrator` with registered migration paths.
303///
304/// This macro creates a `Migrator` instance and registers one or more migration paths,
305/// returning a ready-to-use migrator. This is the recommended way to create a migrator
306/// as it's more concise than manually calling `Migrator::new()` and `register()` for each path.
307///
308/// # Syntax
309///
310/// Single path:
311/// ```ignore
312/// migrator!("entity" => [V1, V2, V3])
313/// ```
314///
315/// Multiple paths:
316/// ```ignore
317/// migrator!(
318/// "task" => [TaskV1, TaskV2, TaskV3],
319/// "user" => [UserV1, UserV2]
320/// )
321/// ```
322///
323/// Single path with custom keys:
324/// ```ignore
325/// migrator!(
326/// "task" => [TaskV1, TaskV2], version_key = "v", data_key = "d"
327/// )
328/// ```
329///
330/// Multiple paths with custom keys (requires `@keys` prefix):
331/// ```ignore
332/// migrator!(
333/// @keys version_key = "v", data_key = "d";
334/// "task" => [TaskV1, TaskV2],
335/// "user" => [UserV1, UserV2]
336/// )
337/// ```
338///
339/// # Examples
340///
341/// ```ignore
342/// use version_migrate::migrator;
343///
344/// // Single entity migration
345/// let migrator = migrator!("task" => [TaskV1, TaskV2, TaskV3]).unwrap();
346///
347/// // Multiple entities
348/// let migrator = migrator!(
349/// "task" => [TaskV1, TaskV2],
350/// "user" => [UserV1, UserV2]
351/// ).unwrap();
352///
353/// // Single entity with custom keys
354/// let migrator = migrator!(
355/// "task" => [TaskV1, TaskV2], version_key = "v", data_key = "d"
356/// ).unwrap();
357///
358/// // Multiple entities with custom keys
359/// let migrator = migrator!(
360/// @keys version_key = "v", data_key = "d";
361/// "task" => [TaskV1, TaskV2],
362/// "user" => [UserV1, UserV2]
363/// ).unwrap();
364///
365/// // Now ready to use
366/// let domain: TaskEntity = migrator.load("task", json_str)?;
367/// ```
368///
369/// # Returns
370///
371/// Returns `Result<Migrator, MigrationError>`. The migrator is ready to use if `Ok`.
372#[macro_export]
373macro_rules! migrator {
374 // Single path with custom keys (most specific, must come first)
375 ($entity:expr => [$first:ty, $($rest:ty),+ $(,)?], version_key = $version_key:expr, data_key = $data_key:expr) => {{
376 let mut migrator = $crate::Migrator::new();
377 let path = $crate::migrate_path!($entity, [$first, $($rest),+], version_key = $version_key, data_key = $data_key);
378 migrator.register(path).map(|_| migrator)
379 }};
380
381 // Multiple paths with custom keys - use @keys prefix to disambiguate
382 (@keys version_key = $version_key:expr, data_key = $data_key:expr; $($entity:expr => [$first:ty, $($rest:ty),+ $(,)?]),+ $(,)?) => {{
383 let mut migrator = $crate::Migrator::new();
384 $(
385 let path = $crate::migrate_path!($entity, [$first, $($rest),+], version_key = $version_key, data_key = $data_key);
386 migrator.register(path)?;
387 )+
388 Ok::<$crate::Migrator, $crate::MigrationError>(migrator)
389 }};
390
391 // Single path without custom keys
392 ($entity:expr => [$first:ty, $($rest:ty),+ $(,)?]) => {{
393 let mut migrator = $crate::Migrator::new();
394 let path = $crate::migrate_path!($entity, [$first, $($rest),+]);
395 migrator.register(path).map(|_| migrator)
396 }};
397
398 // Multiple paths without custom keys (must come last)
399 ($($entity:expr => [$first:ty, $($rest:ty),+ $(,)?]),+ $(,)?) => {{
400 let mut migrator = $crate::Migrator::new();
401 $(
402 let path = $crate::migrate_path!($entity, [$first, $($rest),+]);
403 migrator.register(path)?;
404 )+
405 Ok::<$crate::Migrator, $crate::MigrationError>(migrator)
406 }};
407}
408
409// Re-export error types
410pub use errors::{IoOperationKind, MigrationError};
411
412// Re-export migrator types
413pub use migrator::{ConfigMigrator, MigrationPath, Migrator};
414
415// Re-export storage types
416pub use storage::{
417 AtomicWriteConfig, FileStorage, FileStorageStrategy, FormatStrategy, LoadBehavior,
418};
419
420// Re-export dir_storage types
421pub use dir_storage::{DirStorage, DirStorageStrategy, FilenameEncoding};
422
423#[cfg(feature = "async")]
424pub use dir_storage::AsyncDirStorage;
425
426// Re-export paths types
427pub use paths::{AppPaths, PathStrategy, PrefPath};
428
429// Re-export async-trait for user convenience
430#[cfg(feature = "async")]
431pub use async_trait::async_trait;
432
433/// A trait for versioned data schemas.
434///
435/// This trait marks a type as representing a specific version of a data schema.
436/// It should be derived using `#[derive(Versioned)]` along with the `#[versioned(version = "x.y.z")]` attribute.
437///
438/// # Custom Keys
439///
440/// You can customize the serialization keys:
441///
442/// ```ignore
443/// #[derive(Versioned)]
444/// #[versioned(
445/// version = "1.0.0",
446/// version_key = "schema_version",
447/// data_key = "payload"
448/// )]
449/// struct Task { ... }
450/// // Serializes to: {"schema_version":"1.0.0","payload":{...}}
451/// ```
452pub trait Versioned {
453 /// The semantic version of this schema.
454 const VERSION: &'static str;
455
456 /// The key name for the version field in serialized data.
457 /// Defaults to "version".
458 const VERSION_KEY: &'static str = "version";
459
460 /// The key name for the data field in serialized data.
461 /// Defaults to "data".
462 const DATA_KEY: &'static str = "data";
463}
464
465/// Defines explicit migration logic from one version to another.
466///
467/// Implementing this trait establishes a migration path from `Self` (the source version)
468/// to `T` (the target version).
469pub trait MigratesTo<T: Versioned>: Versioned {
470 /// Migrates from the current version to the target version.
471 fn migrate(self) -> T;
472}
473
474/// Converts a versioned DTO into the application's domain model.
475///
476/// This trait should be implemented on the latest version of a DTO to convert
477/// it into the clean, version-agnostic domain model.
478pub trait IntoDomain<D>: Versioned {
479 /// Converts this versioned data into the domain model.
480 fn into_domain(self) -> D;
481}
482
483/// Converts a domain model back into a versioned DTO.
484///
485/// This trait should be implemented on versioned DTOs to enable conversion
486/// from the domain model back to the versioned format for serialization.
487///
488/// # Example
489///
490/// ```ignore
491/// impl FromDomain<TaskEntity> for TaskV1_1_0 {
492/// fn from_domain(domain: TaskEntity) -> Self {
493/// TaskV1_1_0 {
494/// id: domain.id,
495/// title: domain.title,
496/// description: domain.description,
497/// }
498/// }
499/// }
500/// ```
501pub trait FromDomain<D>: Versioned + Serialize {
502 /// Converts a domain model into this versioned format.
503 fn from_domain(domain: D) -> Self;
504}
505
506/// Associates a domain entity with its latest versioned representation.
507///
508/// This trait enables automatic saving of domain entities using their latest version.
509/// It should typically be derived using the `#[version_migrate]` attribute macro.
510///
511/// # Example
512///
513/// ```ignore
514/// #[derive(Serialize, Deserialize)]
515/// #[version_migrate(entity = "task", latest = TaskV1_1_0)]
516/// struct TaskEntity {
517/// id: String,
518/// title: String,
519/// description: Option<String>,
520/// }
521///
522/// // Now you can save entities directly
523/// let entity = TaskEntity { ... };
524/// let json = migrator.save_entity(entity)?;
525/// ```
526pub trait LatestVersioned: Sized {
527 /// The latest versioned type for this entity.
528 type Latest: Versioned + Serialize + FromDomain<Self>;
529
530 /// The entity name used for migration paths.
531 const ENTITY_NAME: &'static str;
532
533 /// Whether this entity supports saving functionality.
534 /// When `false` (default), uses `into()` for read-only access.
535 /// When `true`, uses `into_with_save()` to enable domain entity saving.
536 const SAVE: bool = false;
537
538 /// Converts this domain entity into its latest versioned format.
539 fn to_latest(self) -> Self::Latest {
540 Self::Latest::from_domain(self)
541 }
542}
543
544/// Marks a domain type as queryable, associating it with an entity name.
545///
546/// This trait enables `ConfigMigrator` to automatically determine which entity
547/// path to use when querying or updating data.
548///
549/// # Example
550///
551/// ```ignore
552/// impl Queryable for TaskEntity {
553/// const ENTITY_NAME: &'static str = "task";
554/// }
555///
556/// let tasks: Vec<TaskEntity> = config.query("tasks")?;
557/// ```
558pub trait Queryable {
559 /// The entity name used to look up migration paths in the `Migrator`.
560 const ENTITY_NAME: &'static str;
561}
562
563/// Async version of `MigratesTo` for migrations requiring I/O operations.
564///
565/// Use this trait when migrations need to perform asynchronous operations
566/// such as database queries or API calls.
567#[cfg(feature = "async")]
568#[async_trait::async_trait]
569pub trait AsyncMigratesTo<T: Versioned>: Versioned + Send {
570 /// Asynchronously migrates from the current version to the target version.
571 ///
572 /// # Errors
573 ///
574 /// Returns `MigrationError` if the migration fails.
575 async fn migrate(self) -> Result<T, MigrationError>;
576}
577
578/// Async version of `IntoDomain` for domain conversions requiring I/O operations.
579///
580/// Use this trait when converting to the domain model requires asynchronous
581/// operations such as fetching additional data from external sources.
582#[cfg(feature = "async")]
583#[async_trait::async_trait]
584pub trait AsyncIntoDomain<D>: Versioned + Send {
585 /// Asynchronously converts this versioned data into the domain model.
586 ///
587 /// # Errors
588 ///
589 /// Returns `MigrationError` if the conversion fails.
590 async fn into_domain(self) -> Result<D, MigrationError>;
591}
592
593/// A wrapper for serialized data that includes explicit version information.
594///
595/// This struct is used for persistence to ensure that the version of the data
596/// is always stored alongside the data itself.
597#[derive(Serialize, Deserialize, Debug, Clone)]
598pub struct VersionedWrapper<T> {
599 /// The semantic version of the data.
600 pub version: String,
601 /// The actual data.
602 pub data: T,
603}
604
605impl<T> VersionedWrapper<T> {
606 /// Creates a new versioned wrapper with the specified version and data.
607 pub fn new(version: String, data: T) -> Self {
608 Self { version, data }
609 }
610}
611
612impl<T: Versioned> VersionedWrapper<T> {
613 /// Creates a wrapper from a versioned value, automatically extracting its version.
614 pub fn from_versioned(data: T) -> Self {
615 Self {
616 version: T::VERSION.to_string(),
617 data,
618 }
619 }
620}
621
622#[cfg(test)]
623mod tests {
624 use super::*;
625
626 #[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]
627 struct TestData {
628 value: String,
629 }
630
631 impl Versioned for TestData {
632 const VERSION: &'static str = "1.0.0";
633 }
634
635 #[test]
636 fn test_versioned_wrapper_from_versioned() {
637 let data = TestData {
638 value: "test".to_string(),
639 };
640 let wrapper = VersionedWrapper::from_versioned(data);
641
642 assert_eq!(wrapper.version, "1.0.0");
643 assert_eq!(wrapper.data.value, "test");
644 }
645
646 #[test]
647 fn test_versioned_wrapper_new() {
648 let data = TestData {
649 value: "manual".to_string(),
650 };
651 let wrapper = VersionedWrapper::new("2.0.0".to_string(), data);
652
653 assert_eq!(wrapper.version, "2.0.0");
654 assert_eq!(wrapper.data.value, "manual");
655 }
656
657 #[test]
658 fn test_versioned_wrapper_serialization() {
659 let data = TestData {
660 value: "serialize_test".to_string(),
661 };
662 let wrapper = VersionedWrapper::from_versioned(data);
663
664 // Serialize
665 let json = serde_json::to_string(&wrapper).expect("Serialization failed");
666
667 // Deserialize
668 let deserialized: VersionedWrapper<TestData> =
669 serde_json::from_str(&json).expect("Deserialization failed");
670
671 assert_eq!(deserialized.version, "1.0.0");
672 assert_eq!(deserialized.data.value, "serialize_test");
673 }
674
675 #[test]
676 fn test_versioned_wrapper_with_complex_data() {
677 #[derive(Serialize, Deserialize, Debug, PartialEq)]
678 struct ComplexData {
679 id: u64,
680 name: String,
681 tags: Vec<String>,
682 metadata: Option<String>,
683 }
684
685 impl Versioned for ComplexData {
686 const VERSION: &'static str = "3.2.1";
687 }
688
689 let data = ComplexData {
690 id: 42,
691 name: "complex".to_string(),
692 tags: vec!["tag1".to_string(), "tag2".to_string()],
693 metadata: Some("meta".to_string()),
694 };
695
696 let wrapper = VersionedWrapper::from_versioned(data);
697 assert_eq!(wrapper.version, "3.2.1");
698 assert_eq!(wrapper.data.id, 42);
699 assert_eq!(wrapper.data.tags.len(), 2);
700 }
701
702 #[test]
703 fn test_versioned_wrapper_clone() {
704 let data = TestData {
705 value: "clone_test".to_string(),
706 };
707 let wrapper = VersionedWrapper::from_versioned(data);
708 let cloned = wrapper.clone();
709
710 assert_eq!(cloned.version, wrapper.version);
711 assert_eq!(cloned.data.value, wrapper.data.value);
712 }
713
714 #[test]
715 fn test_versioned_wrapper_debug() {
716 let data = TestData {
717 value: "debug".to_string(),
718 };
719 let wrapper = VersionedWrapper::from_versioned(data);
720 let debug_str = format!("{:?}", wrapper);
721
722 assert!(debug_str.contains("1.0.0"));
723 assert!(debug_str.contains("debug"));
724 }
725}