Skip to content

Instantly share code, notes, and snippets.

@decatur
Last active September 6, 2024 09:08
Show Gist options
  • Save decatur/52428de4a019db9a9df2bf77c0ce97d7 to your computer and use it in GitHub Desktop.
Save decatur/52428de4a019db9a9df2bf77c0ce97d7 to your computer and use it in GitHub Desktop.
Demonstrate Schema Evolution
//! # Demonstrate Schema Evolution in Rust
//! A structure must deserialized from several, possibly persistent, variants.
//!
//! ┌────────────────────────────────────────────┐
//! │ MyStruct │
//! └────────────────────────────────────────────┘
//! ▲ ▲ ▲ │
//! from from from into
//! │ │ │ ▼
//! ┌───────┐ ┌───────┐ ┌───────┐
//! │ V09 │ │ V10 │ │ V11 │
//! └───────┘ └───────┘ └───────┘
//! ▲ ▲ ▲ │
//! read read read write
//! │ │ │ ▼
//! ┌────────────────────────────────────────────┐
//! │ Persistence │
//! └────────────────────────────────────────────┘
//!
//! - Initial variant may not be tagged, e.g. by oversight.
//! - Legacy DTOs are read-only.
//! - Explicit (verbose) for maintainability.
//!
use serde::{Deserialize, Serialize};
type PowerKW = f64;
#[derive(Debug, Serialize, Deserialize)]
struct PowerRange {
min: PowerKW,
max: PowerKW,
}
#[derive(Debug)]
struct MyStruct {
power_range: PowerRange,
}
/// The current, non-legacy DTO for MyStruct.
#[derive(Serialize, Deserialize)]
struct DtoV11 {
power_range: PowerRange,
}
impl From<DtoV11> for MyStruct {
fn from(value: DtoV11) -> Self {
let DtoV11 { power_range } = value;
assert!(power_range.min <= power_range.max);
MyStruct { power_range }
}
}
impl From<MyStruct> for DtoV11 {
fn from(value: MyStruct) -> Self {
let MyStruct { power_range } = value;
DtoV11 { power_range }
}
}
fn read(json: &str) -> Result<MyStruct, serde_json::Error> {
/// Start legacy DTOs for MyStruct.
#[derive(Deserialize)]
struct DtoV10 {
power_min: PowerKW,
power_max: PowerKW,
}
impl From<DtoV10> for MyStruct {
fn from(value: DtoV10) -> Self {
let DtoV10 {
power_min: min,
power_max: max,
} = value;
MyStruct {
power_range: PowerRange { min, max },
}
}
}
#[derive(Deserialize)]
struct DtoV09 {
power: PowerKW,
}
impl From<DtoV09> for MyStruct {
fn from(value: DtoV09) -> Self {
let DtoV09 { power } = value;
MyStruct {
power_range: PowerRange {
min: 0.,
max: power,
},
}
}
}
/// End legacy DTOs for MyStruct.
#[derive(Deserialize)]
#[serde(tag = "tag")]
enum Version {
V11(DtoV11),
V10(DtoV10),
#[serde(untagged)]
V09(DtoV09),
}
impl From<Version> for MyStruct {
fn from(value: Version) -> Self {
match value {
Version::V11(v) => v.into(),
Version::V10(v) => v.into(),
Version::V09(v) => v.into(),
}
}
}
Ok(serde_json::from_str::<Version>(json)?.into())
}
fn write(s: MyStruct) -> Result<String, serde_json::Error> {
#[derive(Serialize)]
#[serde(tag = "tag")]
enum TaggingEnum {
V11(DtoV11),
}
serde_json::to_string(&TaggingEnum::V11(s.into()))
}
#[test]
fn test_main() {
main().unwrap();
}
#[allow(dead_code)]
fn main() -> Result<(), serde_json::Error> {
// Wrong type, missing attribute, and we do not even have a tag!
let s = read(r#"{"power": 100}"#)?;
println!("{s:?}"); // MyStruct { power_range: PowerRange { min: 0.0, max: 100.0 } }
// Missing attribute
let s = read(r#"{"tag":"V10", "power_min": 10, "power_max":100}"#)?;
println!("{s:?}"); // MyStruct { power_range: PowerRange { min: 10.0, max: 100.0 } }
// This is trailing MyStruct
let s = read(r#"{"tag":"V11", "power_range":{"min":10, "max":100}}"#)?;
println!("{s:?}"); // MyStruct { power_range: PowerRange { min: 10.0, max: 100.0 } }
let json = write(s)?;
println!("{json:?}"); // "{\"tag\":\"V11\",\"power_range\":{\"min\":10.0,\"max\":100.0}}"
Ok(())
}
@decatur
Copy link
Author

decatur commented Sep 6, 2024

Write via DtoV11, do not write MyStruct directly.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment