Skip to main content

edifact_rs/
group.rs

1//! Segment group tree model for structured EDIFACT message navigation.
2//!
3//! Provides a recursive group schema ([`GroupDef`]) and a segment-slice-to-tree
4//! function ([`group_segments_indexed`]) that partitions a flat segment slice into a
5//! [`SegmentGroupIndexed`] tree according to the schema.
6//!
7//! # Model overview
8//!
9//! Every UN/EDIFACT message type has a fixed set of **segment groups**: named,
10//! optionally-repeating sets of segments delimited by a specific *trigger*
11//! segment tag.  For example, ORDERS D.11A has an `SG1` group starting with
12//! `RFF`, an `SG2` group starting with `NAD`, and so on.
13//!
14//! This module provides lightweight, allocation-efficient types for defining
15//! and working with these groups without requiring message-type-specific
16//! generated code.
17//!
18//! # How a tag is resolved
19//!
20//! At every level the traversal asks, in this order:
21//!
22//! 1. **Does a child group at *this* level trigger on the tag?** If so, open it.
23//! 2. **Does a group further out trigger on it?** If so, close this group and
24//!    let the outer level handle it.
25//! 3. Otherwise the segment belongs directly to the group currently open.
26//!
27//! Step 1 comes first because the same trigger tag routinely appears at more
28//! than one level — `UTILMD` triggers both SG2 (message-level parties) and SG12
29//! (a Vorgang's parties, inside SG4) on `NAD`. A group ends at the first segment
30//! the current branch cannot consume, never at one an outer branch could also
31//! have consumed; the other order would make SG12 unreachable from any input.
32//!
33//! A group's own trigger is not among its children, so a repeated trigger
34//! reaches step 2 and opens the **next occurrence** rather than nesting.
35//!
36//! # What a group spans
37//!
38//! Grouping is driven purely by trigger tags, so a tag that triggers nothing —
39//! `UNT`, for one — is a direct segment of whichever group is open when it
40//! arrives. Pass the message **body** ([`MessageWindow::body`][crate::MessageWindow::body])
41//! rather than the full window when the trailing boundary matters.
42//!
43//! # Example
44//!
45//! ```rust,ignore
46//! use edifact_rs::group::{GroupDef, group_segments_indexed};
47//!
48//! static SG32: &[GroupDef] = &[GroupDef::new("SG32", "PRI")];
49//! static ORDERS_GROUPS: &[GroupDef] = &[
50//!     GroupDef::new("SG2", "NAD"),
51//!     GroupDef::with_children("SG7", "LIN", SG32),
52//! ];
53//!
54//! let root = group_segments_indexed(&segments, ORDERS_GROUPS, "ROOT");
55//! for child in &root.children {
56//!     let child_segs = &segments[child.total_span.clone()];
57//!     println!("{} #{}: {} segments", child.definition, child.occurrence_index, child_segs.len());
58//! }
59//! ```
60
61use crate::Segment;
62use smallvec::SmallVec;
63use std::ops::Range;
64
65// ── GroupDef ──────────────────────────────────────────────────────────────────
66
67/// Schema describing one segment group within an EDIFACT message.
68///
69/// The lifetime `'a` is what the schema's strings and nested slices borrow
70/// from.  A `const`/`static` table is `GroupDef<'static>` and costs no
71/// allocation; a schema deserialized from a MIG at startup borrows from an
72/// arena the caller owns.
73///
74/// # Returning a schema from a trait method
75///
76/// `static SCHEMA: &[GroupDef] = …` still compiles unchanged: in a `static`, the
77/// elided lifetime resolves to `'static`. In **return position on a method**, it
78/// does not — it binds to `&self`, so a trait method declared
79/// `fn schema(&self) -> &'static [GroupDef]` fails to compile. Name the inner
80/// lifetime explicitly there:
81///
82/// ```
83/// use edifact_rs::group::GroupDef;
84///
85/// static SCHEMA: &[GroupDef] = &[GroupDef::new("SG1", "RFF")]; // unchanged
86///
87/// trait MessageSchema {
88///     //                          ↓ both lifetimes named
89///     fn groups(&self) -> &'static [GroupDef<'static>];
90/// }
91///
92/// struct Orders;
93/// impl MessageSchema for Orders {
94///     fn groups(&self) -> &'static [GroupDef<'static>] {
95///         SCHEMA
96///     }
97/// }
98/// assert_eq!(Orders.groups()[0].name, "SG1");
99/// ```
100#[derive(Debug, Clone, Copy)]
101pub struct GroupDef<'a> {
102    /// Human-readable group name, e.g. `"SG2"`.
103    pub name: &'a str,
104    /// The segment tag whose appearance starts a new instance of this group.
105    pub trigger: &'a str,
106    /// Nested child groups within this group.
107    ///
108    /// See [the resolution rule][self#how-a-tag-is-resolved] for what happens
109    /// when a tag could open a child here *and* a group further out — the
110    /// nested definition wins, which is what makes a child group whose trigger
111    /// is shared with an outer group reachable at all.
112    pub children: &'a [GroupDef<'a>],
113}
114
115impl<'a> GroupDef<'a> {
116    /// A leaf group: `name` is opened by `trigger` and has no nested groups.
117    #[must_use]
118    pub const fn new(name: &'a str, trigger: &'a str) -> Self {
119        Self {
120            name,
121            trigger,
122            children: &[],
123        }
124    }
125
126    /// A group with nested child groups.
127    #[must_use]
128    pub const fn with_children(
129        name: &'a str,
130        trigger: &'a str,
131        children: &'a [GroupDef<'a>],
132    ) -> Self {
133        Self {
134            name,
135            trigger,
136            children,
137        }
138    }
139}
140
141// ── SegmentGroupIndexed ───────────────────────────────────────────────────────
142
143/// Zero-copy segment group tree.  Stores index ranges into the original flat
144/// segment slice rather than cloning each segment.
145///
146/// Produced by [`group_segments_indexed`].  To access the actual segments use
147/// the original `&[Segment<'a>]` together with [`total_span`]:
148///
149/// ```rust,ignore
150/// let indexed = group_segments_indexed(&segments, MY_SCHEMA, "ROOT");
151/// for child in &indexed.children {
152///     let child_segs = &segments[child.total_span.clone()];
153/// }
154/// ```
155///
156/// [`total_span`]: SegmentGroupIndexed::total_span
157#[derive(Debug)]
158pub struct SegmentGroupIndexed<'a> {
159    /// Group name from the schema, e.g. `"SG2"`, or the root name.
160    ///
161    /// Borrows from the schema, so it lives exactly as long as the schema does.
162    pub definition: &'a str,
163    /// Contiguous span `[start, end)` of absolute indices into the original flat
164    /// segment slice covering **all** segments in this group instance — trigger
165    /// segment, direct segments, and all descendant groups combined.
166    ///
167    /// Use this to slice the original `&[Segment<'_>]` to get every segment
168    /// belonging to this group:
169    ///
170    /// ```rust,ignore
171    /// let all_sg2_segs = &segments[sg2.total_span.clone()];
172    /// ```
173    ///
174    /// To iterate over only the segments that belong *directly* to this group
175    /// (excluding descendants), use [`direct_segment_indices`].
176    ///
177    /// [`direct_segment_indices`]: SegmentGroupIndexed::direct_segment_indices
178    pub total_span: Range<usize>,
179    /// Child group instances, in message order.
180    pub children: Vec<SegmentGroupIndexed<'a>>,
181    /// Zero-based occurrence index of this group instance among all siblings
182    /// with the same `definition` at this level.
183    ///
184    /// For example, the first `SG5` child at a given level has `occurrence_index = 0`,
185    /// the second `SG5` has `occurrence_index = 1`, etc.  Siblings with a
186    /// *different* definition have independent counters.
187    ///
188    /// This field is essential for producing unambiguous rule-violation IDs
189    /// (e.g. `"SG5[2]/DTM"`) when the same group type repeats.
190    pub occurrence_index: usize,
191}
192
193impl<'a> SegmentGroupIndexed<'a> {
194    /// Iterate over the absolute indices of segments that belong *directly* to
195    /// this group — i.e. those within [`total_span`] that are **not** covered
196    /// by any child group's [`total_span`].
197    ///
198    /// Complexity: `O(total_span.len() × children.len())`.  For typical EDIFACT
199    /// message structures (≤ 8 children per group) this is negligible.
200    ///
201    /// [`total_span`]: SegmentGroupIndexed::total_span
202    pub fn direct_segment_indices(&self) -> impl Iterator<Item = usize> + '_ {
203        self.total_span.clone().filter(|i| {
204            !self
205                .children
206                .iter()
207                .any(|child| child.total_span.contains(i))
208        })
209    }
210
211    /// The segments this group spans, resolved against the slice it was built
212    /// from.
213    ///
214    /// The tree stores index ranges rather than copies, so reading a group means
215    /// pairing it back with the slice it was built from.
216    ///
217    /// Returns an empty slice if `all` is shorter than that.
218    ///
219    /// # Example
220    ///
221    /// ```
222    /// use edifact_rs::group::{GroupDef, group_segments_indexed};
223    ///
224    /// static SCHEMA: &[GroupDef] = &[GroupDef::new("SG2", "NAD")];
225    ///
226    /// let segments: Vec<_> = edifact_rs::from_bytes(b"BGM+220'NAD+BY+1'DTM+137:1:102'")
227    ///     .collect::<Result<Vec<_>, _>>()?;
228    /// let tree = group_segments_indexed(&segments, SCHEMA, "ROOT");
229    ///
230    /// let sg2 = tree.children[0].segments(&segments);
231    /// assert_eq!(sg2.iter().map(edifact_rs::Segment::tag).collect::<Vec<_>>(), ["NAD", "DTM"]);
232    /// # Ok::<(), edifact_rs::EdifactError>(())
233    /// ```
234    #[must_use]
235    pub fn segments<'s, 'd>(&self, all: &'s [Segment<'d>]) -> &'s [Segment<'d>] {
236        all.get(self.total_span.clone()).unwrap_or(&[])
237    }
238
239    /// Every group in this subtree, **including this one**, in document order.
240    ///
241    /// Depth-first pre-order, so a parent is always yielded before its children
242    /// and siblings in the order they appear on the wire.
243    pub fn descendants(&self) -> Descendants<'_, 'a> {
244        Descendants { stack: vec![self] }
245    }
246
247    /// Every group in this subtree named `name`, in document order.
248    ///
249    /// Searches the whole subtree, so it finds groups that `children` alone
250    /// does not reach — a nested group can share a trigger with one further out
251    /// and sit at any depth.
252    ///
253    /// # Example
254    ///
255    /// ```
256    /// use edifact_rs::group::{GroupDef, group_segments_indexed};
257    ///
258    /// static SCHEMA: &[GroupDef] = &[
259    ///     GroupDef::new("SG2", "NAD"),
260    ///     GroupDef::with_children("SG4", "IDE", &[GroupDef::new("SG12", "NAD")]),
261    /// ];
262    ///
263    /// let segments: Vec<_> = edifact_rs::from_bytes(
264    ///     b"NAD+MS+SENDER'IDE+24+V1'NAD+Z09+KUNDE'IDE+24+V2'NAD+VY+PARTY'",
265    /// )
266    /// .collect::<Result<Vec<_>, _>>()?;
267    /// let tree = group_segments_indexed(&segments, SCHEMA, "ROOT");
268    ///
269    /// // Both Vorgänge's parties, however deep they sit.
270    /// let parties: Vec<&str> = tree
271    ///     .find("SG12")
272    ///     .filter_map(|group| group.segments(&segments).first())
273    ///     .filter_map(|nad| nad.element_str(0))
274    ///     .collect();
275    /// assert_eq!(parties, ["Z09", "VY"]);
276    /// # Ok::<(), edifact_rs::EdifactError>(())
277    /// ```
278    pub fn find<'q>(
279        &'q self,
280        name: &'q str,
281    ) -> impl Iterator<Item = &'q SegmentGroupIndexed<'a>> + 'q {
282        self.descendants().filter(move |g| g.definition == name)
283    }
284}
285
286/// Depth-first pre-order iterator over a [`SegmentGroupIndexed`] subtree.
287///
288/// Returned by [`SegmentGroupIndexed::descendants`].
289pub struct Descendants<'t, 'a> {
290    stack: Vec<&'t SegmentGroupIndexed<'a>>,
291}
292
293impl<'t, 'a> Iterator for Descendants<'t, 'a> {
294    type Item = &'t SegmentGroupIndexed<'a>;
295
296    fn next(&mut self) -> Option<Self::Item> {
297        let node = self.stack.pop()?;
298        // Pushed in reverse so siblings come back out in document order.
299        self.stack.extend(node.children.iter().rev());
300        Some(node)
301    }
302}
303
304/// Partition `segments` into a [`SegmentGroupIndexed`] tree without cloning.
305///
306/// Stores `Range<usize>` indices into the original flat slice rather than
307/// copying each [`Segment`] into the tree.  Use the original slice together
308/// with [`SegmentGroupIndexed::total_span`] to access segments.
309///
310/// # Worked Example
311///
312/// Consider a simplified 3-level multi-level schema:
313///
314/// ```rust
315/// use edifact_rs::group::{GroupDef, group_segments_indexed};
316/// use edifact_rs::from_bytes;
317///
318/// // Schema: ROOT → SG1 (trigger: RFF) → SG5 (trigger: LOC) → SG6 (trigger: QTY)
319/// static SG6: &[GroupDef] = &[GroupDef::new("SG6", "QTY")];
320/// static SCHEMA: &[GroupDef] = &[
321///     GroupDef::new("SG1", "RFF"),
322///     GroupDef::with_children("SG5", "LOC", SG6),
323/// ];
324///
325/// // A small multi-level message fragment (no envelope for clarity).
326/// let input = b"RFF+Z13:REF1'LOC+172+DE123'DTM+163:20230101:102'QTY+220:100:KWH'";
327/// let segments: Vec<_> = from_bytes(input)
328///     .collect::<Result<_, _>>()
329///     .unwrap();
330///
331/// let tree = group_segments_indexed(&segments, SCHEMA, "ROOT");
332///
333/// // The root contains no direct segments (all consumed by SG1 / SG5).
334/// assert!(tree.direct_segment_indices().next().is_none());
335///
336/// // One SG1 group and one SG5 group at root level.
337/// let sg1 = tree.children.iter().find(|g| g.definition == "SG1").unwrap();
338/// let sg5 = tree.children.iter().find(|g| g.definition == "SG5").unwrap();
339///
340/// // SG1 spans the RFF segment only.
341/// assert_eq!(&segments[sg1.total_span.clone()].iter().map(|s| s.tag()).collect::<Vec<_>>(),
342///            &["RFF"]);
343///
344/// // SG5 spans LOC + DTM + QTY (all three segments, including the SG6 child).
345/// let sg5_tags: Vec<_> = segments[sg5.total_span.clone()].iter().map(|s| s.tag()).collect();
346/// assert_eq!(sg5_tags, &["LOC", "DTM", "QTY"]);
347///
348/// // SG5's direct segments (LOC + DTM) exclude the SG6 child (QTY).
349/// let sg5_direct: Vec<_> = sg5.direct_segment_indices()
350///     .map(|i| segments[i].tag())
351///     .collect();
352/// assert_eq!(sg5_direct, &["LOC", "DTM"]);
353///
354/// // SG6 contains only QTY.
355/// let sg6 = sg5.children.iter().find(|g| g.definition == "SG6").unwrap();
356/// assert_eq!(segments[sg6.total_span.clone()].iter().map(|s| s.tag()).collect::<Vec<_>>(),
357///            &["QTY"]);
358/// ```
359///
360/// # Group validation
361///
362/// `group_segments_indexed` pairs naturally with
363/// [`ValidationContext::validate_grouped`][crate::ValidationContext::validate_grouped] to enforce group-presence rules:
364///
365/// ```rust,ignore
366/// use edifact_rs::{ProfileRulePack, ValidationContext};
367///
368/// let pack = ProfileRulePack::new("MY-PROFILE")
369///     .require_segment_in_group("SG5", "DTM", "SG5-DTM-M")
370///     .forbid_segment_in_group("SG1", "LOC", "SG1-LOC-F");
371/// let ctx = ValidationContext::builder().with_profile_pack(pack).build();
372///
373/// let tree = group_segments_indexed(&segments, SCHEMA, "ORDERS");
374/// let report = ctx.validate_grouped(&tree, &segments);
375/// ```
376///
377/// # What a group spans
378///
379/// Grouping is driven purely by trigger tags: a group runs from its trigger to
380/// the next trigger belonging to a sibling or ancestor, or to the end of the
381/// slice.  Nothing stops the final group at `UNT`, because the trailer is not a
382/// trigger of anything — pass the message *body* when the group boundaries
383/// matter, or accept that the trailer lands inside the last group.
384///
385/// # Complexity
386///
387/// `O(n × schema_depth)` time, `O(tree_nodes)` space.  No `Segment` clones.
388pub fn group_segments_indexed<'g>(
389    segments: &[Segment<'_>],
390    schema: &'g [GroupDef<'g>],
391    root_name: &'g str,
392) -> SegmentGroupIndexed<'g> {
393    let mut root = SegmentGroupIndexed {
394        definition: root_name,
395        total_span: 0..0,
396        children: Vec::new(),
397        occurrence_index: 0,
398    };
399    group_recursive_indexed(segments, &mut root, schema, &[], 0);
400    root
401}
402
403/// Internal recursive indexed grouping.  Returns the number of segments consumed.
404fn group_recursive_indexed<'g>(
405    segments: &[Segment<'_>],
406    parent: &mut SegmentGroupIndexed<'g>,
407    schema: &'g [GroupDef<'g>],
408    stop_triggers: &[&'g str],
409    offset: usize,
410) -> usize {
411    let combined_stop: SmallVec<[&'g str; 16]> = {
412        let mut v: SmallVec<[&'g str; 16]> = SmallVec::from_slice(stop_triggers);
413        for d in schema {
414            if !v.contains(&d.trigger) {
415                v.push(d.trigger);
416            }
417        }
418        v
419    };
420
421    // `span_start` is the absolute index of the first segment in this group.
422    // For child groups the caller pre-seeds `parent.total_span.start` with the
423    // trigger segment position; for the root (or any group with no pre-seeded
424    // trigger) we start at `offset`.
425    let span_start = if !parent.total_span.is_empty() {
426        parent.total_span.start // pre-seeded trigger position
427    } else {
428        offset
429    };
430
431    let mut i = 0;
432    // Track how many children of each definition have been pushed at this level,
433    // so we can stamp `occurrence_index` on each new child.
434    let mut occ_counts: std::collections::HashMap<&'g str, usize> =
435        std::collections::HashMap::new();
436    while i < segments.len() {
437        let tag = segments[i].tag();
438
439        // Children before the stop set: a group ends at the first segment the
440        // current branch *cannot* consume, not at one an outer branch could
441        // also have consumed.  Testing the stop set first would make any child
442        // whose trigger is shared with an outer group unreachable.
443        //
444        // A group's own trigger is not among its children, so a repeated
445        // trigger falls through to the stop check and the parent opens the next
446        // occurrence rather than nesting.
447        let matched = schema.iter().find(|d| d.trigger == tag);
448
449        if matched.is_none() && stop_triggers.iter().copied().any(|t| t == tag) {
450            break;
451        }
452
453        if let Some(def) = matched {
454            let child_offset = offset + i;
455            let occ_idx = {
456                let c = occ_counts.entry(def.name).or_insert(0);
457                let idx = *c;
458                *c += 1;
459                idx
460            };
461            let mut child = SegmentGroupIndexed {
462                definition: def.name,
463                // Pre-seed the trigger segment; the recursive call extends
464                // total_span to cover the full child subtree.
465                total_span: child_offset..child_offset + 1,
466                children: Vec::new(),
467                occurrence_index: occ_idx,
468            };
469            i += 1;
470
471            let consumed = group_recursive_indexed(
472                &segments[i..],
473                &mut child,
474                def.children,
475                &combined_stop,
476                offset + i,
477            );
478            i += consumed;
479
480            parent.children.push(child);
481        } else {
482            i += 1;
483        }
484    }
485
486    // Total span covers everything from the first segment (trigger or first
487    // direct segment) to the last segment consumed in this call.
488    parent.total_span = span_start..(offset + i);
489
490    i
491}
492
493#[cfg(test)]
494mod tests {
495    use super::*;
496    use crate::model::Element;
497
498    fn seg(tag: &'static str) -> Segment<'static> {
499        Segment::new(tag, vec![Element::of(&["x"])])
500    }
501
502    static SCHEMA: &[GroupDef] = &[
503        GroupDef {
504            name: "SG1",
505            trigger: "NAD",
506            children: &[GroupDef {
507                name: "SG2",
508                trigger: "CTA",
509                children: &[],
510            }],
511        },
512        GroupDef {
513            name: "SG3",
514            trigger: "LIN",
515            children: &[],
516        },
517    ];
518
519    /// A child group whose trigger also triggers a group at an ancestor level.
520    ///
521    /// `UTILMD` is the canonical case: SG2 carries the message-level parties,
522    /// SG12 the parties of one Vorgang inside SG4, and both trigger on `NAD`.
523    static NESTED_SAME_TRIGGER: &[GroupDef] = &[
524        GroupDef {
525            name: "SG2",
526            trigger: "NAD",
527            children: &[],
528        },
529        GroupDef {
530            name: "SG4",
531            trigger: "IDE",
532            children: &[GroupDef {
533                name: "SG12",
534                trigger: "NAD",
535                children: &[],
536            }],
537        },
538    ];
539
540    #[test]
541    fn a_nested_group_wins_over_an_ancestors_sibling_with_the_same_trigger() {
542        // UNH BGM NAD NAD IDE NAD DTM NAD — the message *body*, with no `UNT`.
543        // `UNT` triggers nothing, so it would land in whichever group ran last;
544        // see `MessageWindow::body`, which exists for exactly that reason.
545        let segs = vec![
546            seg("UNH"),
547            seg("BGM"),
548            seg("NAD"),
549            seg("NAD"),
550            seg("IDE"),
551            seg("NAD"),
552            seg("DTM"),
553            seg("NAD"),
554        ];
555        let tree = group_segments_indexed(&segs, NESTED_SAME_TRIGGER, "ROOT");
556
557        let top: Vec<&str> = tree.children.iter().map(|c| c.definition).collect();
558        assert_eq!(
559            top,
560            ["SG2", "SG2", "SG4"],
561            "the two message-level NADs are SG2"
562        );
563
564        let sg4 = tree
565            .children
566            .iter()
567            .find(|c| c.definition == "SG4")
568            .expect("SG4 opens on IDE");
569        // SG4 spans IDE through the last segment of its last child, not just IDE.
570        assert_eq!(sg4.total_span, 4..8);
571
572        let nested: Vec<&str> = sg4.children.iter().map(|c| c.definition).collect();
573        assert_eq!(
574            nested,
575            ["SG12", "SG12"],
576            "NAD inside SG4 nests as SG12 rather than reopening SG2",
577        );
578        assert_eq!(sg4.children[0].total_span, 5..7); // NAD + DTM
579        assert_eq!(sg4.children[1].total_span, 7..8); // NAD
580        assert_eq!(sg4.children[0].occurrence_index, 0);
581        assert_eq!(sg4.children[1].occurrence_index, 1);
582    }
583
584    #[test]
585    fn descendants_walk_the_whole_subtree_in_document_order() {
586        let segs = vec![
587            seg("NAD"),
588            seg("IDE"),
589            seg("NAD"),
590            seg("DTM"),
591            seg("IDE"),
592            seg("NAD"),
593        ];
594        let tree = group_segments_indexed(&segs, NESTED_SAME_TRIGGER, "ROOT");
595
596        let walked: Vec<&str> = tree.descendants().map(|g| g.definition).collect();
597        assert_eq!(
598            walked,
599            ["ROOT", "SG2", "SG4", "SG12", "SG4", "SG12"],
600            "pre-order: a parent before its children, siblings in wire order",
601        );
602
603        // `find` is the question a reader has: every SG12 anywhere, not just the
604        // ones one level down.
605        let sg12: Vec<usize> = tree.find("SG12").map(|g| g.total_span.start).collect();
606        assert_eq!(sg12, [2, 5]);
607
608        // …and resolving one back to its segments needs no manual slicing.
609        let first = tree.find("SG12").next().unwrap();
610        assert_eq!(
611            first
612                .segments(&segs)
613                .iter()
614                .map(Segment::tag)
615                .collect::<Vec<_>>(),
616            ["NAD", "DTM"],
617        );
618    }
619
620    #[test]
621    fn a_repeated_trigger_still_reopens_a_sibling_rather_than_nesting_forever() {
622        // A group's own trigger is not among its children, so the second NAD
623        // inside SG12 falls through to the stop set and SG4 opens a sibling —
624        // it does not nest SG12 inside SG12.
625        let segs = vec![seg("IDE"), seg("NAD"), seg("NAD"), seg("NAD")];
626        let tree = group_segments_indexed(&segs, NESTED_SAME_TRIGGER, "ROOT");
627
628        let sg4 = &tree.children[0];
629        assert_eq!(sg4.definition, "SG4");
630        assert_eq!(
631            sg4.children.len(),
632            3,
633            "three sibling SG12s, not one nest of three"
634        );
635        assert!(
636            sg4.children.iter().all(|c| c.children.is_empty()),
637            "SG12 has no children, so nothing may nest inside it",
638        );
639    }
640
641    #[test]
642    fn a_tag_no_nested_definition_accepts_still_closes_the_group() {
643        // `IDE` is not an SG4 child, so a second one closes SG4 and the root
644        // opens the next occurrence — the behaviour the stop set exists for.
645        let segs = vec![seg("IDE"), seg("NAD"), seg("IDE"), seg("NAD")];
646        let tree = group_segments_indexed(&segs, NESTED_SAME_TRIGGER, "ROOT");
647
648        let top: Vec<&str> = tree.children.iter().map(|c| c.definition).collect();
649        assert_eq!(top, ["SG4", "SG4"]);
650        assert_eq!(tree.children[0].total_span, 0..2);
651        assert_eq!(tree.children[1].total_span, 2..4);
652    }
653
654    #[test]
655    fn nesting_is_preferred_at_every_depth() {
656        // Three levels, all triggered by NAD: the deepest definition that can
657        // accept the tag is the one that gets it.
658        static DEEP: &[GroupDef] = &[
659            GroupDef {
660                name: "L1",
661                trigger: "NAD",
662                children: &[],
663            },
664            GroupDef {
665                name: "A",
666                trigger: "IDE",
667                children: &[GroupDef {
668                    name: "L2",
669                    trigger: "NAD",
670                    children: &[GroupDef {
671                        name: "L3",
672                        trigger: "CTA",
673                        children: &[],
674                    }],
675                }],
676            },
677        ];
678
679        let segs = vec![seg("NAD"), seg("IDE"), seg("NAD"), seg("CTA")];
680        let tree = group_segments_indexed(&segs, DEEP, "ROOT");
681
682        assert_eq!(tree.children[0].definition, "L1");
683        let a = &tree.children[1];
684        assert_eq!(a.definition, "A");
685        assert_eq!(a.children[0].definition, "L2");
686        assert_eq!(a.children[0].children[0].definition, "L3");
687    }
688
689    #[test]
690    fn root_segments_before_first_trigger() {
691        let segs = vec![seg("UNH"), seg("BGM"), seg("NAD")];
692        let tree = group_segments_indexed(&segs, SCHEMA, "ROOT");
693        // UNH (0) and BGM (1) are direct root segments; NAD (2) is in SG1.
694        let direct: Vec<_> = tree.direct_segment_indices().collect();
695        assert_eq!(direct, vec![0, 1], "UNH + BGM should be direct in root");
696        assert_eq!(tree.children.len(), 1);
697        assert_eq!(tree.children[0].definition, "SG1");
698    }
699
700    #[test]
701    fn repeated_trigger_creates_multiple_children() {
702        let segs = vec![seg("UNH"), seg("NAD"), seg("NAD"), seg("UNT")];
703        let tree = group_segments_indexed(&segs, SCHEMA, "ROOT");
704        // Two NAD triggers → two SG1 children
705        assert_eq!(
706            tree.children
707                .iter()
708                .filter(|c| c.definition == "SG1")
709                .count(),
710            2
711        );
712    }
713
714    #[test]
715    fn repeated_trigger_occurrence_index_is_stamped() {
716        let segs = vec![seg("NAD"), seg("NAD"), seg("NAD")];
717        let tree = group_segments_indexed(&segs, SCHEMA, "ROOT");
718        let indices: Vec<_> = tree.children.iter().map(|c| c.occurrence_index).collect();
719        assert_eq!(indices, vec![0, 1, 2]);
720    }
721
722    #[test]
723    fn nested_child_groups() {
724        let segs = vec![seg("NAD"), seg("CTA"), seg("CTA")];
725        let tree = group_segments_indexed(&segs, SCHEMA, "ROOT");
726        let sg1 = &tree.children[0];
727        assert_eq!(sg1.definition, "SG1");
728        // Two CTA triggers → two SG2 children inside SG1
729        assert_eq!(sg1.children.len(), 2);
730        assert!(sg1.children.iter().all(|c| c.definition == "SG2"));
731    }
732
733    #[test]
734    fn total_span_covers_all_segments() {
735        let segs = vec![seg("UNH"), seg("NAD"), seg("CTA")];
736        let tree = group_segments_indexed(&segs, SCHEMA, "ROOT");
737        // Root span covers all 3 segments
738        let all_tags: Vec<_> = segs[tree.total_span.clone()]
739            .iter()
740            .map(|s| s.tag())
741            .collect();
742        assert!(all_tags.contains(&"UNH"));
743        assert!(all_tags.contains(&"NAD"));
744        assert!(all_tags.contains(&"CTA"));
745    }
746}