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