Skip to main content

pack_io_derive/
lib.rs

1//! Procedural macros for [`pack-io`].
2//!
3//! Three derives are provided:
4//!
5//! - `#[derive(Serialize)]` — implements `pack_io::Serialize`.
6//! - `#[derive(Deserialize)]` — implements `pack_io::Deserialize` (owning).
7//! - `#[derive(DeserializeView)]` — implements `pack_io::DeserializeView`
8//!   for types whose fields borrow from the input buffer (`&'a str`,
9//!   `&'a [u8]`, primitives).
10//!
11//! All three work on structs (named, tuple, unit) and enums (any variant
12//! shape). Field order in the source code is the encoded byte order; the
13//! wire format adds a `varint(variant_index)` prefix for enums.
14//!
15//! This crate is not intended to be used directly — depend on
16//! [`pack-io`] with the `derive` feature instead, which re-exports the
17//! macros at `pack_io::{Serialize, Deserialize, DeserializeView}`.
18//!
19//! [`pack-io`]: https://crates.io/crates/pack-io
20
21#![deny(missing_docs)]
22#![forbid(unsafe_code)]
23
24use proc_macro::TokenStream;
25use proc_macro2::TokenStream as TokenStream2;
26use quote::{quote, quote_spanned};
27use syn::spanned::Spanned;
28use syn::{
29    Attribute, Data, DataEnum, DataStruct, DeriveInput, Field, Fields, GenericParam, Generics,
30    Ident, Index, Lifetime, LifetimeParam, parse_macro_input, parse_quote,
31};
32
33// ---------------------------------------------------------------------------
34// Public entry points
35// ---------------------------------------------------------------------------
36
37/// Derive `pack_io::Serialize` for a struct or enum.
38///
39/// Fields are encoded in their declaration order. For enums, a
40/// `varint(variant_index)` is emitted first, followed by the variant's
41/// fields.
42///
43/// Each field type must already implement `pack_io::Serialize`.
44#[proc_macro_derive(Serialize, attributes(pack_io))]
45pub fn derive_serialize(input: TokenStream) -> TokenStream {
46    let input = parse_macro_input!(input as DeriveInput);
47    expand_serialize(&input)
48        .unwrap_or_else(syn::Error::into_compile_error)
49        .into()
50}
51
52/// Derive `pack_io::Deserialize` for a struct or enum (owning decode).
53///
54/// Each field type must already implement `pack_io::Deserialize`.
55#[proc_macro_derive(Deserialize, attributes(pack_io))]
56pub fn derive_deserialize(input: TokenStream) -> TokenStream {
57    let input = parse_macro_input!(input as DeriveInput);
58    expand_deserialize(&input)
59        .unwrap_or_else(syn::Error::into_compile_error)
60        .into()
61}
62
63/// Derive `pack_io::DeserializeView` for a struct or enum (zero-copy decode).
64///
65/// The type MUST have exactly one lifetime parameter — used as the borrow
66/// of the underlying input buffer. Field types must implement
67/// `pack_io::DeserializeView<'that_lifetime>`. The built-in impls for
68/// primitives, `&'a str`, `&'a [u8]`, and the standard `Option` / `Result`
69/// / tuple / array types cover the common cases.
70///
71/// Enums use the same `varint(variant_index) ++ fields` wire shape as
72/// `#[derive(Deserialize)]`; the only difference is each variant's fields
73/// are decoded via `DeserializeView` so borrow-shaped fields land as
74/// `&'a str` / `&'a [u8]` rather than `String` / `Vec<u8>`.
75#[proc_macro_derive(DeserializeView, attributes(pack_io))]
76pub fn derive_deserialize_view(input: TokenStream) -> TokenStream {
77    let input = parse_macro_input!(input as DeriveInput);
78    expand_deserialize_view(&input)
79        .unwrap_or_else(syn::Error::into_compile_error)
80        .into()
81}
82
83// ---------------------------------------------------------------------------
84// Serialize
85// ---------------------------------------------------------------------------
86
87fn expand_serialize(input: &DeriveInput) -> syn::Result<TokenStream2> {
88    let name = &input.ident;
89    let generics = add_trait_bound(&input.generics, parse_quote!(::pack_io::Serialize));
90    let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
91    let type_version = parse_type_attrs(&input.attrs)?;
92
93    let body = match &input.data {
94        Data::Struct(data) => serialize_struct_body(name, data, type_version)?,
95        Data::Enum(data) => {
96            if type_version.is_some() {
97                return Err(syn::Error::new_spanned(
98                    name,
99                    "pack-io: `#[pack_io(version = N)]` is not supported on enums in this \
100                     release; wrap the enum in a versioned newtype struct instead",
101                ));
102            }
103            serialize_enum_body(name, data)?
104        }
105        Data::Union(u) => {
106            return Err(syn::Error::new(
107                u.union_token.span(),
108                "pack-io: `union` is not supported (no defined wire format)",
109            ));
110        }
111    };
112
113    Ok(quote! {
114        #[automatically_derived]
115        impl #impl_generics ::pack_io::Serialize for #name #ty_generics #where_clause {
116            #[inline]
117            fn serialize<__E: ::pack_io::Encode + ?Sized>(
118                &self,
119                __encoder: &mut __E,
120            ) -> ::pack_io::Result<()> {
121                #body
122                ::core::result::Result::Ok(())
123            }
124        }
125    })
126}
127
128fn serialize_struct_body(
129    name: &Ident,
130    data: &DataStruct,
131    type_version: Option<u32>,
132) -> syn::Result<TokenStream2> {
133    // Pair each field accessor with its schema attrs so we can filter / sort.
134    let fields_meta = collect_field_meta(&data.fields, quote!(self))?;
135
136    // Reject schema attrs on a non-versioned struct — they have no meaning
137    // without an enclosing version.
138    if type_version.is_none() {
139        for fm in &fields_meta {
140            if fm.attrs.since.is_some() || fm.attrs.deprecated.is_some() {
141                return Err(syn::Error::new_spanned(
142                    name,
143                    "pack-io: `#[pack_io(since = N)]` / `#[pack_io(deprecated = N)]` requires \
144                     the struct itself to carry `#[pack_io(version = N)]`",
145                ));
146            }
147        }
148    }
149
150    match type_version {
151        None => {
152            // Plain (v0.4) encoding: fields concatenated in declaration order.
153            let writes = fields_meta.iter().map(|fm| {
154                let acc = &fm.accessor;
155                quote! { ::pack_io::Serialize::serialize(&#acc, __encoder)?; }
156            });
157            Ok(quote! { #(#writes)* })
158        }
159        Some(version) => {
160            // Versioned encoding: varint(version) ++ varint(body_len) ++ body.
161            // Body holds exactly the fields whose [since, deprecated) window
162            // includes `version`.
163            let writes = fields_meta
164                .iter()
165                .filter(|fm| fm.attrs.live_at(version))
166                .map(|fm| {
167                    let acc = &fm.accessor;
168                    quote! { ::pack_io::Serialize::serialize(&#acc, &mut __body)?; }
169                });
170            Ok(quote! {
171                __encoder.write_varint_u64(#version as u64)?;
172                let mut __body = ::pack_io::Encoder::new();
173                #(#writes)*
174                let __body_bytes = ::pack_io::Encoder::into_inner(__body);
175                __encoder.write_varint_u64(__body_bytes.len() as u64)?;
176                __encoder.write_bytes(&__body_bytes)?;
177            })
178        }
179    }
180}
181
182fn serialize_enum_body(name: &Ident, data: &DataEnum) -> syn::Result<TokenStream2> {
183    if data.variants.is_empty() {
184        return Err(syn::Error::new_spanned(
185            name,
186            "pack-io: empty enums cannot be serialised (no value to encode)",
187        ));
188    }
189
190    let arms: Vec<TokenStream2> = data
191        .variants
192        .iter()
193        .enumerate()
194        .map(|(index, variant)| {
195            let index = u32::try_from(index).expect("u32 enum variants");
196            let var_name = &variant.ident;
197            let bindings = variant_bindings(&variant.fields);
198            let pattern = match &variant.fields {
199                Fields::Named(_) => quote!(Self::#var_name { #(#bindings),* }),
200                Fields::Unnamed(_) => quote!(Self::#var_name(#(#bindings),*)),
201                Fields::Unit => quote!(Self::#var_name),
202            };
203            let writes = bindings.iter().map(|b| {
204                quote! { ::pack_io::Serialize::serialize(#b, __encoder)?; }
205            });
206            quote! {
207                #pattern => {
208                    __encoder.write_varint_u64(#index as u64)?;
209                    #(#writes)*
210                }
211            }
212        })
213        .collect();
214
215    Ok(quote! {
216        match self {
217            #(#arms)*
218        }
219    })
220}
221
222// ---------------------------------------------------------------------------
223// Deserialize (owning)
224// ---------------------------------------------------------------------------
225
226fn expand_deserialize(input: &DeriveInput) -> syn::Result<TokenStream2> {
227    let name = &input.ident;
228    let generics = add_trait_bound(&input.generics, parse_quote!(::pack_io::Deserialize));
229    let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
230    let type_version = parse_type_attrs(&input.attrs)?;
231
232    let body = match &input.data {
233        Data::Struct(data) => deserialize_struct_body(name, &data.fields, type_version)?,
234        Data::Enum(data) => {
235            if type_version.is_some() {
236                return Err(syn::Error::new_spanned(
237                    name,
238                    "pack-io: `#[pack_io(version = N)]` is not supported on enums in this \
239                     release; wrap the enum in a versioned newtype struct instead",
240                ));
241            }
242            deserialize_enum_body(name, data)?
243        }
244        Data::Union(u) => {
245            return Err(syn::Error::new(
246                u.union_token.span(),
247                "pack-io: `union` is not supported",
248            ));
249        }
250    };
251
252    Ok(quote! {
253        #[automatically_derived]
254        impl #impl_generics ::pack_io::Deserialize for #name #ty_generics #where_clause {
255            #[inline]
256            fn deserialize<__D: ::pack_io::Decode + ?Sized>(
257                __decoder: &mut __D,
258            ) -> ::pack_io::Result<Self> {
259                #body
260            }
261        }
262    })
263}
264
265fn deserialize_struct_body(
266    name: &Ident,
267    fields: &Fields,
268    type_version: Option<u32>,
269) -> syn::Result<TokenStream2> {
270    let fields_meta = collect_field_meta(fields, quote!(self))?;
271
272    if type_version.is_none() {
273        for fm in &fields_meta {
274            if fm.attrs.since.is_some() || fm.attrs.deprecated.is_some() {
275                return Err(syn::Error::new_spanned(
276                    name,
277                    "pack-io: `#[pack_io(since = N)]` / `#[pack_io(deprecated = N)]` requires \
278                     the struct itself to carry `#[pack_io(version = N)]`",
279                ));
280            }
281        }
282    }
283
284    match type_version {
285        None => {
286            // Plain (v0.4) decoding: each field's Deserialize impl in order.
287            let constructor = construct_from_fields(quote!(#name), fields, |ty| {
288                quote_spanned! { ty.span() =>
289                    <#ty as ::pack_io::Deserialize>::deserialize(__decoder)?
290                }
291            });
292            Ok(quote! { ::core::result::Result::Ok(#constructor) })
293        }
294        Some(_) => {
295            // Versioned decoding: read varint(version), varint(body_len),
296            // then run each field through a conditional based on its
297            // [since, deprecated) window vs. the wire's version.
298            //
299            // Only fields whose [since, deprecated) window does NOT span
300            // every possible version (i.e. `since > 1` or `deprecated` is
301            // set) need a `Default` fallback. Fields that are always live
302            // skip the conditional entirely — no `Default` bound required.
303            let field_inits: Vec<TokenStream2> = fields_meta
304                .iter()
305                .enumerate()
306                .map(|(i, fm)| {
307                    let var = field_local_ident(fm, i);
308                    let ty = &fm.ty;
309                    let always_live =
310                        fm.attrs.since.unwrap_or(1) == 1 && fm.attrs.deprecated.is_none();
311                    if always_live {
312                        quote! {
313                            let #var: #ty =
314                                <#ty as ::pack_io::Deserialize>::deserialize(&mut __body_dec)?;
315                        }
316                    } else {
317                        let since = fm.attrs.since.unwrap_or(1);
318                        let deprecated_check = match fm.attrs.deprecated {
319                            Some(d) => quote! { __version < (#d as u32) },
320                            None => quote! { true },
321                        };
322                        quote! {
323                            let #var: #ty = if (#since as u32) <= __version
324                                && #deprecated_check
325                            {
326                                <#ty as ::pack_io::Deserialize>::deserialize(&mut __body_dec)?
327                            } else {
328                                ::core::default::Default::default()
329                            };
330                        }
331                    }
332                })
333                .collect();
334
335            let constructor = match fields {
336                Fields::Named(_) => {
337                    let pairs = fields_meta.iter().enumerate().map(|(i, fm)| {
338                        let name = fm.field_ident.as_ref().expect("named");
339                        let var = field_local_ident(fm, i);
340                        quote! { #name: #var }
341                    });
342                    quote! { #name { #(#pairs),* } }
343                }
344                Fields::Unnamed(_) => {
345                    let positions = fields_meta
346                        .iter()
347                        .enumerate()
348                        .map(|(i, fm)| field_local_ident(fm, i));
349                    quote! { #name ( #(#positions),* ) }
350                }
351                Fields::Unit => quote! { #name },
352            };
353
354            Ok(quote! {
355                let __version_u64 = __decoder.read_varint_u64()?;
356                let __version = u32::try_from(__version_u64)
357                    .map_err(|_| ::pack_io::SerialError::IntegerOutOfRange)?;
358                let __body_len_u64 = __decoder.read_varint_u64()?;
359                let __max = ::pack_io::Decode::max_alloc(__decoder) as u64;
360                if __body_len_u64 > __max {
361                    return ::core::result::Result::Err(::pack_io::SerialError::InvalidLength {
362                        declared: __body_len_u64,
363                        remaining: 0,
364                    });
365                }
366                let __body_len = __body_len_u64 as usize;
367                let mut __body_buf = ::std::vec![0u8; __body_len];
368                __decoder.read_into(&mut __body_buf)?;
369                let mut __body_dec = ::pack_io::Decoder::new(&__body_buf);
370                #(#field_inits)*
371                ::core::result::Result::Ok(#constructor)
372            })
373        }
374    }
375}
376
377/// Emit the body of a `DeserializeView::deserialize_view` impl for an enum.
378///
379/// Mirrors [`deserialize_enum_body`] but routes every field decode through
380/// `DeserializeView<'a>::deserialize_view` instead of
381/// `Deserialize::deserialize`, so zero-copy borrows propagate into each
382/// variant's fields.
383fn deserialize_view_enum_body(
384    name: &Ident,
385    data: &DataEnum,
386    lifetime: &Lifetime,
387) -> syn::Result<TokenStream2> {
388    if data.variants.is_empty() {
389        return Err(syn::Error::new_spanned(
390            name,
391            "pack-io: empty enums cannot be deserialised",
392        ));
393    }
394
395    let arms: Vec<TokenStream2> = data
396        .variants
397        .iter()
398        .enumerate()
399        .map(|(index, variant)| {
400            let index = u32::try_from(index).expect("u32 enum variants");
401            let var_name = &variant.ident;
402            let constructor =
403                construct_from_fields(quote!(#name :: #var_name), &variant.fields, |ty| {
404                    quote_spanned! { ty.span() =>
405                        <#ty as ::pack_io::DeserializeView<#lifetime>>::deserialize_view(__decoder)?
406                    }
407                });
408            quote! { #index => ::core::result::Result::Ok(#constructor), }
409        })
410        .collect();
411
412    let enum_name = name.to_string();
413    Ok(quote! {
414        // Fully-qualified trait call — the `Decode` trait isn't in scope at
415        // the user's call site, so a bare `__decoder.read_varint_u64()`
416        // would fail method resolution.
417        let __tag = ::pack_io::Decode::read_varint_u64(__decoder)?;
418        let __idx = u32::try_from(__tag)
419            .map_err(|_| ::pack_io::SerialError::UnknownVariant {
420                kind: #enum_name,
421                index: u64::MAX,
422            })?;
423        match __idx {
424            #(#arms)*
425            other => ::core::result::Result::Err(::pack_io::SerialError::UnknownVariant {
426                kind: #enum_name,
427                index: u64::from(other),
428            }),
429        }
430    })
431}
432
433fn field_local_ident(fm: &FieldMeta<'_>, index: usize) -> Ident {
434    match &fm.field_ident {
435        Some(id) => Ident::new(&format!("__f_{}", id), id.span()),
436        None => Ident::new(&format!("__f_{}", index), fm.ty.span()),
437    }
438}
439
440fn deserialize_enum_body(name: &Ident, data: &DataEnum) -> syn::Result<TokenStream2> {
441    if data.variants.is_empty() {
442        return Err(syn::Error::new_spanned(
443            name,
444            "pack-io: empty enums cannot be deserialised",
445        ));
446    }
447
448    let arms: Vec<TokenStream2> = data
449        .variants
450        .iter()
451        .enumerate()
452        .map(|(index, variant)| {
453            let index = u32::try_from(index).expect("u32 enum variants");
454            let var_name = &variant.ident;
455            let constructor =
456                construct_from_fields(quote!(#name :: #var_name), &variant.fields, |ty| {
457                    quote_spanned! { ty.span() =>
458                        <#ty as ::pack_io::Deserialize>::deserialize(__decoder)?
459                    }
460                });
461            quote! { #index => ::core::result::Result::Ok(#constructor), }
462        })
463        .collect();
464
465    let enum_name = name.to_string();
466    Ok(quote! {
467        let __tag = __decoder.read_varint_u64()?;
468        let __idx = u32::try_from(__tag)
469            .map_err(|_| ::pack_io::SerialError::UnknownVariant {
470                kind: #enum_name,
471                index: u64::MAX,
472            })?;
473        match __idx {
474            #(#arms)*
475            other => ::core::result::Result::Err(::pack_io::SerialError::UnknownVariant {
476                kind: #enum_name,
477                index: u64::from(other),
478            }),
479        }
480    })
481}
482
483// ---------------------------------------------------------------------------
484// DeserializeView (zero-copy)
485// ---------------------------------------------------------------------------
486
487fn expand_deserialize_view(input: &DeriveInput) -> syn::Result<TokenStream2> {
488    let name = &input.ident;
489
490    let lifetime = extract_single_lifetime(&input.generics).ok_or_else(|| {
491        syn::Error::new_spanned(
492            &input.generics,
493            "pack-io: `DeserializeView` requires the type to have exactly one lifetime parameter \
494             (used as the borrow of the input buffer)",
495        )
496    })?;
497
498    let generics = add_trait_bound_with_lifetime(
499        &input.generics,
500        parse_quote!(::pack_io::DeserializeView<#lifetime>),
501        &lifetime,
502    );
503    let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
504
505    let body = match &input.data {
506        Data::Struct(data) => {
507            let constructor = construct_from_fields(quote!(#name), &data.fields, |ty| {
508                quote_spanned! { ty.span() =>
509                    <#ty as ::pack_io::DeserializeView<#lifetime>>::deserialize_view(__decoder)?
510                }
511            });
512            quote! { ::core::result::Result::Ok(#constructor) }
513        }
514        Data::Enum(data) => deserialize_view_enum_body(name, data, &lifetime)?,
515        Data::Union(u) => {
516            return Err(syn::Error::new(
517                u.union_token.span(),
518                "pack-io: `union` is not supported",
519            ));
520        }
521    };
522
523    Ok(quote! {
524        #[automatically_derived]
525        impl #impl_generics ::pack_io::DeserializeView<#lifetime> for #name #ty_generics #where_clause {
526            #[inline]
527            fn deserialize_view(
528                __decoder: &mut ::pack_io::Decoder<#lifetime>,
529            ) -> ::pack_io::Result<Self> {
530                #body
531            }
532        }
533    })
534}
535
536// ---------------------------------------------------------------------------
537// Schema attribute parsing
538// ---------------------------------------------------------------------------
539
540/// Schema attributes attached to a struct or field.
541#[derive(Default, Clone, Copy)]
542struct SchemaAttrs {
543    /// Only meaningful on fields: the type version at which this field was
544    /// added. Absent ⇒ `1` (always present).
545    since: Option<u32>,
546    /// Only meaningful on fields: the type version at which this field was
547    /// removed. Absent ⇒ never deprecated.
548    deprecated: Option<u32>,
549}
550
551impl SchemaAttrs {
552    /// Is the field live at the given type version (`since <= v < deprecated`)?
553    fn live_at(self, version: u32) -> bool {
554        let since = self.since.unwrap_or(1);
555        if version < since {
556            return false;
557        }
558        match self.deprecated {
559            Some(d) => version < d,
560            None => true,
561        }
562    }
563}
564
565/// Per-field metadata used by the version-aware code generator.
566struct FieldMeta<'a> {
567    /// `self.<name>` (named) / `self.<index>` (tuple) / variant binding.
568    accessor: TokenStream2,
569    /// `Some(ident)` for named fields; `None` for tuple-struct positions.
570    field_ident: Option<&'a Ident>,
571    /// The field's type.
572    ty: &'a syn::Type,
573    /// Parsed `#[pack_io(...)]` attributes on the field.
574    attrs: SchemaAttrs,
575}
576
577/// Parse `#[pack_io(...)]` attributes on a type. Only `version = N` is
578/// accepted at the type level.
579fn parse_type_attrs(attrs: &[Attribute]) -> syn::Result<Option<u32>> {
580    let mut version: Option<u32> = None;
581    for attr in attrs {
582        if !attr.path().is_ident("pack_io") {
583            continue;
584        }
585        attr.parse_nested_meta(|meta| {
586            if meta.path.is_ident("version") {
587                let lit: syn::LitInt = meta.value()?.parse()?;
588                let v: u32 = lit.base10_parse()?;
589                if v == 0 {
590                    return Err(meta.error("pack-io: schema versions start at 1, not 0"));
591                }
592                version = Some(v);
593                Ok(())
594            } else if meta.path.is_ident("since") || meta.path.is_ident("deprecated") {
595                Err(meta.error(
596                    "pack-io: `since` / `deprecated` are field-level attributes; only \
597                     `version` is allowed on the type",
598                ))
599            } else {
600                Err(meta.error("pack-io: unknown attribute (expected `version`)"))
601            }
602        })?;
603    }
604    Ok(version)
605}
606
607/// Parse `#[pack_io(...)]` attributes on a struct field.
608fn parse_field_attrs(attrs: &[Attribute]) -> syn::Result<SchemaAttrs> {
609    let mut out = SchemaAttrs::default();
610    for attr in attrs {
611        if !attr.path().is_ident("pack_io") {
612            continue;
613        }
614        attr.parse_nested_meta(|meta| {
615            if meta.path.is_ident("since") {
616                let lit: syn::LitInt = meta.value()?.parse()?;
617                let v: u32 = lit.base10_parse()?;
618                if v == 0 {
619                    return Err(meta.error("pack-io: schema versions start at 1, not 0"));
620                }
621                out.since = Some(v);
622                Ok(())
623            } else if meta.path.is_ident("deprecated") {
624                let lit: syn::LitInt = meta.value()?.parse()?;
625                let v: u32 = lit.base10_parse()?;
626                if v == 0 {
627                    return Err(meta.error("pack-io: schema versions start at 1, not 0"));
628                }
629                out.deprecated = Some(v);
630                Ok(())
631            } else if meta.path.is_ident("version") {
632                Err(meta.error(
633                    "pack-io: `version` is a type-level attribute; use `since` / `deprecated` \
634                     on individual fields",
635                ))
636            } else {
637                Err(meta.error("pack-io: unknown attribute (expected `since` or `deprecated`)"))
638            }
639        })?;
640    }
641    if let (Some(since), Some(dep)) = (out.since, out.deprecated) {
642        if dep <= since {
643            return Err(syn::Error::new(
644                attrs[0].span(),
645                format!(
646                    "pack-io: `deprecated = {dep}` must be strictly greater than \
647                     `since = {since}` — a field cannot be removed before it is introduced",
648                ),
649            ));
650        }
651    }
652    Ok(out)
653}
654
655/// Walk a struct's fields and collect their per-field metadata.
656fn collect_field_meta(fields: &Fields, owner: TokenStream2) -> syn::Result<Vec<FieldMeta<'_>>> {
657    let mut out = Vec::new();
658    match fields {
659        Fields::Named(named) => {
660            for f in &named.named {
661                let ident = f.ident.as_ref().expect("named fields have idents");
662                out.push(FieldMeta {
663                    accessor: quote!(#owner.#ident),
664                    field_ident: Some(ident),
665                    ty: &f.ty,
666                    attrs: parse_field_attrs(&f.attrs)?,
667                });
668            }
669        }
670        Fields::Unnamed(unnamed) => {
671            for (i, f) in unnamed.unnamed.iter().enumerate() {
672                let idx = Index::from(i);
673                out.push(FieldMeta {
674                    accessor: quote!(#owner.#idx),
675                    field_ident: None,
676                    ty: &f.ty,
677                    attrs: parse_field_attrs(&f.attrs)?,
678                });
679            }
680        }
681        Fields::Unit => {}
682    }
683    Ok(out)
684}
685
686// ---------------------------------------------------------------------------
687// Helpers
688// ---------------------------------------------------------------------------
689
690/// Emit one identifier per field, for use in an enum-pattern binding.
691fn variant_bindings(fields: &Fields) -> Vec<TokenStream2> {
692    match fields {
693        Fields::Named(named) => named
694            .named
695            .iter()
696            .map(|f| {
697                let name = f.ident.as_ref().expect("named fields have idents");
698                quote!(#name)
699            })
700            .collect(),
701        Fields::Unnamed(unnamed) => unnamed
702            .unnamed
703            .iter()
704            .enumerate()
705            .map(|(i, _)| {
706                let id = Ident::new(&format!("__f{i}"), unnamed.unnamed[i].span());
707                quote!(#id)
708            })
709            .collect(),
710        Fields::Unit => Vec::new(),
711    }
712}
713
714/// Build `Path { f1: …, f2: … }`, `Path(…, …)`, or `Path` from a `Fields`
715/// description, using `gen_expr(field_type)` to produce each per-field
716/// expression.
717fn construct_from_fields<F>(path: TokenStream2, fields: &Fields, mut gen_expr: F) -> TokenStream2
718where
719    F: FnMut(&syn::Type) -> TokenStream2,
720{
721    match fields {
722        Fields::Named(named) => {
723            let pieces = named.named.iter().map(|f: &Field| {
724                let name = f.ident.as_ref().expect("named fields have idents");
725                let expr = gen_expr(&f.ty);
726                quote! { #name: #expr }
727            });
728            quote! { #path { #(#pieces),* } }
729        }
730        Fields::Unnamed(unnamed) => {
731            let pieces = unnamed.unnamed.iter().map(|f| gen_expr(&f.ty));
732            quote! { #path ( #(#pieces),* ) }
733        }
734        Fields::Unit => quote! { #path },
735    }
736}
737
738/// Add `: Bound` to every type parameter of `generics`, leaving lifetimes
739/// and const generics alone.
740fn add_trait_bound(generics: &Generics, bound: syn::TypeParamBound) -> Generics {
741    let mut generics = generics.clone();
742    for param in &mut generics.params {
743        if let GenericParam::Type(t) = param {
744            t.bounds.push(bound.clone());
745        }
746    }
747    generics
748}
749
750/// Like `add_trait_bound`, plus ensures the named lifetime outlives every
751/// other generic lifetime — required for the `DeserializeView<'a>` bound on
752/// generic type parameters.
753fn add_trait_bound_with_lifetime(
754    generics: &Generics,
755    bound: syn::TypeParamBound,
756    _lifetime: &Lifetime,
757) -> Generics {
758    let mut generics = generics.clone();
759    for param in &mut generics.params {
760        if let GenericParam::Type(t) = param {
761            t.bounds.push(bound.clone());
762        }
763    }
764    generics
765}
766
767/// Return the single lifetime parameter of `generics`, or `None` if there
768/// are zero or more than one.
769fn extract_single_lifetime(generics: &Generics) -> Option<Lifetime> {
770    let lifetimes: Vec<&LifetimeParam> = generics
771        .params
772        .iter()
773        .filter_map(|p| {
774            if let GenericParam::Lifetime(l) = p {
775                Some(l)
776            } else {
777                None
778            }
779        })
780        .collect();
781    if lifetimes.len() == 1 {
782        Some(lifetimes[0].lifetime.clone())
783    } else {
784        None
785    }
786}