Skip to main content

crabka_connect_derive/
lib.rs

1use std::collections::BTreeSet;
2
3use proc_macro::TokenStream;
4use proc_macro_crate::{FoundCrate, crate_name};
5use quote::quote;
6use syn::{
7    AngleBracketedGenericArguments, Attribute, Data, DeriveInput, Expr, Fields, GenericArgument,
8    Generics, PathArguments, Type, parse_macro_input,
9};
10
11#[proc_macro_derive(ConnectorConfig, attributes(config))]
12pub fn derive_connector_config(input: TokenStream) -> TokenStream {
13    let input = parse_macro_input!(input as DeriveInput);
14    expand_connector_config(input)
15        .unwrap_or_else(syn::Error::into_compile_error)
16        .into()
17}
18
19fn expand_connector_config(input: DeriveInput) -> syn::Result<proc_macro2::TokenStream> {
20    let DeriveInput {
21        attrs,
22        ident,
23        generics,
24        data,
25        ..
26    } = input;
27    reject_generics(&generics)?;
28
29    let crate_path = resolve_crabka_connect_path(&attrs)?;
30    let fields = match data {
31        Data::Struct(data) => match data.fields {
32            Fields::Named(fields) => fields.named,
33            _ => {
34                return Err(syn::Error::new_spanned(
35                    ident,
36                    "ConnectorConfig can only be derived for structs with named fields",
37                ));
38            }
39        },
40        _ => {
41            return Err(syn::Error::new_spanned(
42                ident,
43                "ConnectorConfig can only be derived for structs",
44            ));
45        }
46    };
47
48    let mut def_steps = Vec::new();
49    let mut initializers = Vec::new();
50    let mut seen_keys = BTreeSet::new();
51
52    for field in fields {
53        let field_ident = field.ident.expect("named fields have identifiers");
54        let attrs = FieldAttrs::parse(&field.attrs)?;
55        let ty = field.ty;
56
57        let type_info = analyze_type(&ty)?;
58        validate_field_attrs(&attrs, &ty)?;
59        let key = attrs.name.unwrap_or_else(|| field_ident.to_string());
60        if !seen_keys.insert(key.clone()) {
61            return Err(syn::Error::new_spanned(
62                &field_ident,
63                format!("duplicate connector config key `{key}`"),
64            ));
65        }
66        let key_lit = syn::LitStr::new(&key, field_ident.span());
67
68        if attrs.secret {
69            if attrs.required || !type_info.is_option {
70                def_steps.push(quote! {
71                    def = def.secret(#key_lit);
72                });
73            } else {
74                def_steps.push(quote! {
75                    def = def.optional(
76                        #key_lit,
77                        <#ty as #crate_path::FromResolvedValue>::KIND,
78                    );
79                });
80            }
81        } else if let Some(default) = attrs.default {
82            def_steps.push(quote! {
83                def = def.default(
84                    #key_lit,
85                    <#ty as #crate_path::FromResolvedValue>::KIND,
86                    #crate_path::__serde_json::json!(#default),
87                );
88            });
89        } else if attrs.required || !type_info.is_option {
90            def_steps.push(quote! {
91                def = def.required(#key_lit, <#ty as #crate_path::FromResolvedValue>::KIND);
92            });
93        } else {
94            def_steps.push(quote! {
95                def = def.optional(#key_lit, <#ty as #crate_path::FromResolvedValue>::KIND);
96            });
97        }
98
99        initializers.push(quote! {
100            #field_ident: <#ty as #crate_path::FromResolvedValue>::from_resolved_value(
101                config,
102                #key_lit,
103            )?
104        });
105    }
106
107    Ok(quote! {
108        impl #crate_path::ConnectorConfig for #ident {
109            fn config_def() -> #crate_path::ConfigDef {
110                let mut def = #crate_path::ConfigDef::new(stringify!(#ident));
111                #(#def_steps)*
112                def
113            }
114
115            fn from_resolved(
116                config: &#crate_path::ResolvedConfig,
117            ) -> #crate_path::ConfigResult<Self> {
118                Ok(Self {
119                    #(#initializers,)*
120                })
121            }
122        }
123    })
124}
125
126fn reject_generics(generics: &Generics) -> syn::Result<()> {
127    if generics.params.is_empty() && generics.where_clause.is_none() {
128        return Ok(());
129    }
130
131    Err(syn::Error::new_spanned(
132        generics,
133        "ConnectorConfig does not support generic structs",
134    ))
135}
136
137fn resolve_crabka_connect_path(attrs: &[Attribute]) -> syn::Result<proc_macro2::TokenStream> {
138    let mut override_path = None;
139    for attr in attrs {
140        if !attr.path().is_ident("config") {
141            continue;
142        }
143        attr.parse_nested_meta(|meta| {
144            if meta.path.is_ident("crate") {
145                let value = meta.value()?;
146                let lit: syn::LitStr = value.parse()?;
147                let path: syn::Path = lit.parse()?;
148                override_path = Some(quote!(#path));
149                Ok(())
150            } else {
151                Err(meta.error("unsupported container config attribute"))
152            }
153        })?;
154    }
155
156    if let Some(path) = override_path {
157        return Ok(path);
158    }
159
160    match crate_name("crabka-connect") {
161        Ok(FoundCrate::Name(name)) => {
162            let ident = syn::Ident::new(&name, proc_macro2::Span::call_site());
163            Ok(quote!(::#ident))
164        }
165        Ok(FoundCrate::Itself) | Err(_) => Ok(quote!(::crabka_connect)),
166    }
167}
168
169#[derive(Default)]
170struct FieldAttrs {
171    required: bool,
172    secret: bool,
173    default: Option<Expr>,
174    name: Option<String>,
175}
176
177impl FieldAttrs {
178    fn parse(attrs: &[Attribute]) -> syn::Result<Self> {
179        let mut out = Self::default();
180        for attr in attrs {
181            if !attr.path().is_ident("config") {
182                continue;
183            }
184            attr.parse_nested_meta(|meta| {
185                if meta.path.is_ident("required") {
186                    out.required = true;
187                    return Ok(());
188                }
189                if meta.path.is_ident("secret") {
190                    out.secret = true;
191                    return Ok(());
192                }
193                if meta.path.is_ident("default") {
194                    let value = meta.value()?;
195                    out.default = Some(value.parse()?);
196                    return Ok(());
197                }
198                if meta.path.is_ident("name") {
199                    let value = meta.value()?;
200                    let lit: syn::LitStr = value.parse()?;
201                    out.name = Some(lit.value());
202                    return Ok(());
203                }
204                Err(meta.error("unsupported config attribute"))
205            })?;
206
207            if out.secret && out.default.is_some() {
208                return Err(syn::Error::new_spanned(
209                    attr,
210                    "secret fields cannot declare defaults",
211                ));
212            }
213        }
214
215        Ok(out)
216    }
217}
218
219#[derive(Debug, Clone, Copy, Eq, PartialEq)]
220struct TypeInfo {
221    is_option: bool,
222    is_secret: bool,
223}
224
225fn analyze_type(ty: &Type) -> syn::Result<TypeInfo> {
226    type_info(ty)
227        .ok_or_else(|| syn::Error::new_spanned(ty, "unsupported ConnectorConfig field type"))
228}
229
230fn validate_field_attrs(attrs: &FieldAttrs, ty: &Type) -> syn::Result<()> {
231    let type_info = analyze_type(ty)?;
232    if attrs.secret && !type_info.is_secret {
233        return Err(syn::Error::new_spanned(
234            ty,
235            "#[config(secret)] fields must have type SecretString or Option<SecretString>",
236        ));
237    }
238    if type_info.is_secret && !attrs.secret {
239        return Err(syn::Error::new_spanned(
240            ty,
241            "SecretString fields must be marked #[config(secret)]",
242        ));
243    }
244    Ok(())
245}
246
247fn type_info(ty: &Type) -> Option<TypeInfo> {
248    let Type::Path(type_path) = ty else {
249        return None;
250    };
251
252    if type_path.qself.is_some() {
253        return None;
254    }
255
256    let segment = type_path.path.segments.last()?;
257
258    if segment.ident == "Option" {
259        let inner = single_generic_argument(&segment.arguments)?;
260        let inner = type_info(inner)?;
261        return Some(TypeInfo {
262            is_option: true,
263            is_secret: inner.is_secret,
264        });
265    }
266
267    let ident = &segment.ident;
268    let is_supported_scalar = ident == "String"
269        || ident == "bool"
270        || ident == "i8"
271        || ident == "i16"
272        || ident == "i32"
273        || ident == "i64"
274        || ident == "isize"
275        || ident == "u8"
276        || ident == "u16"
277        || ident == "u32"
278        || ident == "u64"
279        || ident == "usize"
280        || ident == "f32"
281        || ident == "f64"
282        || ident == "Value"
283        || ident == "SecretString"
284        || ident == "Duration";
285    if is_supported_scalar && segment.arguments.is_empty() {
286        return Some(TypeInfo {
287            is_option: false,
288            is_secret: segment.ident == "SecretString",
289        });
290    }
291
292    if segment.ident == "Vec"
293        && single_generic_argument(&segment.arguments)
294            .is_some_and(|inner| path_ends_with_ident(inner, "String"))
295    {
296        return Some(TypeInfo {
297            is_option: false,
298            is_secret: false,
299        });
300    }
301
302    None
303}
304
305fn single_generic_argument(args: &PathArguments) -> Option<&Type> {
306    let PathArguments::AngleBracketed(AngleBracketedGenericArguments { args, .. }) = args else {
307        return None;
308    };
309    let mut args = args.iter();
310    let Some(GenericArgument::Type(inner)) = args.next() else {
311        return None;
312    };
313    if args.next().is_none() {
314        Some(inner)
315    } else {
316        None
317    }
318}
319
320fn path_ends_with_ident(ty: &Type, expected: &str) -> bool {
321    let Type::Path(type_path) = ty else {
322        return false;
323    };
324    type_path
325        .path
326        .segments
327        .last()
328        .is_some_and(|segment| segment.ident == expected && segment.arguments.is_empty())
329}
330
331#[cfg(test)]
332mod tests {
333    use assert2::check;
334
335    use super::*;
336
337    fn expanded_config_def(input: &str) -> String {
338        let input: DeriveInput = syn::parse_str(input).unwrap();
339        expand_connector_config(input)
340            .unwrap()
341            .to_string()
342            .split_whitespace()
343            .collect()
344    }
345
346    fn parse_type(input: &str) -> Type {
347        syn::parse_str(input).unwrap()
348    }
349
350    #[test]
351    fn config_def_marks_secret_required_and_optional_fields() {
352        let expanded = expanded_config_def(
353            r"
354            struct Demo {
355                #[config(secret)]
356                password: SecretString,
357                #[config(secret)]
358                maybe_password: Option<SecretString>,
359                #[config(required, secret)]
360                required_maybe_password: Option<SecretString>,
361            }
362            ",
363        );
364
365        for needle in [
366            "def=def.secret(\"password\");",
367            "def=def.optional(\"maybe_password\",",
368            "def=def.secret(\"required_maybe_password\");",
369        ] {
370            assert!(
371                expanded.contains(needle),
372                "missing {needle:?} in {expanded}"
373            );
374        }
375    }
376
377    #[test]
378    fn config_def_marks_plain_required_and_optional_fields() {
379        let expanded = expanded_config_def(
380            r"
381            struct Demo {
382                database_url: String,
383                note: Option<String>,
384                #[config(required)]
385                required_note: Option<String>,
386            }
387            ",
388        );
389
390        for needle in [
391            "def=def.required(\"database_url\",",
392            "def=def.optional(\"note\",",
393            "def=def.required(\"required_note\",",
394        ] {
395            assert!(
396                expanded.contains(needle),
397                "missing {needle:?} in {expanded}"
398            );
399        }
400    }
401
402    #[test]
403    fn type_info_recognizes_options_secrets_and_duration_paths() {
404        let optional_secret = analyze_type(&parse_type("Option<SecretString>")).unwrap();
405        assert!(optional_secret.is_option);
406        assert!(optional_secret.is_secret);
407
408        let string_vec = analyze_type(&parse_type("Vec<String>")).unwrap();
409        check!(!string_vec.is_option);
410        check!(!string_vec.is_secret);
411        check!(analyze_type(&parse_type("Vec<u8>")).is_err());
412
413        for scalar in [
414            "String", "bool", "i8", "i16", "i32", "i64", "isize", "u8", "u16", "u32", "u64",
415            "usize", "f32", "f64", "Value",
416        ] {
417            let info = analyze_type(&parse_type(scalar)).unwrap();
418            assert!(!info.is_option, "{scalar} is not optional");
419            assert!(!info.is_secret, "{scalar} is not secret");
420        }
421
422        let std_duration = analyze_type(&parse_type("std::time::Duration")).unwrap();
423        assert!(!std_duration.is_option);
424        assert!(!std_duration.is_secret);
425
426        let absolute_duration = analyze_type(&parse_type("::std::time::Duration")).unwrap();
427        assert!(!absolute_duration.is_option);
428        assert!(!absolute_duration.is_secret);
429    }
430
431    #[test]
432    fn secret_field_validation_requires_secret_attr_and_secret_type() {
433        let secret_ty = parse_type("SecretString");
434        let string_ty = parse_type("String");
435
436        let explicit_secret = FieldAttrs {
437            secret: true,
438            ..FieldAttrs::default()
439        };
440        validate_field_attrs(&explicit_secret, &secret_ty).unwrap();
441
442        let secret_without_attr = FieldAttrs::default();
443        assert!(validate_field_attrs(&secret_without_attr, &secret_ty).is_err());
444
445        let secret_attr_on_string = FieldAttrs {
446            secret: true,
447            ..FieldAttrs::default()
448        };
449        assert!(validate_field_attrs(&secret_attr_on_string, &string_ty).is_err());
450    }
451}