GitHub

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()
);

Read the original on github.com ↗