Skip to content

Commit 90f5f67

Browse files
committed
Config fragment/validation
A slightly different take of #425, which uses the existing Atomic infra and some trait magic to determine "complex" types. It also tries to provide nice error messages for validation failures.
1 parent 6fa23af commit 90f5f67

File tree

5 files changed

+371
-0
lines changed

5 files changed

+371
-0
lines changed

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ tracing-opentelemetry = "0.17.3"
3535
opentelemetry = { version = "0.17.0", features = ["rt-tokio"] }
3636
opentelemetry-jaeger = { version = "0.16.0", features = ["rt-tokio"] }
3737
stackable-operator-derive = { path = "stackable-operator-derive" }
38+
snafu = "0.7.1"
3839

3940
[dev-dependencies]
4041
rstest = "0.15.0"

src/config/fragment.rs

Lines changed: 198 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,198 @@
1+
use std::fmt::{Display, Write};
2+
3+
pub use stackable_operator_derive::Fragment;
4+
5+
use super::merge::Atomic;
6+
7+
use snafu::Snafu;
8+
9+
pub struct Validator<'a> {
10+
ident: Option<&'a str>,
11+
parent: Option<&'a Validator<'a>>,
12+
}
13+
14+
impl<'a> Validator<'a> {
15+
pub fn field<'b>(&'b self, ident: &'b str) -> Validator<'b> {
16+
Validator {
17+
ident: Some(ident),
18+
parent: Some(self),
19+
}
20+
}
21+
22+
fn error_problem(self, problem: ValidationProblem) -> ValidationError {
23+
let mut idents = Vec::new();
24+
let mut curr = Some(&self);
25+
while let Some(curr_some) = curr {
26+
if let Some(ident) = curr_some.ident {
27+
idents.push(ident.to_string());
28+
}
29+
curr = curr_some.parent;
30+
}
31+
ValidationError {
32+
path: FieldPath { idents },
33+
problem,
34+
}
35+
}
36+
37+
pub fn error_required(self) -> ValidationError {
38+
self.error_problem(ValidationProblem::FieldRequired)
39+
}
40+
}
41+
42+
#[derive(Debug)]
43+
struct FieldPath {
44+
idents: Vec<String>,
45+
}
46+
impl Display for FieldPath {
47+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
48+
for (i, ident) in self.idents.iter().rev().enumerate() {
49+
if i > 0 {
50+
f.write_char('.')?;
51+
}
52+
f.write_str(ident)?;
53+
}
54+
Ok(())
55+
}
56+
}
57+
58+
#[derive(Debug, Snafu)]
59+
#[snafu(display("failed to validate {path}"))]
60+
pub struct ValidationError {
61+
path: FieldPath,
62+
#[snafu(source)]
63+
problem: ValidationProblem,
64+
}
65+
#[derive(Debug, Snafu)]
66+
enum ValidationProblem {
67+
#[snafu(display("field is required"))]
68+
FieldRequired,
69+
}
70+
71+
pub trait FromFragment: Sized {
72+
type Fragment;
73+
74+
fn from_fragment(
75+
fragment: Self::Fragment,
76+
validator: Validator,
77+
) -> Result<Self, ValidationError>;
78+
79+
fn default_fragment() -> Option<Self::Fragment>;
80+
}
81+
impl<T: Atomic> FromFragment for T {
82+
type Fragment = T;
83+
84+
fn from_fragment(
85+
fragment: Self::Fragment,
86+
_validator: Validator,
87+
) -> Result<Self, ValidationError> {
88+
Ok(fragment)
89+
}
90+
91+
fn default_fragment() -> Option<Self::Fragment> {
92+
None
93+
}
94+
}
95+
96+
pub fn validate<T: FromFragment>(fragment: T::Fragment) -> Result<T, ValidationError> {
97+
T::from_fragment(
98+
fragment,
99+
Validator {
100+
ident: None,
101+
parent: None,
102+
},
103+
)
104+
}
105+
106+
#[cfg(test)]
107+
mod tests {
108+
use super::{validate, Fragment, FromFragment, ValidationError, Validator};
109+
110+
#[derive(Fragment, Debug, PartialEq, Eq)]
111+
struct Empty {}
112+
113+
#[derive(Fragment, Debug, PartialEq, Eq)]
114+
struct WithFields {
115+
name: String,
116+
#[fragment(default = "1")]
117+
replicas: u8,
118+
#[fragment(default)]
119+
overhead: u8,
120+
tag: Option<String>,
121+
}
122+
123+
#[derive(Fragment, Debug, PartialEq, Eq)]
124+
struct Nested {
125+
required: WithFields,
126+
optional: Option<WithFields>,
127+
}
128+
129+
#[test]
130+
fn validate_empty() {
131+
assert_eq!(validate::<Empty>(EmptyFragment {}).unwrap(), Empty {});
132+
}
133+
134+
#[test]
135+
fn validate_basics() {
136+
assert_eq!(
137+
validate::<WithFields>(WithFieldsFragment {
138+
name: Some("foo".to_string()),
139+
replicas: Some(23),
140+
overhead: Some(24),
141+
tag: Some("bar".to_string()),
142+
})
143+
.unwrap(),
144+
WithFields {
145+
name: "foo".to_string(),
146+
replicas: 23,
147+
overhead: 24,
148+
tag: Some("bar".to_string()),
149+
}
150+
);
151+
assert_eq!(
152+
validate::<WithFields>(WithFieldsFragment {
153+
name: Some("foo".to_string()),
154+
replicas: None,
155+
overhead: None,
156+
tag: None,
157+
})
158+
.unwrap(),
159+
WithFields {
160+
name: "foo".to_string(),
161+
replicas: 1,
162+
overhead: 0,
163+
tag: None,
164+
}
165+
);
166+
167+
let err = validate::<WithFields>(WithFieldsFragment {
168+
name: None,
169+
replicas: None,
170+
overhead: None,
171+
tag: None,
172+
})
173+
.unwrap_err();
174+
assert!(err.to_string().contains("name"));
175+
}
176+
177+
#[test]
178+
fn validate_nested() {
179+
// required complex fields should automatically be defaulted (so that the "leaf" fields are validated immediately)
180+
let err = validate::<Nested>(NestedFragment {
181+
required: None,
182+
optional: None,
183+
})
184+
.unwrap_err();
185+
assert!(err.to_string().contains("required.name"));
186+
187+
// optional complex fields should still be treated as optional if not provided
188+
let nested = validate::<Nested>(NestedFragment {
189+
required: Some(WithFieldsFragment {
190+
name: Some("name".to_string()),
191+
..Default::default()
192+
}),
193+
optional: None,
194+
})
195+
.unwrap();
196+
assert_eq!(nested.optional, None);
197+
}
198+
}

