Skip to main content

version_migrate/
forward.rs

1//! Forward compatibility support for loading future/unknown versions.
2//!
3//! This module provides types and utilities for handling data from versions
4//! that don't exist in the current codebase yet.
5//!
6//! # ⚠️ Requirements
7//!
8//! Forward compatibility assumes **additive-only schema changes**:
9//!
10//! - ✅ Field additions (V2 has fields V1 doesn't) → OK
11//! - ❌ Field deletions (V1 has fields V2 doesn't) → Deserialization error
12//! - ❌ Field type changes → Data corruption
13//! - ❌ Field semantic changes (same name, different meaning) → Logic bugs
14//!
15//! If your schema has breaking changes, define a proper migration path instead.
16
17use serde::{Deserialize, Serialize};
18use std::ops::{Deref, DerefMut};
19
20/// Default key name for the version field in serialized data.
21pub(crate) const DEFAULT_VERSION_KEY: &str = "version";
22
23/// Default key name for the data field in serialized data.
24pub(crate) const DEFAULT_DATA_KEY: &str = "data";
25
26/// Context for forward compatibility operations.
27///
28/// Stores information about the original data that may be lost during
29/// lossy deserialization to an older schema version.
30#[derive(Debug, Clone)]
31pub struct ForwardContext {
32    /// The original version string from the data
33    pub(crate) original_version: String,
34    /// Fields that were present in the data but not in the target type
35    pub(crate) unknown_fields: serde_json::Map<String, serde_json::Value>,
36    /// Whether the load was lossy (unknown version)
37    pub(crate) was_lossy: bool,
38    /// The version key used in serialization
39    pub(crate) version_key: String,
40    /// The data key used in serialization (for wrapped format)
41    pub(crate) data_key: String,
42    /// Whether the original format was flat
43    pub(crate) was_flat: bool,
44}
45
46impl ForwardContext {
47    /// Creates a new ForwardContext.
48    pub(crate) fn new(
49        original_version: String,
50        unknown_fields: serde_json::Map<String, serde_json::Value>,
51        was_lossy: bool,
52        version_key: String,
53        data_key: String,
54        was_flat: bool,
55    ) -> Self {
56        Self {
57            original_version,
58            unknown_fields,
59            was_lossy,
60            version_key,
61            data_key,
62            was_flat,
63        }
64    }
65
66    /// Returns the original version of the data.
67    pub fn original_version(&self) -> &str {
68        &self.original_version
69    }
70
71    /// Returns true if the load was lossy (unknown version).
72    pub fn was_lossy(&self) -> bool {
73        self.was_lossy
74    }
75
76    /// Returns the unknown fields that were preserved.
77    pub fn unknown_fields(&self) -> &serde_json::Map<String, serde_json::Value> {
78        &self.unknown_fields
79    }
80}
81
82/// A wrapper that holds domain data along with forward compatibility context.
83///
84/// This type preserves information from unknown versions so that when saved,
85/// the data can be written back with minimal information loss.
86///
87/// # Usage
88///
89/// ```ignore
90/// // Load with forward compatibility
91/// let mut task: Forwardable<TaskEntity> = migrator.load_forward("task", json)?;
92///
93/// // Access inner data (Deref makes this transparent)
94/// task.title = "updated".to_string();
95///
96/// // Check if it was a lossy load
97/// if task.was_lossy() {
98///     warn!("Loaded from unknown version: {}", task.original_version());
99/// }
100///
101/// // Save preserving unknown fields and original version
102/// let json = migrator.save_forward(&task)?;
103/// ```
104#[derive(Debug, Clone)]
105pub struct Forwardable<T> {
106    /// The inner domain data.
107    pub inner: T,
108    /// Context for preserving forward compatibility information.
109    ctx: ForwardContext,
110}
111
112impl<T> Forwardable<T> {
113    /// Creates a new Forwardable wrapper.
114    pub(crate) fn new(inner: T, ctx: ForwardContext) -> Self {
115        Self { inner, ctx }
116    }
117
118    /// Returns the original version of the data.
119    pub fn original_version(&self) -> &str {
120        self.ctx.original_version()
121    }
122
123    /// Returns true if the load was lossy (unknown version).
124    pub fn was_lossy(&self) -> bool {
125        self.ctx.was_lossy()
126    }
127
128    /// Returns the unknown fields that were preserved.
129    pub fn unknown_fields(&self) -> &serde_json::Map<String, serde_json::Value> {
130        self.ctx.unknown_fields()
131    }
132
133    /// Returns a reference to the forward context.
134    pub fn context(&self) -> &ForwardContext {
135        &self.ctx
136    }
137
138    /// Consumes the wrapper and returns the inner value.
139    pub fn into_inner(self) -> T {
140        self.inner
141    }
142}
143
144impl<T> Deref for Forwardable<T> {
145    type Target = T;
146
147    fn deref(&self) -> &Self::Target {
148        &self.inner
149    }
150}
151
152impl<T> DerefMut for Forwardable<T> {
153    fn deref_mut(&mut self) -> &mut Self::Target {
154        &mut self.inner
155    }
156}
157
158impl<T: Serialize> Serialize for Forwardable<T> {
159    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
160    where
161        S: serde::Serializer,
162    {
163        // Delegate to inner for normal serialization
164        // save_forward handles the special logic
165        self.inner.serialize(serializer)
166    }
167}
168
169impl<'de, T: Deserialize<'de>> Deserialize<'de> for Forwardable<T> {
170    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
171    where
172        D: serde::Deserializer<'de>,
173    {
174        // For normal deserialization, create with empty context
175        // load_forward handles the special logic
176        let inner = T::deserialize(deserializer)?;
177        Ok(Self {
178            inner,
179            ctx: ForwardContext::new(
180                String::new(),
181                serde_json::Map::new(),
182                false,
183                DEFAULT_VERSION_KEY.to_string(),
184                DEFAULT_DATA_KEY.to_string(),
185                false,
186            ),
187        })
188    }
189}
190
191#[cfg(test)]
192mod tests {
193    use super::*;
194
195    #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
196    struct TestEntity {
197        id: String,
198        name: String,
199    }
200
201    #[test]
202    fn test_forwardable_deref() {
203        let entity = TestEntity {
204            id: "1".to_string(),
205            name: "test".to_string(),
206        };
207        let ctx = ForwardContext::new(
208            "2.0.0".to_string(),
209            serde_json::Map::new(),
210            true,
211            "version".to_string(),
212            "data".to_string(),
213            false,
214        );
215        let forwardable = Forwardable::new(entity, ctx);
216
217        // Deref access
218        assert_eq!(forwardable.id, "1");
219        assert_eq!(forwardable.name, "test");
220    }
221
222    #[test]
223    fn test_forwardable_deref_mut() {
224        let entity = TestEntity {
225            id: "1".to_string(),
226            name: "test".to_string(),
227        };
228        let ctx = ForwardContext::new(
229            "2.0.0".to_string(),
230            serde_json::Map::new(),
231            true,
232            "version".to_string(),
233            "data".to_string(),
234            false,
235        );
236        let mut forwardable = Forwardable::new(entity, ctx);
237
238        // DerefMut access
239        forwardable.name = "updated".to_string();
240        assert_eq!(forwardable.name, "updated");
241    }
242
243    #[test]
244    fn test_forwardable_context_access() {
245        let mut unknown = serde_json::Map::new();
246        unknown.insert(
247            "new_field".to_string(),
248            serde_json::Value::String("value".to_string()),
249        );
250
251        let entity = TestEntity {
252            id: "1".to_string(),
253            name: "test".to_string(),
254        };
255        let ctx = ForwardContext::new(
256            "2.0.0".to_string(),
257            unknown,
258            true,
259            "version".to_string(),
260            "data".to_string(),
261            false,
262        );
263        let forwardable = Forwardable::new(entity, ctx);
264
265        assert_eq!(forwardable.original_version(), "2.0.0");
266        assert!(forwardable.was_lossy());
267        assert_eq!(forwardable.unknown_fields().len(), 1);
268        assert!(forwardable.unknown_fields().contains_key("new_field"));
269    }
270
271    #[test]
272    fn test_forwardable_into_inner() {
273        let entity = TestEntity {
274            id: "1".to_string(),
275            name: "test".to_string(),
276        };
277        let ctx = ForwardContext::new(
278            "1.0.0".to_string(),
279            serde_json::Map::new(),
280            false,
281            "version".to_string(),
282            "data".to_string(),
283            false,
284        );
285        let forwardable = Forwardable::new(entity.clone(), ctx);
286
287        let inner = forwardable.into_inner();
288        assert_eq!(inner, entity);
289    }
290
291    #[test]
292    fn test_forwardable_serialize() {
293        let entity = TestEntity {
294            id: "1".to_string(),
295            name: "test".to_string(),
296        };
297        let ctx = ForwardContext::new(
298            "2.0.0".to_string(),
299            serde_json::Map::new(),
300            true,
301            "version".to_string(),
302            "data".to_string(),
303            false,
304        );
305        let forwardable = Forwardable::new(entity, ctx);
306
307        // Test direct serde serialization
308        let json = serde_json::to_string(&forwardable).unwrap();
309        assert!(json.contains("\"id\":\"1\""));
310        assert!(json.contains("\"name\":\"test\""));
311    }
312
313    #[test]
314    fn test_forwardable_deserialize() {
315        // Test direct serde deserialization
316        let json = r#"{"id":"1","name":"test"}"#;
317        let forwardable: Forwardable<TestEntity> = serde_json::from_str(json).unwrap();
318
319        assert_eq!(forwardable.id, "1");
320        assert_eq!(forwardable.name, "test");
321        // Deserialized without load_forward has empty context
322        assert_eq!(forwardable.original_version(), "");
323        assert!(!forwardable.was_lossy());
324    }
325}