1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
//! Macro to clone a structure for use with configuration data
// (c) 2024 Ross Younger
#![allow(meta_variable_misuse)] // false positives in these macro definitions
use derive_deftly::define_derive_deftly;
define_derive_deftly! {
/// Clones a structure for use with CLI ([`clap`](https://docs.rs/clap/)) and options managers ([`figment`](https://docs.rs/figment/)).
///
/// The expected use case is for configuration structs.
/// The idea is that you define your struct how you want, then automatically generate a variety of the struct
/// to make life easier. The variant:
/// * doesn't require the user to enter all parameters (everything is an `Option`)
/// * implements the [`figment::Provider`](https://docs.rs/figment/latest/figment/trait.Provider.html) helper trait
/// which makes it easy to extract only the parameters the user entered.
///
/// Of course, you would not set `default_value` attributes, because you would normally register
/// the defaults with the configuration system at a different place (e.g. by implementing the [`Default`][std::default::Default] trait).
///
/// The new struct:
/// * is named `{OriginalName}_Optional`
/// * has the same fields as the original, with all their attributes, but with their types wrapped
/// in [`std::option::Option`]. (Yes, even any that were already `Option<...>`.)
/// * contains exactly the same attributes as the original, plus `#[derive(Default)]` *(see note)*.
/// * has the same visibility as the original, though you can override this with `#[deftly(visibility = ...)]`
///
/// **Note:**
/// If you already derived Default for the original struct, add the
/// attribute ```#[deftly(already_has_default)]```.
/// This tells the macro to *not* add ```#[derive(Default)]```, avoiding a compile error.
///
/// <div class="warning">
/// CAUTION: Attribute ordering is crucial. All attributes to be cloned to the new struct
/// must appear <i>after</i> deriving `Optionalify`. It might look something like this:
/// </div>
///
/// ### Example
///
/// ```
/// use derive_deftly::Deftly;
/// use qcp::derive_deftly_template_Optionalify;
/// #[derive(Deftly)]
/// #[derive_deftly(Optionalify)]
/// #[derive(Debug, Clone /*, WhateverElseYouNeed...*/)]
/// struct MyStruct {
/// /* ... */
/// }
/// ```
///
/// ### Overriding serde attributes
///
/// With some types, it is necessary to override the serde deserialization attributes
/// on certain fields of the derived struct. To do this, use a `#[deftly(serde = "...")]` attribute.
///
/// The contents of the `serde` meta-attribute replace any `serde` attribute on the field.
/// Note that it is necessary to escape any quotes in the replacement attribute.
///
/// For example:
///
/// ```ignore
/// struct MyStruct {
/// // ... fields ...
///
/// #[serde(deserialize_with = "foo::bar")]
/// #[deftly(serde = "deserialize_with = \"foo::qux\"")]
/// pub my_field: MyWeirdType,
///
/// // ... more fields ...
/// }
/// ```
///
/// This doesn't work for `serialize_with` if the type also needs to be serialized differently.
/// (case in point: `CongestionController`)
/// For this situation, we support `#[deftly(serialize_with = "path::to::some::function")]`.
///
/// ### Troubleshooting
///
/// As with other template macros created with [`derive-deftly`](https://docs.rs/derive-deftly/), if you need
/// to see what's going on you can use `#[derive_deftly(Optionalify[dbg])]` instead of `#[derive_deftly(Optionalify)]`
/// to see the expanded output at compile time. Or, if you prefer, `cargo expand`.
export Optionalify for struct, expect items:
${define OPTIONAL_TYPE ${paste $tdeftype _Optional}}
/// Auto-derived struct variant with fields wrapped as `Option<...>`
///
#[allow(non_camel_case_types)]
${tattrs}
${if not(tmeta(already_has_default)){
#[derive(Default)]
}}
${if tmeta(visibility) {
${tmeta(visibility) as token_stream}
} else {
${tvis}
}}
struct $OPTIONAL_TYPE {
$(
// The 'serde' meta-attribute is an instruction to remove any serde attribute
// on the field, and use the given string instead.
${if fmeta(serde) {
${fattrs ! serde, deftly}
#[serde(${fmeta(serde) as token_stream})]
} else {
${fattrs}
}}
${fvis} $fname: Option<$ftype>,
// Yes, if $ftype is Option<T>, the derived struct ends up with Option<Option<T>>. That's OK.
)
}
impl figment::Provider for $OPTIONAL_TYPE {
fn metadata(&self) -> figment::Metadata {
figment::Metadata::named("command-line").interpolater(|_profile, path| {
use heck::ToKebabCase;
let key = path.last().map_or("<unknown>".to_string(), |s| s.to_kebab_case());
format!("--{key}")
})
}
fn data(&self) -> Result<figment::value::Map<figment::Profile, figment::value::Dict>, figment::Error> {
use figment::{Profile, value::{Dict, Map, Value}};
let mut dict = Dict::new();
// Curveball: For some types, we want to serialize them differently
// in different contexts. So we have the `serialize_with` meta attribute,
// which works like `#[serde(serialize_with = ...)]`.
$(
if let Some(inner) = &self.${fname} {
let value =
${if fmeta(serialize_with) {
// If there is a special serializer, use that
${fmeta(serialize_with) as token_stream}(inner)?.into()
} else {
// Else, standard serialization.
Value::serialize(inner)?
}};
let _ = dict.insert(stringify!($fname).to_string(), value);
}
)
let mut profile_map = Map::new();
let _ = profile_map.insert(Profile::Global, dict);
Ok(profile_map)
}
}
impl From<&$tdeftype> for $OPTIONAL_TYPE {
fn from(value: &$tdeftype) -> Self {
Self {
$(
$fname: Some(value.$fname.clone()),
)
}
}
}
}
#[allow(clippy::module_name_repetitions)]
pub use derive_deftly_template_Optionalify;
#[cfg(test)]
#[cfg_attr(coverage_nightly, coverage(off))]
mod test {
use derive_deftly::Deftly;
use figment::{Figment, providers::Serialized};
use pretty_assertions::assert_eq;
#[derive(Deftly)]
#[derive_deftly(Optionalify)]
#[deftly(already_has_default)]
#[derive(PartialEq, Debug, Default, serde::Serialize, serde::Deserialize)]
struct Foo {
bar: i32,
baz: String,
wibble: Option<String>,
q: Option<i32>,
}
#[test]
fn optionality() {
let mut entered = Foo_Optional::default();
assert!(entered.bar.is_none());
entered.bar = Some(999);
entered.wibble = Some(Some("hi".to_string()));
entered.q = Some(Some(123));
//println!("simulated cli: {entered:?}");
let f = Figment::new()
.merge(Serialized::defaults(Foo::default()))
.merge(entered);
let working: Foo = f.extract().expect("extract failed");
let expected = Foo {
bar: 999,
baz: String::new(), // default
wibble: Some("hi".into()),
q: Some(123),
};
assert_eq!(expected, working);
}
}