Skip to main content

krabka_protocol_codegen/emit/
borrowed.rs

1//! Emit Rust source for the borrowed flavor of a `MessageSpec`.
2//!
3//! This module mirrors the structure of `emit/owned.rs`. Strings become
4//! `&'a str`, bytes become `&'a [u8]`, the struct carries a `'a` lifetime,
5//! `DecodeBorrow<'de>` replaces `Decode<'de>`, and `to_owned()` bridges to
6//! the matching owned type.
7
8use std::collections::HashMap;
9
10use proc_macro2::TokenStream;
11use quote::{format_ident, quote};
12
13use crate::{
14    emit::{EmitError, common::format_int_literal},
15    ir::{FieldSpec, FlexibleVersions, MessageSpec, MessageType, VersionRange},
16    name_conv,
17    resolve::{Resolution, StructKind},
18};
19
20// ── helpers shared with owned ──────────────────────────────────────────────
21
22pub(crate) fn flex_min(spec: &MessageSpec) -> i16 {
23    match spec.flexible_versions {
24        FlexibleVersions::Range(r) => r.min,
25        FlexibleVersions::None => i16::MAX,
26    }
27}
28
29pub(crate) fn base_type(t: &str) -> &str {
30    t.strip_prefix("[]").unwrap_or(t)
31}
32
33pub(crate) fn is_struct_type(t: &str) -> bool {
34    t.chars().next().is_some_and(char::is_uppercase)
35}
36
37/// Returns true if ANY field in the list, at any depth, would carry a borrowed
38/// lifetime in the generated Rust type. Those fields are string, bytes,
39/// records, or a nested struct that itself has borrowed fields.
40///
41/// This function consults `res_map` for common-struct references, which are
42/// `PascalCase` types where `f.fields.is_empty()`, to check whether the
43/// generator gave that common struct a `<'a>`.
44pub(crate) fn needs_lifetime(fields: &[FieldSpec], res_map: &HashMap<String, Resolution>) -> bool {
45    fields.iter().any(|f| {
46        let base = base_type(&f.field_type);
47        matches!(base, "string" | "bytes" | "records")  // records borrows via RecordsPayloadBorrowed<'a>
48            // Inline nested struct with borrowed fields.
49            || (is_struct_type(base) && !f.fields.is_empty() && needs_lifetime(&f.fields, res_map))
50            // Common-struct reference: consult the resolution to see if it has '<'a>'.
51            || (is_struct_type(base) && f.fields.is_empty()
52                && res_map.get(base).is_some_and(|r| r.needs_lifetime))
53    })
54}
55
56pub(crate) fn is_tagged(f: &FieldSpec) -> bool {
57    f.tag.is_some()
58}
59
60/// Returns true if the top-level struct needs a `'a` lifetime parameter.
61/// Only non-tagged fields contribute borrowed lifetimes. Tagged fields with
62/// string or struct content use owned types, so their data cannot escape the
63/// payload closure.
64pub(crate) fn spec_needs_lifetime(
65    spec: &MessageSpec,
66    res_map: &HashMap<String, Resolution>,
67) -> bool {
68    let non_tagged: Vec<&FieldSpec> = spec.fields.iter().filter(|f| !is_tagged(f)).collect();
69    non_tagged.iter().any(|f| {
70        let base = base_type(&f.field_type);
71        matches!(base, "string" | "bytes" | "records")
72            || (is_struct_type(base) && !f.fields.is_empty() && needs_lifetime(&f.fields, res_map))
73            // Common-struct reference: consult the resolution to see if it has '<'a>'.
74            || (is_struct_type(base) && f.fields.is_empty()
75                && res_map.get(base).is_some_and(|r| r.needs_lifetime))
76    })
77}
78
79/// Returns true if a tagged field's content cannot be zero-copy decoded,
80/// because string or bytes data in its payload would escape the closure.
81/// In that case the field must use the owned type.
82pub(crate) fn tagged_field_needs_owned(
83    f: &FieldSpec,
84    res_map: &HashMap<String, Resolution>,
85) -> bool {
86    if !is_tagged(f) {
87        return false;
88    }
89    let base = base_type(&f.field_type);
90    // Primitive tagged fields (int*, bool, uuid) are fine as borrowed.
91    if !is_struct_type(base) && !matches!(base, "string" | "bytes" | "records") {
92        return false;
93    }
94    // Struct tagged fields: fine only if the nested struct has no borrowed fields.
95    if is_struct_type(base) {
96        if f.fields.is_empty() {
97            // Common-struct reference: needs owned only if the common struct has '<'a>'.
98            return res_map.get(base).is_some_and(|r| r.needs_lifetime);
99        }
100        return needs_lifetime(&f.fields, res_map);
101    }
102    // String/bytes tagged fields also need owned (lifetime escape).
103    true
104}
105
106pub(crate) fn is_nullable(f: &FieldSpec) -> bool {
107    f.nullable_versions.is_some()
108}
109
110/// Version condition under which a field uses its NULLABLE codec.
111///
112/// A field is nullable only within its `nullableVersions` range. Where that
113/// range is narrower than the field's own version range (on either end), the
114/// codec must switch between nullable and non-nullable per version. Returns
115/// `Some(cond)` for that boundary expression, or `None` when nullability is
116/// constant across the whole field range. In that case use `is_nullable(f)`
117/// directly.
118pub(crate) fn nullable_split_cond(f: &FieldSpec) -> Option<String> {
119    let r = f.nullable_versions?;
120    let need_lower = r.min > f.versions.min;
121    let need_upper = r.max < f.versions.max;
122    if !need_lower && !need_upper {
123        return None;
124    }
125    let mut parts = Vec::new();
126    if need_lower {
127        parts.push(if r.min == i16::MAX {
128            "version == i16::MAX".to_owned()
129        } else {
130            format!("version >= {}", r.min)
131        });
132    }
133    if need_upper {
134        parts.push(if r.max == i16::MIN {
135            "version == i16::MIN".to_owned()
136        } else {
137            format!("version <= {}", r.max)
138        });
139    }
140    Some(parts.join(" && "))
141}
142
143pub(crate) fn has_any_flex(spec: &MessageSpec) -> bool {
144    matches!(spec.flexible_versions, FlexibleVersions::Range(_))
145}
146
147fn has_tagged_fields_recursive(fields: &[FieldSpec]) -> bool {
148    fields
149        .iter()
150        .any(|f| f.tag.is_some() || has_tagged_fields_recursive(&f.fields))
151}
152
153fn has_any_tagged_in_spec(spec: &MessageSpec) -> bool {
154    has_tagged_fields_recursive(&spec.fields)
155}
156
157/// Returns true if any top-level tagged field in this spec needs the owned
158/// `Decode` trait rather than `DecodeBorrow`.
159fn has_tagged_fields_needing_owned(
160    spec: &MessageSpec,
161    res_map: &HashMap<String, Resolution>,
162) -> bool {
163    spec.fields
164        .iter()
165        .any(|f| is_tagged(f) && tagged_field_needs_owned(f, res_map))
166}
167
168fn used_field_types_recursive(fields: &[FieldSpec]) -> Vec<String> {
169    let mut types: Vec<String> = Vec::new();
170    for f in fields {
171        let base = base_type(&f.field_type).to_owned();
172        if !types.contains(&base) {
173            types.push(base);
174        }
175        if !f.fields.is_empty() {
176            for t in used_field_types_recursive(&f.fields) {
177                if !types.contains(&t) {
178                    types.push(t);
179                }
180            }
181        }
182    }
183    types
184}
185
186fn uses_fixed_type(types: &[String], t: &str) -> bool {
187    types.iter().any(|s| s == t)
188}
189
190/// Returns true if any field, at any depth, is `float64`.
191/// The emitter uses this to suppress the `Eq` derive, because `f64` does not
192/// implement `Eq`.
193pub(crate) fn has_float64_recursive(fields: &[FieldSpec]) -> bool {
194    fields.iter().any(|f| {
195        let base = base_type(&f.field_type);
196        base == "float64" || has_float64_recursive(&f.fields)
197    })
198}
199
200fn uses_string(types: &[String]) -> bool {
201    types.iter().any(|t| t == "string")
202}
203
204fn uses_bytes(types: &[String]) -> bool {
205    types.iter().any(|t| t.as_str() == "bytes")
206}
207
208fn uses_nullable_string_recursive(fields: &[FieldSpec]) -> bool {
209    fields.iter().any(|f| {
210        let base = base_type(&f.field_type);
211        let here = base == "string" && (f.nullable_versions.is_some() || f.tag.is_some());
212        here || uses_nullable_string_recursive(&f.fields)
213    })
214}
215
216fn uses_nullable_bytes_recursive(fields: &[FieldSpec]) -> bool {
217    fields.iter().any(|f| {
218        let base = base_type(&f.field_type);
219        let here = base == "bytes" && f.nullable_versions.is_some();
220        here || uses_nullable_bytes_recursive(&f.fields)
221    })
222}
223
224fn uses_non_nullable_bytes_recursive(fields: &[FieldSpec]) -> bool {
225    fields.iter().any(|f| {
226        let base = base_type(&f.field_type);
227        let here = base == "bytes" && needs_non_nullable_codec(f);
228        here || uses_non_nullable_bytes_recursive(&f.fields)
229    })
230}
231
232/// True if a field needs the non-nullable codec for at least some versions.
233/// That happens when the field is never nullable, or when its nullable range
234/// is narrower than its own version range, so a per-version split emits a
235/// non-nullable branch.
236fn needs_non_nullable_codec(f: &FieldSpec) -> bool {
237    f.nullable_versions.is_none() || nullable_split_cond(f).is_some()
238}
239
240fn uses_nullable_records_recursive(fields: &[FieldSpec]) -> bool {
241    fields.iter().any(|f| {
242        let base = base_type(&f.field_type);
243        let here = base == "records" && f.nullable_versions.is_some();
244        here || uses_nullable_records_recursive(&f.fields)
245    })
246}
247
248fn uses_non_nullable_records_recursive(fields: &[FieldSpec]) -> bool {
249    fields.iter().any(|f| {
250        let base = base_type(&f.field_type);
251        let here = base == "records" && needs_non_nullable_codec(f);
252        here || uses_non_nullable_records_recursive(&f.fields)
253    })
254}
255
256/// Returns true if any field has a non-array struct type with nullableVersions
257/// set. Such fields need `get_i8` for the nullable prefix byte in decode.
258fn uses_nullable_struct_recursive(fields: &[FieldSpec]) -> bool {
259    fields.iter().any(|f| {
260        let t = &f.field_type;
261        let base = base_type(t);
262        let here = is_struct_type(base) && !t.starts_with("[]") && f.nullable_versions.is_some();
263        here || uses_nullable_struct_recursive(&f.fields)
264    })
265}
266
267/// Returns the Rust type path for a struct-typed field in borrowed flavor.
268/// The path includes `<'a>` only when the nested struct actually has borrowed
269/// fields. `type_map::borrowed_type` uses the path verbatim and adds no `<'a>`
270/// of its own.
271pub(crate) fn struct_path_for(
272    f: &FieldSpec,
273    res_map: &HashMap<String, Resolution>,
274) -> Option<String> {
275    let base = base_type(&f.field_type);
276    if is_struct_type(base) {
277        res_map.get(base).map(|r| {
278            // For inline nested structs, check field contents; for common-struct refs, use
279            // the pre-computed needs_lifetime flag on the Resolution.
280            let needs_lt = if f.fields.is_empty() {
281                r.needs_lifetime
282            } else {
283                needs_lifetime(&f.fields, res_map)
284            };
285            if needs_lt {
286                format!("{}<'a>", r.rust_path)
287            } else {
288                r.rust_path.clone()
289            }
290        })
291    } else {
292        None
293    }
294}
295
296/// Returns the fully-qualified owned-flavor type path for a struct-typed
297/// field. The emitter uses it when a tagged field must store owned data,
298/// because borrowed data would escape the `read_tagged_fields` closure.
299pub(crate) fn owned_struct_path_for(
300    f: &FieldSpec,
301    parent_module: &str,
302    res_map: &HashMap<String, Resolution>,
303) -> Option<String> {
304    let base = base_type(&f.field_type);
305    if is_struct_type(base) {
306        res_map
307            .get(base)
308            .map(|_| resolved_to_owned_path(base, parent_module, res_map))
309    } else {
310        None
311    }
312}
313
314/// Convert a borrowed-flavor resolved `rust_path` to its owned-flavor equivalent.
315///
316/// Common structs are message-scoped under `common/<message_snake>/<struct_snake>`.
317///
318/// - Inline nested structs have a bare `rust_path` like `"TypeName"` →
319///   `"crate::owned::{parent_module}::TypeName"`.
320/// - Common structs from a message-level context have `rust_path` like
321///   `"super::common::<msg>::<struct>::TypeName"` →
322///   `"crate::owned::common::<msg>::<struct>::TypeName"`.
323/// - Common structs from a common-struct-level context, where `parent_module`
324///   is `"common::<msg>::<struct>"`, have `rust_path` like
325///   `"super::<struct>::TypeName"`. This function recovers the `<msg>` segment
326///   from `parent_module` → `"crate::owned::common::<msg>::<struct>::TypeName"`.
327fn resolved_to_owned_path(
328    type_name: &str,
329    parent_module: &str,
330    res_map: &HashMap<String, Resolution>,
331) -> String {
332    match res_map.get(type_name) {
333        Some(r) if r.kind == StructKind::Common => {
334            // Determine the owned path from the rust_path stored in the res_map.
335            if let Some(without_super) = r.rust_path.strip_prefix("super::common::") {
336                // Message-level context:
337                // rust_path = "super::common::<msg>::<struct>::TypeName"
338                return format!("crate::owned::common::{without_super}");
339            }
340            if let Some(sibling) = r.rust_path.strip_prefix("super::") {
341                // Common-struct-level context: rust_path = "super::<struct>::TypeName"
342                // (a sibling common struct of the same message). Recover the
343                // <msg> segment from parent_module = "common::<msg>::<struct>".
344                let msg_seg = parent_module
345                    .strip_prefix("common::")
346                    .and_then(|rest| rest.split("::").next())
347                    .unwrap_or(parent_module);
348                return format!("crate::owned::common::{msg_seg}::{sibling}");
349            }
350            // Fallback: use parent_module
351            format!("crate::owned::{parent_module}::{type_name}")
352        }
353        _ => {
354            // Inline nested struct — lives in the parent message's owned module.
355            format!("crate::owned::{parent_module}::{type_name}")
356        }
357    }
358}
359
360pub(crate) fn version_cond(r: VersionRange, version_var: &str) -> String {
361    if r.min == i16::MIN && r.max == i16::MAX {
362        "true".to_owned()
363    } else if r.min == r.max {
364        format!("{version_var} == {}", r.min)
365    } else if r.min == i16::MIN {
366        format!("{version_var} <= {}", r.max)
367    } else if r.max == i16::MAX {
368        format!("{version_var} >= {}", r.min)
369    } else {
370        format!("({}..={}).contains(&{version_var})", r.min, r.max)
371    }
372}
373
374// ── imports ────────────────────────────────────────────────────────────────
375
376/// Build the fixed-type `use crate::primitives::fixed::{ ... };` import, or an
377/// empty stream when the message uses no fixed primitives. `force_get_i8`
378/// pulls in `get_i8` even with no int8 fields, which the nullable-struct
379/// prefix needs.
380fn fixed_import(types: &[String], force_get_i8: bool) -> TokenStream {
381    let mut gets: Vec<&str> = Vec::new();
382    let mut puts: Vec<&str> = Vec::new();
383    if uses_fixed_type(types, "int8") {
384        gets.push("get_i8");
385        puts.push("put_i8");
386    } else if force_get_i8 {
387        // get_i8 is needed for nullable struct decode (1-byte signed null prefix).
388        gets.push("get_i8");
389    }
390    if uses_fixed_type(types, "int16") {
391        gets.push("get_i16");
392        puts.push("put_i16");
393    }
394    if uses_fixed_type(types, "uint16") {
395        gets.push("get_u16");
396        puts.push("put_u16");
397    }
398    if uses_fixed_type(types, "int32") {
399        gets.push("get_i32");
400        puts.push("put_i32");
401    }
402    if uses_fixed_type(types, "int64") {
403        gets.push("get_i64");
404        puts.push("put_i64");
405    }
406    if uses_fixed_type(types, "bool") {
407        gets.push("get_bool");
408        puts.push("put_bool");
409    }
410    if uses_fixed_type(types, "float64") {
411        gets.push("get_f64");
412        puts.push("put_f64");
413    }
414    if gets.is_empty() {
415        return quote!();
416    }
417    let mut combined: Vec<&str> = gets.into_iter().chain(puts).collect();
418    combined.sort_unstable();
419    let items: Vec<_> = combined.iter().map(|n| format_ident!("{n}")).collect();
420    quote!(use crate::primitives::fixed::{ #(#items),* };)
421}
422
423/// String-field imports: the `string_bytes` len and put helpers, plus the
424/// borrowed getters. `nullable` pulls in the nullable variants.
425fn string_import(nullable: bool) -> TokenStream {
426    if nullable {
427        quote! {
428            use crate::primitives::string_bytes::{
429                compact_nullable_string_len, compact_string_len, nullable_string_len,
430                put_compact_nullable_string, put_compact_string, put_nullable_string, put_string,
431                string_len,
432            };
433            use crate::primitives::string_bytes_borrowed::{
434                get_compact_nullable_string_borrowed, get_compact_string_borrowed,
435                get_nullable_string_borrowed, get_string_borrowed,
436            };
437        }
438    } else {
439        quote! {
440            use crate::primitives::string_bytes::{
441                compact_string_len, put_compact_string, put_string, string_len,
442            };
443            use crate::primitives::string_bytes_borrowed::{
444                get_compact_string_borrowed, get_string_borrowed,
445            };
446        }
447    }
448}
449
450/// `bytes`-field imports: the `put_*` helpers and the borrowed getters.
451fn bytes_import(use_non_nullable: bool, use_nullable: bool) -> TokenStream {
452    let mut put_items: Vec<&str> = Vec::new();
453    let mut get_borrowed_items: Vec<&str> = Vec::new();
454    if use_non_nullable {
455        put_items.extend(["put_bytes", "put_compact_bytes"]);
456        get_borrowed_items.extend(["get_bytes_borrowed", "get_compact_bytes_borrowed"]);
457    }
458    if use_nullable {
459        put_items.extend(["put_compact_nullable_bytes", "put_nullable_bytes"]);
460        get_borrowed_items.extend([
461            "get_compact_nullable_bytes_borrowed",
462            "get_nullable_bytes_borrowed",
463        ]);
464    }
465    put_items.sort_unstable();
466    get_borrowed_items.sort_unstable();
467    let puts: Vec<_> = put_items.iter().map(|n| format_ident!("{n}")).collect();
468    let gets: Vec<_> = get_borrowed_items
469        .iter()
470        .map(|n| format_ident!("{n}"))
471        .collect();
472    quote! {
473        use crate::primitives::string_bytes::{ #(#puts),* };
474        use crate::primitives::string_bytes_borrowed::{ #(#gets),* };
475    }
476}
477
478/// The `tagged_fields` import line. This function pulls in `encode_to_bytes`
479/// only when there are known tagged fields to encode.
480fn tagged_import(flex: bool, tagged: bool) -> TokenStream {
481    if flex && tagged {
482        quote!(
483            use crate::tagged_fields::{
484                encode_to_bytes, read_tagged_fields, tagged_fields_len, WriteTaggedFields,
485            };
486        )
487    } else if flex {
488        quote!(
489            use crate::tagged_fields::{read_tagged_fields, tagged_fields_len, WriteTaggedFields};
490        )
491    } else {
492        quote!()
493    }
494}
495
496pub(crate) fn emit_imports(
497    spec: &MessageSpec,
498    res_map: &HashMap<String, Resolution>,
499) -> TokenStream {
500    let types = used_field_types_recursive(&spec.fields);
501    let tagged = has_any_tagged_in_spec(spec);
502    let flex = has_any_flex(spec);
503    let use_string = uses_string(&types);
504    let use_bytes = uses_bytes(&types);
505    let use_nullable_string = uses_nullable_string_recursive(&spec.fields);
506    let use_nullable_bytes = uses_nullable_bytes_recursive(&spec.fields);
507    let use_non_nullable_bytes = uses_non_nullable_bytes_recursive(&spec.fields);
508
509    let use_nullable_records = uses_nullable_records_recursive(&spec.fields);
510    let use_non_nullable_records = uses_non_nullable_records_recursive(&spec.fields);
511    let use_records = use_nullable_records || use_non_nullable_records;
512    let use_nullable_struct = uses_nullable_struct_recursive(&spec.fields);
513
514    // `Bytes` is needed for to_owned() on bytes fields.
515    // Records fields use `bytes::BytesMut::new()` inline (fully qualified), so no extra import.
516    let mut out = if use_bytes {
517        quote!(
518            use bytes::{Bytes, BufMut};
519        )
520    } else {
521        quote!(
522            use bytes::BufMut;
523        )
524    };
525
526    // Emit only the specific fixed-type imports actually used, to avoid unused-import warnings.
527    out.extend(fixed_import(&types, use_nullable_struct));
528
529    if use_string {
530        out.extend(string_import(use_nullable_string));
531    }
532
533    if use_bytes {
534        out.extend(bytes_import(use_non_nullable_bytes, use_nullable_bytes));
535    }
536
537    // Records fields: import the byte-primitive helpers used in the generated wrapper code.
538    // Note: compact_bytes_len_from_size is used via fully-qualified path in generated code.
539    if use_records {
540        let mut put_items: Vec<&str> = Vec::new();
541        let mut get_borrowed_items: Vec<&str> = Vec::new();
542        if use_non_nullable_records {
543            put_items.extend(["put_bytes", "put_compact_bytes"]);
544            get_borrowed_items.extend(["get_bytes_borrowed", "get_compact_bytes_borrowed"]);
545        }
546        if use_nullable_records {
547            put_items.extend([
548                "put_bytes",
549                "put_compact_bytes",
550                "put_compact_nullable_bytes",
551                "put_nullable_bytes",
552            ]);
553            get_borrowed_items.extend([
554                "get_compact_nullable_bytes_borrowed",
555                "get_nullable_bytes_borrowed",
556            ]);
557        }
558        put_items.sort_unstable();
559        put_items.dedup();
560        get_borrowed_items.sort_unstable();
561        get_borrowed_items.dedup();
562        let puts: Vec<_> = put_items.iter().map(|n| format_ident!("{n}")).collect();
563        out.extend(quote!(use crate::primitives::string_bytes::{ #(#puts),* };));
564        if !get_borrowed_items.is_empty() {
565            let gets: Vec<_> = get_borrowed_items
566                .iter()
567                .map(|n| format_ident!("{n}"))
568                .collect();
569            out.extend(quote!(use crate::primitives::string_bytes_borrowed::{ #(#gets),* };));
570        }
571    }
572
573    out.extend(tagged_import(flex, tagged));
574
575    // `Decode` is needed when any tagged field uses owned decode (to call the trait method).
576    if has_tagged_fields_needing_owned(spec, res_map) {
577        out.extend(quote!(
578            use crate::{Decode, DecodeBorrow, Encode, ProtocolError, UnknownTaggedFields};
579        ));
580    } else {
581        out.extend(quote!(
582            use crate::{DecodeBorrow, Encode, ProtocolError, UnknownTaggedFields};
583        ));
584    }
585    out
586}
587
588/// Build the `use` items for a borrowed common-struct file body.
589///
590/// This function replicates the import selection of the former
591/// `emit_common_struct_file_borrowed`. It has NO records-import block, because
592/// common structs never carry `records` fields. It also never pulls in the
593/// owned `Decode` trait, because common structs have no top-level
594/// tagged-owned fields.
595pub(crate) fn emit_common_imports(fields: &[FieldSpec], flex_min_val: i16) -> TokenStream {
596    let types = used_field_types_recursive(fields);
597    let has_flex = flex_min_val < i16::MAX;
598    let tagged = fields.iter().any(|f| f.tag.is_some());
599    let use_string = uses_string(&types);
600    let use_bytes = uses_bytes(&types);
601    let use_nullable_struct = uses_nullable_struct_recursive(fields);
602
603    let mut out = if use_bytes {
604        quote!(
605            use bytes::{Bytes, BufMut};
606        )
607    } else {
608        quote!(
609            use bytes::BufMut;
610        )
611    };
612
613    out.extend(fixed_import(&types, use_nullable_struct));
614
615    if use_string {
616        out.extend(string_import(uses_nullable_string_recursive(fields)));
617    }
618
619    if use_bytes {
620        out.extend(bytes_import(
621            uses_non_nullable_bytes_recursive(fields),
622            uses_nullable_bytes_recursive(fields),
623        ));
624    }
625
626    out.extend(tagged_import(has_flex, tagged));
627
628    out.extend(quote!(
629        use crate::{DecodeBorrow, Encode, ProtocolError, UnknownTaggedFields};
630    ));
631    out
632}
633
634// ── constants ──────────────────────────────────────────────────────────────
635
636pub(crate) fn emit_constants(spec: &MessageSpec) -> Result<TokenStream, EmitError> {
637    let min_version = proc_macro2::Literal::i16_unsuffixed(spec.valid_versions.min);
638    let max_version = proc_macro2::Literal::i16_unsuffixed(spec.valid_versions.max);
639    let flex_minimum = flex_min(spec);
640    let flex = proc_macro2::Literal::i16_unsuffixed(flex_minimum);
641    let flex_check = match flex_minimum {
642        i16::MIN => quote!(true),
643        i16::MAX => quote!(version == FLEXIBLE_MIN),
644        _ => quote!(version >= FLEXIBLE_MIN),
645    };
646    // Request/Response schemas have an API key; Header/Data schemas do not.
647    let api_key_const = match spec.message_type {
648        MessageType::Request | MessageType::Response => {
649            let api_key = spec
650                .api_key
651                .map(proc_macro2::Literal::i16_unsuffixed)
652                .ok_or_else(|| EmitError::MissingApiKey(spec.name.clone()))?;
653            quote!(pub const API_KEY: i16 = #api_key;)
654        }
655        // No API_KEY const for framing/data types.
656        MessageType::Header | MessageType::Data => quote!(),
657    };
658    // Only a request can mark its latest version unstable. See
659    // `MessageSpec::latest_stable_version`.
660    let latest_stable_const = match spec.message_type {
661        MessageType::Request => {
662            let latest_stable = proc_macro2::Literal::i16_unsuffixed(spec.latest_stable_version());
663            quote!(pub const LATEST_STABLE_VERSION: i16 = #latest_stable;)
664        }
665        MessageType::Response | MessageType::Header | MessageType::Data => quote!(),
666    };
667    Ok(quote! {
668        #api_key_const
669        pub const MIN_VERSION: i16 = #min_version;
670        pub const MAX_VERSION: i16 = #max_version;
671        #latest_stable_const
672        pub const FLEXIBLE_MIN: i16 = #flex;
673
674        #[inline]
675        #[must_use]
676        pub fn is_flexible(version: i16) -> bool { #flex_check }
677    })
678}
679
680/// Returns a Rust expression for the default value of a borrowed field.
681///
682/// A nullable field with no explicit `"default"` in its schema gets the
683/// *empty* value of its type wrapped in `Some`, matching Kafka's Java message
684/// generator (`FieldSpec.fieldDefaultToJava`): an empty array, an empty
685/// string, or a default-constructed nested struct. Only a field whose schema
686/// explicitly says `"default": "null"` defaults to `None`.
687pub(crate) fn borrowed_default_expr(
688    f: &FieldSpec,
689    res_map: &HashMap<String, Resolution>,
690) -> String {
691    let base = base_type(&f.field_type);
692    let is_array = f.field_type.starts_with("[]");
693    let nullable = is_nullable(f) || matches!(&f.default, Some(serde_json::Value::Null));
694    // Kafka schemas use "null" (string) to mean the default is null for nullable fields.
695    let default_is_null = matches!(&f.default, Some(serde_json::Value::Null))
696        || matches!(&f.default, Some(serde_json::Value::String(s)) if s == "null");
697
698    // Kafka's `FieldSpec.fieldDefaultToJava` hardcodes `null` as the default
699    // for `records` fields unconditionally, regardless of any explicit
700    // schema default. This is a permanent special case, not subject to the
701    // general nullable-default-value rule below. It only applies here when
702    // the field is nullable (`Option<T>` on the Rust side); a `records`
703    // field with no `nullableVersions` (e.g.
704    // `FetchSnapshotResponse.UnalignedRecords`) has no `Option` to put
705    // `None` into, and keeps the empty-value default, matching how Kafka's
706    // Java field (always a plain, nullable-by-language reference) still
707    // requires the caller to set a real value before encoding such a field.
708    if base == "records" && nullable {
709        return "None".into();
710    }
711
712    if nullable && default_is_null {
713        return "None".into();
714    }
715
716    let inner = if is_array {
717        "Vec::new()".into()
718    } else if f.default.is_none()
719        && let Some(resolution) = res_map.get(base)
720    {
721        format!("{}::default()", resolution.rust_path)
722    } else {
723        f.default
724            .as_ref()
725            .map_or_else(|| borrowed_zero(base), |v| scalar_borrowed_default(base, v))
726    };
727
728    if nullable {
729        format!("Some({inner})")
730    } else {
731        inner
732    }
733}
734
735fn scalar_borrowed_default(base_type: &str, val: &serde_json::Value) -> String {
736    // Kafka schema defaults are always stored as JSON strings (e.g., "-1", "true", "false").
737    match (base_type, val) {
738        (_, serde_json::Value::String(s)) if s == "null" => "None".into(),
739        ("string", serde_json::Value::String(s)) if s.is_empty() => "\"\"".into(),
740        ("string", serde_json::Value::String(s)) => format!("\"{s}\""),
741        ("bool", serde_json::Value::Bool(b)) => b.to_string(),
742        ("bool", serde_json::Value::String(s)) => match s.as_str() {
743            "true" => "true".into(),
744            _ => "false".into(),
745        },
746        ("int8", serde_json::Value::String(s)) => format!("{}i8", s.trim()),
747        ("int16", serde_json::Value::String(s)) => format!("{}i16", s.trim()),
748        // Use underscored literals to satisfy clippy::unreadable_literal for large defaults.
749        ("int32", serde_json::Value::String(s)) => format_int_literal(s.trim(), "i32"),
750        ("int64", serde_json::Value::String(s)) => format_int_literal(s.trim(), "i64"),
751        ("int8", serde_json::Value::Number(n)) => format!("{n}i8"),
752        ("int16", serde_json::Value::Number(n)) => format!("{n}i16"),
753        ("int32", serde_json::Value::Number(n)) => format_int_literal(&n.to_string(), "i32"),
754        ("int64", serde_json::Value::Number(n)) => format_int_literal(&n.to_string(), "i64"),
755        _ => borrowed_zero(base_type),
756    }
757}
758
759fn borrowed_zero(base: &str) -> String {
760    match base {
761        "string" => "\"\"".into(),
762        "bytes" => "&[]".into(),
763        "bool" => "false".into(),
764        "int8" => "0i8".into(),
765        "int16" => "0i16".into(),
766        "int32" => "0i32".into(),
767        "int64" => "0i64".into(),
768        "uint16" => "0u16".into(),
769        "uint32" => "0u32".into(),
770        "float64" => "0.0f64".into(),
771        "uuid" => "crate::primitives::uuid::Uuid::default()".into(),
772        "records" => "crate::records::RecordsPayloadBorrowed::default()".into(),
773        _ => "Default::default()".into(),
774    }
775}
776
777/// The expression that converts the borrowed field `expr` to its owned form.
778///
779/// `struct_path` is the borrowed struct type a struct-typed field holds, and
780/// `fallible` says its `to_owned` returns a `Result`. A `records` field's
781/// conversion is always fallible. Every fallible conversion ends in `?`.
782pub(crate) fn to_owned_field_expr(
783    schema_type: &str,
784    expr: &str,
785    nullable: bool,
786    struct_path: Option<&str>,
787    fallible: bool,
788) -> String {
789    // `<path>::to_owned` for a known struct type, a closure otherwise.
790    let struct_to_owned = struct_path.map_or_else(
791        || "|it| it.to_owned()".to_owned(),
792        |path| format!("{path}::to_owned"),
793    );
794    let records_to_owned = "crate::records::RecordsPayloadBorrowed::to_owned";
795    if let Some(elem) = schema_type.strip_prefix("[]") {
796        let elem_base = base_type(elem);
797        if is_struct_type(elem_base) {
798            let collect = if fallible {
799                ".collect::<Result<_, _>>()"
800            } else {
801                ".collect()"
802            };
803            if nullable {
804                let transpose = if fallible { ".transpose()?" } else { "" };
805                return format!(
806                    "({expr}).as_ref().map(|v| v.iter().map({struct_to_owned}){collect}){transpose}"
807                );
808            }
809            let question = if fallible { "?" } else { "" };
810            return format!("({expr}).iter().map({struct_to_owned}){collect}{question}");
811        }
812        // Primitive arrays — Copy types or string slices
813        return match elem {
814            "string" if nullable => {
815                format!("({expr}).as_ref().map(|v| v.iter().map(|&s| s.to_owned()).collect())")
816            }
817            "string" => format!("({expr}).iter().map(|&s| s.to_owned()).collect()"),
818            "bytes" if nullable => format!(
819                "({expr}).as_ref().map(|v| v.iter().map(|b| Bytes::copy_from_slice(b)).collect())"
820            ),
821            "bytes" => format!("({expr}).iter().map(|b| Bytes::copy_from_slice(b)).collect()"),
822            "records" if nullable => format!(
823                "({expr}).as_ref().map(|v| v.iter().map({records_to_owned}).collect::<Result<_, _>>()).transpose()?"
824            ),
825            "records" => {
826                format!("({expr}).iter().map({records_to_owned}).collect::<Result<_, _>>()?")
827            }
828            // Copy types (int*, bool, float64, uuid) — owned Vec<T> is the same
829            _ => format!("({expr}).clone()"),
830        };
831    }
832
833    if is_struct_type(schema_type) {
834        return match (nullable, fallible) {
835            (true, true) => format!("({expr}).as_ref().map({struct_to_owned}).transpose()?"),
836            (true, false) => format!("({expr}).as_ref().map({struct_to_owned})"),
837            (false, true) => format!("({expr}).to_owned()?"),
838            (false, false) => format!("({expr}).to_owned()"),
839        };
840    }
841
842    match (schema_type, nullable) {
843        ("string", false) => format!("({expr}).to_owned()"),
844        ("string", true) => format!("({expr}).map(str::to_owned)"),
845        ("bytes", false) => format!("Bytes::copy_from_slice({expr})"),
846        ("bytes", true) => format!("({expr}).map(Bytes::copy_from_slice)"),
847        ("records", false) => format!("({expr}).to_owned()?"),
848        ("records", true) => format!("({expr}).as_ref().map({records_to_owned}).transpose()?"),
849        _ => {
850            // Copy types (int*, bool, float64, uuid) — owned is the same; just copy
851            format!("({expr})")
852        }
853    }
854}
855
856/// Returns `true` if this field has the per-field override
857/// `"flexibleVersions": "none"`. Such a field must always use the legacy
858/// (non-compact) codec, even in flex message versions.
859pub(crate) fn field_forces_non_flex(f: &FieldSpec) -> bool {
860    matches!(f.flexible_versions, Some(FlexibleVersions::None))
861}
862
863/// Encode a field whose Rust type is `Option<T>` but whose wire format is
864/// non-nullable. This function treats None as the empty or default value for
865/// the underlying type.
866pub(crate) fn encode_call_option_as_non_nullable(schema_type: &str, expr: &str) -> String {
867    if let Some(elem) = schema_type.strip_prefix("[]") {
868        let elem_base = base_type(elem);
869        if is_struct_type(elem_base) {
870            return format!(
871                "{{ let v = ({expr}).as_deref().unwrap_or(&[]); \
872                 crate::primitives::array::put_array_len(buf, v.len(), flex)?; \
873                 for it in v {{ it.encode(buf, version)?; }} }}"
874            );
875        }
876        return format!(
877            "{{ let v = ({expr}).as_deref().unwrap_or(&[]); \
878             crate::primitives::array::put_array_len(buf, v.len(), flex)?; \
879             for it in v {{ {inner}; }} }}",
880            inner = encode_call(elem, "it", false),
881        );
882    }
883    match schema_type {
884        "string" => format!(
885            "if flex {{ put_compact_string(buf, ({expr}).unwrap_or(\"\"))?; }} \
886             else {{ put_string(buf, ({expr}).unwrap_or(\"\"))?; }}"
887        ),
888        "uuid" => format!("crate::primitives::uuid::put_uuid(buf, ({expr}).unwrap_or_default())"),
889        // `records` can't go through `unwrap_or_default()` (that would move out of
890        // `&self`), so match by reference and encode an empty payload for None.
891        "records" => format!(
892            "match &{expr} {{ \
893                None => {{ let __rb_buf = bytes::BytesMut::new(); if flex {{ put_compact_bytes(buf, &__rb_buf)?; }} else {{ put_bytes(buf, &__rb_buf)?; }} }}, \
894                Some(__rb) => {{ let mut __rb_buf = bytes::BytesMut::new(); <crate::records::RecordsPayloadBorrowed as crate::Encode>::encode(__rb, &mut __rb_buf, version)?; if flex {{ put_compact_bytes(buf, &__rb_buf)?; }} else {{ put_bytes(buf, &__rb_buf)?; }} }} \
895            }}"
896        ),
897        _ => encode_call(schema_type, &format!("({expr}).unwrap_or_default()"), false),
898    }
899}
900
901/// Compute `encoded_len` for a field whose Rust type is `Option<T>` but whose
902/// wire format is non-nullable.
903pub(crate) fn encoded_len_expr_option_as_non_nullable(schema_type: &str, expr: &str) -> String {
904    if let Some(elem) = schema_type.strip_prefix("[]") {
905        let elem_base = base_type(elem);
906        if is_struct_type(elem_base) {
907            return format!(
908                "{{ let v = ({expr}).as_deref().unwrap_or(&[]); \
909                 let prefix = crate::primitives::array::array_len_prefix_len(v.len(), flex); \
910                 let body: usize = v.iter().map(|it| it.encoded_len(version)).sum(); \
911                 prefix + body }}"
912            );
913        }
914        return format!(
915            "{{ let v = ({expr}).as_deref().unwrap_or(&[]); \
916             let prefix = crate::primitives::array::array_len_prefix_len(v.len(), flex); \
917             let body: usize = v.iter().map(|it| {inner}).sum(); \
918             prefix + body }}",
919            inner = encoded_len_expr(elem, array_element(elem), false),
920        );
921    }
922    match schema_type {
923        "string" => format!(
924            "if flex {{ compact_string_len(({expr}).unwrap_or(\"\")) }} \
925             else {{ string_len(({expr}).unwrap_or(\"\")) }}"
926        ),
927        "uuid" => "16".into(),
928        "records" => format!(
929            "({expr}).as_ref().map_or_else( \
930                || if flex {{ crate::primitives::string_bytes::compact_bytes_len_from_size(0) }} else {{ 4 }}, \
931                |__rb| {{ let __rb_len = <crate::records::RecordsPayloadBorrowed as crate::Encode>::encoded_len(__rb, version); if flex {{ crate::primitives::string_bytes::compact_bytes_len_from_size(__rb_len) }} else {{ 4 + __rb_len }} }})"
932        ),
933        _ => encoded_len_expr(schema_type, &format!("({expr}).unwrap_or_default()"), false),
934    }
935}
936
937/// Returns a Rust boolean expression that is `true` when the tagged field
938/// equals its schema-specified default. This mirrors
939/// `owned::tagged_is_default_cond`.
940pub(crate) fn tagged_is_default_cond(f: &FieldSpec) -> String {
941    let field = name_conv::field_name(&f.name);
942    let base = base_type(&f.field_type);
943    let is_array = f.field_type.starts_with("[]");
944    let nullable = is_nullable(f) || matches!(&f.default, Some(serde_json::Value::Null));
945    let default_is_null = matches!(&f.default, Some(serde_json::Value::Null))
946        || matches!(&f.default, Some(serde_json::Value::String(s)) if s == "null");
947
948    // `records` fields always default to `None`, per Kafka's generator (see
949    // `borrowed_default_expr`), when they are nullable (`Option<T>`).
950    if base == "records" && nullable {
951        return format!("self.{field}.is_none()");
952    }
953    if nullable && default_is_null {
954        // Explicit `"default": "null"`: the field's default is `None`.
955        return format!("self.{field}.is_none()");
956    }
957    if nullable && f.default.is_none() {
958        // No explicit default: Kafka's implicit default for a nullable
959        // array/string/bytes/struct field is the *empty* value wrapped in
960        // `Some` (see `borrowed_default_expr`), not `None`. `None` (the wire
961        // null) is a distinct, non-default value here and must still be
962        // tag-encoded.
963        let inner = if is_array {
964            "Vec::new()".to_owned()
965        } else {
966            borrowed_zero(base)
967        };
968        return format!("self.{field} == Some({inner})");
969    }
970    if let Some(v) = &f.default {
971        let cmp_val = scalar_borrowed_default(base, v);
972        if cmp_val == "None" {
973            return format!("self.{field}.is_none()");
974        }
975        if nullable {
976            return format!("self.{field} == Some({cmp_val})");
977        }
978        if f.field_type.starts_with("[]") {
979            return format!("self.{field}.is_empty()");
980        }
981        return format!("self.{field} == {cmp_val}");
982    }
983    format!("crate::codegen_helpers::is_default(&self.{field})")
984}
985
986/// Like `encoded_len_expr`, but with owned-type conventions such as
987/// `.as_deref()`. The emitter uses it for tagged fields stored as owned
988/// `String` or `Bytes` in borrowed structs.
989pub(crate) fn owned_encoded_len_expr(schema_type: &str, expr: &str, nullable: bool) -> String {
990    match (schema_type, nullable) {
991        ("string", false) => {
992            format!("if flex {{ compact_string_len(&{expr}) }} else {{ string_len(&{expr}) }}")
993        }
994        ("string", true) => format!(
995            "if flex {{ compact_nullable_string_len({expr}.as_deref()) }} else {{ nullable_string_len({expr}.as_deref()) }}"
996        ),
997        ("bytes", false) => format!(
998            "if flex {{ crate::primitives::string_bytes::compact_bytes_len(&{expr}) }} \
999             else {{ crate::primitives::string_bytes::bytes_len(&{expr}) }}"
1000        ),
1001        ("bytes", true) => format!(
1002            "if flex {{ crate::primitives::string_bytes::compact_nullable_bytes_len({expr}.as_deref()) }} \
1003             else {{ crate::primitives::string_bytes::nullable_bytes_len({expr}.as_deref()) }}"
1004        ),
1005        _ => encoded_len_expr(schema_type, expr, nullable),
1006    }
1007}
1008
1009/// Like `encode_call`, but with owned-type conventions such as `.as_deref()`
1010/// and `&expr`. The emitter uses it for tagged fields stored as owned `String`
1011/// or `Bytes` in the borrowed struct, because their data cannot borrow from
1012/// the input buffer.
1013pub(crate) fn owned_encode_call(schema_type: &str, expr: &str, nullable: bool) -> String {
1014    match (schema_type, nullable) {
1015        ("string", false) => format!(
1016            "if flex {{ put_compact_string(buf, &{expr})?; }} else {{ put_string(buf, &{expr})?; }}"
1017        ),
1018        ("string", true) => format!(
1019            "if flex {{ put_compact_nullable_string(buf, {expr}.as_deref())?; }} else {{ put_nullable_string(buf, {expr}.as_deref())?; }}"
1020        ),
1021        ("bytes", false) => format!(
1022            "if flex {{ put_compact_bytes(buf, &{expr})?; }} else {{ put_bytes(buf, &{expr})?; }}"
1023        ),
1024        ("bytes", true) => format!(
1025            "if flex {{ put_compact_nullable_bytes(buf, {expr}.as_deref())?; }} else {{ put_nullable_bytes(buf, {expr}.as_deref())?; }}"
1026        ),
1027        // For non-string/bytes types, fall back to the borrowed encode call.
1028        _ => encode_call(schema_type, expr, nullable),
1029    }
1030}
1031
1032/// Build the populated-value expression for one borrowed field.
1033pub(crate) fn borrowed_populated_value(
1034    f: &FieldSpec,
1035    res_map: &HashMap<String, Resolution>,
1036    parent_module: &str,
1037    option: bool,
1038) -> Result<String, EmitError> {
1039    let base = base_type(&f.field_type);
1040    let is_array = f.field_type.starts_with("[]");
1041    let owned = is_tagged(f) && tagged_field_needs_owned(f, res_map);
1042    let elem = borrowed_populated_scalar(base, f, res_map, parent_module, owned)?;
1043    let inner = if is_array {
1044        format!("vec![{elem}]")
1045    } else {
1046        elem
1047    };
1048    Ok(if option {
1049        format!("Some({inner})")
1050    } else {
1051        inner
1052    })
1053}
1054
1055fn borrowed_populated_scalar(
1056    base: &str,
1057    f: &FieldSpec,
1058    res_map: &HashMap<String, Resolution>,
1059    parent_module: &str,
1060    owned: bool,
1061) -> Result<String, EmitError> {
1062    Ok(match base {
1063        "bool" => "true".to_owned(),
1064        "int8" => "1i8".to_owned(),
1065        "int16" => "1i16".to_owned(),
1066        "int32" => "1i32".to_owned(),
1067        "int64" => "1i64".to_owned(),
1068        "uint16" => "1u16".to_owned(),
1069        "uint32" => "1u32".to_owned(),
1070        "float64" => "1.0f64".to_owned(),
1071        "uuid" => "crate::primitives::uuid::Uuid([1u8; 16])".to_owned(),
1072        "string" if owned => "\"x\".to_owned()".to_owned(),
1073        "string" => "\"x\"".to_owned(),
1074        "bytes" if owned => "::bytes::Bytes::from_static(b\"x\")".to_owned(),
1075        "bytes" => "&b\"x\"[..]".to_owned(),
1076        _ if owned => {
1077            let path = owned_struct_path_for(f, parent_module, res_map)
1078                .ok_or_else(|| EmitError::Unresolved(f.name.clone()))?;
1079            format!("{path}::populated(version)")
1080        }
1081        _ => {
1082            let path = res_map
1083                .get(base)
1084                .map(|r| r.rust_path.clone())
1085                .ok_or_else(|| EmitError::Unresolved(f.name.clone()))?;
1086            format!("{path}::populated(version)")
1087        }
1088    })
1089}
1090
1091// ── primitive encode/decode call generators ────────────────────────────────
1092
1093/// One element `it` of a primitive array, as its encoder or length helper
1094/// takes it. `it` is `&T` from iteration: a Copy primitive is passed as `*it`,
1095/// and a string or bytes element derefs to the slice the helper takes.
1096fn array_element(elem: &str) -> &'static str {
1097    if matches!(elem, "string" | "bytes") {
1098        "it"
1099    } else {
1100        "*it"
1101    }
1102}
1103
1104/// The statement that encodes one element `it` of a primitive array.
1105fn array_element_encode(elem: &str) -> String {
1106    let call = encode_call(elem, array_element(elem), false);
1107    if call.starts_with("if ") {
1108        call
1109    } else {
1110        format!("{call};")
1111    }
1112}
1113
1114pub(crate) fn encode_call(schema_type: &str, expr: &str, nullable: bool) -> String {
1115    if let Some(elem) = schema_type.strip_prefix("[]") {
1116        let elem_base = base_type(elem);
1117        if is_struct_type(elem_base) {
1118            if nullable {
1119                return format!(
1120                    "{{ let len = ({expr}).as_ref().map(Vec::len); \
1121                     crate::primitives::array::put_nullable_array_len(buf, len, flex)?; \
1122                     if let Some(v) = &{expr} {{ for it in v {{ it.encode(buf, version)?; }} }} }}",
1123                );
1124            }
1125            return format!(
1126                "{{ crate::primitives::array::put_array_len(buf, ({expr}).len(), flex)?; \
1127                 for it in &{expr} {{ it.encode(buf, version)?; }} }}",
1128            );
1129        }
1130        if nullable {
1131            return format!(
1132                "{{ let len = ({expr}).as_ref().map(Vec::len); \
1133                 crate::primitives::array::put_nullable_array_len(buf, len, flex)?; \
1134                 if let Some(v) = &{expr} {{ for it in v {{ {inner} }} }} }}",
1135                inner = array_element_encode(elem),
1136            );
1137        }
1138        return format!(
1139            "{{ crate::primitives::array::put_array_len(buf, ({expr}).len(), flex)?; \
1140             for it in &{expr} {{ {inner} }} }}",
1141            inner = array_element_encode(elem),
1142        );
1143    }
1144
1145    if is_struct_type(schema_type) {
1146        // Non-array struct. Nullable structs use a 1-byte signed prefix: -1 = null, 1 = non-null.
1147        if nullable {
1148            return format!(
1149                "match &{expr} {{ \
1150                 None => {{ buf.put_i8(-1); }}, \
1151                 Some(v) => {{ buf.put_i8(1); v.encode(buf, version)?; }} \
1152                 }}"
1153            );
1154        }
1155        return format!("{expr}.encode(buf, version)?");
1156    }
1157
1158    // Borrowed strings/bytes: `expr` is `&str` or `&[u8]` — no extra `&` needed.
1159    match (schema_type, nullable) {
1160        ("int8", _) => format!("put_i8(buf, {expr})"),
1161        ("int16", _) => format!("put_i16(buf, {expr})"),
1162        ("uint16", _) => format!("put_u16(buf, {expr})"),
1163        ("int32", _) => format!("put_i32(buf, {expr})"),
1164        ("int64", _) => format!("put_i64(buf, {expr})"),
1165        ("bool", _) => format!("put_bool(buf, {expr})"),
1166        ("float64", _) => format!("put_f64(buf, {expr})"),
1167        ("uuid", _) => format!("crate::primitives::uuid::put_uuid(buf, {expr})"),
1168        ("string", false) => format!(
1169            "if flex {{ put_compact_string(buf, {expr})?; }} else {{ put_string(buf, {expr})?; }}"
1170        ),
1171        ("string", true) => format!(
1172            "if flex {{ put_compact_nullable_string(buf, {expr})?; }} else {{ put_nullable_string(buf, {expr})?; }}"
1173        ),
1174        ("bytes", false) => format!(
1175            "if flex {{ put_compact_bytes(buf, {expr})?; }} else {{ put_bytes(buf, {expr})?; }}"
1176        ),
1177        ("bytes", true) => format!(
1178            "if flex {{ put_compact_nullable_bytes(buf, {expr})?; }} else {{ put_nullable_bytes(buf, {expr})?; }}"
1179        ),
1180        ("records", false) => format!(
1181            "{{ \
1182                let mut __rb_buf = bytes::BytesMut::new(); \
1183                <crate::records::RecordsPayloadBorrowed as crate::Encode>::encode(&{expr}, &mut __rb_buf, version)?; \
1184                if flex {{ put_compact_bytes(buf, &__rb_buf)?; }} else {{ put_bytes(buf, &__rb_buf)?; }} \
1185            }}"
1186        ),
1187        ("records", true) => format!(
1188            "match &{expr} {{ \
1189                None => if flex {{ put_compact_nullable_bytes(buf, None)?; }} else {{ put_nullable_bytes(buf, None)?; }}, \
1190                Some(__rb) => {{ \
1191                    let mut __rb_buf = bytes::BytesMut::new(); \
1192                    <crate::records::RecordsPayloadBorrowed as crate::Encode>::encode(__rb, &mut __rb_buf, version)?; \
1193                    if flex {{ put_compact_bytes(buf, &__rb_buf)?; }} else {{ put_bytes(buf, &__rb_buf)?; }} \
1194                }} \
1195            }}"
1196        ),
1197        (t, _) => format!("compile_error!(\"unhandled type in encode_call (borrowed): {t}\")"),
1198    }
1199}
1200
1201pub(crate) fn encoded_len_expr(schema_type: &str, expr: &str, nullable: bool) -> String {
1202    if let Some(elem) = schema_type.strip_prefix("[]") {
1203        let elem_base = base_type(elem);
1204        if is_struct_type(elem_base) {
1205            if nullable {
1206                return format!(
1207                    "{{ let opt: Option<&Vec<_>> = ({expr}).as_ref(); \
1208                     let prefix = crate::primitives::array::nullable_array_len_prefix_len(opt.map(std::vec::Vec::len), flex); \
1209                     let body: usize = opt.map_or(0, |v| v.iter().map(|it| it.encoded_len(version)).sum()); \
1210                     prefix + body }}",
1211                );
1212            }
1213            return format!(
1214                "{{ let prefix = crate::primitives::array::array_len_prefix_len(({expr}).len(), flex); \
1215                 let body: usize = ({expr}).iter().map(|it| it.encoded_len(version)).sum(); \
1216                 prefix + body }}",
1217            );
1218        }
1219        {
1220            // Use `|_|` when the inner expression is constant (doesn't reference `*it`),
1221            // to avoid an unused-variable warning.
1222            let closure_arg = if encoded_len_expr(elem, "*it", false).contains("*it") {
1223                "it"
1224            } else {
1225                "_"
1226            };
1227            let inner = encoded_len_expr(elem, array_element(elem), false);
1228            if nullable {
1229                return format!(
1230                    "{{ let opt: Option<&Vec<_>> = ({expr}).as_ref(); \
1231                     let prefix = crate::primitives::array::nullable_array_len_prefix_len(opt.map(std::vec::Vec::len), flex); \
1232                     let body: usize = opt.map_or(0, |v| v.iter().map(|{closure_arg}| {inner}).sum()); \
1233                     prefix + body }}",
1234                );
1235            }
1236            return format!(
1237                "{{ let prefix = crate::primitives::array::array_len_prefix_len(({expr}).len(), flex); \
1238                 let body: usize = ({expr}).iter().map(|{closure_arg}| {inner}).sum(); \
1239                 prefix + body }}",
1240            );
1241        }
1242    }
1243
1244    if is_struct_type(schema_type) {
1245        if nullable {
1246            // 1 byte for the signed null-marker prefix (–1 or 1) + body when present.
1247            return format!("1 + {expr}.as_ref().map_or(0, |v| v.encoded_len(version))");
1248        }
1249        return format!("{expr}.encoded_len(version)");
1250    }
1251
1252    // Borrowed strings/bytes: `expr` is `&str` or `&[u8]`.
1253    match (schema_type, nullable) {
1254        ("int8" | "bool", _) => "1".into(),
1255        ("int16" | "uint16", _) => "2".into(),
1256        ("int32", _) => "4".into(),
1257        ("int64" | "float64", _) => "8".into(),
1258        ("uuid", _) => "16".into(),
1259        ("string", false) => {
1260            format!("if flex {{ compact_string_len({expr}) }} else {{ string_len({expr}) }}")
1261        }
1262        ("string", true) => format!(
1263            "if flex {{ compact_nullable_string_len({expr}) }} else {{ nullable_string_len({expr}) }}"
1264        ),
1265        ("bytes", false) => format!(
1266            "if flex {{ crate::primitives::string_bytes::compact_bytes_len({expr}) }} \
1267             else {{ crate::primitives::string_bytes::bytes_len({expr}) }}"
1268        ),
1269        ("bytes", true) => format!(
1270            "if flex {{ crate::primitives::string_bytes::compact_nullable_bytes_len({expr}) }} \
1271             else {{ crate::primitives::string_bytes::nullable_bytes_len({expr}) }}"
1272        ),
1273        ("records", false) => format!(
1274            "{{ let __rb_len = <crate::records::RecordsPayloadBorrowed as crate::Encode>::encoded_len(&({expr}), version); \
1275               if flex {{ crate::primitives::string_bytes::compact_bytes_len_from_size(__rb_len) }} \
1276               else {{ 4 + __rb_len }} }}"
1277        ),
1278        ("records", true) => format!(
1279            "({expr}).as_ref().map_or_else( \
1280                || if flex {{ crate::primitives::varint::uvarint_len(0) }} else {{ 4 }}, \
1281                |__rb| {{ let __rb_len = <crate::records::RecordsPayloadBorrowed as crate::Encode>::encoded_len(__rb, version); \
1282                    if flex {{ crate::primitives::string_bytes::compact_bytes_len_from_size(__rb_len) }} \
1283                    else {{ 4 + __rb_len }} }})"
1284        ),
1285        (t, _) => format!("compile_error!(\"unhandled type in encoded_len_expr (borrowed): {t}\")"),
1286    }
1287}
1288
1289pub(crate) fn decode_borrow_call(
1290    schema_type: &str,
1291    nullable: bool,
1292    res_map: &HashMap<String, Resolution>,
1293) -> String {
1294    if let Some(elem) = schema_type.strip_prefix("[]") {
1295        let elem_base = base_type(elem);
1296        if is_struct_type(elem_base) {
1297            let type_path = res_map
1298                .get(elem_base)
1299                .map_or(elem_base, |r| r.rust_path.as_str());
1300            if nullable {
1301                return format!(
1302                    "{{ let opt = crate::primitives::array::get_nullable_array_len(buf, flex)?; \
1303                     match opt {{ None => None, Some(n) => {{ let mut v = Vec::with_capacity(n); \
1304                     for _ in 0..n {{ v.push({type_path}::decode_borrow(buf, version)?); }} Some(v) }} }} }}",
1305                );
1306            }
1307            return format!(
1308                "{{ let n = crate::primitives::array::get_array_len(buf, flex)?; \
1309                 let mut v = Vec::with_capacity(n); \
1310                 for _ in 0..n {{ v.push({type_path}::decode_borrow(buf, version)?); }} v }}",
1311            );
1312        }
1313        if nullable {
1314            return format!(
1315                "{{ let opt = crate::primitives::array::get_nullable_array_len(buf, flex)?; \
1316                 match opt {{ None => None, Some(n) => {{ let mut v = Vec::with_capacity(n); \
1317                 for _ in 0..n {{ v.push({inner}); }} Some(v) }} }} }}",
1318                inner = decode_borrow_call(elem, false, res_map),
1319            );
1320        }
1321        return format!(
1322            "{{ let n = crate::primitives::array::get_array_len(buf, flex)?; \
1323             let mut v = Vec::with_capacity(n); for _ in 0..n {{ v.push({inner}); }} v }}",
1324            inner = decode_borrow_call(elem, false, res_map),
1325        );
1326    }
1327
1328    if is_struct_type(schema_type) {
1329        let type_path = res_map
1330            .get(schema_type)
1331            .map_or(schema_type, |r| r.rust_path.as_str());
1332        if nullable {
1333            // Nullable non-array structs: 1-byte signed prefix < 0 = null, else non-null.
1334            return format!(
1335                "if get_i8(buf)? < 0 {{ None }} else {{ Some({type_path}::decode_borrow(buf, version)?) }}"
1336            );
1337        }
1338        return format!("{type_path}::decode_borrow(buf, version)?");
1339    }
1340
1341    match (schema_type, nullable) {
1342        ("int8",    _) => "get_i8(buf)?".into(),
1343        ("int16",   _) => "get_i16(buf)?".into(),
1344        ("uint16",  _) => "get_u16(buf)?".into(),
1345        ("int32",   _) => "get_i32(buf)?".into(),
1346        ("int64",   _) => "get_i64(buf)?".into(),
1347        ("bool",    _) => "get_bool(buf)?".into(),
1348        ("float64", _) => "get_f64(buf)?".into(),
1349        ("uuid",    _) => "crate::primitives::uuid::get_uuid(buf)?".into(),
1350        ("string", false) => {
1351            "if flex { get_compact_string_borrowed(buf)? } else { get_string_borrowed(buf)? }".into()
1352        }
1353        ("string", true) => {
1354            "if flex { get_compact_nullable_string_borrowed(buf)? } else { get_nullable_string_borrowed(buf)? }".into()
1355        }
1356        ("bytes", false) => {
1357            "if flex { get_compact_bytes_borrowed(buf)? } else { get_bytes_borrowed(buf)? }".into()
1358        }
1359        ("bytes", true) => {
1360            "if flex { get_compact_nullable_bytes_borrowed(buf)? } else { get_nullable_bytes_borrowed(buf)? }".into()
1361        }
1362        ("records", false) => "{ \
1363            let __rb_slice = if flex { get_compact_bytes_borrowed(buf)? } else { get_bytes_borrowed(buf)? }; \
1364            let mut __rb_cur = __rb_slice; \
1365            <crate::records::RecordsPayloadBorrowed as crate::DecodeBorrow>::decode_borrow(&mut __rb_cur, version)? \
1366        }".into(),
1367        ("records", true) => "{ \
1368            let __rb_opt = if flex { get_compact_nullable_bytes_borrowed(buf)? } else { get_nullable_bytes_borrowed(buf)? }; \
1369            match __rb_opt { \
1370                None => None, \
1371                Some(__rb_slice) => { \
1372                    let mut __rb_cur = __rb_slice; \
1373                    Some(<crate::records::RecordsPayloadBorrowed as crate::DecodeBorrow>::decode_borrow(&mut __rb_cur, version)?) \
1374                } \
1375            } \
1376        }".into(),
1377        (t, _) => format!("compile_error!(\"unhandled type in decode_borrow_call: {t}\")"),
1378    }
1379}
1380
1381/// Generate a decode call that produces an **owned** value with `Decode`, not
1382/// with `DecodeBorrow`. The emitter uses it for tagged fields whose content
1383/// cannot be zero-copy decoded, such as strings and owned structs.
1384pub(crate) fn decode_owned_call(
1385    schema_type: &str,
1386    nullable: bool,
1387    parent_module: &str,
1388    res_map: &HashMap<String, Resolution>,
1389) -> String {
1390    if let Some(elem) = schema_type.strip_prefix("[]") {
1391        let elem_base = base_type(elem);
1392        if is_struct_type(elem_base) {
1393            // Use the resolved rust_path but rewrite it to the owned crate path.
1394            // For inline nested structs: rust_path = "TypeName" → "crate::owned::{parent_module}::TypeName"
1395            // For common structs: rust_path = "super::<snake>::TypeName" → "crate::owned::common::<snake>::TypeName"
1396            let owned_path = resolved_to_owned_path(elem_base, parent_module, res_map);
1397            if nullable {
1398                return format!(
1399                    "{{ let opt = crate::primitives::array::get_nullable_array_len(buf, flex)?; \
1400                     match opt {{ None => None, Some(n) => {{ let mut v = Vec::with_capacity(n); \
1401                     for _ in 0..n {{ v.push({owned_path}::decode(buf, version)?); }} Some(v) }} }} }}"
1402                );
1403            }
1404            return format!(
1405                "{{ let n = crate::primitives::array::get_array_len(buf, flex)?; \
1406                 let mut v = Vec::with_capacity(n); \
1407                 for _ in 0..n {{ v.push({owned_path}::decode(buf, version)?); }} v }}"
1408            );
1409        }
1410        // Primitive array element
1411        if nullable {
1412            return format!(
1413                "{{ let opt = crate::primitives::array::get_nullable_array_len(buf, flex)?; \
1414                 match opt {{ None => None, Some(n) => {{ let mut v = Vec::with_capacity(n); \
1415                 for _ in 0..n {{ v.push({inner}); }} Some(v) }} }} }}",
1416                inner = decode_owned_call(elem, false, parent_module, res_map),
1417            );
1418        }
1419        return format!(
1420            "{{ let n = crate::primitives::array::get_array_len(buf, flex)?; \
1421             let mut v = Vec::with_capacity(n); for _ in 0..n {{ v.push({inner}); }} v }}",
1422            inner = decode_owned_call(elem, false, parent_module, res_map),
1423        );
1424    }
1425
1426    if is_struct_type(schema_type) {
1427        let owned_path = resolved_to_owned_path(schema_type, parent_module, res_map);
1428        if nullable {
1429            return format!("Some({owned_path}::decode(buf, version)?)");
1430        }
1431        return format!("{owned_path}::decode(buf, version)?");
1432    }
1433
1434    // Primitive types: same as borrow decode (no lifetime involved)
1435    match (schema_type, nullable) {
1436        ("int8", _) => "get_i8(buf)?".into(),
1437        ("int16", _) => "get_i16(buf)?".into(),
1438        ("uint16", _) => "get_u16(buf)?".into(),
1439        ("int32", _) => "get_i32(buf)?".into(),
1440        ("int64", _) => "get_i64(buf)?".into(),
1441        ("bool", _) => "get_bool(buf)?".into(),
1442        ("float64", _) => "get_f64(buf)?".into(),
1443        ("uuid", _) => "crate::primitives::uuid::get_uuid(buf)?".into(),
1444        ("string", false) => {
1445            "if flex { crate::primitives::string_bytes::get_compact_string_owned(buf)? } \
1446             else { crate::primitives::string_bytes::get_string_owned(buf)? }"
1447                .into()
1448        }
1449        ("string", true) => {
1450            "if flex { crate::primitives::string_bytes::get_compact_nullable_string_owned(buf)? } \
1451             else { crate::primitives::string_bytes::get_nullable_string_owned(buf)? }"
1452                .into()
1453        }
1454        (t, _) => format!("compile_error!(\"unhandled type in decode_owned_call: {t}\")"),
1455    }
1456}
1457
1458#[cfg(test)]
1459mod tests {
1460    use assert2::assert;
1461
1462    use super::*;
1463
1464    /// See `owned::tests::tagged_nullable_no_default_array_field` — the same
1465    /// field shape, mirrored here because `borrowed::tagged_is_default_cond`
1466    /// is a separate implementation from `owned::tagged_is_default_cond`.
1467    fn tagged_nullable_no_default_array_field() -> FieldSpec {
1468        serde_json::from_value(serde_json::json!({
1469            "name": "Owners", "type": "[]string", "versions": "1+",
1470            "nullableVersions": "1+", "taggedVersions": "1+", "tag": 5
1471        }))
1472        .unwrap()
1473    }
1474
1475    /// The borrowed-emitter half of the bug `chatgpt-codex-connector` flagged
1476    /// on PR #31. See `owned::tests::tagged_nullable_no_default_field_defaults_to_some_empty_not_none`
1477    /// for the full rationale.
1478    #[test]
1479    fn tagged_nullable_no_default_field_defaults_to_some_empty_not_none() {
1480        let field = tagged_nullable_no_default_array_field();
1481        let res_map: HashMap<String, Resolution> = HashMap::new();
1482
1483        assert!(borrowed_default_expr(&field, &res_map) == "Some(Vec::new())");
1484        assert!(tagged_is_default_cond(&field) == "self.owners == Some(Vec::new())");
1485    }
1486
1487    /// A nullable, tagged, non-array scalar field with no explicit default.
1488    /// This is the `borrowed_zero(base)` half of the no-default branch that
1489    /// `tagged_nullable_no_default_field_defaults_to_some_empty_not_none`
1490    /// does not reach, since that test's field is an array and takes the
1491    /// `is_array` arm instead.
1492    fn tagged_nullable_no_default_scalar_field() -> FieldSpec {
1493        serde_json::from_value(serde_json::json!({
1494            "name": "Owner", "type": "string", "versions": "1+",
1495            "nullableVersions": "1+", "taggedVersions": "1+", "tag": 6
1496        }))
1497        .unwrap()
1498    }
1499
1500    #[test]
1501    fn tagged_nullable_no_default_scalar_field_defaults_to_some_empty_not_none() {
1502        let field = tagged_nullable_no_default_scalar_field();
1503        let res_map: HashMap<String, Resolution> = HashMap::new();
1504
1505        assert!(borrowed_default_expr(&field, &res_map) == "Some(\"\")");
1506        assert!(tagged_is_default_cond(&field) == "self.owner == Some(\"\")");
1507    }
1508
1509    /// A nullable struct-typed field with no explicit default. Unlike the
1510    /// scalar and array cases, its "empty" value comes from the resolved
1511    /// struct type's own `Default` impl, via the `res_map` lookup in
1512    /// `borrowed_default_expr` — not from `borrowed_zero`.
1513    #[test]
1514    fn nullable_struct_field_with_no_default_wraps_resolved_default_in_some() {
1515        let field: FieldSpec = serde_json::from_value(serde_json::json!({
1516            "name": "Owner", "type": "OwnerStruct", "versions": "1+",
1517            "nullableVersions": "1+"
1518        }))
1519        .unwrap();
1520        let mut res_map: HashMap<String, Resolution> = HashMap::new();
1521        res_map.insert(
1522            "OwnerStruct".to_owned(),
1523            Resolution {
1524                kind: StructKind::Nested,
1525                rust_path: "OwnerStruct".to_owned(),
1526                needs_lifetime: false,
1527            },
1528        );
1529
1530        assert!(borrowed_default_expr(&field, &res_map) == "Some(OwnerStruct::default())");
1531    }
1532
1533    /// The borrowed-emitter half of the `records`-null-default bug a review
1534    /// comment on PR #31 flagged. See
1535    /// `owned::tests::nullable_records_field_always_defaults_to_none` for the
1536    /// full rationale.
1537    #[test]
1538    fn nullable_records_field_always_defaults_to_none() {
1539        let field: FieldSpec = serde_json::from_value(serde_json::json!({
1540            "name": "Records", "type": "records", "versions": "0+",
1541            "nullableVersions": "0+"
1542        }))
1543        .unwrap();
1544        let res_map: HashMap<String, Resolution> = HashMap::new();
1545
1546        assert!(borrowed_default_expr(&field, &res_map) == "None");
1547        assert!(tagged_is_default_cond(&field) == "self.records.is_none()");
1548    }
1549
1550    /// A `records` field with no `nullableVersions` at all keeps the
1551    /// ordinary empty-value default, since there is no `Option` on the Rust
1552    /// side to put `None` into. See
1553    /// `owned::tests::non_nullable_records_field_keeps_empty_default`.
1554    #[test]
1555    fn non_nullable_records_field_keeps_empty_default() {
1556        let field: FieldSpec = serde_json::from_value(serde_json::json!({
1557            "name": "UnalignedRecords", "type": "records", "versions": "0+"
1558        }))
1559        .unwrap();
1560        let res_map: HashMap<String, Resolution> = HashMap::new();
1561
1562        assert!(
1563            borrowed_default_expr(&field, &res_map)
1564                == "crate::records::RecordsPayloadBorrowed::default()"
1565        );
1566    }
1567}