src/config/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
1+
pub mod fragment;
12
pub mod merge;
Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
use darling::{ast::Data, FromDeriveInput, FromField, FromMeta, FromVariant};
2+
use proc_macro2::{Ident, TokenStream};
3+
use quote::{format_ident, quote};
4+
use syn::{DeriveInput, Expr, GenericArgument, Type};
5+
6+
#[derive(FromDeriveInput)]
7+
#[darling(attributes(fragment))]
8+
pub struct FragmentInput {
9+
ident: Ident,
10+
data: Data<FragmentVariant, FragmentField>,
11+
}
12+
13+
#[derive(FromVariant)]
14+
struct FragmentVariant {}
15+
16+
#[derive(FromField)]
17+
#[darling(attributes(fragment))]
18+
struct FragmentField {
19+
ident: Option<Ident>,
20+
ty: Type,
21+
default: Default,
22+
}
23+
24+
enum Default {
25+
None,
26+
FromDefaultTrait,
27+
Expr(Box<Expr>),
28+
}
29+
impl FromMeta for Default {
30+
fn from_none() -> Option<Self> {
31+
Some(Self::None)
32+
}
33+
34+
fn from_word() -> darling::Result<Self> {
35+
Ok(Self::FromDefaultTrait)
36+
}
37+
38+
fn from_value(value: &syn::Lit) -> darling::Result<Self> {
39+
Expr::from_value(value).map(Box::new).map(Self::Expr)
40+
}
41+
}
42+
43+
fn only<I: IntoIterator>(iter: I) -> Option<I::Item> {
44+
let mut iter = iter.into_iter();
45+
let item = iter.next()?;
46+
if iter.next().is_some() {
47+
None
48+
} else {
49+
Some(item)
50+
}
51+
}
52+
53+
fn extract_inner_option_type(ty: &Type) -> Option<&Type> {
54+
let path = if let Type::Path(path) = ty {
55+
path
56+
} else {
57+
return None;
58+
};
59+
let seg = only(&path.path.segments)?;
60+
if seg.ident != "Option" {
61+
return None;
62+
}
63+
let args = if let syn::PathArguments::AngleBracketed(args) = &seg.arguments {
64+
args
65+
} else {
66+
return None;
67+
};
68+
let arg = only(&args.args)?;
69+
if let GenericArgument::Type(arg_ty) = arg {
70+
Some(arg_ty)
71+
} else {
72+
None
73+
}
74+
}
75+
76+
pub fn derive(input: DeriveInput) -> TokenStream {
77+
let FragmentInput { ident, data } = match FragmentInput::from_derive_input(&input) {
78+
Ok(input) => input,
79+
Err(err) => return err.write_errors(),
80+
};
81+
let fields = match data {
82+
Data::Enum(_) => todo!(),
83+
Data::Struct(fields) => fields.fields,
84+
};
85+
86+
let fragment_ident = format_ident!("{ident}Fragment");
87+
let fragment_fields = fields
88+
.iter()
89+
.map(
90+
|FragmentField {
91+
ident,
92+
ty,
93+
default: _,
94+
}| {
95+
let ty = extract_inner_option_type(ty).unwrap_or(ty);
96+
quote! { #ident: Option<<#ty as FromFragment>::Fragment>, }
97+
},
98+
)
99+
.collect::<TokenStream>();
100+
101+
let from_fragment_fields = fields
102+
.iter()
103+
.map(|FragmentField { ident, ty, default }| {
104+
let ident_name = ident.as_ref().map(ToString::to_string);
105+
let inner_option_ty = extract_inner_option_type(ty);
106+
let is_option_wrapped = inner_option_ty.is_some();
107+
let ty = inner_option_ty.unwrap_or(ty);
108+
let wrapped_value = if is_option_wrapped {
109+
quote! { Some(value) }
110+
} else {
111+
quote! { value }
112+
};
113+
let default_fragment_value = match default {
114+
Default::Expr(default) => quote! { Some(#default) },
115+
Default::FromDefaultTrait => quote! { Some(Default::default()) },
116+
Default::None => quote! { None },
117+
};
118+
let mut fragment_value = quote! { fragment.#ident.or_else(|| #default_fragment_value) };
119+
if !is_option_wrapped {
120+
fragment_value = quote! { #fragment_value.or_else(#ty::default_fragment) };
121+
}
122+
let default_value = if is_option_wrapped {
123+
quote! { None }
124+
} else {
125+
quote! { return Err(validator.error_required()) }
126+
};
127+
let value = quote! {
128+
if let Some(value) = #fragment_value {
129+
let value = FromFragment::from_fragment(value, validator)?;
130+
#wrapped_value
131+
} else {
132+
#default_value
133+
}
134+
};
135+
quote! {
136+
#ident: {
137+
let validator = validator.field(#ident_name);
138+
#value
139+
},
140+
}
141+
})
142+
.collect::<TokenStream>();
143+
144+
quote! {
145+
#[derive(Default)]
146+
struct #fragment_ident {
147+
#fragment_fields
148+
}
149+
150+
impl FromFragment for #ident {
151+
type Fragment = #fragment_ident;
152+
153+
fn from_fragment(fragment: Self::Fragment, validator: Validator) -> Result<Self, ValidationError> {
154+
Ok(Self {
155+
#from_fragment_fields
156+
})
157+
}
158+
159+
fn default_fragment() -> Option<Self::Fragment> {
160+
Some(#fragment_ident::default())
161+
}
162+
}
163+
}
164+
}

stackable-operator-derive/src/lib.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ use proc_macro2::{Ident, Span, TokenStream};
66
use quote::{format_ident, quote};
77
use syn::{parse_macro_input, parse_quote, Generics, Index, Path, WherePredicate};
88

9+
mod fragment;
10+
911
#[derive(FromMeta)]
1012
struct PathOverrides {
1113
#[darling(default = "PathOverrides::default_merge")]
@@ -224,3 +226,8 @@ fn prefix_ident(ident: Result<&Ident, usize>, prefix: &Ident) -> Ident {
224226
Err(index) => format_ident!("{prefix}_{index}"),
225227
}
226228
}
229+
230+
#[proc_macro_derive(Fragment, attributes(fragment))]
231+
pub fn derive_fragment(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
232+
fragment::derive(parse_macro_input!(input)).into()
233+
}

0 commit comments

Comments
 (0)