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_krabka_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_krabka_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("krabka-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!(::krabka_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 generated_config_def_cases_match_expected_entries() {
352 let cases = [
353 (
354 "secret fields",
355 r"
356 struct Demo {
357 #[config(secret)]
358 password: SecretString,
359 #[config(secret)]
360 maybe_password: Option<SecretString>,
361 #[config(required, secret)]
362 required_maybe_password: Option<SecretString>,
363 }
364 ",
365 [
366 "def=def.secret(\"password\");",
367 "def=def.optional(\"maybe_password\",",
368 "def=def.secret(\"required_maybe_password\");",
369 ],
370 ),
371 (
372 "plain fields",
373 r"
374 struct Demo {
375 database_url: String,
376 note: Option<String>,
377 #[config(required)]
378 required_note: Option<String>,
379 }
380 ",
381 [
382 "def=def.required(\"database_url\",",
383 "def=def.optional(\"note\",",
384 "def=def.required(\"required_note\",",
385 ],
386 ),
387 ];
388
389 for (_name, input, expected) in cases {
390 let expanded = expanded_config_def(input);
391 let actual: Vec<_> = expected
392 .iter()
393 .copied()
394 .filter(|needle| expanded.contains(needle))
395 .collect();
396 assert2::assert!(actual == expected);
397 }
398 }
399
400 #[test]
401 fn type_info_recognizes_options_secrets_and_duration_paths() {
402 let optional_secret = analyze_type(&parse_type("Option<SecretString>")).unwrap();
403 assert2::assert!(optional_secret.is_option);
404 assert2::assert!(optional_secret.is_secret);
405
406 let string_vec = analyze_type(&parse_type("Vec<String>")).unwrap();
407 check!((string_vec.is_option, string_vec.is_secret) == (false, false));
408 check!(analyze_type(&parse_type("Vec<u8>")).is_err());
409
410 for scalar in [
411 "String", "bool", "i8", "i16", "i32", "i64", "isize", "u8", "u16", "u32", "u64",
412 "usize", "f32", "f64", "Value",
413 ] {
414 let info = analyze_type(&parse_type(scalar)).unwrap();
415 assert2::assert!(!info.is_option);
416 assert2::assert!(!info.is_secret);
417 }
418
419 for (_name, ty) in [
420 ("standard_duration_path", "std::time::Duration"),
421 ("absolute_duration_path", "::std::time::Duration"),
422 ] {
423 let info = analyze_type(&parse_type(ty)).unwrap();
424 assert2::assert!(!info.is_option);
425 assert2::assert!(!info.is_secret);
426 }
427 }
428
429 #[test]
430 fn secret_field_validation_requires_secret_attr_and_secret_type() {
431 let secret_ty = parse_type("SecretString");
432 let string_ty = parse_type("String");
433
434 let explicit_secret = FieldAttrs {
435 secret: true,
436 ..FieldAttrs::default()
437 };
438 validate_field_attrs(&explicit_secret, &secret_ty).unwrap();
439
440 let secret_attr_on_string = FieldAttrs {
441 secret: true,
442 ..FieldAttrs::default()
443 };
444 for (_name, attrs, ty) in [
445 (
446 "secret_type_without_attribute",
447 FieldAttrs::default(),
448 &secret_ty,
449 ),
450 (
451 "secret_attribute_on_string",
452 secret_attr_on_string,
453 &string_ty,
454 ),
455 ] {
456 assert2::assert!(validate_field_attrs(&attrs, ty).is_err());
457 }
458 }
459}