3 releases
0.5.2 | Jul 31, 2024 |
---|---|
0.5.1 | Jul 31, 2024 |
0.5.0 | Jul 18, 2024 |
#2234 in Procedural macros
2,432 downloads per month
Used in 2 crates
(via optional_struct)
26KB
688 lines
OptionalStruct
Quick-start
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 },
});
}
Goal
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.
How
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.
Features
- Rename the generated struct:
#[optional_struct(HeyU)]
struct Config();
fn main() {
let me = HeyU();
}
- Handle recursive types:
#[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,
}
- Handle
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) };
}
- Force wrapping (or not) of fields:
#[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) };
}
- Change the default wrapping behavior:
#[optional_struct(OptionalFoo, false)]
struct Foo {
bar: u8,
#[optional_wrap]
baz: i8,
}
fn main() {
let opt_f = OptionalFoo { bar: 1, baz: None };
}
- Add serde's
skip_serializing_if = "Option::is_none"
attribute to generated struct
By 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 realStruct
and sets all its field based on which fields are set inOptionalStruct
. Missing fields are left alone.Option
fields with a force-wrap attributes will NOT overwrite the value e.g.Some(1)
will not overwriteSome(2)
(see the initial example for a concrete situation. -
try_build
tries to build a wholeStruct
from theOptionalStruct
, returning either anOk(Struct)
if things went well, or the initialOptionalStruct
in theErr(OptionalStruct)
in case things were missing. -
apply
takes anOptionalStruct
as a parameter and applies its fields to the left (i.e.self
). Ifself
andother
both define something, the value fromother
is taken. Ifself
defines something but notother
, the value is preserved. Naturally, ifself
does not define something butother
does, this value is used.
Dependencies
~250–700KB
~17K SLoC