A framework for managing versioned structs inspired by OCaml stable type conventions.
The macro generates a tagged Versions enum and converts older versions into the latest version of
the struct. Writes always use the latest version. Two notable design decisions are made:
- All fields must be valid (i.e.
serde(deny_unknown_fields)). Rationale: strict versioning catches mistyped or misconfigured fields. - All fields must be present (i.e. no implicit defaults). Rationale: completeness ensures stability since changing defaults won't affect users. To improve ease-of-use when versioning configs, offer a command that outputs the default config to stdout (or writes it to the config file as in done in Ringboard).
Example usage:
mod config { use stable_type::stable_type; stable_type! { #[derive(Eq, PartialEq, Debug)] pub struct Config [ // Each version of the struct is fully defined "1": { pub starting_field: bool }, "2": { pub starting_field: bool, pub new_field: Vec<u8> }, "3": { pub starting_field: bool, pub actually_meant_this: String }, ] } // Conversions from version N to N+1 must be implemented impl From<V1> for V2 { fn from(V1 { starting_field }: V1) -> Self { Self { starting_field, new_field: b"Some default".into(), } } } impl From<V2> for V3 { fn from( V2 { starting_field, new_field, }: V2, ) -> Self { Self { starting_field, actually_meant_this: String::from_utf8(new_field).unwrap(), } } } } // ------------------------------------------------------- // Now we can use the generated structs let config = r#" version = "1" starting_field = true "#; let config: config::Config = toml::from_str::<config::Stable>(config).unwrap().into(); assert_eq!( config, config::Config { starting_field: true, actually_meant_this: "Some default".into() } ); let serializable = config::Stable::from(config); assert_eq!( toml::to_string_pretty(&serializable).unwrap().trim(), r#" version = "3" starting_field = true actually_meant_this = "Some default" "#.trim() );