# Stable type

A framework for managing versioned structs inspired by
[OCaml stable type conventions](https://github.com/janestreet/ppx_stable).

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](https://github.com/SUPERCILEX/clipboard-history/blob/0cae0740d4e25911434424646658f15157b2d585/cli/src/main.rs#L2216-L2240)).

Example usage:

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