Skip to main content

krabka_protocol_codegen/emit/
api_key_enum_quote.rs

1//! The `ApiKey` enum, built as a `proc_macro2` token stream via `quote!`
2//! instead of hand-written `write!` string templating.
3//!
4//! Compare with `api_key_enum::emit`. The structural payload is identical:
5//! the same variants, discriminants, and doc text. Only the *emission
6//! mechanism* differs. Things to notice:
7//!
8//! * No manual brace or indent bookkeeping. The `quote!` block reads like the
9//!   target code, and repetition is `#(#variants)*` instead of a `for` loop
10//!   that `writeln!`s fragments.
11//! * `syn::parse2` validates that the output is real Rust *at generation
12//!   time*. A malformed template fails here with a parse error, not three
13//!   crates downstream when the compiler reads the generated file.
14//! * `emit` returns the raw token string. The `rustfmt` pass in `crate::fmt`,
15//!   which the regeneration binary runs, does the formatting.
16//!
17//! One drawback remains. `//`-style line comments are not tokens, so this
18//! module prepends the `// AUTO-GENERATED` banner as a string. Doc comments
19//! become `#[doc = "…"]` attributes, and rustfmt keeps them in that form. It
20//! does not rewrite them to `///` without the unstable
21//! `normalize_doc_attributes`.
22
23use std::collections::BTreeMap;
24
25use proc_macro2::Literal;
26use quote::{format_ident, quote};
27
28use crate::{
29    emit::{EmitError, common::validate},
30    ir::{MessageSpec, MessageType},
31};
32
33/// # Errors
34/// Returns an error when the generated code is not valid Rust, which is an
35/// emitter bug.
36pub fn emit(specs: &[MessageSpec], schemas_version: &str) -> Result<String, EmitError> {
37    // Identical selection logic to api_key_enum::emit.
38    let mut by_key: BTreeMap<i16, &MessageSpec> = BTreeMap::new();
39    for s in specs {
40        if matches!(s.message_type, MessageType::Request)
41            && let Some(k) = s.api_key
42            && !s.valid_versions.is_empty()
43        {
44            by_key.insert(k, s);
45        }
46    }
47
48    let variants = by_key.iter().map(|(k, s)| {
49        let pascal = s.name.trim_end_matches("Request");
50        let ident = format_ident!("{pascal}");
51        let disc = Literal::i16_unsuffixed(*k);
52        let r = s.valid_versions;
53        let max = if r.max == i16::MAX {
54            String::from("\u{221e}")
55        } else {
56            r.max.to_string()
57        };
58        let doc = format!(
59            " `{pascal}Request` (versions {min}\u{2013}{max}).",
60            min = r.min
61        );
62        quote! {
63            #[doc = #doc]
64            #ident = #disc,
65        }
66    });
67
68    let all_entries = by_key.values().map(|s| {
69        let ident = format_ident!("{}", s.name.trim_end_matches("Request"));
70        quote!(Self::#ident,)
71    });
72
73    let match_arms = by_key.iter().map(|(k, s)| {
74        let ident = format_ident!("{}", s.name.trim_end_matches("Request"));
75        let lit = Literal::i16_unsuffixed(*k);
76        quote!(#lit => Some(Self::#ident),)
77    });
78
79    let tokens = quote! {
80        #[doc = " Kafka API key registry generated from the vendored schemas."]
81        #[doc = ""]
82        #[doc = " Each variant corresponds to a request/response pair defined in the schemas."]
83        #[doc = " Variants with `validVersions = \"none\"` are omitted (reserved keys with no"]
84        #[doc = " currently-valid wire version)."]
85        #[doc = ""]
86        #[doc = " The enum is `#[non_exhaustive]` so that adding variants for new Kafka API"]
87        #[doc = " keys in future schema updates is non-breaking."]
88        #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, strum::IntoStaticStr)]
89        #[repr(i16)]
90        #[non_exhaustive]
91        pub enum ApiKey {
92            #(#variants)*
93        }
94
95        impl ApiKey {
96            #[doc = " All known API keys, in ascending numeric order."]
97            pub const ALL: &'static [Self] = &[
98                #(#all_entries)*
99            ];
100
101            #[doc = " Resolve from numeric key; returns `None` for unknown keys."]
102            #[must_use]
103            pub fn from_i16(k: i16) -> Option<Self> {
104                match k {
105                    #(#match_arms)*
106                    _ => None,
107                }
108            }
109        }
110    };
111
112    // Validate at generation time; rustfmt (applied by the caller) does the
113    // actual formatting. See `crate::fmt`.
114    validate::<syn::File>(&tokens, "ApiKey enum")?;
115    let banner = format!(
116        "// AUTO-GENERATED by krabka-protocol-codegen against {schemas_version}. Do not edit.\n\
117         // To regenerate: ./tools/regenerate.sh\n\n"
118    );
119    Ok(format!("{banner}{tokens}"))
120}