1use proc_macro::TokenStream;
7use proc_macro2::Span;
8use quote::{format_ident, quote};
9use syn::{
10 Attribute, Error, Expr, ExprLit, Ident, Lit, LitStr, Meta, Result, Token, Type, Visibility,
11 braced,
12 parse::{Parse, ParseStream},
13 parse_macro_input,
14 punctuated::Punctuated,
15};
16
17#[proc_macro]
20pub fn kafka_config(input: TokenStream) -> TokenStream {
21 let input = parse_macro_input!(input as ConfigInput);
22 expand_config(input)
23 .unwrap_or_else(Error::into_compile_error)
24 .into()
25}
26
27struct ConfigInput {
28 attrs: Vec<Attribute>,
29 visibility: Visibility,
30 name: Ident,
31 fields: Punctuated<ConfigField, Token![,]>,
32}
33
34impl Parse for ConfigInput {
35 fn parse(input: ParseStream<'_>) -> Result<Self> {
36 let attrs = input.call(Attribute::parse_outer)?;
37 let visibility = input.parse()?;
38 let name = input.parse()?;
39 let content;
40 braced!(content in input);
41 let fields = content.parse_terminated(ConfigField::parse, Token![,])?;
42 Ok(Self {
43 attrs,
44 visibility,
45 name,
46 fields,
47 })
48 }
49}
50
51struct ConfigField {
52 attrs: Vec<Attribute>,
53 name: Ident,
54 ty: Type,
55}
56
57impl Parse for ConfigField {
58 fn parse(input: ParseStream<'_>) -> Result<Self> {
59 let attrs = input.call(Attribute::parse_outer)?;
60 let name = input.parse()?;
61 let _: Token![:] = input.parse()?;
62 let ty = input.parse()?;
63 Ok(Self { attrs, name, ty })
64 }
65}
66
67#[derive(Clone, Copy)]
68enum ClientKindAttr {
69 Producer,
70 Consumer,
71 Admin,
72}
73
74struct FieldAttrs {
75 key: LitStr,
76 required: bool,
77 default: Option<Expr>,
78 kafka_type: LitStr,
79 kafka_default: LitStr,
80 status: StatusAttr,
81 origin: OriginAttr,
82 source: LitStr,
83 platforms: Vec<LitStr>,
84 feature: Option<LitStr>,
85 comment: LitStr,
86}
87
88#[derive(Clone)]
89enum StatusAttr {
90 Native,
91 NativeReview,
92 SkipJavaOnly,
93 FeatureGated(LitStr),
94 Future(LitStr),
95}
96
97#[derive(Clone, Copy)]
98enum OriginAttr {
99 Kafka,
100 KacrabRuntime,
101}
102
103type Tokens = proc_macro2::TokenStream;
104
105struct FieldExpansion {
106 struct_field: Tokens,
107 builder_field: Tokens,
108 builder_initializer: Tokens,
109 setter: Tokens,
110 build_field: Tokens,
111 constant: Tokens,
112 metadata: Tokens,
113 key_match: Tokens,
114 property_parse: Tokens,
115}
116
117fn expand_config(input: ConfigInput) -> Result<proc_macro2::TokenStream> {
118 let client = parse_client(&input.attrs)?;
119 let client_tokens = client_tokens(client);
120 let client_label = client_label(client);
121 let visibility = input.visibility;
122 let name = input.name;
123 let builder_name = format_ident!("{name}Builder");
124 let struct_doc = LitStr::new(
125 &format!("Typed Kafka {client_label} configuration."),
126 Span::call_site(),
127 );
128 let builder_doc = LitStr::new(&format!("Builder for [`{name}`]."), Span::call_site());
129
130 let expansions = input
131 .fields
132 .into_iter()
133 .map(|field| expand_field(field, &client_tokens))
134 .collect::<Result<Vec<_>>>()?;
135 let struct_fields: Vec<_> = expansions.iter().map(|field| &field.struct_field).collect();
136 let builder_fields: Vec<_> = expansions
137 .iter()
138 .map(|field| &field.builder_field)
139 .collect();
140 let builder_initializers: Vec<_> = expansions
141 .iter()
142 .map(|field| &field.builder_initializer)
143 .collect();
144 let setters: Vec<_> = expansions.iter().map(|field| &field.setter).collect();
145 let build_fields: Vec<_> = expansions.iter().map(|field| &field.build_field).collect();
146 let constants: Vec<_> = expansions.iter().map(|field| &field.constant).collect();
147 let metadata: Vec<_> = expansions.iter().map(|field| &field.metadata).collect();
148 let key_matches: Vec<_> = expansions.iter().map(|field| &field.key_match).collect();
149 let property_parsers: Vec<_> = expansions
150 .iter()
151 .map(|field| &field.property_parse)
152 .collect();
153
154 Ok(quote! {
155 #[doc = #struct_doc]
156 #[derive(Clone, Debug)]
157 #visibility struct #name {
158 #(#struct_fields,)*
159 }
160
161 #[doc = #builder_doc]
162 #[derive(Clone, Debug, Default)]
163 #visibility struct #builder_name {
164 #(#builder_fields,)*
165 }
166
167 impl #name {
168 #(#constants)*
169
170 pub const CONFIG_KEYS: &'static [::kacrab::config::ConfigEntry] = &[
172 #(#metadata,)*
173 ];
174
175 #[must_use]
177 pub fn builder() -> #builder_name {
178 #builder_name {
179 #(#builder_initializers,)*
180 }
181 }
182
183 pub fn from_properties(
185 properties: &::kacrab::config::Properties,
186 unknown_key_policy: ::kacrab::config::UnknownKeyPolicy,
187 ) -> ::core::result::Result<(#name, ::kacrab::config::WarningReport), ::kacrab::config::ConfigError> {
188 let report = ::kacrab::config::validate_properties(
189 #client_tokens,
190 properties,
191 unknown_key_policy,
192 )?;
193 for (key, _value) in properties.iter() {
194 let key = key.as_str();
195 if !::core::matches!(key, #(#key_matches)|*) {
196 return ::core::result::Result::Err(::kacrab::config::ConfigError::UnsupportedKey {
197 client: #client_tokens,
198 key: key.into(),
199 });
200 }
201 }
202 let mut builder = Self::builder();
203 #(#property_parsers)*
204 let config = builder.build()?;
205 ::core::result::Result::Ok((config, report))
206 }
207 }
208
209 impl #builder_name {
210 #(#setters)*
211
212 pub fn build(self) -> ::core::result::Result<#name, ::kacrab::config::ConfigError> {
214 ::core::result::Result::Ok(#name {
215 #(#build_fields,)*
216 })
217 }
218 }
219 })
220}
221
222fn expand_field(field: ConfigField, client_tokens: &Tokens) -> Result<FieldExpansion> {
223 let attrs = parse_field_attrs(&field.attrs)?;
224 if attrs.required && attrs.default.is_some() {
225 return Err(Error::new_spanned(
226 field.name,
227 "config field cannot be both required and defaulted",
228 ));
229 }
230
231 let field_name = field.name;
232 let field_ty = field.ty;
233 let option_field_ty = &field_ty;
234 let key = attrs.key;
235 let kafka_type = attrs.kafka_type;
236 let kafka_default = attrs.kafka_default;
237 let source = attrs.source;
238 let comment = attrs.comment;
239 let status = status_tokens(attrs.status);
240 let origin = origin_tokens(attrs.origin);
241 let platforms = attrs.platforms;
242 let feature = option_lit_str_tokens(attrs.feature.as_ref());
243 let const_name = format_ident!("{}_CONFIG", screaming_snake(&field_name.to_string()));
244 let const_doc = LitStr::new(
245 &format!("Kafka property key `{}`.", key.value()),
246 key.span(),
247 );
248 let setter_doc = LitStr::new(
249 &format!("Sets the `{}` Kafka property.", key.value()),
250 key.span(),
251 );
252
253 let struct_field = quote! {
254 #[doc = #comment]
255 pub #field_name: #field_ty
256 };
257 let builder_field = quote! {
258 #field_name: ::core::option::Option<#option_field_ty>
259 };
260 let builder_initializer = quote! {
261 #field_name: ::core::option::Option::None
262 };
263 let setter = quote! {
264 #[doc = #setter_doc]
265 #[must_use]
266 pub fn #field_name(mut self, value: impl ::core::convert::Into<#field_ty>) -> Self {
267 self.#field_name = ::core::option::Option::Some(value.into());
268 self
269 }
270 };
271 let constant = quote! {
272 #[doc = #const_doc]
273 pub const #const_name: &'static str = #key;
274 };
275 let metadata = quote! {
276 ::kacrab::config::ConfigEntry {
277 client: #client_tokens,
278 origin: #origin,
279 key: #key,
280 rust_field: ::core::stringify!(#field_name),
281 kafka_type: #kafka_type,
282 default: #kafka_default,
283 status: #status,
284 comment: #comment,
285 documentation: "",
286 source: #source,
287 platforms: &[#(#platforms),*],
288 feature: #feature,
289 }
290 };
291 let key_match = quote!(#key);
292 let build_field = build_field_tokens(
293 attrs.required,
294 attrs.default,
295 &field_name,
296 &key,
297 client_tokens,
298 )?;
299 let property_parse = property_parse_tokens(&field_name, &field_ty, &key, client_tokens);
300
301 Ok(FieldExpansion {
302 struct_field,
303 builder_field,
304 builder_initializer,
305 setter,
306 build_field,
307 constant,
308 metadata,
309 key_match,
310 property_parse,
311 })
312}
313
314fn property_parse_tokens(
315 field_name: &Ident,
316 field_ty: &Type,
317 key: &LitStr,
318 client_tokens: &Tokens,
319) -> Tokens {
320 quote! {
321 if let ::core::option::Option::Some(value) = properties.get(#key) {
322 let parsed = <#field_ty as ::kacrab::config::ParseConfigValue>::parse_config_value(value)
323 .map_err(|error| ::kacrab::config::ConfigError::InvalidValue {
324 client: #client_tokens,
325 key: #key,
326 target: error.target,
327 value: error.value,
328 })?;
329 builder = builder.#field_name(parsed);
330 }
331 }
332}
333
334fn build_field_tokens(
335 required: bool,
336 default: Option<Expr>,
337 field_name: &Ident,
338 key: &LitStr,
339 client_tokens: &Tokens,
340) -> Result<Tokens> {
341 if required {
342 return Ok(quote! {
343 #field_name: match self.#field_name {
344 ::core::option::Option::Some(value) => value,
345 ::core::option::Option::None => {
346 return ::core::result::Result::Err(::kacrab::config::ConfigError::MissingRequired {
347 client: #client_tokens,
348 key: #key,
349 });
350 }
351 }
352 });
353 }
354
355 let Some(default) = default else {
356 return Err(Error::new_spanned(
357 field_name,
358 "optional config field must define #[default(...)]",
359 ));
360 };
361 Ok(quote! {
362 #field_name: self.#field_name.unwrap_or_else(|| #default)
363 })
364}
365
366fn parse_client(attrs: &[Attribute]) -> Result<ClientKindAttr> {
367 for attr in attrs {
368 if !attr.path().is_ident("client") {
369 continue;
370 }
371 let expr = parse_expr_attr(attr, "client")?;
372 return parse_client_expr(&expr);
373 }
374 Err(Error::new(
375 Span::call_site(),
376 "kafka_config! requires #[client(Producer|Consumer|Admin)]",
377 ))
378}
379
380fn parse_client_expr(expr: &Expr) -> Result<ClientKindAttr> {
381 let Expr::Path(path) = expr else {
382 return Err(Error::new_spanned(
383 expr,
384 "expected Producer, Consumer, or Admin",
385 ));
386 };
387 let Some(ident) = path.path.get_ident() else {
388 return Err(Error::new_spanned(
389 expr,
390 "expected Producer, Consumer, or Admin",
391 ));
392 };
393
394 match ident.to_string().as_str() {
395 "Producer" => Ok(ClientKindAttr::Producer),
396 "Consumer" => Ok(ClientKindAttr::Consumer),
397 "Admin" => Ok(ClientKindAttr::Admin),
398 _ => Err(Error::new_spanned(
399 ident,
400 "expected Producer, Consumer, or Admin",
401 )),
402 }
403}
404
405fn parse_field_attrs(attrs: &[Attribute]) -> Result<FieldAttrs> {
406 let mut key = None;
407 let mut required = false;
408 let mut default = None;
409 let mut kafka_type = None;
410 let mut kafka_default = None;
411 let mut status = None;
412 let mut origin = None;
413 let mut source = None;
414 let mut platforms = Vec::new();
415 let mut feature = None;
416 let mut comment = None;
417
418 for attr in attrs {
419 if attr.path().is_ident("key") {
420 key = Some(parse_lit_str_attr(attr, "key")?);
421 } else if attr.path().is_ident("required") {
422 parse_marker_attr(attr, "required")?;
423 required = true;
424 } else if attr.path().is_ident("default") {
425 default = Some(parse_expr_attr(attr, "default")?);
426 } else if attr.path().is_ident("kafka_type") {
427 kafka_type = Some(parse_lit_str_attr(attr, "kafka_type")?);
428 } else if attr.path().is_ident("kafka_default") {
429 kafka_default = Some(parse_lit_str_attr(attr, "kafka_default")?);
430 } else if attr.path().is_ident("status") {
431 status = Some(parse_status(attr)?);
432 } else if attr.path().is_ident("origin") {
433 origin = Some(parse_origin(attr)?);
434 } else if attr.path().is_ident("source") {
435 source = Some(parse_lit_str_attr(attr, "source")?);
436 } else if attr.path().is_ident("platforms") {
437 platforms = parse_lit_str_list_attr(attr, "platforms")?;
438 } else if attr.path().is_ident("feature") {
439 feature = Some(parse_lit_str_attr(attr, "feature")?);
440 } else if attr.path().is_ident("comment") {
441 comment = Some(parse_lit_str_attr(attr, "comment")?);
442 }
443 }
444
445 Ok(FieldAttrs {
446 key: required_attr(key, "key")?,
447 required,
448 default,
449 kafka_type: required_attr(kafka_type, "kafka_type")?,
450 kafka_default: required_attr(kafka_default, "kafka_default")?,
451 status: required_attr(status, "status")?,
452 origin: origin.unwrap_or(OriginAttr::Kafka),
453 source: required_attr(source, "source")?,
454 platforms,
455 feature,
456 comment: required_attr(comment, "comment")?,
457 })
458}
459
460fn parse_status(attr: &Attribute) -> Result<StatusAttr> {
461 let expr = parse_expr_attr(attr, "status")?;
462 parse_status_expr(&expr)
463}
464
465fn parse_origin(attr: &Attribute) -> Result<OriginAttr> {
466 let expr = parse_expr_attr(attr, "origin")?;
467 let Expr::Path(path) = &expr else {
468 return Err(Error::new_spanned(expr, "expected kafka or kacrab_runtime"));
469 };
470 let Some(ident) = path.path.get_ident() else {
471 return Err(Error::new_spanned(expr, "expected kafka or kacrab_runtime"));
472 };
473 match ident.to_string().as_str() {
474 "kafka" => Ok(OriginAttr::Kafka),
475 "kacrab_runtime" => Ok(OriginAttr::KacrabRuntime),
476 _ => Err(Error::new_spanned(
477 ident,
478 "expected kafka or kacrab_runtime",
479 )),
480 }
481}
482
483fn parse_status_expr(expr: &Expr) -> Result<StatusAttr> {
484 if let Expr::Path(path) = expr {
485 let Some(ident) = path.path.get_ident() else {
486 return Err(Error::new_spanned(expr, "unknown config status"));
487 };
488 return match ident.to_string().as_str() {
489 "native" => Ok(StatusAttr::Native),
490 "native_review" => Ok(StatusAttr::NativeReview),
491 "skip_java_only" => Ok(StatusAttr::SkipJavaOnly),
492 _ => Err(Error::new_spanned(ident, "unknown config status")),
493 };
494 }
495
496 if let Expr::Call(call) = expr
497 && let Expr::Path(path) = call.func.as_ref()
498 && call.args.len() == 1
499 && let Some(Expr::Lit(expr_lit)) = call.args.first()
500 && let Lit::Str(feature) = &expr_lit.lit
501 {
502 if path.path.is_ident("feature_gated") {
503 return Ok(StatusAttr::FeatureGated(feature.clone()));
504 }
505 if path.path.is_ident("future") {
506 return Ok(StatusAttr::Future(feature.clone()));
507 }
508 }
509
510 Err(Error::new_spanned(
511 expr,
512 "expected status native, native_review, skip_java_only, feature_gated(\"feature\"), or \
513 future(\"feature\")",
514 ))
515}
516
517fn parse_lit_str_attr(attr: &Attribute, name: &str) -> Result<LitStr> {
518 let Expr::Lit(ExprLit {
519 lit: Lit::Str(value),
520 ..
521 }) = parse_expr_attr(attr, name)?
522 else {
523 return Err(Error::new_spanned(
524 attr,
525 format!("expected #[{name}(\"...\")]"),
526 ));
527 };
528 Ok(value)
529}
530
531fn parse_lit_str_list_attr(attr: &Attribute, name: &str) -> Result<Vec<LitStr>> {
532 let parser = Punctuated::<LitStr, Token![,]>::parse_terminated;
533 match &attr.meta {
534 Meta::List(_) => Ok(attr.parse_args_with(parser)?.into_iter().collect()),
535 _ => Err(Error::new_spanned(
536 attr,
537 format!("expected #[{name}(\"...\", ...)]"),
538 )),
539 }
540}
541
542fn parse_expr_attr(attr: &Attribute, name: &str) -> Result<Expr> {
543 match &attr.meta {
544 Meta::NameValue(meta) => Ok(meta.value.clone()),
545 Meta::List(_) => attr.parse_args(),
546 Meta::Path(_) => Err(Error::new_spanned(attr, format!("expected #[{name}(...)]"))),
547 }
548}
549
550fn parse_marker_attr(attr: &Attribute, name: &str) -> Result<()> {
551 if matches!(attr.meta, Meta::Path(_)) {
552 Ok(())
553 } else {
554 Err(Error::new_spanned(attr, format!("expected #[{name}]")))
555 }
556}
557
558fn required_attr<T>(value: Option<T>, name: &str) -> Result<T> {
559 value.ok_or_else(|| {
560 Error::new(
561 Span::call_site(),
562 format!("config field requires #[{name}(...)]"),
563 )
564 })
565}
566
567fn client_tokens(client: ClientKindAttr) -> proc_macro2::TokenStream {
568 match client {
569 ClientKindAttr::Producer => quote!(::kacrab::config::ClientKind::Producer),
570 ClientKindAttr::Consumer => quote!(::kacrab::config::ClientKind::Consumer),
571 ClientKindAttr::Admin => quote!(::kacrab::config::ClientKind::Admin),
572 }
573}
574
575const fn client_label(client: ClientKindAttr) -> &'static str {
576 match client {
577 ClientKindAttr::Producer => "producer",
578 ClientKindAttr::Consumer => "consumer",
579 ClientKindAttr::Admin => "admin",
580 }
581}
582
583fn status_tokens(status: StatusAttr) -> proc_macro2::TokenStream {
584 match status {
585 StatusAttr::Native => quote!(::kacrab::config::ConfigStatus::Native),
586 StatusAttr::NativeReview => quote!(::kacrab::config::ConfigStatus::NativeReview),
587 StatusAttr::SkipJavaOnly => quote!(::kacrab::config::ConfigStatus::SkipJavaOnly),
588 StatusAttr::FeatureGated(feature) => {
589 quote!(::kacrab::config::ConfigStatus::FeatureGated { feature: #feature })
590 },
591 StatusAttr::Future(feature) => {
592 quote!(::kacrab::config::ConfigStatus::Future { feature: #feature })
593 },
594 }
595}
596
597fn origin_tokens(origin: OriginAttr) -> proc_macro2::TokenStream {
598 match origin {
599 OriginAttr::Kafka => quote!(::kacrab::config::ConfigOrigin::Kafka),
600 OriginAttr::KacrabRuntime => quote!(::kacrab::config::ConfigOrigin::KacrabRuntime),
601 }
602}
603
604fn option_lit_str_tokens(value: Option<&LitStr>) -> proc_macro2::TokenStream {
605 value.map_or_else(
606 || quote!(::core::option::Option::None),
607 |value| quote!(::core::option::Option::Some(#value)),
608 )
609}
610
611fn screaming_snake(value: &str) -> String {
612 let mut result = String::with_capacity(value.len());
613 for (index, ch) in value.chars().enumerate() {
614 if ch.is_ascii_uppercase() && index > 0 {
615 result.push('_');
616 }
617 result.push(ch.to_ascii_uppercase());
618 }
619 result
620}