Skip to main content

edifact_rs_derive/
lib.rs

1#![deny(unsafe_code)]
2//! Derive macros for `EdifactSerialize` and `EdifactDeserialize`.
3//!
4//! # Segment struct (single segment)
5//!
6//! ```ignore
7//! #[derive(EdifactSerialize, EdifactDeserialize)]
8//! #[edifact(segment = "BGM")]
9//! pub struct BgmSegment {
10//!     #[edifact(element = 0)]
11//!     pub doc_name_code: String,
12//!     #[edifact(element = 1)]
13//!     pub doc_id: String,
14//!     #[edifact(element = 2)]
15//!     pub msg_function: Option<String>,
16//! }
17//! ```
18//!
19//! # Segment struct with qualifier
20//!
21//! ```ignore
22//! #[derive(EdifactSerialize, EdifactDeserialize)]
23//! #[edifact(segment = "NAD", qualifier = "MS")]
24//! pub struct NadMs {
25//!     #[edifact(element = 1)]
26//!     pub party_id: String,
27//! }
28//! ```
29//!
30//! # Message struct (multiple segments)
31//!
32//! ```ignore
33//! #[derive(EdifactSerialize, EdifactDeserialize)]
34//! pub struct OrdersMessage {
35//!     pub bgm: BgmSegment,
36//!     pub buyer: NadMs,
37//!     #[edifact(group)]
38//!     pub lines: Vec<LinSegment>,
39//! }
40//! ```
41//!
42//! # `#[edifact(group)]` and `Vec<T>` fields
43//!
44//! The `#[edifact(group)]` attribute marks a `Vec<T>` field as a contiguous group of
45//! repeated segments.  Without the attribute, `Vec<T>` on a segment struct collects
46//! all matching segments from the window into the `Vec`.
47//!
48//! `#[edifact(group)]` is a documentation and validation marker: it makes the
49//! repeating-group intent explicit and enforces two compile-time constraints:
50//!
51//! 1. The annotated field **must** be of type `Vec<T>` — any other type is rejected
52//!    with a clear error message.
53//! 2. `#[edifact(group)]` cannot be combined with `#[edifact(element = ...)]` or
54//!    `#[edifact(component = ...)]` — positional placement and group semantics are
55//!    mutually exclusive.
56//!
57//! At runtime it generates the same code as a bare `Vec<T>`: every segment matching
58//! `T`'s tag and qualifier is collected, in document order, without a contiguity
59//! requirement.  When you need contiguity enforced, use
60//! [`contiguous_groups_by_qualifier`] directly on the parsed segments.
61//!
62//! [`contiguous_groups_by_qualifier`]: https://docs.rs/edifact-rs/latest/edifact_rs/fn.contiguous_groups_by_qualifier.html
63//!
64//! # `#[edifact(required)]` on `Option<T>` fields
65//!
66//! By default, `Option<T>` fields produce `None` when the element is absent.
67//! Annotating an `Option<T>` field with `#[edifact(required)]` changes this:
68//! instead of `None`, deserialization returns
69//! `edifact_rs::EdifactError::MissingRequiredElement`
70//! when the element is absent or empty.  The Rust type stays `Option<T>`, which
71//! is useful when the EDIFACT specification mandates the element but your domain
72//! model treats it as optional for other reasons.
73//!
74//! ```ignore
75//! #[derive(EdifactSerialize, EdifactDeserialize)]
76//! #[edifact(segment = "DTM")]
77//! pub struct DtmSegment {
78//!     #[edifact(element = 0)]
79//!     qualifier: String,
80//!     /// Required by the spec but kept as Option in the domain model.
81//!     #[edifact(element = 1, required)]
82//!     date_time: Option<String>,
83//!     #[edifact(element = 2)]
84//!     format_code: Option<String>,
85//! }
86//! ```
87//!
88//! # Non-`String` fields and `Display` / `FromStr`
89//! Non-`String` field types (e.g. `u32`, `bool`, your own newtype) are serialized via
90//! `Display` and deserialized via `FromStr`.  The derive macro does **not** add a
91//! compile-time bound; if the type does not implement both traits the generated code
92//! will fail to compile with a standard "trait not satisfied" error.
93//!
94//! To avoid surprises, ensure any non-`String` field type implements both:
95//! ```ignore
96//! impl std::fmt::Display for MyCode { ... }
97//! impl std::str::FromStr for MyCode { ... }
98//! ```
99
100use proc_macro::TokenStream;
101use proc_macro2::TokenStream as TokenStream2;
102use quote::quote;
103use syn::{Data, DeriveInput, Field, Fields, Type, parse_macro_input, spanned::Spanned};
104
105// ── entry points ───────────────────────────────────────────────────────────────
106
107#[proc_macro_derive(EdifactSerialize, attributes(edifact))]
108/// Derive `edifact_rs::EdifactSerialize` for segment or message structs.
109///
110/// # Limitations
111///
112/// - **No generics**: the struct must not have generic type parameters.
113/// - **No lifetime parameters**: the struct must own all its data (`String`,
114///   not `&str`).  Borrow-based structs such as `Segment<'a>` cannot use this
115///   derive macro.
116pub fn derive_edifact_serialize(input: TokenStream) -> TokenStream {
117    let input = parse_macro_input!(input as DeriveInput);
118    impl_serialize(&input)
119        .unwrap_or_else(|e| e.to_compile_error())
120        .into()
121}
122
123#[proc_macro_derive(EdifactDeserialize, attributes(edifact))]
124/// Derive `edifact_rs::EdifactDeserialize` for segment or message structs.
125///
126/// # Limitations
127///
128/// - **No generics**: the struct must not have generic type parameters.
129/// - **No lifetime parameters**: the struct must own all its data (`String`,
130///   not `&str`).  Add owned wrapper types or clone components at the
131///   deserialization site if lifetime flexibility is required.
132pub fn derive_edifact_deserialize(input: TokenStream) -> TokenStream {
133    let input = parse_macro_input!(input as DeriveInput);
134    impl_deserialize(&input)
135        .unwrap_or_else(|e| e.to_compile_error())
136        .into()
137}
138
139// ── attribute containers ───────────────────────────────────────────────────────
140
141/// How a field's element slot was written in the attribute.
142///
143/// `Index` is the historical positional form.  `Code` is a UN/EDIFACT data
144/// element identifier resolved against the struct's `layout` — during *const
145/// evaluation*, so a stale or mistyped identifier is a compile error rather
146/// than a silent read of the neighbouring element.
147#[derive(Clone)]
148enum Position {
149    /// `#[edifact(element = 4)]`
150    Index(u32),
151    /// `#[edifact(element = "3055")]`
152    Code(String),
153}
154
155#[derive(Default)]
156struct StructAttrs {
157    /// `#[edifact(segment = "TAG")]`
158    segment: Option<String>,
159    /// `#[edifact(qualifier = "Q")]` — element 0 value for segment matching
160    qualifier: Option<String>,
161    qualifier_span: Option<proc_macro2::Span>,
162    /// `#[edifact(qualifier_from = N)]` — zero-based element index; qualifier is dynamic at runtime.
163    qualifier_from: Option<u32>,
164    qualifier_from_span: Option<proc_macro2::Span>,
165    /// `#[edifact(layout = path::to::SEGMENT_DEFINITION)]` — the directory
166    /// definition that code-based `element` attributes resolve against.
167    layout: Option<syn::Path>,
168    layout_span: Option<proc_macro2::Span>,
169}
170
171#[derive(Default)]
172struct FieldAttrs {
173    /// `#[edifact(element = N)]` or `#[edifact(element = "3055")]`
174    element: Option<Position>,
175    element_span: Option<proc_macro2::Span>,
176    /// `#[edifact(component = N)]` — component index within the element (for composite data elements)
177    component: Option<u32>,
178    component_span: Option<proc_macro2::Span>,
179    /// `#[edifact(composite)]` — map the field as a full composite element via composite serde traits.
180    composite: bool,
181    composite_span: Option<proc_macro2::Span>,
182    /// `#[edifact(group)]` — `Vec<T>`: each item is a separate segment
183    group: bool,
184    group_span: Option<proc_macro2::Span>,
185    /// `#[edifact(qualifier = "Q")]` — message field constrained to qualifier.
186    qualifier: Option<String>,
187    qualifier_span: Option<proc_macro2::Span>,
188    /// `#[edifact(required)]` — treat an `Option<T>` field as mandatory.
189    ///
190    /// Without this attribute, `Option<T>` fields produce `None` when the element
191    /// is absent.  With `#[edifact(required)]` the deserialization emits
192    /// `EdifactError::MissingRequiredElement` instead, even though the Rust type is
193    /// still `Option<T>`.  This is useful for elements that the EDIFACT spec marks as
194    /// mandatory but which your domain model represents as optional for other reasons.
195    required: bool,
196    required_span: Option<proc_macro2::Span>,
197}
198
199// ── attribute parsing ──────────────────────────────────────────────────────────
200
201/// Largest accepted `element` / `component` index.
202///
203/// Serialization emits one statement per slot up to the highest declared index,
204/// so an unbounded value makes rustc generate an arbitrary amount of code — a
205/// typo'd `element = 200000` was enough to exhaust memory and kill the compiler.
206/// UN/EDIFACT caps segments at 99 data elements and composites at 99 components,
207/// so 256 is generous.
208const MAX_POSITION_INDEX: u32 = 256;
209
210fn check_index_bound(lit: &syn::LitInt, value: u32, key: &str) -> syn::Result<()> {
211    if value > MAX_POSITION_INDEX {
212        return Err(syn::Error::new(
213            lit.span(),
214            format!(
215                "`{key}` index {value} exceeds the maximum of {MAX_POSITION_INDEX}; \
216                 UN/EDIFACT allows at most 99 elements per segment and 99 components per composite"
217            ),
218        ));
219    }
220    Ok(())
221}
222
223fn parse_struct_attrs(input: &DeriveInput) -> syn::Result<StructAttrs> {
224    let mut out = StructAttrs::default();
225    for attr in &input.attrs {
226        if !attr.path().is_ident("edifact") {
227            continue;
228        }
229        attr.parse_nested_meta(|meta| {
230            if meta.path.is_ident("segment") {
231                if out.segment.is_some() {
232                    return Err(meta.error("duplicate `segment`"));
233                }
234                let lit = meta.value()?.parse::<syn::LitStr>()?;
235                let tag = lit.value();
236                if tag.len() != 3 || !tag.bytes().all(|b| b.is_ascii_uppercase()) {
237                    return Err(syn::Error::new(
238                        lit.span(),
239                        format!(
240                            "segment tag must be exactly 3 ASCII uppercase letters; got {tag:?}"
241                        ),
242                    ));
243                }
244                out.segment = Some(tag);
245            } else if meta.path.is_ident("qualifier") {
246                if out.qualifier.is_some() {
247                    return Err(meta.error("duplicate `qualifier`"));
248                }
249                out.qualifier = Some(meta.value()?.parse::<syn::LitStr>()?.value());
250                out.qualifier_span = Some(meta.path.span());
251            } else if meta.path.is_ident("qualifier_from") {
252                if out.qualifier_from.is_some() {
253                    return Err(meta.error("duplicate `qualifier_from`"));
254                }
255                let lit = meta.value()?.parse::<syn::LitInt>()?;
256                let idx: u32 = lit.base10_parse()?;
257                check_index_bound(&lit, idx, "qualifier_from")?;
258                out.qualifier_from = Some(idx);
259                out.qualifier_from_span = Some(meta.path.span());
260            } else if meta.path.is_ident("layout") {
261                if out.layout.is_some() {
262                    return Err(meta.error("duplicate `layout`"));
263                }
264                out.layout_span = Some(meta.path.span());
265                let value = meta.value()?;
266                // Accept both `layout = crate::defs::NAD` and the string form
267                // `layout = "crate::defs::NAD"`, since attribute paths are
268                // commonly written either way.
269                out.layout = Some(if value.peek(syn::LitStr) {
270                    value.parse::<syn::LitStr>()?.parse()?
271                } else {
272                    value.parse::<syn::Path>()?
273                });
274            } else {
275                return Err(meta.error("unknown struct-level `edifact` key; expected `segment`, `qualifier`, `qualifier_from`, or `layout`"));
276            }
277            Ok(())
278        })?;
279    }
280    if out.layout.is_some() && out.segment.is_none() {
281        return Err(syn::Error::new(
282            out.layout_span.unwrap_or_else(|| input.span()),
283            "#[edifact(layout = ...)] requires #[edifact(segment = ...)]: a layout describes one segment",
284        ));
285    }
286    if (out.qualifier.is_some() || out.qualifier_from.is_some()) && out.segment.is_none() {
287        return Err(syn::Error::new(
288            out.qualifier_span
289                .or(out.qualifier_from_span)
290                .unwrap_or_else(|| input.span()),
291            "#[edifact(qualifier = ...)] / #[edifact(qualifier_from = ...)] require #[edifact(segment = ...)]",
292        ));
293    }
294    if out.qualifier.is_some() && out.qualifier_from.is_some() {
295        return Err(syn::Error::new(
296            out.qualifier_from_span
297                .or(out.qualifier_span)
298                .unwrap_or_else(|| input.span()),
299            "use either #[edifact(qualifier = ...)] or #[edifact(qualifier_from = ...)], not both",
300        ));
301    }
302    Ok(out)
303}
304
305fn parse_field_attrs(field: &Field) -> syn::Result<FieldAttrs> {
306    let mut out = FieldAttrs::default();
307    for attr in &field.attrs {
308        if !attr.path().is_ident("edifact") {
309            continue;
310        }
311        attr.parse_nested_meta(|meta| {
312            if meta.path.is_ident("element") {
313                if out.element.is_some() {
314                    return Err(meta.error("duplicate `element`"));
315                }
316                out.element_span = Some(meta.path.span());
317                let value = meta.value()?;
318                out.element = Some(if value.peek(syn::LitStr) {
319                    let lit = value.parse::<syn::LitStr>()?;
320                    let code = lit.value();
321                    if code.is_empty() {
322                        return Err(syn::Error::new(
323                            lit.span(),
324                            "`element` data element identifier must not be empty",
325                        ));
326                    }
327                    Position::Code(code)
328                } else {
329                    let lit = value.parse::<syn::LitInt>()?;
330                    let idx: u32 = lit.base10_parse()?;
331                    check_index_bound(&lit, idx, "element")?;
332                    Position::Index(idx)
333                });
334            } else if meta.path.is_ident("component") {
335                if out.component.is_some() {
336                    return Err(meta.error("duplicate `component`"));
337                }
338                out.component_span = Some(meta.path.span());
339                let value = meta.value()?;
340                if value.peek(syn::LitStr) {
341                    let lit = value.parse::<syn::LitStr>()?;
342                    return Err(syn::Error::new(
343                        lit.span(),
344                        "put the data element identifier in `element`: \
345                         `#[edifact(element = \"3055\")]` resolves both the element and the \
346                         component position from the directory",
347                    ));
348                }
349                let lit = value.parse::<syn::LitInt>()?;
350                let idx: u32 = lit.base10_parse()?;
351                check_index_bound(&lit, idx, "component")?;
352                out.component = Some(idx);
353            } else if meta.path.is_ident("composite") {
354                out.composite = true;
355                out.composite_span = Some(meta.path.span());
356            } else if meta.path.is_ident("group") {
357                out.group = true;
358                out.group_span = Some(meta.path.span());
359            } else if meta.path.is_ident("qualifier") {
360                if out.qualifier.is_some() {
361                    return Err(meta.error("duplicate `qualifier`"));
362                }
363                out.qualifier = Some(meta.value()?.parse::<syn::LitStr>()?.value());
364                out.qualifier_span = Some(meta.path.span());
365            } else if meta.path.is_ident("required") {
366                out.required = true;
367                out.required_span = Some(meta.path.span());
368            } else {
369                return Err(meta.error("unknown field-level `edifact` key; expected `element`, `component`, `composite`, `group`, `qualifier`, or `required`"));
370            }
371            Ok(())
372        })?;
373    }
374    Ok(out)
375}
376
377// ── slot resolution ────────────────────────────────────────────────────────────
378
379/// The element/component slot a field maps to, as token expressions.
380struct Slots {
381    /// `usize` expression for the zero-based element index.
382    element: TokenStream2,
383    /// `usize` expression for the zero-based component index.
384    component: TokenStream2,
385    /// Whether the field reads through a component accessor.
386    ///
387    /// Always true for a code-addressed field: whether the identifier landed on
388    /// a component or a whole element is only known after const evaluation, and
389    /// reading component 0 is equivalent to reading the element either way.
390    has_component: bool,
391    /// `bool` expression: does this field address a component *inside* a
392    /// composite?
393    ///
394    /// Decides between [`EdifactError::MissingRequiredComponent`] and
395    /// [`EdifactError::MissingRequiredElement`].  It cannot be folded into
396    /// `has_component`, because a code naming the first component of a composite
397    /// resolves to component index 0 just like a whole element does — and
398    /// reporting `E021` where `E008` belongs sends downstream routing to the
399    /// wrong branch.  For a code slot this is a `const` lookup, so the branch
400    /// folds away.
401    names_component: TokenStream2,
402}
403
404/// Resolve every field's slot, emitting the `const` items that code-addressed
405/// fields need.
406///
407/// The returned prelude must be placed at the top of each generated function
408/// body that uses the slots.  It carries three compile-time guarantees:
409/// every identifier exists in the layout, none is ambiguous, and no two fields
410/// claim the same slot.
411fn resolve_slots(
412    struct_attrs: &StructAttrs,
413    field_data: &[(&syn::Ident, &Type, FieldAttrs)],
414) -> syn::Result<(TokenStream2, Vec<Slots>)> {
415    let mut consts: Vec<TokenStream2> = Vec::new();
416    let mut slots: Vec<Slots> = Vec::with_capacity(field_data.len());
417    let mut slot_entries: Vec<TokenStream2> = Vec::new();
418
419    if struct_attrs.qualifier.is_some() {
420        // The struct-level qualifier owns element 0 / component 0.
421        slot_entries.push(quote! { (0usize, 0usize) });
422    }
423
424    for (i, (ident, _, attrs)) in field_data.iter().enumerate() {
425        if attrs.group {
426            slots.push(Slots {
427                element: quote! { 0usize },
428                component: quote! { 0usize },
429                has_component: false,
430                names_component: quote! { false },
431            });
432            continue;
433        }
434        let component_index = attrs.component.unwrap_or(0);
435        let slot = match &attrs.element {
436            Some(Position::Code(code)) => {
437                let Some(layout) = &struct_attrs.layout else {
438                    return Err(syn::Error::new(
439                        attrs.element_span.unwrap_or_else(|| ident.span()),
440                        format!(
441                            "field `{ident}`: `element = \"{code}\"` addresses a UN/EDIFACT data \
442                             element identifier, which needs a directory to resolve against; add \
443                             #[edifact(layout = path::to::SEGMENT_DEFINITION)] to the struct"
444                        ),
445                    ));
446                };
447                let slot_ident = syn::Ident::new(
448                    &format!("__EDIFACT_SLOT_{i}"),
449                    attrs.element_span.unwrap_or_else(|| ident.span()),
450                );
451                let unknown_msg = format!(
452                    "field `{ident}`: data element {code} is not defined exactly once in the \
453                     segment layout — check the identifier against the directory"
454                );
455                consts.push(quote! {
456                    const _: () = ::core::assert!(#layout.code_positions(#code) == 1, #unknown_msg);
457                });
458                if attrs.component.is_some() {
459                    let conflict_msg = format!(
460                        "field `{ident}`: `component = N` may only accompany an identifier that \
461                         names a whole data element, but {code} names a component inside one"
462                    );
463                    consts.push(quote! {
464                        const _: () =
465                            ::core::assert!(#layout.component_slot(#code) == 0, #conflict_msg);
466                    });
467                    consts.push(quote! {
468                        // The guard above already reported an unresolvable
469                        // identifier by name; short-circuit so `element_slot`
470                        // does not panic a second time with a vaguer message.
471                        const #slot_ident: (usize, usize) =
472                            if #layout.code_positions(#code) == 1 {
473                                (#layout.element_slot(#code), #component_index as usize)
474                            } else {
475                                (0, 0)
476                            };
477                    });
478                } else {
479                    consts.push(quote! {
480                        const #slot_ident: (usize, usize) =
481                            if #layout.code_positions(#code) == 1 {
482                                (#layout.element_slot(#code), #layout.component_slot(#code))
483                            } else {
484                                (0, 0)
485                            };
486                    });
487                }
488                slot_entries.push(quote! { #slot_ident });
489                let names_component = if attrs.component.is_some() {
490                    // `component = N` on a whole-element identifier: the field
491                    // does address a component inside that composite.
492                    quote! { true }
493                } else {
494                    quote! { #layout.code_is_component(#code) }
495                };
496                Slots {
497                    element: quote! { #slot_ident.0 },
498                    component: quote! { #slot_ident.1 },
499                    has_component: true,
500                    names_component,
501                }
502            }
503            Some(Position::Index(idx)) => {
504                let idx = *idx;
505                let explicit = attrs.component.is_some();
506                slot_entries.push(quote! { (#idx as usize, #component_index as usize) });
507                Slots {
508                    element: quote! { #idx as usize },
509                    component: quote! { #component_index as usize },
510                    has_component: explicit,
511                    names_component: quote! { #explicit },
512                }
513            }
514            None => {
515                // Declaration order is the implicit element index.
516                let idx = i as u32;
517                let explicit = attrs.component.is_some();
518                slot_entries.push(quote! { (#idx as usize, #component_index as usize) });
519                Slots {
520                    element: quote! { #idx as usize },
521                    component: quote! { #component_index as usize },
522                    has_component: explicit,
523                    names_component: quote! { #explicit },
524                }
525            }
526        };
527        slots.push(slot);
528    }
529
530    // Slot collisions between code-addressed fields (and between a code and a
531    // positional field) can only be seen after const evaluation, so the check
532    // itself has to run there.  Macro-time `check_duplicate_slots` still covers
533    // the all-positional case with a friendlier message.
534    if struct_attrs.layout.is_some() && !slot_entries.is_empty() {
535        let count = slot_entries.len();
536        consts.push(quote! {
537            const __EDIFACT_SLOTS: [(usize, usize); #count] = [#(#slot_entries),*];
538            const _: () = {
539                let mut i = 0;
540                while i < __EDIFACT_SLOTS.len() {
541                    let mut j = i + 1;
542                    while j < __EDIFACT_SLOTS.len() {
543                        ::core::assert!(
544                            !(__EDIFACT_SLOTS[i].0 == __EDIFACT_SLOTS[j].0
545                                && __EDIFACT_SLOTS[i].1 == __EDIFACT_SLOTS[j].1),
546                            "two fields resolve to the same element/component slot; \
547                             give each field a distinct data element identifier or index"
548                        );
549                        j += 1;
550                    }
551                    i += 1;
552                }
553            };
554        });
555    }
556
557    Ok((quote! { #(#consts)* }, slots))
558}
559
560// ── type helpers ───────────────────────────────────────────────────────────────
561
562fn is_option_type(ty: &Type) -> bool {
563    matches!(ty, Type::Path(p) if p.path.segments.last().is_some_and(|s| s.ident == "Option"))
564}
565
566fn is_vec_type(ty: &Type) -> bool {
567    matches!(ty, Type::Path(p) if p.path.segments.last().is_some_and(|s| s.ident == "Vec"))
568}
569
570/// Returns `true` for the `String` path type.
571///
572/// Accepts only:
573/// - `String` (bare, single-segment)
574/// - `std::string::String` (fully qualified standard library path)
575/// - `alloc::string::String` (fully qualified alloc path for `no_std` contexts)
576///
577/// A user-defined type whose last segment is `String` but that does not match
578/// one of these three forms is **not** treated as a string type, which prevents
579/// accidental string-extraction code generation for unrelated user types.
580///
581/// **Shadowing caveat:** bare `String` (single-segment, no path prefix) is matched
582/// by name only. If a crate shadows the standard-library `String` with a local type
583/// of the same name, this function will still classify it as a string type and the
584/// derive macro will generate incorrect string-extraction code rather than a
585/// composite or element parse. To avoid this, always use the fully-qualified path
586/// (`std::string::String`) in struct fields when `String` is shadowed in scope.
587fn is_string_type(ty: &Type) -> bool {
588    let Type::Path(p) = ty else { return false };
589    // Single-segment bare "String"
590    if p.path.is_ident("String") {
591        return true;
592    }
593    // Fully-qualified std::string::String or alloc::string::String
594    let segs = &p.path.segments;
595    segs.len() == 3
596        && (segs[0].ident == "std" || segs[0].ident == "alloc")
597        && segs[1].ident == "string"
598        && segs[2].ident == "String"
599}
600
601/// Returns `true` for `&str` or `&'_ str` reference types.
602fn is_str_ref_type(ty: &Type) -> bool {
603    let Type::Reference(r) = ty else { return false };
604    matches!(r.elem.as_ref(), Type::Path(p) if p.path.is_ident("str"))
605}
606
607/// Returns `true` when `ty` is a type that can yield `&str` without allocating
608/// (i.e. `String` or `&str`).
609fn is_str_like(ty: &Type) -> bool {
610    is_string_type(ty) || is_str_ref_type(ty)
611}
612
613fn option_inner_type(ty: &Type) -> Option<&Type> {
614    let Type::Path(path) = ty else { return None };
615    let seg = path.path.segments.last()?;
616    if seg.ident != "Option" {
617        return None;
618    }
619    let syn::PathArguments::AngleBracketed(args) = &seg.arguments else {
620        return None;
621    };
622    let syn::GenericArgument::Type(inner) = args.args.first()? else {
623        return None;
624    };
625    Some(inner)
626}
627
628fn vec_inner_type(ty: &Type) -> Option<&Type> {
629    let Type::Path(path) = ty else { return None };
630    let seg = path.path.segments.last()?;
631    if seg.ident != "Vec" {
632        return None;
633    }
634    let syn::PathArguments::AngleBracketed(args) = &seg.arguments else {
635        return None;
636    };
637    let syn::GenericArgument::Type(inner) = args.args.first()? else {
638        return None;
639    };
640    Some(inner)
641}
642
643// ── named field extraction ─────────────────────────────────────────────────────
644
645/// Reject two fields that map to the same `(element, component)` slot.
646///
647/// Serialization keys its emit table by slot, so a duplicate silently dropped
648/// one field from the output while deserialization still read both — an
649/// asymmetric, compile-clean data loss.
650///
651/// Only positional fields are checked here; code-addressed fields have no
652/// macro-time index, and are covered by the const-evaluated uniqueness check
653/// emitted by [`resolve_slots`].
654fn check_duplicate_slots(
655    field_data: &[(&syn::Ident, &Type, FieldAttrs)],
656    is_segment_struct: bool,
657) -> syn::Result<()> {
658    if !is_segment_struct {
659        return Ok(());
660    }
661    let mut seen: Vec<((u32, u32), &syn::Ident)> = Vec::with_capacity(field_data.len());
662    for (i, (ident, _, attrs)) in field_data.iter().enumerate() {
663        // Group fields are not positional, so they occupy no slot.
664        if attrs.group {
665            continue;
666        }
667        let element = match &attrs.element {
668            Some(Position::Index(idx)) => *idx,
669            Some(Position::Code(_)) => continue,
670            None => i as u32,
671        };
672        let slot = (element, attrs.component.unwrap_or(0));
673        if let Some((_, first)) = seen.iter().find(|(s, _)| *s == slot) {
674            return Err(syn::Error::new(
675                ident.span(),
676                format!(
677                    "field `{ident}` maps to element {} component {}, which is already \
678                     claimed by field `{first}`; give each field a distinct \
679                     `#[edifact(element = ..., component = ...)]` slot",
680                    slot.0, slot.1
681                ),
682            ));
683        }
684        seen.push((slot, ident));
685    }
686    Ok(())
687}
688
689fn get_named_fields(input: &DeriveInput) -> syn::Result<&syn::FieldsNamed> {
690    if !input.generics.params.is_empty() {
691        return Err(syn::Error::new(
692            input.generics.params.span(),
693            "EdifactSerialize/EdifactDeserialize do not support generic structs",
694        ));
695    }
696    match &input.data {
697        Data::Struct(s) => match &s.fields {
698            Fields::Named(f) => Ok(f),
699            _ => Err(syn::Error::new(
700                input.span(),
701                "EdifactSerialize/EdifactDeserialize only support structs with named fields",
702            )),
703        },
704        _ => Err(syn::Error::new(
705            input.span(),
706            "EdifactSerialize/EdifactDeserialize only support structs",
707        )),
708    }
709}
710
711fn validate_field_attrs(
712    ident: &syn::Ident,
713    ty: &Type,
714    attrs: &FieldAttrs,
715    is_segment_struct: bool,
716) -> syn::Result<()> {
717    if attrs.group && !is_vec_type(ty) {
718        return Err(syn::Error::new(
719            attrs.group_span.unwrap_or_else(|| ident.span()),
720            format!("field `{ident}`: #[edifact(group)] requires Vec<T>"),
721        ));
722    }
723    if attrs.group && (attrs.element.is_some() || attrs.component.is_some()) {
724        return Err(syn::Error::new(
725            attrs.group_span.unwrap_or_else(|| ident.span()),
726            format!(
727                "field `{ident}`: #[edifact(group)] cannot be combined with element/component positioning"
728            ),
729        ));
730    }
731    if attrs.composite && attrs.component.is_some() {
732        return Err(syn::Error::new(
733            attrs.component_span.unwrap_or_else(|| ident.span()),
734            format!(
735                "field `{ident}`: #[edifact(component = ...)] cannot be combined with #[edifact(composite)]"
736            ),
737        ));
738    }
739    if attrs.composite && attrs.group {
740        return Err(syn::Error::new(
741            attrs.composite_span.unwrap_or_else(|| ident.span()),
742            format!(
743                "field `{ident}`: #[edifact(composite)] cannot be combined with #[edifact(group)]"
744            ),
745        ));
746    }
747    if is_segment_struct && attrs.group {
748        return Err(syn::Error::new(
749            attrs.group_span.unwrap_or_else(|| ident.span()),
750            format!("field `{ident}`: #[edifact(group)] is only valid on message structs"),
751        ));
752    }
753    if !is_segment_struct && (attrs.element.is_some() || attrs.component.is_some()) {
754        return Err(syn::Error::new(
755            attrs
756                .element_span
757                .or(attrs.component_span)
758                .unwrap_or_else(|| ident.span()),
759            format!(
760                "field `{ident}`: element/component positioning is only valid on segment structs"
761            ),
762        ));
763    }
764    if !is_segment_struct && attrs.composite {
765        return Err(syn::Error::new(
766            attrs.composite_span.unwrap_or_else(|| ident.span()),
767            format!("field `{ident}`: #[edifact(composite)] is only valid on segment structs"),
768        ));
769    }
770    if is_segment_struct && attrs.qualifier.is_some() {
771        return Err(syn::Error::new(
772            attrs.qualifier_span.unwrap_or_else(|| ident.span()),
773            format!(
774                "field `{ident}`: #[edifact(qualifier = ...)] is only valid on message struct fields"
775            ),
776        ));
777    }
778    if attrs.qualifier.is_some() && attrs.group && !is_vec_type(ty) {
779        return Err(syn::Error::new(
780            attrs.qualifier_span.unwrap_or_else(|| ident.span()),
781            format!("field `{ident}`: qualifier-constrained groups must be Vec<T>"),
782        ));
783    }
784    if attrs.required && !is_option_type(ty) {
785        return Err(syn::Error::new(
786            attrs.required_span.unwrap_or_else(|| ident.span()),
787            format!(
788                "field `{ident}`: #[edifact(required)] only applies to Option<T> fields; \
789                 non-Option fields are always required"
790            ),
791        ));
792    }
793    if attrs.required && attrs.composite {
794        return Err(syn::Error::new(
795            attrs.required_span.unwrap_or_else(|| ident.span()),
796            format!(
797                "field `{ident}`: #[edifact(required)] cannot be combined with \
798                 #[edifact(composite)]; use a non-optional field type to require the \
799                 composite element"
800            ),
801        ));
802    }
803    if attrs.required && !is_segment_struct {
804        return Err(syn::Error::new(
805            attrs.required_span.unwrap_or_else(|| ident.span()),
806            format!(
807                "field `{ident}`: #[edifact(required)] is only valid on segment struct \
808                 element fields; to require a segment in a message struct, use a \
809                 non-optional field type"
810            ),
811        ));
812    }
813    Ok(())
814}
815
816// ── EdifactSerialize ───────────────────────────────────────────────────────────
817
818/// Element index known at macro-expansion time; declaration order is the default.
819///
820/// Code-addressed fields have no such index — callers reach this only on the
821/// positional path, which `resolve_slots` keeps separate.
822fn static_element_index(attrs: &FieldAttrs, decl_index: usize) -> u32 {
823    match &attrs.element {
824        Some(Position::Index(idx)) => *idx,
825        _ => decl_index as u32,
826    }
827}
828
829/// Generate `EdifactSerialize` for a segment struct that addresses fields by
830/// UN/EDIFACT data element identifier.
831///
832/// The positional path lays out its emit order at macro-expansion time, which a
833/// code-addressed struct cannot do: its slots are only known once the `const`
834/// items in `slot_prelude` are evaluated.  So each field contributes a
835/// `(element, component, value)` triple and
836/// [`emit_sparse_segment`][edifact_rs::emit_sparse_segment] orders them.
837/// Absent optional values still contribute an empty triple, so a trailing `None`
838/// produces the same empty element the positional path emits.
839fn impl_serialize_sparse(
840    name: &syn::Ident,
841    seg_tag: &str,
842    struct_attrs: &StructAttrs,
843    field_data: &[(&syn::Ident, &Type, FieldAttrs)],
844    slots: &[Slots],
845    slot_prelude: &TokenStream2,
846) -> TokenStream2 {
847    let mut stmts: Vec<TokenStream2> = Vec::new();
848
849    if let Some(qual) = &struct_attrs.qualifier {
850        stmts.push(quote! {
851            __parts.push((0usize, 0usize, ::std::borrow::Cow::Borrowed(#qual)));
852        });
853    }
854
855    for ((ident, ty, attrs), slot) in field_data.iter().zip(slots) {
856        let (element, component) = (&slot.element, &slot.component);
857        if attrs.composite {
858            // A composite field owns its whole element; replay its own events
859            // into consecutive component slots.
860            let serialize_composite = if is_option_type(ty) {
861                quote! {
862                    if let ::core::option::Option::Some(__v) = &self.#ident {
863                        ::edifact_rs::EdifactCompositeSerialize::edifact_serialize_composite(
864                            __v, &mut __sub,
865                        )?;
866                    }
867                }
868            } else {
869                quote! {
870                    ::edifact_rs::EdifactCompositeSerialize::edifact_serialize_composite(
871                        &self.#ident, &mut __sub,
872                    )?;
873                }
874            };
875            stmts.push(quote! {
876                {
877                    let mut __sub = ::edifact_rs::VecEmitter::default();
878                    #serialize_composite
879                    let mut __comp = 0usize;
880                    let mut __any = false;
881                    for __event in __sub.events {
882                        match __event {
883                            ::edifact_rs::OwnedEdifactEvent::Element { value } => {
884                                __comp = 0;
885                                __any = true;
886                                __parts.push((#element, 0usize, ::std::borrow::Cow::Owned(value)));
887                            }
888                            ::edifact_rs::OwnedEdifactEvent::ComponentElement { value } => {
889                                __comp += 1;
890                                __any = true;
891                                __parts.push((#element, __comp, ::std::borrow::Cow::Owned(value)));
892                            }
893                            _ => {}
894                        }
895                    }
896                    if !__any {
897                        __parts.push((#element, 0usize, ::std::borrow::Cow::Borrowed("")));
898                    }
899                }
900            });
901            continue;
902        }
903
904        let value_expr = if is_option_type(ty) {
905            let inner_is_str = option_inner_type(ty).is_some_and(is_str_like);
906            if inner_is_str {
907                quote! {
908                    match &self.#ident {
909                        ::core::option::Option::Some(__v) => ::std::borrow::Cow::Borrowed(__v.as_str()),
910                        ::core::option::Option::None => ::std::borrow::Cow::Borrowed(""),
911                    }
912                }
913            } else {
914                quote! {
915                    match &self.#ident {
916                        ::core::option::Option::Some(__v) => {
917                            ::std::borrow::Cow::Owned(::std::string::ToString::to_string(__v))
918                        }
919                        ::core::option::Option::None => ::std::borrow::Cow::Borrowed(""),
920                    }
921                }
922            }
923        } else if is_string_type(ty) {
924            quote! { ::std::borrow::Cow::Borrowed(self.#ident.as_str()) }
925        } else if is_str_ref_type(ty) {
926            quote! { ::std::borrow::Cow::Borrowed(self.#ident) }
927        } else {
928            quote! { ::std::borrow::Cow::Owned(::std::string::ToString::to_string(&self.#ident)) }
929        };
930
931        stmts.push(quote! {
932            __parts.push((#element, #component, #value_expr));
933        });
934    }
935
936    let capacity = field_data.len() + usize::from(struct_attrs.qualifier.is_some());
937
938    quote! {
939        impl ::edifact_rs::EdifactSerialize for #name {
940            fn edifact_serialize<__E: ::edifact_rs::EventEmitter>(
941                &self,
942                emitter: &mut __E,
943            ) -> ::core::result::Result<(), ::edifact_rs::EdifactError> {
944                #slot_prelude
945                let mut __parts: ::std::vec::Vec<(
946                    usize,
947                    usize,
948                    ::std::borrow::Cow<'_, str>,
949                )> = ::std::vec::Vec::with_capacity(#capacity);
950                #(#stmts)*
951                ::edifact_rs::emit_sparse_segment(emitter, #seg_tag, &mut __parts)
952            }
953        }
954    }
955}
956
957fn impl_serialize(input: &DeriveInput) -> syn::Result<TokenStream2> {
958    let name = &input.ident;
959    let struct_attrs = parse_struct_attrs(input)?;
960    let fields = get_named_fields(input)?;
961    let is_segment_struct = struct_attrs.segment.is_some();
962
963    // Collect (field_ident, field_type, FieldAttrs).
964    let field_data: Vec<(&syn::Ident, &Type, FieldAttrs)> = fields
965        .named
966        .iter()
967        .map(|f| {
968            let attrs = parse_field_attrs(f)?;
969            let ident = f
970                .ident
971                .as_ref()
972                .ok_or_else(|| syn::Error::new_spanned(f, "only named fields are supported"))?;
973            validate_field_attrs(ident, &f.ty, &attrs, is_segment_struct)?;
974            Ok((ident, &f.ty, attrs))
975        })
976        .collect::<syn::Result<_>>()?;
977    check_duplicate_slots(&field_data, is_segment_struct)?;
978    let (slot_prelude, slots) = resolve_slots(&struct_attrs, &field_data)?;
979    let uses_code_slots = field_data
980        .iter()
981        .any(|(_, _, attrs)| matches!(attrs.element, Some(Position::Code(_))));
982
983    let body = if let Some(seg_tag) = &struct_attrs.segment {
984        if uses_code_slots {
985            return Ok(impl_serialize_sparse(
986                name,
987                seg_tag,
988                &struct_attrs,
989                &field_data,
990                &slots,
991                &slot_prelude,
992            ));
993        }
994        // ── Segment struct: emit one EDIFACT segment ──────────────────────────
995        // When a struct-level qualifier is declared, inject it at slot 0.
996        // Fields at (element=0, component>=1) extend it as composite components.
997        // Fields at element >= 1 are emitted as regular elements.
998        let (qualifier_emit, start_slot, elem0_comp_stmts) = if let Some(qual) =
999            &struct_attrs.qualifier
1000        {
1001            // Error only if a field claims element=0 with no component or component=0.
1002            for (i, (ident, _, attrs)) in field_data.iter().enumerate() {
1003                let elem = static_element_index(attrs, i);
1004                let comp = attrs.component.unwrap_or(0);
1005                if elem == 0 && comp == 0 {
1006                    return Err(syn::Error::new(
1007                        attrs
1008                            .element_span
1009                            .or(attrs.component_span)
1010                            .unwrap_or_else(|| ident.span()),
1011                        format!(
1012                            "field `{}`: cannot use #[edifact(qualifier = ...)] with a field at element = 0 without component >= 1; the qualifier occupies component 0",
1013                            ident
1014                        ),
1015                    ));
1016                }
1017            }
1018            // Collect fields at element=0, component>0, sorted by component.
1019            let mut comp_fields: Vec<(u32, usize)> = field_data
1020                .iter()
1021                .enumerate()
1022                .filter_map(|(i, (_, _, attrs))| {
1023                    let elem = static_element_index(attrs, i);
1024                    let comp = attrs.component.unwrap_or(0);
1025                    if elem == 0 && comp > 0 {
1026                        Some((comp, i))
1027                    } else {
1028                        None
1029                    }
1030                })
1031                .collect();
1032            comp_fields.sort_by_key(|(c, _)| *c);
1033            let comp_stmts: Vec<TokenStream2> = comp_fields
1034                .iter()
1035                .map(|(_, fi)| {
1036                    let (ident, ty, _) = &field_data[*fi];
1037                    emit_component_element(ident, ty)
1038                })
1039                .collect();
1040            let q = quote! {
1041                emitter.emit(::edifact_rs::EdifactEvent::Element { value: #qual })?;
1042            };
1043            (q, 1u32, quote! { #(#comp_stmts)* })
1044        } else {
1045            (quote! {}, 0u32, quote! {})
1046        };
1047
1048        // Rebuild indexed/field_map excluding element=0 fields (handled above).
1049        let regular_field_data: Vec<(u32, usize)> = field_data
1050            .iter()
1051            .enumerate()
1052            .filter_map(|(i, (_, _, attrs))| {
1053                let elem = static_element_index(attrs, i);
1054                if elem < start_slot {
1055                    None
1056                } else {
1057                    Some((elem, i))
1058                }
1059            })
1060            .collect();
1061        let reg_max_idx = regular_field_data
1062            .iter()
1063            .map(|(e, _)| *e)
1064            .max()
1065            .unwrap_or(start_slot.saturating_sub(1));
1066        let reg_field_map: std::collections::HashMap<u32, usize> =
1067            regular_field_data.iter().copied().collect();
1068
1069        let mut elem_stmts: Vec<TokenStream2> = Vec::new();
1070        for slot in start_slot..=reg_max_idx {
1071            if let Some(&fi) = reg_field_map.get(&slot) {
1072                let (ident, ty, attrs) = &field_data[fi];
1073                if attrs.composite {
1074                    elem_stmts.push(emit_composite_field(ident, ty));
1075                } else {
1076                    elem_stmts.push(emit_element(ident, ty));
1077                }
1078            } else {
1079                // Gap: emit an empty element separator.
1080                elem_stmts.push(quote! {
1081                    emitter.emit(::edifact_rs::EdifactEvent::Element { value: "" })?;
1082                });
1083            }
1084        }
1085
1086        quote! {
1087            emitter.emit(::edifact_rs::EdifactEvent::StartSegment { tag: #seg_tag })?;
1088            #qualifier_emit
1089            #elem0_comp_stmts
1090            #(#elem_stmts)*
1091            emitter.emit(::edifact_rs::EdifactEvent::EndSegment)?;
1092        }
1093    } else {
1094        // ── Message struct: delegate to each field ────────────────────────────
1095        let stmts: Vec<TokenStream2> = field_data
1096            .iter()
1097            .map(|(ident, ty, attrs)| {
1098                if attrs.group || is_vec_type(ty) {
1099                    quote! {
1100                        for __item in &self.#ident {
1101                            ::edifact_rs::EdifactSerialize::edifact_serialize(__item, emitter)?;
1102                        }
1103                    }
1104                } else {
1105                    quote! {
1106                        ::edifact_rs::EdifactSerialize::edifact_serialize(&self.#ident, emitter)?;
1107                    }
1108                }
1109            })
1110            .collect();
1111        quote! { #(#stmts)* }
1112    };
1113
1114    Ok(quote! {
1115        impl ::edifact_rs::EdifactSerialize for #name {
1116            fn edifact_serialize<__E: ::edifact_rs::EventEmitter>(
1117                &self,
1118                emitter: &mut __E,
1119            ) -> ::core::result::Result<(), ::edifact_rs::EdifactError> {
1120                #body
1121                ::core::result::Result::Ok(())
1122            }
1123        }
1124    })
1125}
1126
1127/// Generate the token stream that emits field `ident` (of type `ty`) as one element.
1128///
1129/// For `String` and `&str` fields the value is emitted zero-copy via `.as_str()`
1130/// (or directly).  All other types fall back to `ToString::to_string`.
1131fn emit_element(ident: &syn::Ident, ty: &Type) -> TokenStream2 {
1132    if is_option_type(ty) {
1133        let inner_is_str = option_inner_type(ty).is_some_and(is_str_like);
1134        if inner_is_str {
1135            quote! {
1136                match &self.#ident {
1137                    ::core::option::Option::Some(__v) => {
1138                        emitter.emit(::edifact_rs::EdifactEvent::Element { value: __v.as_str() })?;
1139                    }
1140                    ::core::option::Option::None => {
1141                        emitter.emit(::edifact_rs::EdifactEvent::Element { value: "" })?;
1142                    }
1143                }
1144            }
1145        } else {
1146            quote! {
1147                match &self.#ident {
1148                    ::core::option::Option::Some(__v) => {
1149                        let __s = ::std::string::ToString::to_string(__v);
1150                        emitter.emit(::edifact_rs::EdifactEvent::Element { value: &__s })?;
1151                    }
1152                    ::core::option::Option::None => {
1153                        emitter.emit(::edifact_rs::EdifactEvent::Element { value: "" })?;
1154                    }
1155                }
1156            }
1157        }
1158    } else if is_string_type(ty) {
1159        quote! {
1160            emitter.emit(::edifact_rs::EdifactEvent::Element { value: self.#ident.as_str() })?;
1161        }
1162    } else if is_str_ref_type(ty) {
1163        quote! {
1164            emitter.emit(::edifact_rs::EdifactEvent::Element { value: self.#ident })?;
1165        }
1166    } else {
1167        quote! {
1168            {
1169                let __s = ::std::string::ToString::to_string(&self.#ident);
1170                emitter.emit(::edifact_rs::EdifactEvent::Element { value: &__s })?;
1171            }
1172        }
1173    }
1174}
1175
1176/// Generate the token stream that emits field `ident` as a composite component (`ComponentElement`).
1177///
1178/// For `String` and `&str` fields the value is emitted zero-copy.
1179fn emit_component_element(ident: &syn::Ident, ty: &Type) -> TokenStream2 {
1180    if is_option_type(ty) {
1181        let inner_is_str = option_inner_type(ty).is_some_and(is_str_like);
1182        if inner_is_str {
1183            quote! {
1184                match &self.#ident {
1185                    ::core::option::Option::Some(__v) => {
1186                        emitter.emit(::edifact_rs::EdifactEvent::ComponentElement { value: __v.as_str() })?;
1187                    }
1188                    ::core::option::Option::None => {
1189                        emitter.emit(::edifact_rs::EdifactEvent::ComponentElement { value: "" })?;
1190                    }
1191                }
1192            }
1193        } else {
1194            quote! {
1195                match &self.#ident {
1196                    ::core::option::Option::Some(__v) => {
1197                        let __s = ::std::string::ToString::to_string(__v);
1198                        emitter.emit(::edifact_rs::EdifactEvent::ComponentElement { value: &__s })?;
1199                    }
1200                    ::core::option::Option::None => {
1201                        emitter.emit(::edifact_rs::EdifactEvent::ComponentElement { value: "" })?;
1202                    }
1203                }
1204            }
1205        }
1206    } else if is_string_type(ty) {
1207        quote! {
1208            emitter.emit(::edifact_rs::EdifactEvent::ComponentElement { value: self.#ident.as_str() })?;
1209        }
1210    } else if is_str_ref_type(ty) {
1211        quote! {
1212            emitter.emit(::edifact_rs::EdifactEvent::ComponentElement { value: self.#ident })?;
1213        }
1214    } else {
1215        quote! {
1216            {
1217                let __s = ::std::string::ToString::to_string(&self.#ident);
1218                emitter.emit(::edifact_rs::EdifactEvent::ComponentElement { value: &__s })?;
1219            }
1220        }
1221    }
1222}
1223
1224/// Generate the token stream that emits a full composite field via `EdifactCompositeSerialize`.
1225fn emit_composite_field(ident: &syn::Ident, ty: &Type) -> TokenStream2 {
1226    if is_option_type(ty) {
1227        quote! {
1228            match &self.#ident {
1229                ::core::option::Option::Some(__v) => {
1230                    ::edifact_rs::EdifactCompositeSerialize::edifact_serialize_composite(__v, emitter)?;
1231                }
1232                ::core::option::Option::None => {
1233                    emitter.emit(::edifact_rs::EdifactEvent::Element { value: "" })?;
1234                }
1235            }
1236        }
1237    } else {
1238        quote! {
1239            ::edifact_rs::EdifactCompositeSerialize::edifact_serialize_composite(&self.#ident, emitter)?;
1240        }
1241    }
1242}
1243
1244// ── EdifactDeserialize ─────────────────────────────────────────────────────────
1245
1246fn impl_deserialize(input: &DeriveInput) -> syn::Result<TokenStream2> {
1247    let name = &input.ident;
1248    let struct_attrs = parse_struct_attrs(input)?;
1249    let fields = get_named_fields(input)?;
1250    let is_segment_struct = struct_attrs.segment.is_some();
1251
1252    let field_data: Vec<(&syn::Ident, &Type, FieldAttrs)> = fields
1253        .named
1254        .iter()
1255        .map(|f| {
1256            let attrs = parse_field_attrs(f)?;
1257            let ident = f
1258                .ident
1259                .as_ref()
1260                .ok_or_else(|| syn::Error::new_spanned(f, "only named fields are supported"))?;
1261            validate_field_attrs(ident, &f.ty, &attrs, is_segment_struct)?;
1262            Ok((ident, &f.ty, attrs))
1263        })
1264        .collect::<syn::Result<_>>()?;
1265    check_duplicate_slots(&field_data, is_segment_struct)?;
1266    let (slot_prelude, slots) = resolve_slots(&struct_attrs, &field_data)?;
1267
1268    let field_names: Vec<&syn::Ident> = field_data.iter().map(|(id, _, _)| *id).collect();
1269
1270    let (body, owned_body, segment_tag_impl) = if let Some(seg_tag) = &struct_attrs.segment {
1271        // ── Segment struct ────────────────────────────────────────────────────
1272        let qualifier_guard = if let Some(qual) = &struct_attrs.qualifier {
1273            quote! {
1274                if __seg.element_str(0).unwrap_or("") != #qual {
1275                    return ::core::result::Result::Err(
1276                        ::edifact_rs::EdifactError::MissingRequiredElement {
1277                            tag: #seg_tag.to_owned(),
1278                            element_index: 0,
1279                        }
1280                    );
1281                }
1282            }
1283        } else if let Some(idx) = struct_attrs.qualifier_from {
1284            quote! {
1285                // Fully-qualified patterns: a user type named `Some`/`None` in
1286                // scope (e.g. `pub use MyOpt::*`) would otherwise shadow the
1287                // std variants and break the generated code.
1288                match __seg.element_str(#idx as usize) {
1289                    ::core::option::Option::None => return ::core::result::Result::Err(
1290                        ::edifact_rs::EdifactError::MissingRequiredElement {
1291                            tag: #seg_tag.to_owned(),
1292                            element_index: #idx as usize,
1293                        }
1294                    ),
1295                    ::core::option::Option::Some("") => return ::core::result::Result::Err(
1296                        ::edifact_rs::EdifactError::InvalidFieldValue {
1297                            tag: #seg_tag.to_owned(),
1298                            element_index: #idx as usize,
1299                            value: ::std::string::String::new(),
1300                        }
1301                    ),
1302                    ::core::option::Option::Some(__qual_val) => { let _ = __qual_val; }
1303                }
1304            }
1305        } else {
1306            quote! {}
1307        };
1308
1309        let find_seg = if let Some(qual) = &struct_attrs.qualifier {
1310            quote! {
1311                ::edifact_rs::find_qualified_segment(segments, #seg_tag, #qual)
1312            }
1313        } else {
1314            quote! {
1315                ::edifact_rs::find_segment(segments, #seg_tag)
1316            }
1317        };
1318
1319        let field_inits: Vec<TokenStream2> = field_data
1320            .iter()
1321            .zip(slots.iter())
1322            .map(|((ident, ty, attrs), slot)| -> syn::Result<TokenStream2> {
1323                let idx = &slot.element;
1324                if attrs.composite {
1325                    if is_option_type(ty) {
1326                        let inner_ty = option_inner_type(ty)
1327                            .ok_or_else(|| syn::Error::new(ident.span(), "expected Option<T>"))?;
1328                        return Ok(quote! {
1329                            let #ident = match ::edifact_rs::composite_element(__seg, #idx) {
1330                                ::core::option::Option::Some(__composite) => {
1331                                    ::core::option::Option::Some(
1332                                        <#inner_ty as ::edifact_rs::EdifactCompositeDeserialize>::edifact_deserialize_composite(__composite)?
1333                                    )
1334                                }
1335                                ::core::option::Option::None => ::core::option::Option::None,
1336                            };
1337                        });
1338                    }
1339                    return Ok(quote! {
1340                        let #ident = <#ty as ::edifact_rs::EdifactCompositeDeserialize>::edifact_deserialize_composite(
1341                            ::edifact_rs::composite_element(__seg, #idx).ok_or_else(|| ::edifact_rs::EdifactError::MissingRequiredElement {
1342                                tag: #seg_tag.to_owned(),
1343                                element_index: #idx,
1344                            })?
1345                        )?;
1346                    });
1347                }
1348                let comp = &slot.component;
1349                let value_expr = if slot.has_component {
1350                    quote! {
1351                        __seg.get_element(#idx).and_then(|__e| __e.get_component(#comp))
1352                    }
1353                } else {
1354                    quote! { __seg.element_str(#idx) }
1355                };
1356                // Report the variant that matches what the field actually
1357                // addresses.  For a code slot `names_component` is a `const`
1358                // lookup, so this branch folds away.
1359                let names_component = &slot.names_component;
1360                let missing_required_err = quote! {
1361                    if #names_component {
1362                        ::edifact_rs::EdifactError::MissingRequiredComponent {
1363                            tag: #seg_tag.to_owned(),
1364                            element_index: #idx,
1365                            component_index: #comp,
1366                        }
1367                    } else {
1368                        ::edifact_rs::EdifactError::MissingRequiredElement {
1369                            tag: #seg_tag.to_owned(),
1370                            element_index: #idx,
1371                        }
1372                    }
1373                };
1374                Ok(if is_option_type(ty) {
1375                    let inner_ty = option_inner_type(ty);
1376                    let inner_is_str = inner_ty.is_some_and(is_str_like);
1377                    if attrs.required {
1378                        // #[edifact(required)] on Option<T>: treat absence as an error.
1379                        // Emits MissingRequiredComponent when combined with component = N,
1380                        // MissingRequiredElement otherwise.
1381                        if inner_is_str {
1382                            quote! {
1383                                let #ident = ::core::option::Option::Some(
1384                                    #value_expr
1385                                        .filter(|__s| !__s.is_empty())
1386                                        .ok_or_else(|| #missing_required_err)?
1387                                        .to_owned()
1388                                );
1389                            }
1390                        } else {
1391                            let inner_ty = inner_ty
1392                                .ok_or_else(|| syn::Error::new(ident.span(), "expected Option<T>"))?;
1393                            quote! {
1394                                let #ident = ::core::option::Option::Some(
1395                                    #value_expr
1396                                        .filter(|__s| !__s.is_empty())
1397                                        .ok_or_else(|| #missing_required_err)?
1398                                        .parse::<#inner_ty>()
1399                                        .map_err(|_| ::edifact_rs::EdifactError::InvalidText { offset: __seg.span.start })?
1400                                );
1401                            }
1402                        }
1403                    } else if inner_is_str {
1404                        quote! {
1405                            let #ident = #value_expr
1406                                .filter(|__s| !__s.is_empty())
1407                                .map(::std::string::String::from);
1408                        }
1409                    } else {
1410                        let inner_ty = inner_ty
1411                            .ok_or_else(|| syn::Error::new(ident.span(), "expected Option<T>"))?;
1412                        quote! {
1413                            let #ident = #value_expr
1414                                .filter(|__s| !__s.is_empty())
1415                                .map(|__s| __s.parse::<#inner_ty>()
1416                                    .map_err(|_| ::edifact_rs::EdifactError::InvalidText { offset: __seg.span.start })
1417                                )
1418                                .transpose()?;
1419                        }
1420                    }
1421                } else if is_str_like(ty) {
1422                    quote! {
1423                        let #ident = #value_expr
1424                            .filter(|__s| !__s.is_empty())
1425                            .ok_or_else(|| ::edifact_rs::EdifactError::MissingRequiredElement {
1426                                tag: #seg_tag.to_owned(),
1427                                element_index: #idx,
1428                            })?
1429                            .to_owned();
1430                    }
1431                } else {
1432                    quote! {
1433                        let #ident = #value_expr
1434                            .filter(|__s| !__s.is_empty())
1435                            .ok_or_else(|| ::edifact_rs::EdifactError::MissingRequiredElement {
1436                                tag: #seg_tag.to_owned(),
1437                                element_index: #idx,
1438                            })?
1439                            .parse::<#ty>()
1440                            .map_err(|_| ::edifact_rs::EdifactError::InvalidText { offset: __seg.span.start })?;
1441                    }
1442                })
1443            })
1444            .collect::<syn::Result<_>>()?;
1445
1446        let body = quote! {
1447            #slot_prelude
1448            let __seg = #find_seg
1449                .ok_or_else(|| ::edifact_rs::EdifactError::MissingSegment {
1450                    tag: #seg_tag.to_owned(),
1451                    expected_position: "message body".to_owned(),
1452                })?;
1453            #qualifier_guard
1454            #(#field_inits)*
1455            ::core::result::Result::Ok(Self { #(#field_names),* })
1456        };
1457
1458        // Also generate EdifactSegmentTag impl.
1459        // Declare the qualifier through `QUALIFIER_PATTERN` rather than by
1460        // hand-rolling `matches_segment`.  Overriding only the borrowed matcher
1461        // left `matches_owned_segment` (which consults `QUALIFIER_PATTERN`)
1462        // matching on tag alone, so the owned deserialization path picked up
1463        // wrongly-qualified segments and then failed to parse them.
1464        let qualifier_match = if let Some(qual) = &struct_attrs.qualifier {
1465            quote! {
1466                const QUALIFIER_PATTERN: ::core::option::Option<&'static str> =
1467                    ::core::option::Option::Some(#qual);
1468            }
1469        } else if let Some(idx) = struct_attrs.qualifier_from {
1470            // "Any non-empty value at element `idx`" cannot be expressed as a
1471            // `QUALIFIER_PATTERN` (which is element 0 only), so both matchers are
1472            // overridden explicitly and must stay in agreement.
1473            quote! {
1474                fn matches_segment(seg: &::edifact_rs::Segment<'_>) -> bool {
1475                    seg.tag == Self::SEGMENT_TAG
1476                        && !seg.element_str(#idx as usize).unwrap_or("").is_empty()
1477                }
1478
1479                fn matches_owned_segment(seg: &::edifact_rs::OwnedSegment) -> bool {
1480                    seg.tag == Self::SEGMENT_TAG
1481                        && !seg
1482                            .elements
1483                            .get(#idx as usize)
1484                            .and_then(|e| e.components.first())
1485                            .map(|(c, _)| c.as_str())
1486                            .unwrap_or("")
1487                            .is_empty()
1488                }
1489            }
1490        } else {
1491            quote! {}
1492        };
1493
1494        let seg_tag_impl = quote! {
1495            impl ::edifact_rs::EdifactSegmentTag for #name {
1496                const SEGMENT_TAG: &'static str = #seg_tag;
1497                #qualifier_match
1498            }
1499        };
1500
1501        // ── Owned-segment deserialization path ────────────────────────────────
1502        // Works directly on `&[OwnedSegment]` without allocating a `Vec<Segment>`.
1503        let find_seg_owned = if let Some(qual) = &struct_attrs.qualifier {
1504            quote! {
1505                ::edifact_rs::find_qualified_segment_owned(segments, #seg_tag, #qual)
1506            }
1507        } else {
1508            quote! {
1509                ::edifact_rs::find_segment_owned(segments, #seg_tag)
1510            }
1511        };
1512
1513        let field_inits_owned: Vec<TokenStream2> = field_data
1514            .iter()
1515            .zip(slots.iter())
1516            .map(|((ident, ty, attrs), slot)| -> syn::Result<TokenStream2> {
1517                let idx = &slot.element;
1518                if attrs.composite {
1519                    if is_option_type(ty) {
1520                        let inner_ty = option_inner_type(ty)
1521                            .ok_or_else(|| syn::Error::new(ident.span(), "expected Option<T>"))?;
1522                        return Ok(quote! {
1523                            let #ident = match __seg.elements.get(#idx) {
1524                                ::core::option::Option::Some(__e) => {
1525                                    let __cows = __e.components.iter()
1526                                        .map(|(s, _)| ::std::borrow::Cow::Borrowed(s.as_str()))
1527                                        .collect::<::std::vec::Vec<::std::borrow::Cow<'_, str>>>();
1528                                    ::core::option::Option::Some(
1529                                        <#inner_ty as ::edifact_rs::EdifactCompositeDeserialize>::edifact_deserialize_composite(
1530                                            ::edifact_rs::CompositeElement::from_slice(&__cows)
1531                                        )?
1532                                    )
1533                                }
1534                                ::core::option::Option::None => ::core::option::Option::None,
1535                            };
1536                        });
1537                    }
1538                    return Ok(quote! {
1539                        let #ident = {
1540                            let __cows = __seg.elements.get(#idx)
1541                                .ok_or_else(|| ::edifact_rs::EdifactError::MissingRequiredElement {
1542                                    tag: #seg_tag.to_owned(),
1543                                    element_index: #idx,
1544                                })?
1545                                .components.iter()
1546                                .map(|(s, _)| ::std::borrow::Cow::Borrowed(s.as_str()))
1547                                .collect::<::std::vec::Vec<::std::borrow::Cow<'_, str>>>();
1548                            <#ty as ::edifact_rs::EdifactCompositeDeserialize>::edifact_deserialize_composite(
1549                                ::edifact_rs::CompositeElement::from_slice(&__cows)
1550                            )?
1551                        };
1552                    });
1553                }
1554                let comp = &slot.component;
1555                let value_expr_owned = if slot.has_component {
1556                    quote! { __seg.component_str(#idx, #comp) }
1557                } else {
1558                    quote! { __seg.element_str(#idx) }
1559                };
1560                // Same variant selection as the borrowed path; the two must
1561                // agree or the same input yields different error codes.
1562                let names_component = &slot.names_component;
1563                let missing_required_err_owned = quote! {
1564                    if #names_component {
1565                        ::edifact_rs::EdifactError::MissingRequiredComponent {
1566                            tag: #seg_tag.to_owned(),
1567                            element_index: #idx,
1568                            component_index: #comp,
1569                        }
1570                    } else {
1571                        ::edifact_rs::EdifactError::MissingRequiredElement {
1572                            tag: #seg_tag.to_owned(),
1573                            element_index: #idx,
1574                        }
1575                    }
1576                };
1577                Ok(if is_option_type(ty) {
1578                    if let Some(inner_ty) = option_inner_type(ty) {
1579                        if attrs.required {
1580                            // #[edifact(required)] on Option<T>: absence is an error.
1581                            // Emits MissingRequiredComponent when combined with component = N,
1582                            // MissingRequiredElement otherwise.
1583                            if is_str_like(inner_ty) {
1584                                quote! {
1585                                    let #ident = ::core::option::Option::Some(
1586                                        #value_expr_owned
1587                                            .filter(|__s| !__s.is_empty())
1588                                            .ok_or_else(|| #missing_required_err_owned)?
1589                                            .to_owned()
1590                                    );
1591                                }
1592                            } else {
1593                                quote! {
1594                                    let #ident = ::core::option::Option::Some(
1595                                        #value_expr_owned
1596                                            .filter(|__s| !__s.is_empty())
1597                                            .ok_or_else(|| #missing_required_err_owned)?
1598                                            .parse::<#inner_ty>()
1599                                            .map_err(|_| ::edifact_rs::EdifactError::InvalidText { offset: __seg.span.start })?
1600                                    );
1601                                }
1602                            }
1603                        } else if is_str_like(inner_ty) {
1604                            quote! {
1605                                let #ident = #value_expr_owned
1606                                    .filter(|__s| !__s.is_empty())
1607                                    .map(::std::string::String::from);
1608                            }
1609                        } else {
1610                            quote! {
1611                                let #ident = #value_expr_owned
1612                                    .filter(|__s| !__s.is_empty())
1613                                    .map(|__s| __s.parse::<#inner_ty>()
1614                                        .map_err(|_| ::edifact_rs::EdifactError::InvalidText { offset: __seg.span.start })
1615                                    )
1616                                    .transpose()?;
1617                            }
1618                        }
1619                    } else {
1620                        // Fallback: treat as String (should not happen with well-formed types).
1621                        quote! {
1622                            let #ident = #value_expr_owned
1623                                .filter(|__s| !__s.is_empty())
1624                                .map(::std::string::String::from);
1625                        }
1626                    }
1627                } else if is_str_like(ty) {
1628                    quote! {
1629                        let #ident = #value_expr_owned
1630                            .filter(|__s| !__s.is_empty())
1631                            .ok_or_else(|| ::edifact_rs::EdifactError::MissingRequiredElement {
1632                                tag: #seg_tag.to_owned(),
1633                                element_index: #idx,
1634                            })?
1635                            .to_owned();
1636                    }
1637                } else {
1638                    quote! {
1639                        let #ident = #value_expr_owned
1640                            .filter(|__s| !__s.is_empty())
1641                            .ok_or_else(|| ::edifact_rs::EdifactError::MissingRequiredElement {
1642                                tag: #seg_tag.to_owned(),
1643                                element_index: #idx,
1644                            })?
1645                            .parse::<#ty>()
1646                            .map_err(|_| ::edifact_rs::EdifactError::InvalidText { offset: __seg.span.start })?;
1647                    }
1648                })
1649            })
1650            .collect::<syn::Result<_>>()?;
1651
1652        let owned_body = quote! {
1653            #slot_prelude
1654            let __seg = #find_seg_owned
1655                .ok_or_else(|| ::edifact_rs::EdifactError::MissingSegment {
1656                    tag: #seg_tag.to_owned(),
1657                    expected_position: "message body".to_owned(),
1658                })?;
1659            #qualifier_guard
1660            #(#field_inits_owned)*
1661            ::core::result::Result::Ok(Self { #(#field_names),* })
1662        };
1663
1664        (body, owned_body, seg_tag_impl)
1665    } else {
1666        // ── Message struct: delegate to each field ────────────────────────────
1667        let field_inits: Vec<TokenStream2> = field_data
1668            .iter()
1669            .map(|(ident, ty, attrs)| -> syn::Result<TokenStream2> {
1670                Ok(if let Some(qual) = &attrs.qualifier {
1671                    if attrs.group || is_vec_type(ty) {
1672                        let inner_ty = vec_inner_type(ty)
1673                            .ok_or_else(|| syn::Error::new(ident.span(), "expected Vec<T>"))?;
1674                        quote! {
1675                            let #ident = segments
1676                                .iter()
1677                                .filter(|__seg| {
1678                                    __seg.tag == <#inner_ty as ::edifact_rs::EdifactSegmentTag>::SEGMENT_TAG
1679                                        && __seg.element_str(0).unwrap_or("") == #qual
1680                                })
1681                                .map(|__seg| {
1682                                    ::edifact_rs::EdifactDeserialize::edifact_deserialize(
1683                                        ::core::slice::from_ref(__seg),
1684                                    )
1685                                })
1686                                .collect::<::core::result::Result<::std::vec::Vec<#inner_ty>, ::edifact_rs::EdifactError>>()?;
1687                        }
1688                    } else if is_option_type(ty) {
1689                        let inner_ty = option_inner_type(ty)
1690                            .ok_or_else(|| syn::Error::new(ident.span(), "expected Option<T>"))?;
1691                        quote! {
1692                            let #ident = match ::edifact_rs::find_qualified_segment(
1693                                segments,
1694                                <#inner_ty as ::edifact_rs::EdifactSegmentTag>::SEGMENT_TAG,
1695                                #qual,
1696                            ) {
1697                                ::core::option::Option::Some(__seg) => {
1698                                    ::core::option::Option::Some(
1699                                        ::edifact_rs::EdifactDeserialize::edifact_deserialize(
1700                                            ::core::slice::from_ref(__seg),
1701                                        )?
1702                                    )
1703                                }
1704                                ::core::option::Option::None => ::core::option::Option::None,
1705                            };
1706                        }
1707                    } else {
1708                        quote! {
1709                            let __seg = ::edifact_rs::find_qualified_segment(
1710                                segments,
1711                                <#ty as ::edifact_rs::EdifactSegmentTag>::SEGMENT_TAG,
1712                                #qual,
1713                            )
1714                            .ok_or_else(|| ::edifact_rs::EdifactError::MissingSegment {
1715                                tag: <#ty as ::edifact_rs::EdifactSegmentTag>::SEGMENT_TAG.to_owned(),
1716                                expected_position: "message body".to_owned(),
1717                            })?;
1718                            let #ident = ::edifact_rs::EdifactDeserialize::edifact_deserialize(
1719                                ::core::slice::from_ref(__seg),
1720                            )?;
1721                        }
1722                    }
1723                } else if attrs.group || is_vec_type(ty) {
1724                    let inner_ty = vec_inner_type(ty)
1725                        .ok_or_else(|| syn::Error::new(ident.span(), "expected Vec<T>"))?;
1726                    quote! {
1727                        let #ident = ::edifact_rs::find_segments_typed::<#inner_ty>(segments)
1728                            .map(|__seg| {
1729                                <#inner_ty as ::edifact_rs::EdifactDeserialize>::edifact_deserialize(
1730                                    ::core::slice::from_ref(__seg),
1731                                )
1732                            })
1733                            .collect::<::core::result::Result<::std::vec::Vec<#inner_ty>, _>>()?;
1734                    }
1735                } else if is_option_type(ty) {
1736                    let inner_ty = option_inner_type(ty)
1737                        .ok_or_else(|| syn::Error::new(ident.span(), "expected Option<T>"))?;
1738                    quote! {
1739                        let #ident = if segments
1740                            .iter()
1741                            .any(|__seg| __seg.tag == <#inner_ty as ::edifact_rs::EdifactSegmentTag>::SEGMENT_TAG)
1742                        {
1743                            ::core::option::Option::Some(
1744                                <#inner_ty as ::edifact_rs::EdifactDeserialize>::edifact_deserialize(segments)?
1745                            )
1746                        } else {
1747                            ::core::option::Option::None
1748                        };
1749                    }
1750                } else {
1751                    quote! {
1752                        let #ident = ::edifact_rs::EdifactDeserialize::edifact_deserialize(segments)?;
1753                    }
1754                })
1755            })
1756            .collect::<syn::Result<_>>()?;
1757
1758        let body = quote! {
1759            #(#field_inits)*
1760            ::core::result::Result::Ok(Self { #(#field_names),* })
1761        };
1762
1763        // ── Owned-segment message deserialization path ────────────────────────
1764        // Works directly on `&[OwnedSegment]` without converting to `Vec<Segment>`.
1765        let field_inits_owned: Vec<TokenStream2> = field_data
1766            .iter()
1767            .map(|(ident, ty, attrs)| -> syn::Result<TokenStream2> {
1768                Ok(if let Some(qual) = &attrs.qualifier {
1769                    if attrs.group || is_vec_type(ty) {
1770                        let inner_ty = vec_inner_type(ty)
1771                            .ok_or_else(|| syn::Error::new(ident.span(), "expected Vec<T>"))?;
1772                        quote! {
1773                            let #ident = segments
1774                                .iter()
1775                                .filter(|__seg| {
1776                                    __seg.tag == <#inner_ty as ::edifact_rs::EdifactSegmentTag>::SEGMENT_TAG
1777                                        && __seg.element_str(0).unwrap_or("") == #qual
1778                                })
1779                                .map(|__seg| {
1780                                    <#inner_ty as ::edifact_rs::EdifactDeserialize>::edifact_deserialize_owned(
1781                                        ::core::slice::from_ref(__seg),
1782                                    )
1783                                })
1784                                .collect::<::core::result::Result<::std::vec::Vec<#inner_ty>, ::edifact_rs::EdifactError>>()?;
1785                        }
1786                    } else if is_option_type(ty) {
1787                        let inner_ty = option_inner_type(ty)
1788                            .ok_or_else(|| syn::Error::new(ident.span(), "expected Option<T>"))?;
1789                        quote! {
1790                            let #ident = match ::edifact_rs::find_qualified_segment_owned(
1791                                segments,
1792                                <#inner_ty as ::edifact_rs::EdifactSegmentTag>::SEGMENT_TAG,
1793                                #qual,
1794                            ) {
1795                                ::core::option::Option::Some(__seg) => {
1796                                    ::core::option::Option::Some(
1797                                        <#inner_ty as ::edifact_rs::EdifactDeserialize>::edifact_deserialize_owned(
1798                                            ::core::slice::from_ref(__seg),
1799                                        )?
1800                                    )
1801                                }
1802                                ::core::option::Option::None => ::core::option::Option::None,
1803                            };
1804                        }
1805                    } else {
1806                        quote! {
1807                            let __seg = ::edifact_rs::find_qualified_segment_owned(
1808                                segments,
1809                                <#ty as ::edifact_rs::EdifactSegmentTag>::SEGMENT_TAG,
1810                                #qual,
1811                            )
1812                            .ok_or_else(|| ::edifact_rs::EdifactError::MissingSegment {
1813                                tag: <#ty as ::edifact_rs::EdifactSegmentTag>::SEGMENT_TAG.to_owned(),
1814                                expected_position: "message body".to_owned(),
1815                            })?;
1816                            let #ident = <#ty as ::edifact_rs::EdifactDeserialize>::edifact_deserialize_owned(
1817                                ::core::slice::from_ref(__seg),
1818                            )?;
1819                        }
1820                    }
1821                } else if attrs.group || is_vec_type(ty) {
1822                    let inner_ty = vec_inner_type(ty)
1823                        .ok_or_else(|| syn::Error::new(ident.span(), "expected Vec<T>"))?;
1824                    quote! {
1825                        let #ident = segments
1826                            .iter()
1827                            .filter(|__seg| <#inner_ty as ::edifact_rs::EdifactSegmentTag>::matches_owned_segment(__seg))
1828                            .map(|__seg| {
1829                                <#inner_ty as ::edifact_rs::EdifactDeserialize>::edifact_deserialize_owned(
1830                                    ::core::slice::from_ref(__seg),
1831                                )
1832                            })
1833                            .collect::<::core::result::Result<::std::vec::Vec<#inner_ty>, _>>()?;
1834                    }
1835                } else if is_option_type(ty) {
1836                    let inner_ty = option_inner_type(ty)
1837                        .ok_or_else(|| syn::Error::new(ident.span(), "expected Option<T>"))?;
1838                    quote! {
1839                        let #ident = if segments
1840                            .iter()
1841                            .any(|__seg| __seg.tag == <#inner_ty as ::edifact_rs::EdifactSegmentTag>::SEGMENT_TAG)
1842                        {
1843                            ::core::option::Option::Some(
1844                                <#inner_ty as ::edifact_rs::EdifactDeserialize>::edifact_deserialize_owned(segments)?
1845                            )
1846                        } else {
1847                            ::core::option::Option::None
1848                        };
1849                    }
1850                } else {
1851                    quote! {
1852                        let #ident = <#ty as ::edifact_rs::EdifactDeserialize>::edifact_deserialize_owned(segments)?;
1853                    }
1854                })
1855            })
1856            .collect::<syn::Result<_>>()?;
1857
1858        let owned_body = quote! {
1859            #(#field_inits_owned)*
1860            ::core::result::Result::Ok(Self { #(#field_names),* })
1861        };
1862
1863        (body, owned_body, quote! {})
1864    };
1865
1866    Ok(quote! {
1867        impl ::edifact_rs::EdifactDeserialize for #name {
1868            fn edifact_deserialize(
1869                segments: &[::edifact_rs::Segment<'_>],
1870            ) -> ::core::result::Result<Self, ::edifact_rs::EdifactError> {
1871                #body
1872            }
1873
1874            fn edifact_deserialize_owned(
1875                segments: &[::edifact_rs::OwnedSegment],
1876            ) -> ::core::result::Result<Self, ::edifact_rs::EdifactError> {
1877                #owned_body
1878            }
1879        }
1880        #segment_tag_impl
1881    })
1882}
1883
1884#[cfg(test)]
1885mod tests {
1886    /// Compile-fail / compile-pass suite for the derive macros.
1887    ///
1888    /// The blessed `.stderr` files hold only this crate's own diagnostics, so
1889    /// they are stable across toolchains and CI runs the suite on both MSRV and
1890    /// stable.  Keep it that way: an expectation that captures a *rustc*
1891    /// warning or note will drift on the next release and drown real
1892    /// regressions in noise.  If a UI case triggers an incidental lint, silence
1893    /// it at the source (see `tests/ui/support.rs`) rather than blessing it.
1894    ///
1895    /// The suite is off by default because it is slow; set `EDIFACT_UI_TESTS=1`
1896    /// to run it, or use `just ui` / `just ui-msrv`.
1897    ///
1898    /// Re-bless after intentional message changes with `just ui-bless`.
1899    #[test]
1900    fn trybuild_ui() {
1901        if std::env::var_os("EDIFACT_UI_TESTS").is_none() {
1902            eprintln!(
1903                "skipping derive UI suite: set EDIFACT_UI_TESTS=1 to run it \
1904                 (expectations are pinned to the MSRV toolchain)"
1905            );
1906            return;
1907        }
1908        let t = trybuild::TestCases::new();
1909        t.pass("tests/ui/pass_*.rs");
1910        t.compile_fail("tests/ui/fail_*.rs");
1911    }
1912}