-
Notifications
You must be signed in to change notification settings - Fork 1
Description
Problem
Say a user has instantiated a ModelingCmd struct field-by-field (i.e. not using a constructor function, but rather, providing each field), like
let x = Export {
entity_ids: Vec:new(),
output_format: OutputFormat::Stl,
};
Problem is, adding a new field is not backwards-compatible. The above code will no longer compile if we add a third field, e.g. "compression: bool". Users will now get this compile error:
missing field `compression` in initializer of `Export`
Even if we add the field with #[serde(default)]
, this compile error will still happen. Serde can make JSON deserialization backwards-compatible (at runtime), but not instantiating the struct in Rust code (at compiletime).
Solution
One solution is to simply stop users from instantiating this way. There's a #[non_exhaustive]
attribute we could put on a struct definition, which prevents users from instantiating it field-by-field. That forces users to use constructor methods. Currently we don't have any constructor methods, and defining our own constructor methods for each type would be a lot of work -- we have 60+ commands, so I don't want to handwrite a constructor for each. We have to generate them via a macro.
Luckily such a macro already exists -- the bon crate, which makes type-safe builders for structs. E.g.
use bon::Builder;
use serde::Deserialize;
#[derive(Builder, Deserialize)]
#[non_exhaustive]
struct Export {
entity_ids: Vec<i32>,
output_format: OutputFormat,
#[serde(default)]
#[builder(default)]
compression: bool,
}
#[derive(Deserialize)]
enum OutputFormat {
Stl,
Fdx,
}
fn main() {
let f = Export::builder()
.entity_ids(Vec::new())
.output_format(OutputFormat::Stl)
.build();
}
In this example, the compression
field is optional and will default to the default value of that field's type (i.e. false). So users don't have to call the .compression()
method. But if they leave off a required method like .entity_ids()
they'll get a compile error:
can't finish building yet; the member `ExportBuilder__output_format` was not set
This way, we can add new fields to the struct without breaking users -- we can default them to a certain value.