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}