Skip to main content

edifact_rs_derive/
lib.rs

1#![deny(unsafe_code)]
2//! Derive macros for `EdifactSerialize` and `EdifactDeserialize`.
3//!
4//! # Segment struct (single segment)
5//!
6//! ```ignore
7//! #[derive(EdifactSerialize, EdifactDeserialize)]
8//! #[edifact(segment = "BGM")]
9//! pub struct BgmSegment {
10//!     #[edifact(element = 0)]
11//!     pub doc_name_code: String,
12//!     #[edifact(element = 1)]
13//!     pub doc_id: String,
14//!     #[edifact(element = 2)]
15//!     pub msg_function: Option<String>,
16//! }
17//! ```
18//!
19//! # Segment struct with qualifier
20//!
21//! ```ignore
22//! #[derive(EdifactSerialize, EdifactDeserialize)]
23//! #[edifact(segment = "NAD", qualifier = "MS")]
24//! pub struct NadMs {
25//!     #[edifact(element = 1)]
26//!     pub party_id: String,
27//! }
28//! ```
29//!
30//! # Message struct (multiple segments)
31//!
32//! ```ignore
33//! #[derive(EdifactSerialize, EdifactDeserialize)]
34//! pub struct OrdersMessage {
35//!     pub bgm: BgmSegment,
36//!     pub buyer: NadMs,
37//!     #[edifact(group)]
38//!     pub lines: Vec<LinSegment>,
39//! }
40//! ```
41//!
42//! # `#[edifact(group)]` and `Vec<T>` fields
43//!
44//! The `#[edifact(group)]` attribute marks a `Vec<T>` field as a contiguous group of
45//! repeated segments.  Without the attribute, `Vec<T>` on a segment struct collects
46//! all matching segments from the window into the `Vec`.
47//!
48//! `#[edifact(group)]` is a documentation and validation marker: it makes the
49//! repeating-group intent explicit and enforces two compile-time constraints:
50//!
51//! 1. The annotated field **must** be of type `Vec<T>` — any other type is rejected
52//!    with a clear error message.
53//! 2. `#[edifact(group)]` cannot be combined with `#[edifact(element = ...)]` or
54//!    `#[edifact(component = ...)]` — positional placement and group semantics are
55//!    mutually exclusive.
56//!
57//! At runtime it generates the same code as a bare `Vec<T>`: every segment matching
58//! `T`'s tag and qualifier is collected, in document order, without a contiguity
59//! requirement.  When you need contiguity enforced, use
60//! [`contiguous_groups_by_qualifier`] directly on the parsed segments.
61//!
62//! [`contiguous_groups_by_qualifier`]: https://docs.rs/edifact-rs/latest/edifact_rs/fn.contiguous_groups_by_qualifier.html
63//!
64//! # `#[edifact(required)]` on `Option<T>` fields
65//!
66//! By default, `Option<T>` fields produce `None` when the element is absent.
67//! Annotating an `Option<T>` field with `#[edifact(required)]` changes this:
68//! instead of `None`, deserialization returns
69//! `edifact_rs::EdifactError::MissingRequiredElement`
70//! when the element is absent or empty.  The Rust type stays `Option<T>`, which
71//! is useful when the EDIFACT specification mandates the element but your domain
72//! model treats it as optional for other reasons.
73//!
74//! ```ignore
75//! #[derive(EdifactSerialize, EdifactDeserialize)]
76//! #[edifact(segment = "DTM")]
77//! pub struct DtmSegment {
78//!     #[edifact(element = 0)]
79//!     qualifier: String,
80//!     /// Required by the spec but kept as Option in the domain model.
81//!     #[edifact(element = 1, required)]
82//!     date_time: Option<String>,
83//!     #[edifact(element = 2)]
84//!     format_code: Option<String>,
85//! }
86//! ```
87//!
88//! # Non-`String` fields and `Display` / `FromStr`
89//! Non-`String` field types (e.g. `u32`, `bool`, your own newtype) are serialized via
90//! `Display` and deserialized via `FromStr`.  The derive macro does **not** add a
91//! compile-time bound; if the type does not implement both traits the generated code
92//! will fail to compile with a standard "trait not satisfied" error.
93//!
94//! To avoid surprises, ensure any non-`String` field type implements both:
95//! ```ignore
96//! impl std::fmt::Display for MyCode { ... }
97//! impl std::str::FromStr for MyCode { ... }
98//! ```
99
100use proc_macro::TokenStream;
101use proc_macro2::TokenStream as TokenStream2;
102use quote::quote;
103use syn::{Data, DeriveInput, Field, Fields, Type, parse_macro_input, spanned::Spanned};
104
105// ── entry points ───────────────────────────────────────────────────────────────
106
107#[proc_macro_derive(EdifactSerialize, attributes(edifact))]
108/// Derive `edifact_rs::EdifactSerialize` for segment or message structs.
109///
110/// # Limitations
111///
112/// - **No generics**: the struct must not have generic type parameters.
113/// - **No lifetime parameters**: the struct must own all its data (`String`,
114///   not `&str`).  Borrow-based structs such as `Segment<'a>` cannot use this
115///   derive macro.
116pub fn derive_edifact_serialize(input: TokenStream) -> TokenStream {
117    let input = parse_macro_input!(input as DeriveInput);
118    impl_serialize(&input)
119        .unwrap_or_else(|e| e.to_compile_error())
120        .into()
121}
122
123#[proc_macro_derive(EdifactDeserialize, attributes(edifact))]
124/// Derive `edifact_rs::EdifactDeserialize` for segment or message structs.
125///
126/// # Limitations
127///
128/// - **No generics**: the struct must not have generic type parameters.
129/// - **No lifetime parameters**: the struct must own all its data (`String`,
130///   not `&str`).  Add owned wrapper types or clone components at the
131///   deserialization site if lifetime flexibility is required.
132pub fn derive_edifact_deserialize(input: TokenStream) -> TokenStream {
133    let input = parse_macro_input!(input as DeriveInput);
134    impl_deserialize(&input)
135        .unwrap_or_else(|e| e.to_compile_error())
136        .into()
137}
138
139// ── attribute containers ───────────────────────────────────────────────────────
140
141#[derive(Default)]
142struct StructAttrs {
143    /// `#[edifact(segment = "TAG")]`
144    segment: Option<String>,
145    /// `#[edifact(qualifier = "Q")]` — element 0 value for segment matching
146    qualifier: Option<String>,
147    qualifier_span: Option<proc_macro2::Span>,
148    /// `#[edifact(qualifier_from = N)]` — zero-based element index; qualifier is dynamic at runtime.
149    qualifier_from: Option<u32>,
150    qualifier_from_span: Option<proc_macro2::Span>,
151}
152
153#[derive(Default)]
154struct FieldAttrs {
155    /// `#[edifact(element = N)]` — zero-based element index
156    element: Option<u32>,
157    element_span: Option<proc_macro2::Span>,
158    /// `#[edifact(component = N)]` — component index within the element (for composite data elements)
159    component: Option<u32>,
160    component_span: Option<proc_macro2::Span>,
161    /// `#[edifact(composite)]` — map the field as a full composite element via composite serde traits.
162    composite: bool,
163    composite_span: Option<proc_macro2::Span>,
164    /// `#[edifact(group)]` — `Vec<T>`: each item is a separate segment
165    group: bool,
166    group_span: Option<proc_macro2::Span>,
167    /// `#[edifact(qualifier = "Q")]` — message field constrained to qualifier.
168    qualifier: Option<String>,
169    qualifier_span: Option<proc_macro2::Span>,
170    /// `#[edifact(required)]` — treat an `Option<T>` field as mandatory.
171    ///
172    /// Without this attribute, `Option<T>` fields produce `None` when the element
173    /// is absent.  With `#[edifact(required)]` the deserialization emits
174    /// `EdifactError::MissingRequiredElement` instead, even though the Rust type is
175    /// still `Option<T>`.  This is useful for elements that the EDIFACT spec marks as
176    /// mandatory but which your domain model represents as optional for other reasons.
177    required: bool,
178    required_span: Option<proc_macro2::Span>,
179}
180
181// ── attribute parsing ──────────────────────────────────────────────────────────
182
183/// Largest accepted `element` / `component` index.
184///
185/// Serialization emits one statement per slot up to the highest declared index,
186/// so an unbounded value makes rustc generate an arbitrary amount of code — a
187/// typo'd `element = 200000` was enough to exhaust memory and kill the compiler.
188/// UN/EDIFACT caps segments at 99 data elements and composites at 99 components,
189/// so 256 is generous.
190const MAX_POSITION_INDEX: u32 = 256;
191
192fn check_index_bound(lit: &syn::LitInt, value: u32, key: &str) -> syn::Result<()> {
193    if value > MAX_POSITION_INDEX {
194        return Err(syn::Error::new(
195            lit.span(),
196            format!(
197                "`{key}` index {value} exceeds the maximum of {MAX_POSITION_INDEX}; \
198                 UN/EDIFACT allows at most 99 elements per segment and 99 components per composite"
199            ),
200        ));
201    }
202    Ok(())
203}
204
205fn parse_struct_attrs(input: &DeriveInput) -> syn::Result<StructAttrs> {
206    let mut out = StructAttrs::default();
207    for attr in &input.attrs {
208        if !attr.path().is_ident("edifact") {
209            continue;
210        }
211        attr.parse_nested_meta(|meta| {
212            if meta.path.is_ident("segment") {
213                if out.segment.is_some() {
214                    return Err(meta.error("duplicate `segment`"));
215                }
216                let lit = meta.value()?.parse::<syn::LitStr>()?;
217                let tag = lit.value();
218                if tag.len() != 3 || !tag.bytes().all(|b| b.is_ascii_uppercase()) {
219                    return Err(syn::Error::new(
220                        lit.span(),
221                        format!(
222                            "segment tag must be exactly 3 ASCII uppercase letters; got {tag:?}"
223                        ),
224                    ));
225                }
226                out.segment = Some(tag);
227            } else if meta.path.is_ident("qualifier") {
228                if out.qualifier.is_some() {
229                    return Err(meta.error("duplicate `qualifier`"));
230                }
231                out.qualifier = Some(meta.value()?.parse::<syn::LitStr>()?.value());
232                out.qualifier_span = Some(meta.path.span());
233            } else if meta.path.is_ident("qualifier_from") {
234                if out.qualifier_from.is_some() {
235                    return Err(meta.error("duplicate `qualifier_from`"));
236                }
237                let lit = meta.value()?.parse::<syn::LitInt>()?;
238                let idx: u32 = lit.base10_parse()?;
239                check_index_bound(&lit, idx, "qualifier_from")?;
240                out.qualifier_from = Some(idx);
241                out.qualifier_from_span = Some(meta.path.span());
242            } else {
243                return Err(meta.error("unknown struct-level `edifact` key; expected `segment`, `qualifier`, or `qualifier_from`"));
244            }
245            Ok(())
246        })?;
247    }
248    if (out.qualifier.is_some() || out.qualifier_from.is_some()) && out.segment.is_none() {
249        return Err(syn::Error::new(
250            out.qualifier_span
251                .or(out.qualifier_from_span)
252                .unwrap_or_else(|| input.span()),
253            "#[edifact(qualifier = ...)] / #[edifact(qualifier_from = ...)] require #[edifact(segment = ...)]",
254        ));
255    }
256    if out.qualifier.is_some() && out.qualifier_from.is_some() {
257        return Err(syn::Error::new(
258            out.qualifier_from_span
259                .or(out.qualifier_span)
260                .unwrap_or_else(|| input.span()),
261            "use either #[edifact(qualifier = ...)] or #[edifact(qualifier_from = ...)], not both",
262        ));
263    }
264    Ok(out)
265}
266
267fn parse_field_attrs(field: &Field) -> syn::Result<FieldAttrs> {
268    let mut out = FieldAttrs::default();
269    for attr in &field.attrs {
270        if !attr.path().is_ident("edifact") {
271            continue;
272        }
273        attr.parse_nested_meta(|meta| {
274            if meta.path.is_ident("element") {
275                if out.element.is_some() {
276                    return Err(meta.error("duplicate `element`"));
277                }
278                let lit = meta.value()?.parse::<syn::LitInt>()?;
279                let idx: u32 = lit.base10_parse()?;
280                check_index_bound(&lit, idx, "element")?;
281                out.element = Some(idx);
282                out.element_span = Some(meta.path.span());
283            } else if meta.path.is_ident("component") {
284                if out.component.is_some() {
285                    return Err(meta.error("duplicate `component`"));
286                }
287                let lit = meta.value()?.parse::<syn::LitInt>()?;
288                let idx: u32 = lit.base10_parse()?;
289                check_index_bound(&lit, idx, "component")?;
290                out.component = Some(idx);
291                out.component_span = Some(meta.path.span());
292            } else if meta.path.is_ident("composite") {
293                out.composite = true;
294                out.composite_span = Some(meta.path.span());
295            } else if meta.path.is_ident("group") {
296                out.group = true;
297                out.group_span = Some(meta.path.span());
298            } else if meta.path.is_ident("qualifier") {
299                if out.qualifier.is_some() {
300                    return Err(meta.error("duplicate `qualifier`"));
301                }
302                out.qualifier = Some(meta.value()?.parse::<syn::LitStr>()?.value());
303                out.qualifier_span = Some(meta.path.span());
304            } else if meta.path.is_ident("required") {
305                out.required = true;
306                out.required_span = Some(meta.path.span());
307            } else {
308                return Err(meta.error("unknown field-level `edifact` key; expected `element`, `component`, `composite`, `group`, `qualifier`, or `required`"));
309            }
310            Ok(())
311        })?;
312    }
313    Ok(out)
314}
315
316// ── type helpers ───────────────────────────────────────────────────────────────
317
318fn is_option_type(ty: &Type) -> bool {
319    matches!(ty, Type::Path(p) if p.path.segments.last().is_some_and(|s| s.ident == "Option"))
320}
321
322fn is_vec_type(ty: &Type) -> bool {
323    matches!(ty, Type::Path(p) if p.path.segments.last().is_some_and(|s| s.ident == "Vec"))
324}
325
326/// Returns `true` for the `String` path type.
327///
328/// Accepts only:
329/// - `String` (bare, single-segment)
330/// - `std::string::String` (fully qualified standard library path)
331/// - `alloc::string::String` (fully qualified alloc path for `no_std` contexts)
332///
333/// A user-defined type whose last segment is `String` but that does not match
334/// one of these three forms is **not** treated as a string type, which prevents
335/// accidental string-extraction code generation for unrelated user types.
336///
337/// **Shadowing caveat:** bare `String` (single-segment, no path prefix) is matched
338/// by name only. If a crate shadows the standard-library `String` with a local type
339/// of the same name, this function will still classify it as a string type and the
340/// derive macro will generate incorrect string-extraction code rather than a
341/// composite or element parse. To avoid this, always use the fully-qualified path
342/// (`std::string::String`) in struct fields when `String` is shadowed in scope.
343fn is_string_type(ty: &Type) -> bool {
344    let Type::Path(p) = ty else { return false };
345    // Single-segment bare "String"
346    if p.path.is_ident("String") {
347        return true;
348    }
349    // Fully-qualified std::string::String or alloc::string::String
350    let segs = &p.path.segments;
351    segs.len() == 3
352        && (segs[0].ident == "std" || segs[0].ident == "alloc")
353        && segs[1].ident == "string"
354        && segs[2].ident == "String"
355}
356
357/// Returns `true` for `&str` or `&'_ str` reference types.
358fn is_str_ref_type(ty: &Type) -> bool {
359    let Type::Reference(r) = ty else { return false };
360    matches!(r.elem.as_ref(), Type::Path(p) if p.path.is_ident("str"))
361}
362
363/// Returns `true` when `ty` is a type that can yield `&str` without allocating
364/// (i.e. `String` or `&str`).
365fn is_str_like(ty: &Type) -> bool {
366    is_string_type(ty) || is_str_ref_type(ty)
367}
368
369fn option_inner_type(ty: &Type) -> Option<&Type> {
370    let Type::Path(path) = ty else { return None };
371    let seg = path.path.segments.last()?;
372    if seg.ident != "Option" {
373        return None;
374    }
375    let syn::PathArguments::AngleBracketed(args) = &seg.arguments else {
376        return None;
377    };
378    let syn::GenericArgument::Type(inner) = args.args.first()? else {
379        return None;
380    };
381    Some(inner)
382}
383
384fn vec_inner_type(ty: &Type) -> Option<&Type> {
385    let Type::Path(path) = ty else { return None };
386    let seg = path.path.segments.last()?;
387    if seg.ident != "Vec" {
388        return None;
389    }
390    let syn::PathArguments::AngleBracketed(args) = &seg.arguments else {
391        return None;
392    };
393    let syn::GenericArgument::Type(inner) = args.args.first()? else {
394        return None;
395    };
396    Some(inner)
397}
398
399// ── named field extraction ─────────────────────────────────────────────────────
400
401/// Reject two fields that map to the same `(element, component)` slot.
402///
403/// Serialization keys its emit table by slot, so a duplicate silently dropped
404/// one field from the output while deserialization still read both — an
405/// asymmetric, compile-clean data loss.
406fn check_duplicate_slots(
407    field_data: &[(&syn::Ident, &Type, FieldAttrs)],
408    is_segment_struct: bool,
409) -> syn::Result<()> {
410    if !is_segment_struct {
411        return Ok(());
412    }
413    let mut seen: Vec<((u32, u32), &syn::Ident)> = Vec::with_capacity(field_data.len());
414    for (i, (ident, _, attrs)) in field_data.iter().enumerate() {
415        // Group fields are not positional, so they occupy no slot.
416        if attrs.group {
417            continue;
418        }
419        let slot = (
420            attrs.element.unwrap_or(i as u32),
421            attrs.component.unwrap_or(0),
422        );
423        if let Some((_, first)) = seen.iter().find(|(s, _)| *s == slot) {
424            return Err(syn::Error::new(
425                ident.span(),
426                format!(
427                    "field `{ident}` maps to element {} component {}, which is already \
428                     claimed by field `{first}`; give each field a distinct \
429                     `#[edifact(element = ..., component = ...)]` slot",
430                    slot.0, slot.1
431                ),
432            ));
433        }
434        seen.push((slot, ident));
435    }
436    Ok(())
437}
438
439fn get_named_fields(input: &DeriveInput) -> syn::Result<&syn::FieldsNamed> {
440    if !input.generics.params.is_empty() {
441        return Err(syn::Error::new(
442            input.generics.params.span(),
443            "EdifactSerialize/EdifactDeserialize do not support generic structs",
444        ));
445    }
446    match &input.data {
447        Data::Struct(s) => match &s.fields {
448            Fields::Named(f) => Ok(f),
449            _ => Err(syn::Error::new(
450                input.span(),
451                "EdifactSerialize/EdifactDeserialize only support structs with named fields",
452            )),
453        },
454        _ => Err(syn::Error::new(
455            input.span(),
456            "EdifactSerialize/EdifactDeserialize only support structs",
457        )),
458    }
459}
460
461fn validate_field_attrs(
462    ident: &syn::Ident,
463    ty: &Type,
464    attrs: &FieldAttrs,
465    is_segment_struct: bool,
466) -> syn::Result<()> {
467    if attrs.group && !is_vec_type(ty) {
468        return Err(syn::Error::new(
469            attrs.group_span.unwrap_or_else(|| ident.span()),
470            format!("field `{ident}`: #[edifact(group)] requires Vec<T>"),
471        ));
472    }
473    if attrs.group && (attrs.element.is_some() || attrs.component.is_some()) {
474        return Err(syn::Error::new(
475            attrs.group_span.unwrap_or_else(|| ident.span()),
476            format!(
477                "field `{ident}`: #[edifact(group)] cannot be combined with element/component positioning"
478            ),
479        ));
480    }
481    if attrs.composite && attrs.component.is_some() {
482        return Err(syn::Error::new(
483            attrs.component_span.unwrap_or_else(|| ident.span()),
484            format!(
485                "field `{ident}`: #[edifact(component = ...)] cannot be combined with #[edifact(composite)]"
486            ),
487        ));
488    }
489    if attrs.composite && attrs.group {
490        return Err(syn::Error::new(
491            attrs.composite_span.unwrap_or_else(|| ident.span()),
492            format!(
493                "field `{ident}`: #[edifact(composite)] cannot be combined with #[edifact(group)]"
494            ),
495        ));
496    }
497    if is_segment_struct && attrs.group {
498        return Err(syn::Error::new(
499            attrs.group_span.unwrap_or_else(|| ident.span()),
500            format!("field `{ident}`: #[edifact(group)] is only valid on message structs"),
501        ));
502    }
503    if !is_segment_struct && (attrs.element.is_some() || attrs.component.is_some()) {
504        return Err(syn::Error::new(
505            attrs
506                .element_span
507                .or(attrs.component_span)
508                .unwrap_or_else(|| ident.span()),
509            format!(
510                "field `{ident}`: element/component positioning is only valid on segment structs"
511            ),
512        ));
513    }
514    if !is_segment_struct && attrs.composite {
515        return Err(syn::Error::new(
516            attrs.composite_span.unwrap_or_else(|| ident.span()),
517            format!("field `{ident}`: #[edifact(composite)] is only valid on segment structs"),
518        ));
519    }
520    if is_segment_struct && attrs.qualifier.is_some() {
521        return Err(syn::Error::new(
522            attrs.qualifier_span.unwrap_or_else(|| ident.span()),
523            format!(
524                "field `{ident}`: #[edifact(qualifier = ...)] is only valid on message struct fields"
525            ),
526        ));
527    }
528    if attrs.qualifier.is_some() && attrs.group && !is_vec_type(ty) {
529        return Err(syn::Error::new(
530            attrs.qualifier_span.unwrap_or_else(|| ident.span()),
531            format!("field `{ident}`: qualifier-constrained groups must be Vec<T>"),
532        ));
533    }
534    if attrs.required && !is_option_type(ty) {
535        return Err(syn::Error::new(
536            attrs.required_span.unwrap_or_else(|| ident.span()),
537            format!(
538                "field `{ident}`: #[edifact(required)] only applies to Option<T> fields; \
539                 non-Option fields are always required"
540            ),
541        ));
542    }
543    if attrs.required && attrs.composite {
544        return Err(syn::Error::new(
545            attrs.required_span.unwrap_or_else(|| ident.span()),
546            format!(
547                "field `{ident}`: #[edifact(required)] cannot be combined with \
548                 #[edifact(composite)]; use a non-optional field type to require the \
549                 composite element"
550            ),
551        ));
552    }
553    if attrs.required && !is_segment_struct {
554        return Err(syn::Error::new(
555            attrs.required_span.unwrap_or_else(|| ident.span()),
556            format!(
557                "field `{ident}`: #[edifact(required)] is only valid on segment struct \
558                 element fields; to require a segment in a message struct, use a \
559                 non-optional field type"
560            ),
561        ));
562    }
563    Ok(())
564}
565
566// ── EdifactSerialize ───────────────────────────────────────────────────────────
567
568fn impl_serialize(input: &DeriveInput) -> syn::Result<TokenStream2> {
569    let name = &input.ident;
570    let struct_attrs = parse_struct_attrs(input)?;
571    let fields = get_named_fields(input)?;
572    let is_segment_struct = struct_attrs.segment.is_some();
573
574    // Collect (field_ident, field_type, FieldAttrs).
575    let field_data: Vec<(&syn::Ident, &Type, FieldAttrs)> = fields
576        .named
577        .iter()
578        .map(|f| {
579            let attrs = parse_field_attrs(f)?;
580            let ident = f
581                .ident
582                .as_ref()
583                .ok_or_else(|| syn::Error::new_spanned(f, "only named fields are supported"))?;
584            validate_field_attrs(ident, &f.ty, &attrs, is_segment_struct)?;
585            Ok((ident, &f.ty, attrs))
586        })
587        .collect::<syn::Result<_>>()?;
588    check_duplicate_slots(&field_data, is_segment_struct)?;
589
590    let body = if let Some(seg_tag) = &struct_attrs.segment {
591        // ── Segment struct: emit one EDIFACT segment ──────────────────────────
592        // When a struct-level qualifier is declared, inject it at slot 0.
593        // Fields at (element=0, component>=1) extend it as composite components.
594        // Fields at element >= 1 are emitted as regular elements.
595        let (qualifier_emit, start_slot, elem0_comp_stmts) = if let Some(qual) =
596            &struct_attrs.qualifier
597        {
598            // Error only if a field claims element=0 with no component or component=0.
599            for (i, (ident, _, attrs)) in field_data.iter().enumerate() {
600                let elem = attrs.element.unwrap_or(i as u32);
601                let comp = attrs.component.unwrap_or(0);
602                if elem == 0 && comp == 0 {
603                    return Err(syn::Error::new(
604                        attrs
605                            .element_span
606                            .or(attrs.component_span)
607                            .unwrap_or_else(|| ident.span()),
608                        format!(
609                            "field `{}`: cannot use #[edifact(qualifier = ...)] with a field at element = 0 without component >= 1; the qualifier occupies component 0",
610                            ident
611                        ),
612                    ));
613                }
614            }
615            // Collect fields at element=0, component>0, sorted by component.
616            let mut comp_fields: Vec<(u32, usize)> = field_data
617                .iter()
618                .enumerate()
619                .filter_map(|(i, (_, _, attrs))| {
620                    let elem = attrs.element.unwrap_or(i as u32);
621                    let comp = attrs.component.unwrap_or(0);
622                    if elem == 0 && comp > 0 {
623                        Some((comp, i))
624                    } else {
625                        None
626                    }
627                })
628                .collect();
629            comp_fields.sort_by_key(|(c, _)| *c);
630            let comp_stmts: Vec<TokenStream2> = comp_fields
631                .iter()
632                .map(|(_, fi)| {
633                    let (ident, ty, _) = &field_data[*fi];
634                    emit_component_element(ident, ty)
635                })
636                .collect();
637            let q = quote! {
638                emitter.emit(::edifact_rs::EdifactEvent::Element { value: #qual })?;
639            };
640            (q, 1u32, quote! { #(#comp_stmts)* })
641        } else {
642            (quote! {}, 0u32, quote! {})
643        };
644
645        // Rebuild indexed/field_map excluding element=0 fields (handled above).
646        let regular_field_data: Vec<(u32, usize)> = field_data
647            .iter()
648            .enumerate()
649            .filter_map(|(i, (_, _, attrs))| {
650                let elem = attrs.element.unwrap_or(i as u32);
651                if elem < start_slot {
652                    None
653                } else {
654                    Some((elem, i))
655                }
656            })
657            .collect();
658        let reg_max_idx = regular_field_data
659            .iter()
660            .map(|(e, _)| *e)
661            .max()
662            .unwrap_or(start_slot.saturating_sub(1));
663        let reg_field_map: std::collections::HashMap<u32, usize> =
664            regular_field_data.iter().copied().collect();
665
666        let mut elem_stmts: Vec<TokenStream2> = Vec::new();
667        for slot in start_slot..=reg_max_idx {
668            if let Some(&fi) = reg_field_map.get(&slot) {
669                let (ident, ty, attrs) = &field_data[fi];
670                if attrs.composite {
671                    elem_stmts.push(emit_composite_field(ident, ty));
672                } else {
673                    elem_stmts.push(emit_element(ident, ty));
674                }
675            } else {
676                // Gap: emit an empty element separator.
677                elem_stmts.push(quote! {
678                    emitter.emit(::edifact_rs::EdifactEvent::Element { value: "" })?;
679                });
680            }
681        }
682
683        quote! {
684            emitter.emit(::edifact_rs::EdifactEvent::StartSegment { tag: #seg_tag })?;
685            #qualifier_emit
686            #elem0_comp_stmts
687            #(#elem_stmts)*
688            emitter.emit(::edifact_rs::EdifactEvent::EndSegment)?;
689        }
690    } else {
691        // ── Message struct: delegate to each field ────────────────────────────
692        let stmts: Vec<TokenStream2> = field_data
693            .iter()
694            .map(|(ident, ty, attrs)| {
695                if attrs.group || is_vec_type(ty) {
696                    quote! {
697                        for __item in &self.#ident {
698                            ::edifact_rs::EdifactSerialize::edifact_serialize(__item, emitter)?;
699                        }
700                    }
701                } else {
702                    quote! {
703                        ::edifact_rs::EdifactSerialize::edifact_serialize(&self.#ident, emitter)?;
704                    }
705                }
706            })
707            .collect();
708        quote! { #(#stmts)* }
709    };
710
711    Ok(quote! {
712        impl ::edifact_rs::EdifactSerialize for #name {
713            fn edifact_serialize<__E: ::edifact_rs::EventEmitter>(
714                &self,
715                emitter: &mut __E,
716            ) -> ::core::result::Result<(), ::edifact_rs::EdifactError> {
717                #body
718                ::core::result::Result::Ok(())
719            }
720        }
721    })
722}
723
724/// Generate the token stream that emits field `ident` (of type `ty`) as one element.
725///
726/// For `String` and `&str` fields the value is emitted zero-copy via `.as_str()`
727/// (or directly).  All other types fall back to `ToString::to_string`.
728fn emit_element(ident: &syn::Ident, ty: &Type) -> TokenStream2 {
729    if is_option_type(ty) {
730        let inner_is_str = option_inner_type(ty).is_some_and(is_str_like);
731        if inner_is_str {
732            quote! {
733                match &self.#ident {
734                    ::core::option::Option::Some(__v) => {
735                        emitter.emit(::edifact_rs::EdifactEvent::Element { value: __v.as_str() })?;
736                    }
737                    ::core::option::Option::None => {
738                        emitter.emit(::edifact_rs::EdifactEvent::Element { value: "" })?;
739                    }
740                }
741            }
742        } else {
743            quote! {
744                match &self.#ident {
745                    ::core::option::Option::Some(__v) => {
746                        let __s = ::std::string::ToString::to_string(__v);
747                        emitter.emit(::edifact_rs::EdifactEvent::Element { value: &__s })?;
748                    }
749                    ::core::option::Option::None => {
750                        emitter.emit(::edifact_rs::EdifactEvent::Element { value: "" })?;
751                    }
752                }
753            }
754        }
755    } else if is_string_type(ty) {
756        quote! {
757            emitter.emit(::edifact_rs::EdifactEvent::Element { value: self.#ident.as_str() })?;
758        }
759    } else if is_str_ref_type(ty) {
760        quote! {
761            emitter.emit(::edifact_rs::EdifactEvent::Element { value: self.#ident })?;
762        }
763    } else {
764        quote! {
765            {
766                let __s = ::std::string::ToString::to_string(&self.#ident);
767                emitter.emit(::edifact_rs::EdifactEvent::Element { value: &__s })?;
768            }
769        }
770    }
771}
772
773/// Generate the token stream that emits field `ident` as a composite component (`ComponentElement`).
774///
775/// For `String` and `&str` fields the value is emitted zero-copy.
776fn emit_component_element(ident: &syn::Ident, ty: &Type) -> TokenStream2 {
777    if is_option_type(ty) {
778        let inner_is_str = option_inner_type(ty).is_some_and(is_str_like);
779        if inner_is_str {
780            quote! {
781                match &self.#ident {
782                    ::core::option::Option::Some(__v) => {
783                        emitter.emit(::edifact_rs::EdifactEvent::ComponentElement { value: __v.as_str() })?;
784                    }
785                    ::core::option::Option::None => {
786                        emitter.emit(::edifact_rs::EdifactEvent::ComponentElement { value: "" })?;
787                    }
788                }
789            }
790        } else {
791            quote! {
792                match &self.#ident {
793                    ::core::option::Option::Some(__v) => {
794                        let __s = ::std::string::ToString::to_string(__v);
795                        emitter.emit(::edifact_rs::EdifactEvent::ComponentElement { value: &__s })?;
796                    }
797                    ::core::option::Option::None => {
798                        emitter.emit(::edifact_rs::EdifactEvent::ComponentElement { value: "" })?;
799                    }
800                }
801            }
802        }
803    } else if is_string_type(ty) {
804        quote! {
805            emitter.emit(::edifact_rs::EdifactEvent::ComponentElement { value: self.#ident.as_str() })?;
806        }
807    } else if is_str_ref_type(ty) {
808        quote! {
809            emitter.emit(::edifact_rs::EdifactEvent::ComponentElement { value: self.#ident })?;
810        }
811    } else {
812        quote! {
813            {
814                let __s = ::std::string::ToString::to_string(&self.#ident);
815                emitter.emit(::edifact_rs::EdifactEvent::ComponentElement { value: &__s })?;
816            }
817        }
818    }
819}
820
821/// Generate the token stream that emits a full composite field via `EdifactCompositeSerialize`.
822fn emit_composite_field(ident: &syn::Ident, ty: &Type) -> TokenStream2 {
823    if is_option_type(ty) {
824        quote! {
825            match &self.#ident {
826                ::core::option::Option::Some(__v) => {
827                    ::edifact_rs::EdifactCompositeSerialize::edifact_serialize_composite(__v, emitter)?;
828                }
829                ::core::option::Option::None => {
830                    emitter.emit(::edifact_rs::EdifactEvent::Element { value: "" })?;
831                }
832            }
833        }
834    } else {
835        quote! {
836            ::edifact_rs::EdifactCompositeSerialize::edifact_serialize_composite(&self.#ident, emitter)?;
837        }
838    }
839}
840
841// ── EdifactDeserialize ─────────────────────────────────────────────────────────
842
843fn impl_deserialize(input: &DeriveInput) -> syn::Result<TokenStream2> {
844    let name = &input.ident;
845    let struct_attrs = parse_struct_attrs(input)?;
846    let fields = get_named_fields(input)?;
847    let is_segment_struct = struct_attrs.segment.is_some();
848
849    let field_data: Vec<(&syn::Ident, &Type, FieldAttrs)> = fields
850        .named
851        .iter()
852        .map(|f| {
853            let attrs = parse_field_attrs(f)?;
854            let ident = f
855                .ident
856                .as_ref()
857                .ok_or_else(|| syn::Error::new_spanned(f, "only named fields are supported"))?;
858            validate_field_attrs(ident, &f.ty, &attrs, is_segment_struct)?;
859            Ok((ident, &f.ty, attrs))
860        })
861        .collect::<syn::Result<_>>()?;
862    check_duplicate_slots(&field_data, is_segment_struct)?;
863
864    let field_names: Vec<&syn::Ident> = field_data.iter().map(|(id, _, _)| *id).collect();
865
866    let (body, owned_body, segment_tag_impl) = if let Some(seg_tag) = &struct_attrs.segment {
867        // ── Segment struct ────────────────────────────────────────────────────
868        let qualifier_guard = if let Some(qual) = &struct_attrs.qualifier {
869            quote! {
870                if __seg.element_str(0).unwrap_or("") != #qual {
871                    return ::core::result::Result::Err(
872                        ::edifact_rs::EdifactError::MissingRequiredElement {
873                            tag: #seg_tag.to_owned(),
874                            element_index: 0,
875                        }
876                    );
877                }
878            }
879        } else if let Some(idx) = struct_attrs.qualifier_from {
880            quote! {
881                // Fully-qualified patterns: a user type named `Some`/`None` in
882                // scope (e.g. `pub use MyOpt::*`) would otherwise shadow the
883                // std variants and break the generated code.
884                match __seg.element_str(#idx as usize) {
885                    ::core::option::Option::None => return ::core::result::Result::Err(
886                        ::edifact_rs::EdifactError::MissingRequiredElement {
887                            tag: #seg_tag.to_owned(),
888                            element_index: #idx as usize,
889                        }
890                    ),
891                    ::core::option::Option::Some("") => return ::core::result::Result::Err(
892                        ::edifact_rs::EdifactError::InvalidFieldValue {
893                            tag: #seg_tag.to_owned(),
894                            element_index: #idx as usize,
895                            value: ::std::string::String::new(),
896                        }
897                    ),
898                    ::core::option::Option::Some(__qual_val) => { let _ = __qual_val; }
899                }
900            }
901        } else {
902            quote! {}
903        };
904
905        let find_seg = if let Some(qual) = &struct_attrs.qualifier {
906            quote! {
907                ::edifact_rs::find_qualified_segment(segments, #seg_tag, #qual)
908            }
909        } else {
910            quote! {
911                ::edifact_rs::find_segment(segments, #seg_tag)
912            }
913        };
914
915        let field_inits: Vec<TokenStream2> = field_data
916            .iter()
917            .enumerate()
918            .map(|(decl_i, (ident, ty, attrs))| -> syn::Result<TokenStream2> {
919                let idx = attrs.element.unwrap_or(decl_i as u32) as usize;
920                if attrs.composite {
921                    if is_option_type(ty) {
922                        let inner_ty = option_inner_type(ty)
923                            .ok_or_else(|| syn::Error::new(ident.span(), "expected Option<T>"))?;
924                        return Ok(quote! {
925                            let #ident = match ::edifact_rs::composite_element(__seg, #idx) {
926                                ::core::option::Option::Some(__composite) => {
927                                    ::core::option::Option::Some(
928                                        <#inner_ty as ::edifact_rs::EdifactCompositeDeserialize>::edifact_deserialize_composite(__composite)?
929                                    )
930                                }
931                                ::core::option::Option::None => ::core::option::Option::None,
932                            };
933                        });
934                    }
935                    return Ok(quote! {
936                        let #ident = <#ty as ::edifact_rs::EdifactCompositeDeserialize>::edifact_deserialize_composite(
937                            ::edifact_rs::composite_element(__seg, #idx).ok_or_else(|| ::edifact_rs::EdifactError::MissingRequiredElement {
938                                tag: #seg_tag.to_owned(),
939                                element_index: #idx as usize,
940                            })?
941                        )?;
942                    });
943                }
944                let component_idx: Option<usize> = attrs.component.map(|c| c as usize);
945                let value_expr = if let Some(comp) = component_idx {
946                    quote! {
947                        __seg.get_element(#idx).and_then(|__e| __e.get_component(#comp))
948                    }
949                } else {
950                    quote! { __seg.element_str(#idx) }
951                };
952                // Build the correct "missing required" error depending on whether the
953                // field targets a component within an element or a whole element.
954                let missing_required_err = if let Some(comp) = component_idx {
955                    quote! {
956                        ::edifact_rs::EdifactError::MissingRequiredComponent {
957                            tag: #seg_tag.to_owned(),
958                            element_index: #idx as usize,
959                            component_index: #comp as usize,
960                        }
961                    }
962                } else {
963                    quote! {
964                        ::edifact_rs::EdifactError::MissingRequiredElement {
965                            tag: #seg_tag.to_owned(),
966                            element_index: #idx as usize,
967                        }
968                    }
969                };
970                Ok(if is_option_type(ty) {
971                    let inner_ty = option_inner_type(ty);
972                    let inner_is_str = inner_ty.is_some_and(is_str_like);
973                    if attrs.required {
974                        // #[edifact(required)] on Option<T>: treat absence as an error.
975                        // Emits MissingRequiredComponent when combined with component = N,
976                        // MissingRequiredElement otherwise.
977                        if inner_is_str {
978                            quote! {
979                                let #ident = ::core::option::Option::Some(
980                                    #value_expr
981                                        .filter(|__s| !__s.is_empty())
982                                        .ok_or_else(|| #missing_required_err)?
983                                        .to_owned()
984                                );
985                            }
986                        } else {
987                            let inner_ty = inner_ty
988                                .ok_or_else(|| syn::Error::new(ident.span(), "expected Option<T>"))?;
989                            quote! {
990                                let #ident = ::core::option::Option::Some(
991                                    #value_expr
992                                        .filter(|__s| !__s.is_empty())
993                                        .ok_or_else(|| #missing_required_err)?
994                                        .parse::<#inner_ty>()
995                                        .map_err(|_| ::edifact_rs::EdifactError::InvalidText { offset: __seg.span.start })?
996                                );
997                            }
998                        }
999                    } else if inner_is_str {
1000                        quote! {
1001                            let #ident = #value_expr
1002                                .filter(|__s| !__s.is_empty())
1003                                .map(::std::string::String::from);
1004                        }
1005                    } else {
1006                        let inner_ty = inner_ty
1007                            .ok_or_else(|| syn::Error::new(ident.span(), "expected Option<T>"))?;
1008                        quote! {
1009                            let #ident = #value_expr
1010                                .filter(|__s| !__s.is_empty())
1011                                .map(|__s| __s.parse::<#inner_ty>()
1012                                    .map_err(|_| ::edifact_rs::EdifactError::InvalidText { offset: __seg.span.start })
1013                                )
1014                                .transpose()?;
1015                        }
1016                    }
1017                } else if is_str_like(ty) {
1018                    quote! {
1019                        let #ident = #value_expr
1020                            .filter(|__s| !__s.is_empty())
1021                            .ok_or_else(|| ::edifact_rs::EdifactError::MissingRequiredElement {
1022                                tag: #seg_tag.to_owned(),
1023                                element_index: #idx as usize,
1024                            })?
1025                            .to_owned();
1026                    }
1027                } else {
1028                    quote! {
1029                        let #ident = #value_expr
1030                            .filter(|__s| !__s.is_empty())
1031                            .ok_or_else(|| ::edifact_rs::EdifactError::MissingRequiredElement {
1032                                tag: #seg_tag.to_owned(),
1033                                element_index: #idx as usize,
1034                            })?
1035                            .parse::<#ty>()
1036                            .map_err(|_| ::edifact_rs::EdifactError::InvalidText { offset: __seg.span.start })?;
1037                    }
1038                })
1039            })
1040            .collect::<syn::Result<_>>()?;
1041
1042        let body = quote! {
1043            let __seg = #find_seg
1044                .ok_or_else(|| ::edifact_rs::EdifactError::MissingSegment {
1045                    tag: #seg_tag.to_owned(),
1046                    expected_position: "message body".to_owned(),
1047                })?;
1048            #qualifier_guard
1049            #(#field_inits)*
1050            ::core::result::Result::Ok(Self { #(#field_names),* })
1051        };
1052
1053        // Also generate EdifactSegmentTag impl.
1054        // Declare the qualifier through `QUALIFIER_PATTERN` rather than by
1055        // hand-rolling `matches_segment`.  Overriding only the borrowed matcher
1056        // left `matches_owned_segment` (which consults `QUALIFIER_PATTERN`)
1057        // matching on tag alone, so the owned deserialization path picked up
1058        // wrongly-qualified segments and then failed to parse them.
1059        let qualifier_match = if let Some(qual) = &struct_attrs.qualifier {
1060            quote! {
1061                const QUALIFIER_PATTERN: ::core::option::Option<&'static str> =
1062                    ::core::option::Option::Some(#qual);
1063            }
1064        } else if let Some(idx) = struct_attrs.qualifier_from {
1065            // "Any non-empty value at element `idx`" cannot be expressed as a
1066            // `QUALIFIER_PATTERN` (which is element 0 only), so both matchers are
1067            // overridden explicitly and must stay in agreement.
1068            quote! {
1069                fn matches_segment(seg: &::edifact_rs::Segment<'_>) -> bool {
1070                    seg.tag == Self::SEGMENT_TAG
1071                        && !seg.element_str(#idx as usize).unwrap_or("").is_empty()
1072                }
1073
1074                fn matches_owned_segment(seg: &::edifact_rs::OwnedSegment) -> bool {
1075                    seg.tag == Self::SEGMENT_TAG
1076                        && !seg
1077                            .elements
1078                            .get(#idx as usize)
1079                            .and_then(|e| e.components.first())
1080                            .map(|(c, _)| c.as_str())
1081                            .unwrap_or("")
1082                            .is_empty()
1083                }
1084            }
1085        } else {
1086            quote! {}
1087        };
1088
1089        let seg_tag_impl = quote! {
1090            impl ::edifact_rs::EdifactSegmentTag for #name {
1091                const SEGMENT_TAG: &'static str = #seg_tag;
1092                #qualifier_match
1093            }
1094        };
1095
1096        // ── Owned-segment deserialization path ────────────────────────────────
1097        // Works directly on `&[OwnedSegment]` without allocating a `Vec<Segment>`.
1098        let find_seg_owned = if let Some(qual) = &struct_attrs.qualifier {
1099            quote! {
1100                ::edifact_rs::find_qualified_segment_owned(segments, #seg_tag, #qual)
1101            }
1102        } else {
1103            quote! {
1104                ::edifact_rs::find_segment_owned(segments, #seg_tag)
1105            }
1106        };
1107
1108        let field_inits_owned: Vec<TokenStream2> = field_data
1109            .iter()
1110            .enumerate()
1111            .map(|(decl_i, (ident, ty, attrs))| -> syn::Result<TokenStream2> {
1112                let idx = attrs.element.unwrap_or(decl_i as u32) as usize;
1113                if attrs.composite {
1114                    if is_option_type(ty) {
1115                        let inner_ty = option_inner_type(ty)
1116                            .ok_or_else(|| syn::Error::new(ident.span(), "expected Option<T>"))?;
1117                        return Ok(quote! {
1118                            let #ident = match __seg.elements.get(#idx) {
1119                                ::core::option::Option::Some(__e) => {
1120                                    let __cows = __e.components.iter()
1121                                        .map(|(s, _)| ::std::borrow::Cow::Borrowed(s.as_str()))
1122                                        .collect::<::std::vec::Vec<::std::borrow::Cow<'_, str>>>();
1123                                    ::core::option::Option::Some(
1124                                        <#inner_ty as ::edifact_rs::EdifactCompositeDeserialize>::edifact_deserialize_composite(
1125                                            ::edifact_rs::CompositeElement::from_slice(&__cows)
1126                                        )?
1127                                    )
1128                                }
1129                                ::core::option::Option::None => ::core::option::Option::None,
1130                            };
1131                        });
1132                    }
1133                    return Ok(quote! {
1134                        let #ident = {
1135                            let __cows = __seg.elements.get(#idx)
1136                                .ok_or_else(|| ::edifact_rs::EdifactError::MissingRequiredElement {
1137                                    tag: #seg_tag.to_owned(),
1138                                    element_index: #idx as usize,
1139                                })?
1140                                .components.iter()
1141                                .map(|(s, _)| ::std::borrow::Cow::Borrowed(s.as_str()))
1142                                .collect::<::std::vec::Vec<::std::borrow::Cow<'_, str>>>();
1143                            <#ty as ::edifact_rs::EdifactCompositeDeserialize>::edifact_deserialize_composite(
1144                                ::edifact_rs::CompositeElement::from_slice(&__cows)
1145                            )?
1146                        };
1147                    });
1148                }
1149                let component_idx_owned: Option<usize> = attrs.component.map(|c| c as usize);
1150                let value_expr_owned = if let Some(comp) = component_idx_owned {
1151                    quote! { __seg.component_str(#idx, #comp) }
1152                } else {
1153                    quote! { __seg.element_str(#idx) }
1154                };
1155                // Build the correct "missing required" error for the owned path.
1156                let missing_required_err_owned = if let Some(comp) = component_idx_owned {
1157                    quote! {
1158                        ::edifact_rs::EdifactError::MissingRequiredComponent {
1159                            tag: #seg_tag.to_owned(),
1160                            element_index: #idx as usize,
1161                            component_index: #comp as usize,
1162                        }
1163                    }
1164                } else {
1165                    quote! {
1166                        ::edifact_rs::EdifactError::MissingRequiredElement {
1167                            tag: #seg_tag.to_owned(),
1168                            element_index: #idx as usize,
1169                        }
1170                    }
1171                };
1172                Ok(if is_option_type(ty) {
1173                    if let Some(inner_ty) = option_inner_type(ty) {
1174                        if attrs.required {
1175                            // #[edifact(required)] on Option<T>: absence is an error.
1176                            // Emits MissingRequiredComponent when combined with component = N,
1177                            // MissingRequiredElement otherwise.
1178                            if is_str_like(inner_ty) {
1179                                quote! {
1180                                    let #ident = ::core::option::Option::Some(
1181                                        #value_expr_owned
1182                                            .filter(|__s| !__s.is_empty())
1183                                            .ok_or_else(|| #missing_required_err_owned)?
1184                                            .to_owned()
1185                                    );
1186                                }
1187                            } else {
1188                                quote! {
1189                                    let #ident = ::core::option::Option::Some(
1190                                        #value_expr_owned
1191                                            .filter(|__s| !__s.is_empty())
1192                                            .ok_or_else(|| #missing_required_err_owned)?
1193                                            .parse::<#inner_ty>()
1194                                            .map_err(|_| ::edifact_rs::EdifactError::InvalidText { offset: __seg.span.start })?
1195                                    );
1196                                }
1197                            }
1198                        } else if is_str_like(inner_ty) {
1199                            quote! {
1200                                let #ident = #value_expr_owned
1201                                    .filter(|__s| !__s.is_empty())
1202                                    .map(::std::string::String::from);
1203                            }
1204                        } else {
1205                            quote! {
1206                                let #ident = #value_expr_owned
1207                                    .filter(|__s| !__s.is_empty())
1208                                    .map(|__s| __s.parse::<#inner_ty>()
1209                                        .map_err(|_| ::edifact_rs::EdifactError::InvalidText { offset: __seg.span.start })
1210                                    )
1211                                    .transpose()?;
1212                            }
1213                        }
1214                    } else {
1215                        // Fallback: treat as String (should not happen with well-formed types).
1216                        quote! {
1217                            let #ident = #value_expr_owned
1218                                .filter(|__s| !__s.is_empty())
1219                                .map(::std::string::String::from);
1220                        }
1221                    }
1222                } else if is_str_like(ty) {
1223                    quote! {
1224                        let #ident = #value_expr_owned
1225                            .filter(|__s| !__s.is_empty())
1226                            .ok_or_else(|| ::edifact_rs::EdifactError::MissingRequiredElement {
1227                                tag: #seg_tag.to_owned(),
1228                                element_index: #idx as usize,
1229                            })?
1230                            .to_owned();
1231                    }
1232                } else {
1233                    quote! {
1234                        let #ident = #value_expr_owned
1235                            .filter(|__s| !__s.is_empty())
1236                            .ok_or_else(|| ::edifact_rs::EdifactError::MissingRequiredElement {
1237                                tag: #seg_tag.to_owned(),
1238                                element_index: #idx as usize,
1239                            })?
1240                            .parse::<#ty>()
1241                            .map_err(|_| ::edifact_rs::EdifactError::InvalidText { offset: __seg.span.start })?;
1242                    }
1243                })
1244            })
1245            .collect::<syn::Result<_>>()?;
1246
1247        let owned_body = quote! {
1248            let __seg = #find_seg_owned
1249                .ok_or_else(|| ::edifact_rs::EdifactError::MissingSegment {
1250                    tag: #seg_tag.to_owned(),
1251                    expected_position: "message body".to_owned(),
1252                })?;
1253            #qualifier_guard
1254            #(#field_inits_owned)*
1255            ::core::result::Result::Ok(Self { #(#field_names),* })
1256        };
1257
1258        (body, owned_body, seg_tag_impl)
1259    } else {
1260        // ── Message struct: delegate to each field ────────────────────────────
1261        let field_inits: Vec<TokenStream2> = field_data
1262            .iter()
1263            .map(|(ident, ty, attrs)| -> syn::Result<TokenStream2> {
1264                Ok(if let Some(qual) = &attrs.qualifier {
1265                    if attrs.group || is_vec_type(ty) {
1266                        let inner_ty = vec_inner_type(ty)
1267                            .ok_or_else(|| syn::Error::new(ident.span(), "expected Vec<T>"))?;
1268                        quote! {
1269                            let #ident = segments
1270                                .iter()
1271                                .filter(|__seg| {
1272                                    __seg.tag == <#inner_ty as ::edifact_rs::EdifactSegmentTag>::SEGMENT_TAG
1273                                        && __seg.element_str(0).unwrap_or("") == #qual
1274                                })
1275                                .map(|__seg| {
1276                                    ::edifact_rs::EdifactDeserialize::edifact_deserialize(
1277                                        ::core::slice::from_ref(__seg),
1278                                    )
1279                                })
1280                                .collect::<::core::result::Result<::std::vec::Vec<#inner_ty>, ::edifact_rs::EdifactError>>()?;
1281                        }
1282                    } else if is_option_type(ty) {
1283                        let inner_ty = option_inner_type(ty)
1284                            .ok_or_else(|| syn::Error::new(ident.span(), "expected Option<T>"))?;
1285                        quote! {
1286                            let #ident = match ::edifact_rs::find_qualified_segment(
1287                                segments,
1288                                <#inner_ty as ::edifact_rs::EdifactSegmentTag>::SEGMENT_TAG,
1289                                #qual,
1290                            ) {
1291                                ::core::option::Option::Some(__seg) => {
1292                                    ::core::option::Option::Some(
1293                                        ::edifact_rs::EdifactDeserialize::edifact_deserialize(
1294                                            ::core::slice::from_ref(__seg),
1295                                        )?
1296                                    )
1297                                }
1298                                ::core::option::Option::None => ::core::option::Option::None,
1299                            };
1300                        }
1301                    } else {
1302                        quote! {
1303                            let __seg = ::edifact_rs::find_qualified_segment(
1304                                segments,
1305                                <#ty as ::edifact_rs::EdifactSegmentTag>::SEGMENT_TAG,
1306                                #qual,
1307                            )
1308                            .ok_or_else(|| ::edifact_rs::EdifactError::MissingSegment {
1309                                tag: <#ty as ::edifact_rs::EdifactSegmentTag>::SEGMENT_TAG.to_owned(),
1310                                expected_position: "message body".to_owned(),
1311                            })?;
1312                            let #ident = ::edifact_rs::EdifactDeserialize::edifact_deserialize(
1313                                ::core::slice::from_ref(__seg),
1314                            )?;
1315                        }
1316                    }
1317                } else if attrs.group || is_vec_type(ty) {
1318                    let inner_ty = vec_inner_type(ty)
1319                        .ok_or_else(|| syn::Error::new(ident.span(), "expected Vec<T>"))?;
1320                    quote! {
1321                        let #ident = ::edifact_rs::find_segments_typed::<#inner_ty>(segments)
1322                            .map(|__seg| {
1323                                <#inner_ty as ::edifact_rs::EdifactDeserialize>::edifact_deserialize(
1324                                    ::core::slice::from_ref(__seg),
1325                                )
1326                            })
1327                            .collect::<::core::result::Result<::std::vec::Vec<#inner_ty>, _>>()?;
1328                    }
1329                } else if is_option_type(ty) {
1330                    let inner_ty = option_inner_type(ty)
1331                        .ok_or_else(|| syn::Error::new(ident.span(), "expected Option<T>"))?;
1332                    quote! {
1333                        let #ident = if segments
1334                            .iter()
1335                            .any(|__seg| __seg.tag == <#inner_ty as ::edifact_rs::EdifactSegmentTag>::SEGMENT_TAG)
1336                        {
1337                            ::core::option::Option::Some(
1338                                <#inner_ty as ::edifact_rs::EdifactDeserialize>::edifact_deserialize(segments)?
1339                            )
1340                        } else {
1341                            ::core::option::Option::None
1342                        };
1343                    }
1344                } else {
1345                    quote! {
1346                        let #ident = ::edifact_rs::EdifactDeserialize::edifact_deserialize(segments)?;
1347                    }
1348                })
1349            })
1350            .collect::<syn::Result<_>>()?;
1351
1352        let body = quote! {
1353            #(#field_inits)*
1354            ::core::result::Result::Ok(Self { #(#field_names),* })
1355        };
1356
1357        // ── Owned-segment message deserialization path ────────────────────────
1358        // Works directly on `&[OwnedSegment]` without converting to `Vec<Segment>`.
1359        let field_inits_owned: Vec<TokenStream2> = field_data
1360            .iter()
1361            .map(|(ident, ty, attrs)| -> syn::Result<TokenStream2> {
1362                Ok(if let Some(qual) = &attrs.qualifier {
1363                    if attrs.group || is_vec_type(ty) {
1364                        let inner_ty = vec_inner_type(ty)
1365                            .ok_or_else(|| syn::Error::new(ident.span(), "expected Vec<T>"))?;
1366                        quote! {
1367                            let #ident = segments
1368                                .iter()
1369                                .filter(|__seg| {
1370                                    __seg.tag == <#inner_ty as ::edifact_rs::EdifactSegmentTag>::SEGMENT_TAG
1371                                        && __seg.element_str(0).unwrap_or("") == #qual
1372                                })
1373                                .map(|__seg| {
1374                                    <#inner_ty as ::edifact_rs::EdifactDeserialize>::edifact_deserialize_owned(
1375                                        ::core::slice::from_ref(__seg),
1376                                    )
1377                                })
1378                                .collect::<::core::result::Result<::std::vec::Vec<#inner_ty>, ::edifact_rs::EdifactError>>()?;
1379                        }
1380                    } else if is_option_type(ty) {
1381                        let inner_ty = option_inner_type(ty)
1382                            .ok_or_else(|| syn::Error::new(ident.span(), "expected Option<T>"))?;
1383                        quote! {
1384                            let #ident = match ::edifact_rs::find_qualified_segment_owned(
1385                                segments,
1386                                <#inner_ty as ::edifact_rs::EdifactSegmentTag>::SEGMENT_TAG,
1387                                #qual,
1388                            ) {
1389                                ::core::option::Option::Some(__seg) => {
1390                                    ::core::option::Option::Some(
1391                                        <#inner_ty as ::edifact_rs::EdifactDeserialize>::edifact_deserialize_owned(
1392                                            ::core::slice::from_ref(__seg),
1393                                        )?
1394                                    )
1395                                }
1396                                ::core::option::Option::None => ::core::option::Option::None,
1397                            };
1398                        }
1399                    } else {
1400                        quote! {
1401                            let __seg = ::edifact_rs::find_qualified_segment_owned(
1402                                segments,
1403                                <#ty as ::edifact_rs::EdifactSegmentTag>::SEGMENT_TAG,
1404                                #qual,
1405                            )
1406                            .ok_or_else(|| ::edifact_rs::EdifactError::MissingSegment {
1407                                tag: <#ty as ::edifact_rs::EdifactSegmentTag>::SEGMENT_TAG.to_owned(),
1408                                expected_position: "message body".to_owned(),
1409                            })?;
1410                            let #ident = <#ty as ::edifact_rs::EdifactDeserialize>::edifact_deserialize_owned(
1411                                ::core::slice::from_ref(__seg),
1412                            )?;
1413                        }
1414                    }
1415                } else if attrs.group || is_vec_type(ty) {
1416                    let inner_ty = vec_inner_type(ty)
1417                        .ok_or_else(|| syn::Error::new(ident.span(), "expected Vec<T>"))?;
1418                    quote! {
1419                        let #ident = segments
1420                            .iter()
1421                            .filter(|__seg| <#inner_ty as ::edifact_rs::EdifactSegmentTag>::matches_owned_segment(__seg))
1422                            .map(|__seg| {
1423                                <#inner_ty as ::edifact_rs::EdifactDeserialize>::edifact_deserialize_owned(
1424                                    ::core::slice::from_ref(__seg),
1425                                )
1426                            })
1427                            .collect::<::core::result::Result<::std::vec::Vec<#inner_ty>, _>>()?;
1428                    }
1429                } else if is_option_type(ty) {
1430                    let inner_ty = option_inner_type(ty)
1431                        .ok_or_else(|| syn::Error::new(ident.span(), "expected Option<T>"))?;
1432                    quote! {
1433                        let #ident = if segments
1434                            .iter()
1435                            .any(|__seg| __seg.tag == <#inner_ty as ::edifact_rs::EdifactSegmentTag>::SEGMENT_TAG)
1436                        {
1437                            ::core::option::Option::Some(
1438                                <#inner_ty as ::edifact_rs::EdifactDeserialize>::edifact_deserialize_owned(segments)?
1439                            )
1440                        } else {
1441                            ::core::option::Option::None
1442                        };
1443                    }
1444                } else {
1445                    quote! {
1446                        let #ident = <#ty as ::edifact_rs::EdifactDeserialize>::edifact_deserialize_owned(segments)?;
1447                    }
1448                })
1449            })
1450            .collect::<syn::Result<_>>()?;
1451
1452        let owned_body = quote! {
1453            #(#field_inits_owned)*
1454            ::core::result::Result::Ok(Self { #(#field_names),* })
1455        };
1456
1457        (body, owned_body, quote! {})
1458    };
1459
1460    Ok(quote! {
1461        impl ::edifact_rs::EdifactDeserialize for #name {
1462            fn edifact_deserialize(
1463                segments: &[::edifact_rs::Segment<'_>],
1464            ) -> ::core::result::Result<Self, ::edifact_rs::EdifactError> {
1465                #body
1466            }
1467
1468            fn edifact_deserialize_owned(
1469                segments: &[::edifact_rs::OwnedSegment],
1470            ) -> ::core::result::Result<Self, ::edifact_rs::EdifactError> {
1471                #owned_body
1472            }
1473        }
1474        #segment_tag_impl
1475    })
1476}
1477
1478#[cfg(test)]
1479mod tests {
1480    /// Compile-fail / compile-pass suite for the derive macros.
1481    ///
1482    /// The blessed `.stderr` files capture rustc's exact diagnostic rendering,
1483    /// which changes between toolchain releases — so this suite is pinned to the
1484    /// MSRV toolchain and skipped elsewhere.  Set `EDIFACT_UI_TESTS=1` to run it
1485    /// (CI does so in the MSRV job; `scripts/check.sh` does so locally).
1486    ///
1487    /// Re-bless after intentional message changes with:
1488    ///   `EDIFACT_UI_TESTS=1 TRYBUILD=overwrite cargo test -p edifact-rs-derive`
1489    #[test]
1490    fn trybuild_ui() {
1491        if std::env::var_os("EDIFACT_UI_TESTS").is_none() {
1492            eprintln!(
1493                "skipping derive UI suite: set EDIFACT_UI_TESTS=1 to run it \
1494                 (expectations are pinned to the MSRV toolchain)"
1495            );
1496            return;
1497        }
1498        let t = trybuild::TestCases::new();
1499        t.pass("tests/ui/pass_*.rs");
1500        t.compile_fail("tests/ui/fail_*.rs");
1501    }
1502}