Skip to main content

krabka_protocol_codegen/emit/
owned_quote.rs

1//! A `quote!`-based reimplementation of the `owned` emitter.
2//!
3//! This module builds the structural scaffolding as a
4//! `proc_macro2::TokenStream` instead of a concatenation with `write!`. That
5//! scaffolding is the struct definition, `Default`, the `Encode` and `Decode`
6//! impls, tagged-field framing, nested structs, and the `populated`
7//! constructor. `emit` returns the raw token text, plus a banner and the
8//! reused `default_json` and `ProtocolRequest` tails. The rustfmt pass in
9//! `crate::fmt`, which the regeneration binary runs, formats it.
10//!
11//! *Reuse* guarantees wire correctness. The leaf codec expressions
12//! (`encode_call`, `decode_call`, `encoded_len_expr`, and their variants), the
13//! import and constant blocks, the `default_json` tail, and the common-struct
14//! files all come verbatim from `super::owned`, whose bytes the protocol
15//! crate's round-trip and JVM-differential tests already cover. This module
16//! rebuilds only the *shape* around those leaves, which is where the `write!`
17//! templating was densest. `parse_tokens` lexes each leaf string into tokens.
18
19use std::collections::HashMap;
20
21use proc_macro2::{Ident, Literal, TokenStream};
22use quote::{format_ident, quote};
23
24use crate::{
25    emit::{
26        EmittedMessage,
27        common::{banner, parse_tokens, statement, tag_literal},
28        default_json,
29        owned::{
30            self, EmitError, base_type, decode_call, decode_call_with_buf, emit_common_imports,
31            encode_call, encode_call_option_as_non_nullable, encode_call_with_buf,
32            encoded_len_expr, field_forces_non_flex, flex_min, has_any_flex, has_float64_recursive,
33            is_nullable, is_tagged, needs_manual_default, nullable_split_cond, owned_default_expr,
34            owned_populated_value, struct_path_for, tagged_is_default_cond, tagged_version_cond,
35            version_cond, wrap_non_nullable_for_option,
36        },
37        protocol_request, tagged_fixture,
38    },
39    ir::{FieldSpec, MessageSpec, MessageType},
40    name_conv,
41    resolve::{self, Resolution, StructKind},
42    type_map,
43};
44
45type ResMap = HashMap<String, Resolution>;
46
47fn tagged_should_encode(f: &FieldSpec) -> Result<TokenStream, EmitError> {
48    if base_type(&f.field_type) == "bool" {
49        let field = format_ident!("{}", name_conv::field_name(&f.name));
50        return Ok(match &f.default {
51            Some(serde_json::Value::Bool(true)) => {
52                quote!(!self.#field)
53            }
54            Some(serde_json::Value::String(value)) if value == "true" => {
55                quote!(!self.#field)
56            }
57            Some(serde_json::Value::Bool(false)) => {
58                quote!(self.#field)
59            }
60            Some(serde_json::Value::String(value)) if value == "false" => quote!(self.#field),
61            _ => tagged_should_encode_from_default(f)?,
62        });
63    }
64    tagged_should_encode_from_default(f)
65}
66
67/// `tagged_should_encode` with the field's tagged-version range in front of
68/// it. KIP-482 binds a tag to the versions its schema declares, so a message
69/// must not write the tag below that range even when the value is non-default.
70fn gated_tagged_should_encode(f: &FieldSpec, flex_minimum: i16) -> Result<TokenStream, EmitError> {
71    let value = tagged_should_encode(f)?;
72    Ok(match tagged_version_cond(f, flex_minimum) {
73        // Every leaf of `tagged_should_encode` and `version_cond` binds tighter
74        // than `&&`, so neither side needs parentheses.
75        Some(cond) => {
76            let cond = parse_tokens(&cond)?;
77            quote!(#cond && #value)
78        }
79        None => value,
80    })
81}
82
83/// The `read_tagged_fields` match arm for one tagged field. `read` stores the
84/// decoded value and evaluates to `Ok(true)`.
85///
86/// KIP-482 binds a tag to the versions its schema declares. Kafka's generated
87/// `read` method throws `Tag N is not valid for version V` when a known tag
88/// arrives at any other flexible version, and does not keep the tag as an
89/// unknown field. A guarded arm plus a fallback arm for the same tag does the
90/// same here. A tag that covers every flexible version of its struct needs no
91/// guard.
92pub(crate) fn tagged_decode_arm(
93    f: &FieldSpec,
94    flex_minimum: i16,
95    read: &TokenStream,
96) -> Result<TokenStream, EmitError> {
97    let tag = tag_literal(f)?;
98    let Some(cond) = tagged_version_cond(f, flex_minimum) else {
99        return Ok(quote!(#tag => { #read }));
100    };
101    let cond = parse_tokens(&cond)?;
102    Ok(quote! {
103        #tag if #cond => { #read }
104        #tag => Err(ProtocolError::TagNotValidForVersion { tag: #tag, version }),
105    })
106}
107
108fn tagged_should_encode_from_default(f: &FieldSpec) -> Result<TokenStream, EmitError> {
109    let default = tagged_is_default_cond(f);
110    if let Some(option) = default.strip_suffix(".is_none()") {
111        return parse_tokens(&format!("{option}.is_some()"));
112    }
113    if let Some((value, expected)) = default.split_once(" == ") {
114        return parse_tokens(&format!("{value} != {expected}"));
115    }
116    if let Some(positive) = default.strip_prefix('!') {
117        return parse_tokens(positive);
118    }
119    let default = parse_tokens(&default)?;
120    Ok(quote!(!(#default)))
121}
122
123/// `is_flexible(version)` for top-level messages, and `version >= N` for
124/// nested structs whose version flows in from a parent. `N` is the message's
125/// flexible threshold.
126enum FlexSource {
127    TopLevel(i16),
128    Nested(i16),
129}
130
131impl FlexSource {
132    /// The flexible threshold of the struct. Tagged fields exist only at or
133    /// above it, so a tag whose range starts there needs no version test.
134    fn minimum(&self) -> i16 {
135        match self {
136            Self::TopLevel(fm) | Self::Nested(fm) => *fm,
137        }
138    }
139
140    fn tokens(&self) -> TokenStream {
141        match self {
142            Self::TopLevel(_) => quote!(is_flexible(version)),
143            Self::Nested(i16::MIN) => quote!(true),
144            Self::Nested(i16::MAX) => quote!(version == i16::MAX),
145            Self::Nested(fm) => {
146                let n = Literal::i16_unsuffixed(*fm);
147                quote!(version >= #n)
148            }
149        }
150    }
151}
152
153/// # Errors
154/// Returns an error when the schema model is invalid or generated Rust cannot be formatted or written.
155pub fn emit(spec: &MessageSpec, schemas_version: &str) -> Result<EmittedMessage, EmitError> {
156    // Resolve struct references — also validates that everything resolves.
157    let res_map = resolve::resolve_message(spec)?;
158    let lenient = matches!(spec.message_type, MessageType::Response);
159
160    // Top-level struct + impls + populated, then sibling structs for nested types.
161    let version_err = version_err(spec);
162    let mut structural = struct_block(
163        &name_conv::type_name(&spec.name),
164        &spec.fields,
165        &res_map,
166        &FlexSource::TopLevel(flex_min(spec)),
167        Some(version_err),
168        has_any_flex(spec),
169        lenient,
170    )?;
171    structural.extend(nested_structs(
172        &spec.fields,
173        flex_min(spec),
174        &res_map,
175        lenient,
176    )?);
177
178    // Build imports and constants as token streams, then render them.
179    let imports = owned::emit_imports(spec).to_string();
180    let constants = owned::emit_constants(spec)?.to_string();
181    let banner = format!(
182        "// AUTO-GENERATED by krabka-protocol-codegen against {schemas_version}. Do not edit.\n\n"
183    );
184    let dj = default_json::emit_default_json(spec)?;
185    let tf = tagged_fixture::emit_tagged_fixture(spec, &res_map)?;
186    let pr = protocol_request::emit_protocol_request(spec)?.unwrap_or_default();
187
188    let primary = format!("{banner}{imports}{constants}\n{structural}\n{dj}{tf}{pr}");
189
190    // Validate at generation time; rustfmt (the caller's secondary step) also
191    // re-checks, but a syn error here is more precise.
192    syn::parse_str::<syn::File>(&primary).map_err(|e| {
193        EmitError::Unsupported(format!(
194            "owned_quote produced invalid Rust for {}: {e}",
195            spec.name
196        ))
197    })?;
198
199    let commons = emit_commons(spec, &res_map, schemas_version, lenient)?;
200
201    Ok(EmittedMessage { primary, commons })
202}
203
204/// Emit the standalone `.rs` file bodies for the message's `commonStructs`.
205///
206/// `commonStructs` are message-local. The emitter writes each one under a
207/// per-message nested module `common/<message_snake>/<struct_snake>`. The
208/// `commons` key is the relative path stem `<message_snake>/<struct_snake>`.
209/// The caller turns that stem into the on-disk body path and the wrapper
210/// module nesting.
211fn emit_commons(
212    spec: &MessageSpec,
213    res_map: &ResMap,
214    schemas_version: &str,
215    lenient: bool,
216) -> Result<Vec<(String, String)>, EmitError> {
217    let message_snake = name_conv::module_name(&spec.name);
218    let fm = flex_min(spec);
219    let mut commons: Vec<(String, String)> = Vec::new();
220    for cs in &spec.common_structs {
221        // Build a modified res_map for the common-struct context: common-struct
222        // references use sibling paths `super::<struct_snake>::TypeName` (the body
223        // lands in `src/{flavor}/common/<message_snake>/<struct_snake>.rs`, and
224        // sibling common structs of the same message share that parent module).
225        let common_res_map: ResMap = res_map
226            .iter()
227            .map(|(k, v)| {
228                let new_path = if v.kind == StructKind::Common {
229                    let snake = name_conv::module_name(k);
230                    format!("super::{snake}::{k}")
231                } else {
232                    v.rust_path.clone()
233                };
234                (
235                    k.clone(),
236                    Resolution {
237                        kind: v.kind.clone(),
238                        rust_path: new_path,
239                        needs_lifetime: v.needs_lifetime,
240                    },
241                )
242            })
243            .collect();
244
245        let name = name_conv::type_name(&cs.name);
246        let struct_tokens = struct_block(
247            &name,
248            &cs.fields,
249            &common_res_map,
250            &FlexSource::Nested(fm),
251            None,
252            fm < i16::MAX,
253            lenient,
254        )?;
255        let nested = nested_structs(&cs.fields, fm, &common_res_map, lenient)?;
256
257        let body = format!(
258            "{}{}{}{}",
259            banner(schemas_version),
260            emit_common_imports(&cs.fields, fm),
261            struct_tokens,
262            nested,
263        );
264
265        let cs_snake = name_conv::module_name(&cs.name);
266        commons.push((format!("{message_snake}/{cs_snake}"), body));
267    }
268    Ok(commons)
269}
270
271fn version_err(spec: &MessageSpec) -> TokenStream {
272    match spec.message_type {
273        MessageType::Request | MessageType::Response => {
274            quote!(return Err(ProtocolError::UnsupportedVersion { api_key: API_KEY, version });)
275        }
276        MessageType::Header | MessageType::Data => {
277            let msg = format!("{} version out of range", spec.name);
278            quote!(return Err(ProtocolError::SchemaMismatch(#msg));)
279        }
280    }
281}
282
283/// A struct definition, its `Default` when that is non-trivial, the `Encode`
284/// and `Decode` impls, and the test `populated` constructor. The top-level
285/// message type and every nested struct share this function, and they differ
286/// only in flex source and guard.
287fn struct_block(
288    name: &str,
289    fields: &[FieldSpec],
290    res_map: &ResMap,
291    flex: &FlexSource,
292    guard: Option<TokenStream>,
293    has_flex: bool,
294    lenient: bool,
295) -> Result<TokenStream, EmitError> {
296    let ty = format_ident!("{name}");
297    let eq = if has_float64_recursive(fields) {
298        quote!()
299    } else {
300        quote!(, Eq)
301    };
302    let manual_default = needs_manual_default(fields);
303    let default_derive = if manual_default {
304        quote!()
305    } else {
306        quote!(, Default)
307    };
308
309    let field_defs = ordered(fields)
310        .map(|f| {
311            let name = format_ident!("{}", name_conv::field_name(&f.name));
312            let nullable = is_nullable(f) || (is_tagged(f) && default_is_null(f));
313            let ty = parse_tokens(&type_map::owned_type(
314                &f.field_type,
315                nullable,
316                struct_path_for(f, res_map).as_deref(),
317            ))?;
318            Ok(quote!(pub #name: #ty,))
319        })
320        .collect::<Result<Vec<_>, EmitError>>()?;
321
322    let default_impl = if manual_default {
323        let assigns = ordered(fields)
324            .map(|f| {
325                let name = format_ident!("{}", name_conv::field_name(&f.name));
326                let expr = parse_tokens(&owned_default_expr(f, res_map))?;
327                Ok(quote!(#name: #expr,))
328            })
329            .collect::<Result<Vec<_>, EmitError>>()?;
330        Some(quote! {
331            impl Default for #ty {
332                fn default() -> Self {
333                    Self {
334                        #(#assigns)*
335                        unknown_tagged_fields: UnknownTaggedFields::default(),
336                    }
337                }
338            }
339        })
340    } else {
341        None
342    };
343
344    let flex_src = flex.tokens();
345    let guard = guard.map(|err| {
346        quote! {
347            if !(MIN_VERSION..=MAX_VERSION).contains(&version) { #err }
348        }
349    });
350
351    let flex_minimum = flex.minimum();
352    let encode_body = encode_body(fields, has_flex, flex_minimum)?;
353    let len_body = len_body(fields, has_flex, flex_minimum)?;
354    let decode_body = decode_body(fields, res_map, has_flex, flex_minimum, lenient)?;
355    let (codec_helpers, encode_body, decode_body) = if fields.len() >= 8 {
356        let (encode_helpers, encode_calls) = split_encode_body(fields, has_flex, flex_minimum)?;
357        let (decode_helpers, decode_calls) =
358            split_decode_body(fields, res_map, has_flex, flex_minimum, lenient)?;
359        (
360            quote!(impl #ty { #encode_helpers #decode_helpers }),
361            encode_calls,
362            decode_calls,
363        )
364    } else {
365        (quote!(), encode_body, decode_body)
366    };
367    let encode_flex = flex_binding(&encode_body, &flex_src);
368    let len_flex = flex_binding(&len_body, &flex_src);
369    let decode_flex = flex_binding(&decode_body, &flex_src);
370    let populated = populated_impl(&ty, fields, res_map)?;
371
372    Ok(quote! {
373        #[derive(Debug, Clone, PartialEq #eq #default_derive)]
374        pub struct #ty {
375            #(#field_defs)*
376            pub unknown_tagged_fields: UnknownTaggedFields,
377        }
378
379        #default_impl
380
381        #codec_helpers
382
383        impl Encode for #ty {
384            fn encode<B: BufMut>(&self, buf: &mut B, version: i16) -> Result<(), ProtocolError> {
385                #guard
386                #encode_flex
387                #encode_body
388                Ok(())
389            }
390            fn encoded_len(&self, version: i16) -> usize {
391                #len_flex
392                let mut n: usize = 0;
393                #len_body
394                n
395            }
396        }
397
398        impl Decode<'_> for #ty {
399            fn decode<B: Buf>(buf: &mut B, version: i16) -> Result<Self, ProtocolError> {
400                #guard
401                #decode_flex
402                let mut out = Self::default();
403                #decode_body
404                Ok(out)
405            }
406        }
407
408        #populated
409    })
410}
411
412fn flex_binding(body: &TokenStream, flex_source: &TokenStream) -> Option<TokenStream> {
413    body.to_string()
414        .contains("flex")
415        .then(|| quote!(let flex = #flex_source;))
416}
417
418/// Fields in emission order: non-tagged first, then tagged. This matches the
419/// string emitter, so struct layout and round-trips are identical.
420fn ordered(fields: &[FieldSpec]) -> impl Iterator<Item = &FieldSpec> {
421    fields
422        .iter()
423        .filter(|f| !is_tagged(f))
424        .chain(fields.iter().filter(|f| is_tagged(f)))
425}
426
427fn default_is_null(f: &FieldSpec) -> bool {
428    matches!(&f.default, Some(serde_json::Value::Null))
429}
430
431// --- encode -----------------------------------------------------------------
432
433fn encode_body(
434    fields: &[FieldSpec],
435    has_flex: bool,
436    flex_minimum: i16,
437) -> Result<TokenStream, EmitError> {
438    let stmts = fields
439        .iter()
440        .filter(|f| !is_tagged(f))
441        .map(encode_one)
442        .collect::<Result<Vec<_>, EmitError>>()?;
443    let trailer = if has_flex {
444        Some(encode_tagged_trailer(fields, flex_minimum)?)
445    } else {
446        None
447    };
448    Ok(quote!(#(#stmts)* #trailer))
449}
450
451/// The `if flex { .. }` block that writes the known and unknown tagged fields.
452fn encode_tagged_trailer(
453    fields: &[FieldSpec],
454    flex_minimum: i16,
455) -> Result<TokenStream, EmitError> {
456    let has_tagged = fields.iter().any(is_tagged);
457    let mut_kw = if has_tagged { quote!(mut) } else { quote!() };
458    let adds = fields
459        .iter()
460        .filter(|f| is_tagged(f))
461        .map(|f| encode_tagged(f, flex_minimum))
462        .collect::<Result<Vec<_>, EmitError>>()?;
463    Ok(quote! {
464        if flex {
465            let #mut_kw tagged = WriteTaggedFields::new();
466            #(#adds)*
467            tagged.write(buf, &self.unknown_tagged_fields)?;
468        }
469    })
470}
471
472fn split_encode_body(
473    fields: &[FieldSpec],
474    has_flex: bool,
475    flex_minimum: i16,
476) -> Result<(TokenStream, TokenStream), EmitError> {
477    let mut helpers = Vec::new();
478    let mut calls = Vec::new();
479    for (index, field) in fields.iter().filter(|field| !is_tagged(field)).enumerate() {
480        let helper = format_ident!("encode_field_{index}");
481        let body = encode_one(field)?;
482        let flex = if body.to_string().contains("flex") {
483            quote!(flex)
484        } else {
485            quote!(_flex)
486        };
487        if body.to_string().contains('?') {
488            helpers.push(quote! {
489                fn #helper<B: BufMut>(
490                    &self,
491                    buf: &mut B,
492                    version: i16,
493                    #flex: bool,
494                ) -> Result<(), ProtocolError> {
495                    #body
496                    Ok(())
497                }
498            });
499            calls.push(quote!(self.#helper(buf, version, flex)?;));
500        } else {
501            helpers.push(quote! {
502                fn #helper<B: BufMut>(&self, buf: &mut B, version: i16, #flex: bool) {
503                    #body
504                }
505            });
506            calls.push(quote!(self.#helper(buf, version, flex);));
507        }
508    }
509    if has_flex {
510        let helper = format_ident!("encode_tagged_fields");
511        let body = encode_tagged_trailer(fields, flex_minimum)?;
512        let version = if body.to_string().contains("version") {
513            quote!(version)
514        } else {
515            quote!(_version)
516        };
517        helpers.push(quote! {
518            fn #helper<B: BufMut>(
519                &self,
520                buf: &mut B,
521                #version: i16,
522                flex: bool,
523            ) -> Result<(), ProtocolError> {
524                #body
525                Ok(())
526            }
527        });
528        calls.push(quote!(self.#helper(buf, version, flex)?;));
529    }
530    Ok((quote!(#(#helpers)*), quote!(#(#calls)*)))
531}
532
533fn encode_one(f: &FieldSpec) -> Result<TokenStream, EmitError> {
534    let field = name_conv::field_name(&f.name);
535    let cond = parse_tokens(&version_cond(f.versions, "version"))?;
536    let expr = format!("self.{field}");
537    let inner = if let Some(ncond) = nullable_split_cond(f) {
538        let nb = parse_tokens(&encode_call(&f.field_type, &expr, true))?;
539        let nnb = parse_tokens(&encode_call_option_as_non_nullable(&f.field_type, &expr))?;
540        let nc = parse_tokens(&ncond)?;
541        non_flex_wrap(f, quote!(if #nc { #nb } else { #nnb }))
542    } else {
543        let b = parse_tokens(&encode_call(&f.field_type, &expr, is_nullable(f)))?;
544        non_flex_wrap(f, b)
545    };
546    let inner = statement(&inner);
547    Ok(quote!(if #cond { #inner }))
548}
549
550/// `flexibleVersions: "none"` on a field forces the legacy codec. This
551/// function shadows `flex` to `false` for that field's body.
552fn non_flex_wrap(f: &FieldSpec, body: TokenStream) -> TokenStream {
553    if field_forces_non_flex(f) {
554        quote!({ let flex = false; #body })
555    } else {
556        body
557    }
558}
559
560fn encode_tagged(f: &FieldSpec, flex_minimum: i16) -> Result<TokenStream, EmitError> {
561    let field = name_conv::field_name(&f.name);
562    let tag = tag_literal(f)?;
563    let nullable = is_nullable(f) || default_is_null(f);
564    let expr = format!("self.{field}");
565    let body = statement(&parse_tokens(&encode_call_with_buf(
566        &f.field_type,
567        &expr,
568        nullable,
569        "b",
570    ))?);
571    let len = parse_tokens(&encoded_len_expr(&f.field_type, &expr, nullable))?;
572    let should_encode = gated_tagged_should_encode(f, flex_minimum)?;
573    Ok(quote! {
574        if #should_encode {
575            let payload = encode_to_bytes(#len, |b| { #body Ok(()) })?;
576            tagged.add(#tag, payload);
577        }
578    })
579}
580
581// --- encoded_len ------------------------------------------------------------
582
583fn len_body(
584    fields: &[FieldSpec],
585    has_flex: bool,
586    flex_minimum: i16,
587) -> Result<TokenStream, EmitError> {
588    let stmts = fields
589        .iter()
590        .filter(|f| !is_tagged(f))
591        .map(len_one)
592        .collect::<Result<Vec<_>, EmitError>>()?;
593    let trailer = if has_flex {
594        let has_tagged = fields.iter().any(is_tagged);
595        let mut_kw = if has_tagged { quote!(mut) } else { quote!() };
596        let pushes = fields
597            .iter()
598            .filter(|f| is_tagged(f))
599            .map(|f| len_tagged(f, flex_minimum))
600            .collect::<Result<Vec<_>, EmitError>>()?;
601        Some(quote! {
602            if flex {
603                let #mut_kw known_pairs: Vec<(u32, usize)> = Vec::new();
604                #(#pushes)*
605                n += tagged_fields_len(&known_pairs, &self.unknown_tagged_fields);
606            }
607        })
608    } else {
609        None
610    };
611    Ok(quote!(#(#stmts)* #trailer))
612}
613
614fn len_one(f: &FieldSpec) -> Result<TokenStream, EmitError> {
615    let field = name_conv::field_name(&f.name);
616    let cond = parse_tokens(&version_cond(f.versions, "version"))?;
617    let expr = format!("self.{field}");
618    let inner = if let Some(ncond) = nullable_split_cond(f) {
619        let nb = parse_tokens(&encoded_len_expr(&f.field_type, &expr, true))?;
620        let nnb = parse_tokens(&owned::encoded_len_expr_option_as_non_nullable(
621            &f.field_type,
622            &expr,
623        ))?;
624        let nc = parse_tokens(&ncond)?;
625        non_flex_wrap(f, quote!(if #nc { #nb } else { #nnb }))
626    } else {
627        let b = parse_tokens(&encoded_len_expr(&f.field_type, &expr, is_nullable(f)))?;
628        non_flex_wrap(f, b)
629    };
630    Ok(quote!(if #cond { n += #inner; }))
631}
632
633fn len_tagged(f: &FieldSpec, flex_minimum: i16) -> Result<TokenStream, EmitError> {
634    let field = name_conv::field_name(&f.name);
635    let tag = tag_literal(f)?;
636    let nullable = is_nullable(f) || default_is_null(f);
637    let len = parse_tokens(&encoded_len_expr(
638        &f.field_type,
639        &format!("self.{field}"),
640        nullable,
641    ))?;
642    let should_encode = gated_tagged_should_encode(f, flex_minimum)?;
643    Ok(quote! {
644        if #should_encode {
645            known_pairs.push((#tag, #len));
646        }
647    })
648}
649
650// --- decode -----------------------------------------------------------------
651
652fn decode_body(
653    fields: &[FieldSpec],
654    res_map: &ResMap,
655    has_flex: bool,
656    flex_minimum: i16,
657    lenient: bool,
658) -> Result<TokenStream, EmitError> {
659    let stmts = fields
660        .iter()
661        .filter(|f| !is_tagged(f))
662        .map(|f| decode_one(f, res_map, lenient))
663        .collect::<Result<Vec<_>, EmitError>>()?;
664    let trailer = if has_flex {
665        Some(decode_tagged_block(fields, res_map, flex_minimum, lenient)?)
666    } else {
667        None
668    };
669    Ok(quote!(#(#stmts)* #trailer))
670}
671
672fn split_decode_body(
673    fields: &[FieldSpec],
674    res_map: &ResMap,
675    has_flex: bool,
676    flex_minimum: i16,
677    lenient: bool,
678) -> Result<(TokenStream, TokenStream), EmitError> {
679    let mut helpers = Vec::new();
680    let mut calls = Vec::new();
681    for (index, field) in fields.iter().filter(|field| !is_tagged(field)).enumerate() {
682        let helper = format_ident!("decode_field_{index}");
683        let body = decode_one(field, res_map, lenient)?;
684        let flex = if body.to_string().contains("flex") {
685            quote!(flex)
686        } else {
687            quote!(_flex)
688        };
689        helpers.push(quote! {
690            fn #helper<B: Buf>(
691                out: &mut Self,
692                buf: &mut B,
693                version: i16,
694                #flex: bool,
695            ) -> Result<(), ProtocolError> {
696                #body
697                Ok(())
698            }
699        });
700        calls.push(quote!(Self::#helper(&mut out, buf, version, flex)?;));
701    }
702    if has_flex {
703        let helper = format_ident!("decode_tagged_fields");
704        let body = decode_tagged_block(fields, res_map, flex_minimum, lenient)?;
705        let version = if body.to_string().contains("version") {
706            quote!(version)
707        } else {
708            quote!(_version)
709        };
710        helpers.push(quote! {
711            fn #helper<B: Buf>(
712                out: &mut Self,
713                buf: &mut B,
714                #version: i16,
715                flex: bool,
716            ) -> Result<(), ProtocolError> {
717                #body
718                Ok(())
719            }
720        });
721        calls.push(quote!(Self::#helper(&mut out, buf, version, flex)?;));
722    }
723    Ok((quote!(#(#helpers)*), quote!(#(#calls)*)))
724}
725
726fn decode_one(f: &FieldSpec, res_map: &ResMap, lenient: bool) -> Result<TokenStream, EmitError> {
727    let field = format_ident!("{}", name_conv::field_name(&f.name));
728    let cond = parse_tokens(&version_cond(f.versions, "version"))?;
729    let inner = if let Some(ncond) = nullable_split_cond(f) {
730        let nb = parse_tokens(&decode_call(&f.field_type, true, res_map, lenient))?;
731        let nnb_call = decode_call(&f.field_type, false, res_map, lenient);
732        let nnb = parse_tokens(&wrap_non_nullable_for_option(
733            &f.field_type,
734            &nnb_call,
735            res_map,
736        ))?;
737        let nc = parse_tokens(&ncond)?;
738        non_flex_wrap(f, quote!(if #nc { #nb } else { #nnb }))
739    } else {
740        let b = parse_tokens(&decode_call(
741            &f.field_type,
742            is_nullable(f),
743            res_map,
744            lenient,
745        ))?;
746        non_flex_wrap(f, b)
747    };
748    Ok(quote!(if #cond { out.#field = #inner; }))
749}
750
751fn decode_tagged_block(
752    fields: &[FieldSpec],
753    res_map: &ResMap,
754    flex_minimum: i16,
755    lenient: bool,
756) -> Result<TokenStream, EmitError> {
757    let has_tagged = fields.iter().any(is_tagged);
758    if !has_tagged {
759        return Ok(quote! {
760            if flex {
761                out.unknown_tagged_fields = read_tagged_fields(buf, |_tag, _payload| { Ok(false) })?;
762            }
763        });
764    }
765    let slots = fields.iter().filter(|f| is_tagged(f)).map(|f| {
766        let slot = format_ident!("tag_{}", name_conv::field_name(&f.name));
767        quote!(let mut #slot = None;)
768    });
769    let arms = fields
770        .iter()
771        .filter(|f| is_tagged(f))
772        .map(|f| {
773            let slot = format_ident!("tag_{}", name_conv::field_name(&f.name));
774            let nullable = is_nullable(f) || default_is_null(f);
775            let call = parse_tokens(&decode_call_with_buf(
776                &f.field_type,
777                nullable,
778                res_map,
779                "b",
780                lenient,
781            ))?;
782            let read = quote!(#slot = Some({ let b: &mut &[u8] = payload; #call }); Ok(true));
783            tagged_decode_arm(f, flex_minimum, &read)
784        })
785        .collect::<Result<Vec<_>, EmitError>>()?;
786    let writebacks = fields.iter().filter(|f| is_tagged(f)).map(|f| {
787        let field = format_ident!("{}", name_conv::field_name(&f.name));
788        let slot = format_ident!("tag_{}", name_conv::field_name(&f.name));
789        quote!(if let Some(v) = #slot { out.#field = v; })
790    });
791    Ok(quote! {
792        if flex {
793            #(#slots)*
794            out.unknown_tagged_fields = read_tagged_fields(buf, |tag, payload| {
795                match tag {
796                    #(#arms)*
797                    _ => Ok(false),
798                }
799            })?;
800            #(#writebacks)*
801        }
802    })
803}
804
805// --- populated + nested -----------------------------------------------------
806
807fn populated_impl(
808    ty: &Ident,
809    fields: &[FieldSpec],
810    res_map: &ResMap,
811) -> Result<TokenStream, EmitError> {
812    let assigns = ordered(fields)
813        .filter(|f| base_type(&f.field_type) != "records")
814        .map(|f| {
815            let field = format_ident!("{}", name_conv::field_name(&f.name));
816            let option = is_nullable(f) || (is_tagged(f) && default_is_null(f));
817            let value = parse_tokens(&owned_populated_value(f, res_map, option)?)?;
818            let cond = parse_tokens(&version_cond(f.versions, "version"))?;
819            Ok(quote!(if #cond { m.#field = #value; }))
820        })
821        .collect::<Result<Vec<_>, EmitError>>()?;
822    if assigns.is_empty() {
823        return Ok(quote! {
824            #[cfg(test)]
825            impl #ty {
826                #[must_use]
827                pub fn populated(_version: i16) -> Self {
828                    Self::default()
829                }
830            }
831        });
832    }
833    let mut_kw = quote!(mut);
834    let version = if assigns
835        .iter()
836        .any(|assign| assign.to_string().contains("version"))
837    {
838        quote!(version)
839    } else {
840        quote!(_version)
841    };
842    Ok(quote! {
843        #[cfg(test)]
844        impl #ty {
845            #[must_use]
846            pub fn populated(#version: i16) -> Self {
847                let #mut_kw m = Self::default();
848                #(#assigns)*
849                m
850            }
851        }
852    })
853}
854
855/// Sibling struct definitions for every field that has its own `fields:` list,
856/// depth-first. `flex_min_val` is the message's flexible threshold, and every
857/// nested struct inherits it.
858fn nested_structs(
859    fields: &[FieldSpec],
860    flex_min_val: i16,
861    res_map: &ResMap,
862    lenient: bool,
863) -> Result<TokenStream, EmitError> {
864    let mut out = TokenStream::new();
865    for f in fields.iter().filter(|f| !f.fields.is_empty()) {
866        let name = base_type(&f.field_type);
867        out.extend(struct_block(
868            name,
869            &f.fields,
870            res_map,
871            &FlexSource::Nested(flex_min_val),
872            None,
873            flex_min_val < i16::MAX,
874            lenient,
875        )?);
876        out.extend(nested_structs(&f.fields, flex_min_val, res_map, lenient)?);
877    }
878    Ok(out)
879}