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        //
1222        // Fields are laid out on a two-dimensional grid — data element index by
1223        // component index — at macro-expansion time, so the generated code stays
1224        // straight-line event emission with no runtime allocation.
1225        //
1226        // The grid is what makes components work at all. Keying the layout on
1227        // the element index alone collapsed every field sharing an element into
1228        // one entry, so all but the last were silently dropped on write:
1229        // a `DTM` with three `component` fields went out as `DTM+102'` instead of
1230        // `DTM+137:20260101:102'`, and the loss was invisible until a partner
1231        // rejected the file.
1232
1233        /// What occupies one `(element, component)` cell of the layout.
1234        enum Cell {
1235            /// The struct-level `#[edifact(qualifier = "…")]`, which owns (0, 0).
1236            Qualifier,
1237            /// An index into `field_data`.
1238            Field(usize),
1239        }
1240
1241        let mut grid: std::collections::BTreeMap<u32, std::collections::BTreeMap<u32, Cell>> =
1242            std::collections::BTreeMap::new();
1243
1244        if struct_attrs.qualifier.is_some() {
1245            // The qualifier occupies element 0 / component 0, so a field cannot.
1246            for (i, (ident, _, attrs)) in field_data.iter().enumerate() {
1247                let elem = static_element_index(attrs, i);
1248                let comp = attrs.component.unwrap_or(0);
1249                if elem == 0 && comp == 0 {
1250                    return Err(syn::Error::new(
1251                        attrs
1252                            .element_span
1253                            .or(attrs.component_span)
1254                            .unwrap_or_else(|| ident.span()),
1255                        format!(
1256                            "field `{ident}`: cannot use #[edifact(qualifier = ...)] with a field at element = 0 without component >= 1; the qualifier occupies component 0"
1257                        ),
1258                    ));
1259                }
1260            }
1261            grid.entry(0).or_default().insert(0, Cell::Qualifier);
1262        }
1263
1264        for (i, (_, _, attrs)) in field_data.iter().enumerate() {
1265            // Group fields belong to message structs and occupy no slot;
1266            // `validate_field_attrs` already rejects them on a segment struct.
1267            if attrs.group {
1268                continue;
1269            }
1270            let element = static_element_index(attrs, i);
1271            let component = attrs.component.unwrap_or(0);
1272            // `check_duplicate_slots` has already proved this cell is free.
1273            grid.entry(element)
1274                .or_default()
1275                .insert(component, Cell::Field(i));
1276        }
1277
1278        let empty_element = quote! {
1279            emitter.emit(::edifact_rs::EdifactEvent::Element { value: "" })?;
1280        };
1281        let empty_component = quote! {
1282            emitter.emit(::edifact_rs::EdifactEvent::ComponentElement { value: "" })?;
1283        };
1284
1285        let mut elem_stmts: Vec<TokenStream2> = Vec::new();
1286        if let Some(&max_element) = grid.keys().max() {
1287            for element in 0..=max_element {
1288                let Some(components) = grid.get(&element) else {
1289                    // A declared gap between elements is an empty element.
1290                    elem_stmts.push(empty_element.clone());
1291                    continue;
1292                };
1293                let max_component = components.keys().max().copied().unwrap_or(0);
1294                for component in 0..=max_component {
1295                    match components.get(&component) {
1296                        Some(Cell::Qualifier) => {
1297                            let qual = struct_attrs.qualifier.as_deref().unwrap_or("");
1298                            elem_stmts.push(quote! {
1299                                emitter.emit(::edifact_rs::EdifactEvent::Element { value: #qual })?;
1300                            });
1301                        }
1302                        Some(Cell::Field(index)) => {
1303                            let (ident, ty, attrs) = &field_data[*index];
1304                            if attrs.composite {
1305                                // A composite field emits its own Element plus
1306                                // ComponentElement events for the whole slot.
1307                                elem_stmts.push(emit_composite_field(ident, ty));
1308                            } else if component == 0 {
1309                                elem_stmts.push(emit_element(ident, ty));
1310                            } else {
1311                                elem_stmts.push(emit_component_element(ident, ty));
1312                            }
1313                        }
1314                        None if component == 0 => elem_stmts.push(empty_element.clone()),
1315                        None => elem_stmts.push(empty_component.clone()),
1316                    }
1317                }
1318            }
1319        }
1320
1321        quote! {
1322            emitter.emit(::edifact_rs::EdifactEvent::StartSegment { tag: #seg_tag })?;
1323            #(#elem_stmts)*
1324            emitter.emit(::edifact_rs::EdifactEvent::EndSegment)?;
1325        }
1326    } else {
1327        // ── Message struct: delegate to each field ────────────────────────────
1328        let stmts: Vec<TokenStream2> = field_data
1329            .iter()
1330            .map(|(ident, ty, attrs)| {
1331                if attrs.group || is_vec_type(ty) {
1332                    quote! {
1333                        for __item in &self.#ident {
1334                            ::edifact_rs::EdifactSerialize::edifact_serialize(__item, emitter)?;
1335                        }
1336                    }
1337                } else {
1338                    quote! {
1339                        ::edifact_rs::EdifactSerialize::edifact_serialize(&self.#ident, emitter)?;
1340                    }
1341                }
1342            })
1343            .collect();
1344        quote! { #(#stmts)* }
1345    };
1346
1347    Ok(quote! {
1348        impl ::edifact_rs::EdifactSerialize for #name {
1349            fn edifact_serialize<__E: ::edifact_rs::EventEmitter>(
1350                &self,
1351                emitter: &mut __E,
1352            ) -> ::core::result::Result<(), ::edifact_rs::EdifactError> {
1353                #body
1354                ::core::result::Result::Ok(())
1355            }
1356        }
1357    })
1358}
1359
1360/// Generate the token stream that emits field `ident` (of type `ty`) as one element.
1361///
1362/// For `String` and `&str` fields the value is emitted zero-copy via `.as_str()`
1363/// (or directly).  All other types fall back to `ToString::to_string`.
1364fn emit_element(ident: &syn::Ident, ty: &Type) -> TokenStream2 {
1365    if is_option_type(ty) {
1366        let inner_is_str = option_inner_type(ty).is_some_and(is_str_like);
1367        if inner_is_str {
1368            quote! {
1369                match &self.#ident {
1370                    ::core::option::Option::Some(__v) => {
1371                        emitter.emit(::edifact_rs::EdifactEvent::Element { value: __v.as_str() })?;
1372                    }
1373                    ::core::option::Option::None => {
1374                        emitter.emit(::edifact_rs::EdifactEvent::Element { value: "" })?;
1375                    }
1376                }
1377            }
1378        } else {
1379            quote! {
1380                match &self.#ident {
1381                    ::core::option::Option::Some(__v) => {
1382                        let __s = ::std::string::ToString::to_string(__v);
1383                        emitter.emit(::edifact_rs::EdifactEvent::Element { value: &__s })?;
1384                    }
1385                    ::core::option::Option::None => {
1386                        emitter.emit(::edifact_rs::EdifactEvent::Element { value: "" })?;
1387                    }
1388                }
1389            }
1390        }
1391    } else if is_string_type(ty) {
1392        quote! {
1393            emitter.emit(::edifact_rs::EdifactEvent::Element { value: self.#ident.as_str() })?;
1394        }
1395    } else if is_str_ref_type(ty) {
1396        quote! {
1397            emitter.emit(::edifact_rs::EdifactEvent::Element { value: self.#ident })?;
1398        }
1399    } else {
1400        quote! {
1401            {
1402                let __s = ::std::string::ToString::to_string(&self.#ident);
1403                emitter.emit(::edifact_rs::EdifactEvent::Element { value: &__s })?;
1404            }
1405        }
1406    }
1407}
1408
1409/// Generate the token stream that emits field `ident` as a composite component (`ComponentElement`).
1410///
1411/// For `String` and `&str` fields the value is emitted zero-copy.
1412fn emit_component_element(ident: &syn::Ident, ty: &Type) -> TokenStream2 {
1413    if is_option_type(ty) {
1414        let inner_is_str = option_inner_type(ty).is_some_and(is_str_like);
1415        if inner_is_str {
1416            quote! {
1417                match &self.#ident {
1418                    ::core::option::Option::Some(__v) => {
1419                        emitter.emit(::edifact_rs::EdifactEvent::ComponentElement { value: __v.as_str() })?;
1420                    }
1421                    ::core::option::Option::None => {
1422                        emitter.emit(::edifact_rs::EdifactEvent::ComponentElement { value: "" })?;
1423                    }
1424                }
1425            }
1426        } else {
1427            quote! {
1428                match &self.#ident {
1429                    ::core::option::Option::Some(__v) => {
1430                        let __s = ::std::string::ToString::to_string(__v);
1431                        emitter.emit(::edifact_rs::EdifactEvent::ComponentElement { value: &__s })?;
1432                    }
1433                    ::core::option::Option::None => {
1434                        emitter.emit(::edifact_rs::EdifactEvent::ComponentElement { value: "" })?;
1435                    }
1436                }
1437            }
1438        }
1439    } else if is_string_type(ty) {
1440        quote! {
1441            emitter.emit(::edifact_rs::EdifactEvent::ComponentElement { value: self.#ident.as_str() })?;
1442        }
1443    } else if is_str_ref_type(ty) {
1444        quote! {
1445            emitter.emit(::edifact_rs::EdifactEvent::ComponentElement { value: self.#ident })?;
1446        }
1447    } else {
1448        quote! {
1449            {
1450                let __s = ::std::string::ToString::to_string(&self.#ident);
1451                emitter.emit(::edifact_rs::EdifactEvent::ComponentElement { value: &__s })?;
1452            }
1453        }
1454    }
1455}
1456
1457/// Generate the token stream that emits a full composite field via `EdifactCompositeSerialize`.
1458fn emit_composite_field(ident: &syn::Ident, ty: &Type) -> TokenStream2 {
1459    if is_option_type(ty) {
1460        quote! {
1461            match &self.#ident {
1462                ::core::option::Option::Some(__v) => {
1463                    ::edifact_rs::EdifactCompositeSerialize::edifact_serialize_composite(__v, emitter)?;
1464                }
1465                ::core::option::Option::None => {
1466                    emitter.emit(::edifact_rs::EdifactEvent::Element { value: "" })?;
1467                }
1468            }
1469        }
1470    } else {
1471        quote! {
1472            ::edifact_rs::EdifactCompositeSerialize::edifact_serialize_composite(&self.#ident, emitter)?;
1473        }
1474    }
1475}
1476
1477// ── EdifactDeserialize ─────────────────────────────────────────────────────────
1478
1479fn impl_deserialize(input: &DeriveInput) -> syn::Result<TokenStream2> {
1480    let name = &input.ident;
1481    let struct_attrs = parse_struct_attrs(input)?;
1482    let fields = get_named_fields(input)?;
1483    let is_segment_struct = struct_attrs.segment.is_some();
1484
1485    let field_data: Vec<(&syn::Ident, &Type, FieldAttrs)> = fields
1486        .named
1487        .iter()
1488        .map(|f| {
1489            let attrs = parse_field_attrs(f)?;
1490            let ident = f
1491                .ident
1492                .as_ref()
1493                .ok_or_else(|| syn::Error::new_spanned(f, "only named fields are supported"))?;
1494            validate_field_attrs(ident, &f.ty, &attrs, is_segment_struct)?;
1495            Ok((ident, &f.ty, attrs))
1496        })
1497        .collect::<syn::Result<_>>()?;
1498    check_duplicate_slots(&field_data, is_segment_struct)?;
1499    let (slot_prelude, slots) = resolve_slots(&struct_attrs, &field_data)?;
1500
1501    let field_names: Vec<&syn::Ident> = field_data.iter().map(|(id, _, _)| *id).collect();
1502
1503    let (body, owned_body, segment_tag_impl) = if let Some(seg_tag) = &struct_attrs.segment {
1504        // ── Segment struct ────────────────────────────────────────────────────
1505        let qualifier_guard = if let Some(qual) = &struct_attrs.qualifier {
1506            quote! {
1507                if __seg.element_str(0).unwrap_or("") != #qual {
1508                    return ::core::result::Result::Err(
1509                        ::edifact_rs::EdifactError::MissingRequiredElement {
1510                            tag: #seg_tag.to_owned(),
1511                            element_index: 0,
1512                        }
1513                    );
1514                }
1515            }
1516        } else if let Some(idx) = struct_attrs.qualifier_from {
1517            quote! {
1518                // Fully-qualified patterns: a user type named `Some`/`None` in
1519                // scope (e.g. `pub use MyOpt::*`) would otherwise shadow the
1520                // std variants and break the generated code.
1521                match __seg.element_str(#idx as usize) {
1522                    ::core::option::Option::None => return ::core::result::Result::Err(
1523                        ::edifact_rs::EdifactError::MissingRequiredElement {
1524                            tag: #seg_tag.to_owned(),
1525                            element_index: #idx as usize,
1526                        }
1527                    ),
1528                    ::core::option::Option::Some("") => return ::core::result::Result::Err(
1529                        ::edifact_rs::EdifactError::InvalidFieldValue {
1530                            tag: #seg_tag.to_owned(),
1531                            element_index: #idx as usize,
1532                            value: ::std::string::String::new(),
1533                        }
1534                    ),
1535                    ::core::option::Option::Some(__qual_val) => { let _ = __qual_val; }
1536                }
1537            }
1538        } else {
1539            quote! {}
1540        };
1541
1542        let find_seg = if let Some(qual) = &struct_attrs.qualifier {
1543            quote! {
1544                ::edifact_rs::find_qualified_segment(segments, #seg_tag, #qual)
1545            }
1546        } else {
1547            quote! {
1548                ::edifact_rs::find_segment(segments, #seg_tag)
1549            }
1550        };
1551
1552        let field_inits: Vec<TokenStream2> = field_data
1553            .iter()
1554            .zip(slots.iter())
1555            .map(|((ident, ty, attrs), slot)| -> syn::Result<TokenStream2> {
1556                let idx = &slot.element;
1557                if attrs.composite {
1558                    if is_option_type(ty) {
1559                        let inner_ty = option_inner_type(ty)
1560                            .ok_or_else(|| syn::Error::new(ident.span(), "expected Option<T>"))?;
1561                        return Ok(quote! {
1562                            let #ident = match ::edifact_rs::composite_element(__seg, #idx) {
1563                                ::core::option::Option::Some(__composite) => {
1564                                    ::core::option::Option::Some(
1565                                        <#inner_ty as ::edifact_rs::EdifactCompositeDeserialize>::edifact_deserialize_composite(__composite)?
1566                                    )
1567                                }
1568                                ::core::option::Option::None => ::core::option::Option::None,
1569                            };
1570                        });
1571                    }
1572                    return Ok(quote! {
1573                        let #ident = <#ty as ::edifact_rs::EdifactCompositeDeserialize>::edifact_deserialize_composite(
1574                            ::edifact_rs::composite_element(__seg, #idx).ok_or_else(|| ::edifact_rs::EdifactError::MissingRequiredElement {
1575                                tag: #seg_tag.to_owned(),
1576                                element_index: #idx,
1577                            })?
1578                        )?;
1579                    });
1580                }
1581                let comp = &slot.component;
1582                let value_expr = if slot.has_component {
1583                    quote! {
1584                        __seg.get_element(#idx).and_then(|__e| __e.get_component(#comp))
1585                    }
1586                } else {
1587                    quote! { __seg.element_str(#idx) }
1588                };
1589                // Report the variant that matches what the field actually
1590                // addresses.  For a code slot `names_component` is a `const`
1591                // lookup, so this branch folds away.
1592                let names_component = &slot.names_component;
1593                let missing_required_err = quote! {
1594                    if #names_component {
1595                        ::edifact_rs::EdifactError::MissingRequiredComponent {
1596                            tag: #seg_tag.to_owned(),
1597                            element_index: #idx,
1598                            component_index: #comp,
1599                        }
1600                    } else {
1601                        ::edifact_rs::EdifactError::MissingRequiredElement {
1602                            tag: #seg_tag.to_owned(),
1603                            element_index: #idx,
1604                        }
1605                    }
1606                };
1607                Ok(if is_option_type(ty) {
1608                    let inner_ty = option_inner_type(ty);
1609                    let inner_is_str = inner_ty.is_some_and(is_str_like);
1610                    if attrs.required {
1611                        // #[edifact(required)] on Option<T>: treat absence as an error.
1612                        // Emits MissingRequiredComponent when combined with component = N,
1613                        // MissingRequiredElement otherwise.
1614                        if inner_is_str {
1615                            quote! {
1616                                let #ident = ::core::option::Option::Some(
1617                                    #value_expr
1618                                        .filter(|__s| !__s.is_empty())
1619                                        .ok_or_else(|| #missing_required_err)?
1620                                        .to_owned()
1621                                );
1622                            }
1623                        } else {
1624                            let inner_ty = inner_ty
1625                                .ok_or_else(|| syn::Error::new(ident.span(), "expected Option<T>"))?;
1626                            quote! {
1627                                let #ident = ::core::option::Option::Some(
1628                                    #value_expr
1629                                        .filter(|__s| !__s.is_empty())
1630                                        .ok_or_else(|| #missing_required_err)?
1631                                        .parse::<#inner_ty>()
1632                                        .map_err(|_| ::edifact_rs::EdifactError::InvalidText { offset: __seg.span.start })?
1633                                );
1634                            }
1635                        }
1636                    } else if inner_is_str {
1637                        quote! {
1638                            let #ident = #value_expr
1639                                .filter(|__s| !__s.is_empty())
1640                                .map(::std::string::String::from);
1641                        }
1642                    } else {
1643                        let inner_ty = inner_ty
1644                            .ok_or_else(|| syn::Error::new(ident.span(), "expected Option<T>"))?;
1645                        quote! {
1646                            let #ident = #value_expr
1647                                .filter(|__s| !__s.is_empty())
1648                                .map(|__s| __s.parse::<#inner_ty>()
1649                                    .map_err(|_| ::edifact_rs::EdifactError::InvalidText { offset: __seg.span.start })
1650                                )
1651                                .transpose()?;
1652                        }
1653                    }
1654                } else if is_str_like(ty) {
1655                    quote! {
1656                        let #ident = #value_expr
1657                            .filter(|__s| !__s.is_empty())
1658                            .ok_or_else(|| ::edifact_rs::EdifactError::MissingRequiredElement {
1659                                tag: #seg_tag.to_owned(),
1660                                element_index: #idx,
1661                            })?
1662                            .to_owned();
1663                    }
1664                } else {
1665                    quote! {
1666                        let #ident = #value_expr
1667                            .filter(|__s| !__s.is_empty())
1668                            .ok_or_else(|| ::edifact_rs::EdifactError::MissingRequiredElement {
1669                                tag: #seg_tag.to_owned(),
1670                                element_index: #idx,
1671                            })?
1672                            .parse::<#ty>()
1673                            .map_err(|_| ::edifact_rs::EdifactError::InvalidText { offset: __seg.span.start })?;
1674                    }
1675                })
1676            })
1677            .collect::<syn::Result<_>>()?;
1678
1679        let body = quote! {
1680            #slot_prelude
1681            let __seg = #find_seg
1682                .ok_or_else(|| ::edifact_rs::EdifactError::MissingSegment {
1683                    tag: #seg_tag.to_owned(),
1684                    expected_position: "message body".to_owned(),
1685                })?;
1686            #qualifier_guard
1687            #(#field_inits)*
1688            ::core::result::Result::Ok(Self { #(#field_names),* })
1689        };
1690
1691        // Also generate EdifactSegmentTag impl.
1692        // Declare the qualifier through `QUALIFIER_PATTERN` rather than by
1693        // hand-rolling `matches_segment`.  Overriding only the borrowed matcher
1694        // left `matches_owned_segment` (which consults `QUALIFIER_PATTERN`)
1695        // matching on tag alone, so the owned deserialization path picked up
1696        // wrongly-qualified segments and then failed to parse them.
1697        let qualifier_match = if let Some(qual) = &struct_attrs.qualifier {
1698            quote! {
1699                const QUALIFIER_PATTERN: ::core::option::Option<&'static str> =
1700                    ::core::option::Option::Some(#qual);
1701            }
1702        } else if let Some(idx) = struct_attrs.qualifier_from {
1703            // "Any non-empty value at element `idx`" cannot be expressed as a
1704            // `QUALIFIER_PATTERN` (which is element 0 only), so both matchers are
1705            // overridden explicitly and must stay in agreement.
1706            quote! {
1707                fn matches_segment(seg: &::edifact_rs::Segment<'_>) -> bool {
1708                    seg.tag == Self::SEGMENT_TAG
1709                        && !seg.element_str(#idx as usize).unwrap_or("").is_empty()
1710                }
1711
1712                fn matches_owned_segment(seg: &::edifact_rs::OwnedSegment) -> bool {
1713                    seg.tag == Self::SEGMENT_TAG
1714                        && !seg
1715                            .elements
1716                            .get(#idx as usize)
1717                            .and_then(|e| e.components.first())
1718                            .map(|(c, _)| c.as_str())
1719                            .unwrap_or("")
1720                            .is_empty()
1721                }
1722            }
1723        } else {
1724            quote! {}
1725        };
1726
1727        let seg_tag_impl = quote! {
1728            impl ::edifact_rs::EdifactSegmentTag for #name {
1729                const SEGMENT_TAG: &'static str = #seg_tag;
1730                #qualifier_match
1731            }
1732        };
1733
1734        // ── Owned-segment deserialization path ────────────────────────────────
1735        // Works directly on `&[OwnedSegment]` without allocating a `Vec<Segment>`.
1736        let find_seg_owned = if let Some(qual) = &struct_attrs.qualifier {
1737            quote! {
1738                ::edifact_rs::find_qualified_segment_owned(segments, #seg_tag, #qual)
1739            }
1740        } else {
1741            quote! {
1742                ::edifact_rs::find_segment_owned(segments, #seg_tag)
1743            }
1744        };
1745
1746        let field_inits_owned: Vec<TokenStream2> = field_data
1747            .iter()
1748            .zip(slots.iter())
1749            .map(|((ident, ty, attrs), slot)| -> syn::Result<TokenStream2> {
1750                let idx = &slot.element;
1751                if attrs.composite {
1752                    if is_option_type(ty) {
1753                        let inner_ty = option_inner_type(ty)
1754                            .ok_or_else(|| syn::Error::new(ident.span(), "expected Option<T>"))?;
1755                        return Ok(quote! {
1756                            let #ident = match __seg.elements.get(#idx) {
1757                                ::core::option::Option::Some(__e) => {
1758                                    let __cows = __e.components.iter()
1759                                        .map(|(s, _)| ::std::borrow::Cow::Borrowed(s.as_str()))
1760                                        .collect::<::std::vec::Vec<::std::borrow::Cow<'_, str>>>();
1761                                    ::core::option::Option::Some(
1762                                        <#inner_ty as ::edifact_rs::EdifactCompositeDeserialize>::edifact_deserialize_composite(
1763                                            ::edifact_rs::CompositeElement::from_slice(&__cows)
1764                                        )?
1765                                    )
1766                                }
1767                                ::core::option::Option::None => ::core::option::Option::None,
1768                            };
1769                        });
1770                    }
1771                    return Ok(quote! {
1772                        let #ident = {
1773                            let __cows = __seg.elements.get(#idx)
1774                                .ok_or_else(|| ::edifact_rs::EdifactError::MissingRequiredElement {
1775                                    tag: #seg_tag.to_owned(),
1776                                    element_index: #idx,
1777                                })?
1778                                .components.iter()
1779                                .map(|(s, _)| ::std::borrow::Cow::Borrowed(s.as_str()))
1780                                .collect::<::std::vec::Vec<::std::borrow::Cow<'_, str>>>();
1781                            <#ty as ::edifact_rs::EdifactCompositeDeserialize>::edifact_deserialize_composite(
1782                                ::edifact_rs::CompositeElement::from_slice(&__cows)
1783                            )?
1784                        };
1785                    });
1786                }
1787                let comp = &slot.component;
1788                let value_expr_owned = if slot.has_component {
1789                    quote! { __seg.component_str(#idx, #comp) }
1790                } else {
1791                    quote! { __seg.element_str(#idx) }
1792                };
1793                // Same variant selection as the borrowed path; the two must
1794                // agree or the same input yields different error codes.
1795                let names_component = &slot.names_component;
1796                let missing_required_err_owned = quote! {
1797                    if #names_component {
1798                        ::edifact_rs::EdifactError::MissingRequiredComponent {
1799                            tag: #seg_tag.to_owned(),
1800                            element_index: #idx,
1801                            component_index: #comp,
1802                        }
1803                    } else {
1804                        ::edifact_rs::EdifactError::MissingRequiredElement {
1805                            tag: #seg_tag.to_owned(),
1806                            element_index: #idx,
1807                        }
1808                    }
1809                };
1810                Ok(if is_option_type(ty) {
1811                    if let Some(inner_ty) = option_inner_type(ty) {
1812                        if attrs.required {
1813                            // #[edifact(required)] on Option<T>: absence is an error.
1814                            // Emits MissingRequiredComponent when combined with component = N,
1815                            // MissingRequiredElement otherwise.
1816                            if is_str_like(inner_ty) {
1817                                quote! {
1818                                    let #ident = ::core::option::Option::Some(
1819                                        #value_expr_owned
1820                                            .filter(|__s| !__s.is_empty())
1821                                            .ok_or_else(|| #missing_required_err_owned)?
1822                                            .to_owned()
1823                                    );
1824                                }
1825                            } else {
1826                                quote! {
1827                                    let #ident = ::core::option::Option::Some(
1828                                        #value_expr_owned
1829                                            .filter(|__s| !__s.is_empty())
1830                                            .ok_or_else(|| #missing_required_err_owned)?
1831                                            .parse::<#inner_ty>()
1832                                            .map_err(|_| ::edifact_rs::EdifactError::InvalidText { offset: __seg.span.start })?
1833                                    );
1834                                }
1835                            }
1836                        } else if is_str_like(inner_ty) {
1837                            quote! {
1838                                let #ident = #value_expr_owned
1839                                    .filter(|__s| !__s.is_empty())
1840                                    .map(::std::string::String::from);
1841                            }
1842                        } else {
1843                            quote! {
1844                                let #ident = #value_expr_owned
1845                                    .filter(|__s| !__s.is_empty())
1846                                    .map(|__s| __s.parse::<#inner_ty>()
1847                                        .map_err(|_| ::edifact_rs::EdifactError::InvalidText { offset: __seg.span.start })
1848                                    )
1849                                    .transpose()?;
1850                            }
1851                        }
1852                    } else {
1853                        // Fallback: treat as String (should not happen with well-formed types).
1854                        quote! {
1855                            let #ident = #value_expr_owned
1856                                .filter(|__s| !__s.is_empty())
1857                                .map(::std::string::String::from);
1858                        }
1859                    }
1860                } else if is_str_like(ty) {
1861                    quote! {
1862                        let #ident = #value_expr_owned
1863                            .filter(|__s| !__s.is_empty())
1864                            .ok_or_else(|| ::edifact_rs::EdifactError::MissingRequiredElement {
1865                                tag: #seg_tag.to_owned(),
1866                                element_index: #idx,
1867                            })?
1868                            .to_owned();
1869                    }
1870                } else {
1871                    quote! {
1872                        let #ident = #value_expr_owned
1873                            .filter(|__s| !__s.is_empty())
1874                            .ok_or_else(|| ::edifact_rs::EdifactError::MissingRequiredElement {
1875                                tag: #seg_tag.to_owned(),
1876                                element_index: #idx,
1877                            })?
1878                            .parse::<#ty>()
1879                            .map_err(|_| ::edifact_rs::EdifactError::InvalidText { offset: __seg.span.start })?;
1880                    }
1881                })
1882            })
1883            .collect::<syn::Result<_>>()?;
1884
1885        let owned_body = quote! {
1886            #slot_prelude
1887            let __seg = #find_seg_owned
1888                .ok_or_else(|| ::edifact_rs::EdifactError::MissingSegment {
1889                    tag: #seg_tag.to_owned(),
1890                    expected_position: "message body".to_owned(),
1891                })?;
1892            #qualifier_guard
1893            #(#field_inits_owned)*
1894            ::core::result::Result::Ok(Self { #(#field_names),* })
1895        };
1896
1897        (body, owned_body, seg_tag_impl)
1898    } else {
1899        // ── Message struct: delegate to each field ────────────────────────────
1900        let field_inits: Vec<TokenStream2> = field_data
1901            .iter()
1902            .map(|(ident, ty, attrs)| -> syn::Result<TokenStream2> {
1903                Ok(if let Some(qual) = &attrs.qualifier {
1904                    if attrs.group || is_vec_type(ty) {
1905                        let inner_ty = vec_inner_type(ty)
1906                            .ok_or_else(|| syn::Error::new(ident.span(), "expected Vec<T>"))?;
1907                        quote! {
1908                            let #ident = segments
1909                                .iter()
1910                                .filter(|__seg| {
1911                                    __seg.tag == <#inner_ty as ::edifact_rs::EdifactSegmentTag>::SEGMENT_TAG
1912                                        && __seg.element_str(0).unwrap_or("") == #qual
1913                                })
1914                                .map(|__seg| {
1915                                    ::edifact_rs::EdifactDeserialize::edifact_deserialize(
1916                                        ::core::slice::from_ref(__seg),
1917                                    )
1918                                })
1919                                .collect::<::core::result::Result<::std::vec::Vec<#inner_ty>, ::edifact_rs::EdifactError>>()?;
1920                        }
1921                    } else if is_option_type(ty) {
1922                        let inner_ty = option_inner_type(ty)
1923                            .ok_or_else(|| syn::Error::new(ident.span(), "expected Option<T>"))?;
1924                        quote! {
1925                            let #ident = match ::edifact_rs::find_qualified_segment(
1926                                segments,
1927                                <#inner_ty as ::edifact_rs::EdifactSegmentTag>::SEGMENT_TAG,
1928                                #qual,
1929                            ) {
1930                                ::core::option::Option::Some(__seg) => {
1931                                    ::core::option::Option::Some(
1932                                        ::edifact_rs::EdifactDeserialize::edifact_deserialize(
1933                                            ::core::slice::from_ref(__seg),
1934                                        )?
1935                                    )
1936                                }
1937                                ::core::option::Option::None => ::core::option::Option::None,
1938                            };
1939                        }
1940                    } else {
1941                        quote! {
1942                            let __seg = ::edifact_rs::find_qualified_segment(
1943                                segments,
1944                                <#ty as ::edifact_rs::EdifactSegmentTag>::SEGMENT_TAG,
1945                                #qual,
1946                            )
1947                            .ok_or_else(|| ::edifact_rs::EdifactError::MissingSegment {
1948                                tag: <#ty as ::edifact_rs::EdifactSegmentTag>::SEGMENT_TAG.to_owned(),
1949                                expected_position: "message body".to_owned(),
1950                            })?;
1951                            let #ident = ::edifact_rs::EdifactDeserialize::edifact_deserialize(
1952                                ::core::slice::from_ref(__seg),
1953                            )?;
1954                        }
1955                    }
1956                } else if attrs.group || is_vec_type(ty) {
1957                    let inner_ty = vec_inner_type(ty)
1958                        .ok_or_else(|| syn::Error::new(ident.span(), "expected Vec<T>"))?;
1959                    quote! {
1960                        let #ident = ::edifact_rs::find_segments_typed::<#inner_ty>(segments)
1961                            .map(|__seg| {
1962                                <#inner_ty as ::edifact_rs::EdifactDeserialize>::edifact_deserialize(
1963                                    ::core::slice::from_ref(__seg),
1964                                )
1965                            })
1966                            .collect::<::core::result::Result<::std::vec::Vec<#inner_ty>, _>>()?;
1967                    }
1968                } else if is_option_type(ty) {
1969                    let inner_ty = option_inner_type(ty)
1970                        .ok_or_else(|| syn::Error::new(ident.span(), "expected Option<T>"))?;
1971                    quote! {
1972                        let #ident = if segments
1973                            .iter()
1974                            .any(|__seg| __seg.tag == <#inner_ty as ::edifact_rs::EdifactSegmentTag>::SEGMENT_TAG)
1975                        {
1976                            ::core::option::Option::Some(
1977                                <#inner_ty as ::edifact_rs::EdifactDeserialize>::edifact_deserialize(segments)?
1978                            )
1979                        } else {
1980                            ::core::option::Option::None
1981                        };
1982                    }
1983                } else {
1984                    quote! {
1985                        let #ident = ::edifact_rs::EdifactDeserialize::edifact_deserialize(segments)?;
1986                    }
1987                })
1988            })
1989            .collect::<syn::Result<_>>()?;
1990
1991        let body = quote! {
1992            #(#field_inits)*
1993            ::core::result::Result::Ok(Self { #(#field_names),* })
1994        };
1995
1996        // ── Owned-segment message deserialization path ────────────────────────
1997        // Works directly on `&[OwnedSegment]` without converting to `Vec<Segment>`.
1998        let field_inits_owned: Vec<TokenStream2> = field_data
1999            .iter()
2000            .map(|(ident, ty, attrs)| -> syn::Result<TokenStream2> {
2001                Ok(if let Some(qual) = &attrs.qualifier {
2002                    if attrs.group || is_vec_type(ty) {
2003                        let inner_ty = vec_inner_type(ty)
2004                            .ok_or_else(|| syn::Error::new(ident.span(), "expected Vec<T>"))?;
2005                        quote! {
2006                            let #ident = segments
2007                                .iter()
2008                                .filter(|__seg| {
2009                                    __seg.tag == <#inner_ty as ::edifact_rs::EdifactSegmentTag>::SEGMENT_TAG
2010                                        && __seg.element_str(0).unwrap_or("") == #qual
2011                                })
2012                                .map(|__seg| {
2013                                    <#inner_ty as ::edifact_rs::EdifactDeserialize>::edifact_deserialize_owned(
2014                                        ::core::slice::from_ref(__seg),
2015                                    )
2016                                })
2017                                .collect::<::core::result::Result<::std::vec::Vec<#inner_ty>, ::edifact_rs::EdifactError>>()?;
2018                        }
2019                    } else if is_option_type(ty) {
2020                        let inner_ty = option_inner_type(ty)
2021                            .ok_or_else(|| syn::Error::new(ident.span(), "expected Option<T>"))?;
2022                        quote! {
2023                            let #ident = match ::edifact_rs::find_qualified_segment_owned(
2024                                segments,
2025                                <#inner_ty as ::edifact_rs::EdifactSegmentTag>::SEGMENT_TAG,
2026                                #qual,
2027                            ) {
2028                                ::core::option::Option::Some(__seg) => {
2029                                    ::core::option::Option::Some(
2030                                        <#inner_ty as ::edifact_rs::EdifactDeserialize>::edifact_deserialize_owned(
2031                                            ::core::slice::from_ref(__seg),
2032                                        )?
2033                                    )
2034                                }
2035                                ::core::option::Option::None => ::core::option::Option::None,
2036                            };
2037                        }
2038                    } else {
2039                        quote! {
2040                            let __seg = ::edifact_rs::find_qualified_segment_owned(
2041                                segments,
2042                                <#ty as ::edifact_rs::EdifactSegmentTag>::SEGMENT_TAG,
2043                                #qual,
2044                            )
2045                            .ok_or_else(|| ::edifact_rs::EdifactError::MissingSegment {
2046                                tag: <#ty as ::edifact_rs::EdifactSegmentTag>::SEGMENT_TAG.to_owned(),
2047                                expected_position: "message body".to_owned(),
2048                            })?;
2049                            let #ident = <#ty as ::edifact_rs::EdifactDeserialize>::edifact_deserialize_owned(
2050                                ::core::slice::from_ref(__seg),
2051                            )?;
2052                        }
2053                    }
2054                } else if attrs.group || is_vec_type(ty) {
2055                    let inner_ty = vec_inner_type(ty)
2056                        .ok_or_else(|| syn::Error::new(ident.span(), "expected Vec<T>"))?;
2057                    quote! {
2058                        let #ident = segments
2059                            .iter()
2060                            .filter(|__seg| <#inner_ty as ::edifact_rs::EdifactSegmentTag>::matches_owned_segment(__seg))
2061                            .map(|__seg| {
2062                                <#inner_ty as ::edifact_rs::EdifactDeserialize>::edifact_deserialize_owned(
2063                                    ::core::slice::from_ref(__seg),
2064                                )
2065                            })
2066                            .collect::<::core::result::Result<::std::vec::Vec<#inner_ty>, _>>()?;
2067                    }
2068                } else if is_option_type(ty) {
2069                    let inner_ty = option_inner_type(ty)
2070                        .ok_or_else(|| syn::Error::new(ident.span(), "expected Option<T>"))?;
2071                    quote! {
2072                        let #ident = if segments
2073                            .iter()
2074                            .any(|__seg| __seg.tag == <#inner_ty as ::edifact_rs::EdifactSegmentTag>::SEGMENT_TAG)
2075                        {
2076                            ::core::option::Option::Some(
2077                                <#inner_ty as ::edifact_rs::EdifactDeserialize>::edifact_deserialize_owned(segments)?
2078                            )
2079                        } else {
2080                            ::core::option::Option::None
2081                        };
2082                    }
2083                } else {
2084                    quote! {
2085                        let #ident = <#ty as ::edifact_rs::EdifactDeserialize>::edifact_deserialize_owned(segments)?;
2086                    }
2087                })
2088            })
2089            .collect::<syn::Result<_>>()?;
2090
2091        let owned_body = quote! {
2092            #(#field_inits_owned)*
2093            ::core::result::Result::Ok(Self { #(#field_names),* })
2094        };
2095
2096        (body, owned_body, quote! {})
2097    };
2098
2099    Ok(quote! {
2100        impl ::edifact_rs::EdifactDeserialize for #name {
2101            fn edifact_deserialize(
2102                segments: &[::edifact_rs::Segment<'_>],
2103            ) -> ::core::result::Result<Self, ::edifact_rs::EdifactError> {
2104                #body
2105            }
2106
2107            fn edifact_deserialize_owned(
2108                segments: &[::edifact_rs::OwnedSegment],
2109            ) -> ::core::result::Result<Self, ::edifact_rs::EdifactError> {
2110                #owned_body
2111            }
2112        }
2113        #segment_tag_impl
2114    })
2115}
2116
2117#[cfg(test)]
2118mod tests {
2119    /// Compile-fail / compile-pass suite for the derive macros.
2120    ///
2121    /// The blessed `.stderr` files hold only this crate's own diagnostics, so
2122    /// they are stable across toolchains and CI runs the suite on both MSRV and
2123    /// stable.  Keep it that way: an expectation that captures a *rustc*
2124    /// warning or note will drift on the next release and drown real
2125    /// regressions in noise.  If a UI case triggers an incidental lint, silence
2126    /// it at the source (see `tests/ui/support.rs`) rather than blessing it.
2127    ///
2128    /// The suite is off by default because it is slow; set `EDIFACT_UI_TESTS=1`
2129    /// to run it, or use `just ui` / `just ui-msrv`.
2130    ///
2131    /// Re-bless after intentional message changes with `just ui-bless`.
2132    #[test]
2133    fn trybuild_ui() {
2134        if std::env::var_os("EDIFACT_UI_TESTS").is_none() {
2135            eprintln!(
2136                "skipping derive UI suite: set EDIFACT_UI_TESTS=1 to run it \
2137                 (expectations are pinned to the MSRV toolchain)"
2138            );
2139            return;
2140        }
2141        let t = trybuild::TestCases::new();
2142        t.pass("tests/ui/pass_*.rs");
2143        t.compile_fail("tests/ui/fail_*.rs");
2144    }
2145}