// 767. Versioned Serialization with Migration
// Version tag in payload, migration chain
#[derive(Debug, PartialEq)]
pub struct UserV1 {
pub name: String,
pub age: u32,
}
#[derive(Debug, PartialEq, Clone)]
pub struct UserV2 {
pub name: String,
pub age: u32,
pub email: String, // new in V2
}
#[derive(Debug, PartialEq, Clone)]
pub struct UserV3 {
pub name: String,
pub age: u32,
pub email: String,
pub active: bool, // new in V3
}
// โโ Migration chain โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
impl From<UserV1> for UserV2 {
fn from(u: UserV1) -> Self {
UserV2 {
email: format!("{}@example.com", u.name.to_lowercase().replace(' ', ".")),
name: u.name,
age: u.age,
}
}
}
impl From<UserV2> for UserV3 {
fn from(u: UserV2) -> Self {
UserV3 {
name: u.name,
age: u.age,
email: u.email,
active: true, // sensible default for migrated records
}
}
}
impl From<UserV1> for UserV3 {
fn from(u: UserV1) -> Self {
UserV3::from(UserV2::from(u))
}
}
// โโ Versioned enum โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
#[derive(Debug)]
pub enum VersionedUser {
V1(UserV1),
V2(UserV2),
V3(UserV3),
}
impl VersionedUser {
/// Always get the latest version (migrate if needed)
pub fn into_current(self) -> UserV3 {
match self {
VersionedUser::V1(u) => UserV3::from(u),
VersionedUser::V2(u) => UserV3::from(u),
VersionedUser::V3(u) => u,
}
}
}
// โโ Serialization โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
pub fn serialize_v3(u: &UserV3) -> String {
format!("version=3|name={}|age={}|email={}|active={}", u.name, u.age, u.email, u.active)
}
pub fn serialize_v1(u: &UserV1) -> String {
format!("version=1|name={}|age={}", u.name, u.age)
}
fn fields(s: &str) -> std::collections::HashMap<&str, &str> {
s.split('|').filter_map(|p| p.split_once('=')).collect()
}
// โโ Deserialization โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
#[derive(Debug)]
pub enum DeError { MissingField(&'static str), UnsupportedVersion(String), ParseError(String) }
pub fn deserialize(s: &str) -> Result<VersionedUser, DeError> {
let map = fields(s);
match *map.get("version").ok_or(DeError::MissingField("version"))? {
"1" => {
let name = map.get("name").ok_or(DeError::MissingField("name"))?.to_string();
let age = map.get("age").ok_or(DeError::MissingField("age"))?
.parse().map_err(|e: std::num::ParseIntError| DeError::ParseError(e.to_string()))?;
Ok(VersionedUser::V1(UserV1 { name, age }))
}
"2" => {
let name = map.get("name").ok_or(DeError::MissingField("name"))?.to_string();
let age = map.get("age").ok_or(DeError::MissingField("age"))?
.parse().map_err(|e: std::num::ParseIntError| DeError::ParseError(e.to_string()))?;
let email = map.get("email").ok_or(DeError::MissingField("email"))?.to_string();
Ok(VersionedUser::V2(UserV2 { name, age, email }))
}
"3" => {
let name = map.get("name").ok_or(DeError::MissingField("name"))?.to_string();
let age = map.get("age").ok_or(DeError::MissingField("age"))?
.parse().map_err(|e: std::num::ParseIntError| DeError::ParseError(e.to_string()))?;
let email = map.get("email").ok_or(DeError::MissingField("email"))?.to_string();
let active = map.get("active").map(|v| *v == "true").unwrap_or(true);
Ok(VersionedUser::V3(UserV3 { name, age, email, active }))
}
v => Err(DeError::UnsupportedVersion(v.to_string())),
}
}
fn main() {
// Simulate reading old V1 data and migrating to V3
let old = UserV1 { name: "Alice".into(), age: 30 };
let wire = serialize_v1(&old);
println!("Old wire: {wire}");
let versioned = deserialize(&wire).expect("decode failed");
let current = versioned.into_current();
println!("Migrated to V3: {current:?}");
// Current format round-trip
let wire3 = serialize_v3(¤t);
println!("V3 wire: {wire3}");
let back = deserialize(&wire3).expect("v3 decode").into_current();
println!("V3 round-trip: {back:?}");
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn v1_migrates_to_v3() {
let u1 = UserV1 { name: "Bob".into(), age: 25 };
let wire = serialize_v1(&u1);
let v3 = deserialize(&wire).unwrap().into_current();
assert_eq!(v3.name, "Bob");
assert_eq!(v3.age, 25);
assert!(v3.email.contains("bob"));
assert!(v3.active);
}
#[test]
fn unknown_version_errors() {
let result = deserialize("version=99|name=X|age=1");
assert!(matches!(result, Err(DeError::UnsupportedVersion(_))));
}
#[test]
fn v3_round_trip() {
let u = UserV3 { name: "Carol".into(), age: 35, email: "c@test.com".into(), active: false };
let wire = serialize_v3(&u);
let back = deserialize(&wire).unwrap().into_current();
assert_eq!(back, u);
}
}
(* Versioned serialization with migration in OCaml *)
(* โโ V1 schema: name + age *)
type user_v1 = { name: string; age: int }
(* โโ V2 schema: name + age + email (new field) *)
type user_v2 = { name: string; age: int; email: string }
(* โโ Migration: V1 โ V2 *)
let migrate_v1_to_v2 (u: user_v1) : user_v2 =
{ name = u.name;
age = u.age;
email = u.name ^ "@example.com" } (* synthesized default *)
(* โโ Versioned union *)
type versioned_user =
| V1User of user_v1
| V2User of user_v2
(* โโ Serialize *)
let serialize_v2 u =
Printf.sprintf "version=2|name=%s|age=%d|email=%s" u.name u.age u.email
let serialize_v1 u =
Printf.sprintf "version=1|name=%s|age=%d" u.name u.age
(* โโ Deserialize with migration *)
let field pairs key =
match List.assoc_opt key pairs with
| Some v -> Ok v
| None -> Error ("missing field: " ^ key)
let parse_pairs s =
String.split_on_char '|' s
|> List.filter_map (fun p ->
match String.split_on_char '=' p with
| [k; v] -> Some (k, v)
| _ -> None)
let deserialize s =
let pairs = parse_pairs s in
match field pairs "version" with
| Error e -> Error e
| Ok "1" ->
(match field pairs "name", field pairs "age" with
| Ok name, Ok age_s ->
(try
let u1 = V1User { name; age = int_of_string age_s } in
Ok u1
with Failure e -> Error e)
| Error e, _ | _, Error e -> Error e)
| Ok "2" ->
(match field pairs "name", field pairs "age", field pairs "email" with
| Ok name, Ok age_s, Ok email ->
(try Ok (V2User { name; age = int_of_string age_s; email })
with Failure e -> Error e)
| Error e, _, _ | _, Error e, _ | _, _, Error e -> Error e)
| Ok v -> Error ("unsupported version: " ^ v)
(* Normalize to V2 (migrating if needed) *)
let to_v2 = function
| V1User u1 -> migrate_v1_to_v2 u1
| V2User u2 -> u2
let () =
(* Write in old format, read as new *)
let old_data = serialize_v1 { name = "Alice"; age = 30 } in
Printf.printf "Old wire: %s\n" old_data;
match deserialize old_data with
| Ok v ->
let u2 = to_v2 v in
Printf.printf "Migrated: name=%s age=%d email=%s\n" u2.name u2.age u2.email
| Error e -> Printf.printf "Error: %s\n" e