#![allow(missing_docs)]
use serde_evolve::Versioned;
#[derive(Clone, Debug, PartialEq, Eq, Versioned)]
#[versioned(
// mode = "fallible", // this is the default
error = anyhow::Error,
chain(versions::V1, versions::V2, versions::V3)
)]
pub struct Product {
pub name: String,
pub price_cents: u32,
pub sku: String,
}
mod versions {
use super::Product;
use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct V1 {
pub name: String,
pub price: f64, }
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct V2 {
pub name: String,
pub price_cents: i64, }
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct V3 {
pub name: String,
pub price_cents: u32, pub sku: String, }
#[allow(clippy::cast_precision_loss)]
const MAX_I64_AS_F64: f64 = i64::MAX as f64;
#[allow(clippy::cast_precision_loss)]
const MIN_I64_AS_F64: f64 = i64::MIN as f64;
impl TryFrom<V1> for V2 {
type Error = anyhow::Error;
fn try_from(v1: V1) -> Result<Self, Self::Error> {
if !v1.price.is_finite() {
anyhow::bail!("Price must be finite, got: {}", v1.price);
}
let cents = (v1.price * 100.0).round();
if !(MIN_I64_AS_F64..=MAX_I64_AS_F64).contains(¢s) {
anyhow::bail!("Price too large to convert: {}", v1.price);
}
#[allow(clippy::cast_possible_truncation)]
let price_cents = cents as i64;
Ok(Self {
name: v1.name,
price_cents,
})
}
}
impl TryFrom<V2> for V3 {
type Error = anyhow::Error;
fn try_from(v2: V2) -> Result<Self, Self::Error> {
if v2.price_cents < 0 {
anyhow::bail!("Negative price not allowed: {}", v2.price_cents);
}
let sku = generate_sku(&v2.name)?;
let price_cents = u32::try_from(v2.price_cents).map_err(|_| {
anyhow::anyhow!(
"Price does not fit in unsigned representation: {}",
v2.price_cents
)
})?;
Ok(Self {
name: v2.name,
price_cents,
sku,
})
}
}
impl TryFrom<V3> for Product {
type Error = anyhow::Error;
fn try_from(v3: V3) -> Result<Self, Self::Error> {
Ok(Self {
name: v3.name,
price_cents: v3.price_cents,
sku: v3.sku,
})
}
}
impl From<&Product> for V3 {
fn from(product: &Product) -> Self {
Self {
name: product.name.clone(),
price_cents: product.price_cents,
sku: product.sku.clone(),
}
}
}
fn generate_sku(name: &str) -> anyhow::Result<String> {
let prefix: String = name
.chars()
.filter(|c| c.is_alphanumeric())
.take(3)
.collect::<String>()
.to_uppercase();
if prefix.is_empty() {
anyhow::bail!("Product name contains no valid characters for SKU");
}
Ok(format!("{prefix:0<8}"))
}
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
println!("=== Versioned Data: Simple Fallible Example ===\n");
println!("1. Successfully migrating V1 data...");
let json_v1 = r#"{"_version":"1","name":"Widget","price":19.99}"#;
println!(" JSON: {json_v1}");
let rep_v1: ProductVersions = serde_json::from_str(json_v1)?;
println!(" Version: {}", rep_v1.version());
match Product::try_from(rep_v1) {
Ok(product) => {
println!(" ✓ Migration successful!");
println!(" Product: {product:?}\n");
assert_eq!(product.name, "Widget");
assert_eq!(product.price_cents, 1999);
assert_eq!(product.sku, "WID00000");
}
Err(e) => {
println!(" ✗ Migration failed: {e}\n");
return Err(e.into());
}
}
println!("2. Successfully migrating V2 data...");
let json_v2 = r#"{"_version":"2","name":"Gadget","price_cents":2500}"#;
println!(" JSON: {json_v2}");
let rep_v2: ProductVersions = serde_json::from_str(json_v2)?;
println!(" Version: {}", rep_v2.version());
match Product::try_from(rep_v2) {
Ok(product) => {
println!(" ✓ Migration successful!");
println!(" Product: {product:?}\n");
assert_eq!(product.name, "Gadget");
assert_eq!(product.price_cents, 2500);
assert_eq!(product.sku, "GAD00000");
}
Err(e) => {
println!(" ✗ Migration failed: {e}\n");
return Err(e.into());
}
}
println!("3. Attempting to migrate V2 data with negative price...");
let json_negative = r#"{"_version":"2","name":"Doohickey","price_cents":-500}"#;
println!(" JSON: {json_negative}");
let rep_negative: ProductVersions = serde_json::from_str(json_negative)?;
match Product::try_from(rep_negative) {
Ok(_) => {
println!(" ✗ Unexpected success!\n");
return Err("Expected migration to fail with negative price".into());
}
Err(e) => {
println!(" ✓ Migration correctly failed: {e}\n");
}
}
println!("4. Attempting to migrate V2 data with invalid product name...");
let json_invalid_name = r#"{"_version":"2","name":"!!!","price_cents":1000}"#;
println!(" JSON: {json_invalid_name}");
let rep_invalid_name: ProductVersions = serde_json::from_str(json_invalid_name)?;
match Product::try_from(rep_invalid_name) {
Ok(_) => {
println!(" ✗ Unexpected success!\n");
return Err("Expected migration to fail with invalid product name".into());
}
Err(e) => {
println!(" ✓ Migration correctly failed: {e}\n");
}
}
println!("5. Writing current version...");
let thingamajig = Product {
name: "Thingamajig".to_string(),
price_cents: 2499,
sku: "THI12345".to_string(),
};
let rep_thingamajig: ProductVersions = (&thingamajig).into();
println!(" Writing as version: {}", rep_thingamajig.version());
println!(" Current version: {}", ProductVersions::CURRENT);
let json_thingamajig = serde_json::to_string_pretty(&rep_thingamajig)?;
println!(" JSON:\n{json_thingamajig}\n");
println!("6. Verifying round-trip...");
let rep_back: ProductVersions = serde_json::from_str(&json_thingamajig)?;
let thingamajig_back: Product = rep_back.try_into()?;
assert_eq!(thingamajig, thingamajig_back);
println!(" ✓ Round-trip successful!\n");
println!("=== Example completed successfully! ===");
println!("\nKey takeaways:");
println!(" • Fallible migrations use TryFrom instead of From");
println!(" • Validation errors are caught during migration, not serialization");
println!(" • Invalid historical data can be identified and handled gracefully");
println!(" • The latest version can still be serialized/deserialized normally");
Ok(())
}