From the tests/builder.rs
file:
use optional_struct::*;
#[optional_struct]
#[derive(Eq, PartialEq, Debug)]
struct Foo {
paf: u16,
bar: Option<u8>,
#[optional_wrap]
baz: Option<char>,
#[optional_rename(OptionalMiaou)]
#[optional_wrap]
miaou: Miaou,
}
#[optional_struct]
#[derive(Eq, PartialEq, Debug)]
struct Miaou {
a: i8,
b: i16,
}
#[test]
fn test_builder() {
let default = Foo {
paf: 12,
bar: None,
baz: Some('a'),
miaou: Miaou {
a: 1,
b: -1,
},
};
let first = OptionalFoo {
paf: Some(42),
bar: Some(7),
baz: Some(None),
miaou: None,
};
let second = OptionalFoo {
paf: Some(24),
bar: None,
baz: Some(Some('c')),
miaou: Some(OptionalMiaou {
a: Some(2),
b: None,
}),
};
let collapsed = first.apply(second).build(default);
assert_eq!(collapsed, Foo {
paf: 24,
bar: Some(7),
baz: Some('c'),
miaou: Miaou { a: 2, b: -1 },
});
}
Since rust does not have default arguments, and some tools are strict when deserializing data (e.g. serde), missing configuration values can be quite frustrating to deal with. For example:
#[derive(Deserialize)]
struct Config {
log_file: PathBuf,
}
If we read the configuration from a file, and the log_file
is not specified,
serde will fail to create the struct. While serde offers ways to set the
default value for a field with e.g.
#[derive(Deserialize)]
struct Config {
#[serde(default = "get_next_log_filename")]
log_file: PathBuf,
}
there are obvious limitations. This crate aims to fill this gap by allowing optional values, and providing an easy way to apply values obtained from different sources to construct our configuration.
With optional_struct
, one can define the required
configuration as it shall be used and only use the generated struct
to handle configuration/missing values/default values.
The macro optional_struct
generates a structure containing the same fields as the one it was tagged on, but wrapped by an Option
.
A function on the new structure allows applying its values to the original one
(if the Option
s are not None
). This can be called multiple times, to apply
configuration from different source, while giving the caller complete control
over how to set the values, since the generated struct can be easily manipulated
and passed around before constructing the final configuration.
#[optional_struct(HeyU)]
struct Config();
fn main() {
let me = HeyU();
}
#[optional_struct]
struct Foo {
// Replaces Option<Bar> with OptionalBar
// To generate Option<OptionalBar> instead, add an extra #[optional_wrap]
// as described later
#[optional_rename(OptionalBar)]
bar: Bar,
}
Option
s in the original struct (by ignoring them):#[optional_struct]
struct Foo {
bar: Option<u8>,
}
fn main() {
let opt_f = OptionalFoo { bar: Some(1) };
}
#[optional_struct]
struct Foo {
#[optional_skip_wrap]
bar: char,
// Useless here since we wrap by default
#[optional_wrap]
baz: bool,
}
fn main() {
let opt_f = OptionalFoo { bar: 'a', baz: Some(false) };
}
#[optional_struct(OptionalFoo, false)]
struct Foo {
bar: u8,
#[optional_wrap]
baz: i8,
}
fn main() {
let opt_f = OptionalFoo { bar: 1, baz: None };
}
skip_serializing_if = "Option::is_none"
attribute to generated
structBy adding the attribute #[optional_serde_skip_none]
to a field, the generated
struct will have the same field tagged #[serde(skip_serializing_if = "Option::is_none")]
.
This attribute makes serde skip fields entirely if the value of the Option
is
none (rather than saving e.g. "value" = null
if serializing to json).
apply
, build
, and try_build
Those three functions are used to build the final version of the structure, by collapsing the values "on the left".
The signatures of the functions are (in pseudo-code):
impl OptionalStruct {
fn build(self, s: Struct) -> Struct;
fn try_build(self) -> Result<Struct, OptionalStruct>;
fn apply(self, other: OptionalStruct) -> OptionalStruct;
}
What those functions do:
build
takes a real Struct
and sets all its field based on which fields
are set in OptionalStruct
. Missing fields are left alone. Option
fields
with a force-wrap attributes will NOT overwrite the value e.g. Some(1)
will
not overwrite Some(2)
(see the initial example for a concrete situation.
try_build
tries to build a whole Struct
from the OptionalStruct
,
returning either an Ok(Struct)
if things went well,
or the initial OptionalStruct
in the Err(OptionalStruct)
in case things were missing.
apply
takes an OptionalStruct
as a parameter and applies its fields to
the left (i.e. self
). If self
and other
both define something, the value
from other
is taken. If self
defines something but not other
, the value
is preserved. Naturally, if self
does not define something but other
does,
this value is used.