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}