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//! # Example
19//!
20//! ```rust,ignore
21//! use edifact_rs::group::{GroupDef, group_segments_indexed};
22//!
23//! static SG32: &[GroupDef] = &[GroupDef::new("SG32", "PRI")];
24//! static ORDERS_GROUPS: &[GroupDef] = &[
25//!     GroupDef::new("SG2", "NAD"),
26//!     GroupDef::with_children("SG7", "LIN", SG32),
27//! ];
28//!
29//! let root = group_segments_indexed(&segments, ORDERS_GROUPS, "ROOT");
30//! for child in &root.children {
31//!     let child_segs = &segments[child.total_span.clone()];
32//!     println!("{} #{}: {} segments", child.definition, child.occurrence_index, child_segs.len());
33//! }
34//! ```
35
36use crate::{OwnedSegment, Segment};
37use smallvec::SmallVec;
38use std::ops::Range;
39
40// ── GroupDef ──────────────────────────────────────────────────────────────────
41
42/// Schema describing one segment group within an EDIFACT message.
43///
44/// The lifetime `'a` is what the schema's strings and nested slices borrow
45/// from.  A `const`/`static` table is `GroupDef<'static>` and costs no
46/// allocation; a schema deserialized from a MIG at startup borrows from an
47/// arena the caller owns.  Earlier releases hard-coded `&'static str`, which
48/// made runtime-loaded schemas impossible even though the directory side of the
49/// crate ([`OwnedSegmentDef`][crate::OwnedSegmentDef],
50/// [`DirectoryValidatorBuilder`][crate::DirectoryValidatorBuilder]) has always
51/// supported them.
52///
53/// # Returning a schema from a trait method
54///
55/// `static SCHEMA: &[GroupDef] = …` still compiles unchanged: in a `static`, the
56/// elided lifetime resolves to `'static`. In **return position on a method**, it
57/// does not — it binds to `&self`, so a trait method declared
58/// `fn schema(&self) -> &'static [GroupDef]` fails to compile. Name the inner
59/// lifetime explicitly there:
60///
61/// ```
62/// use edifact_rs::group::GroupDef;
63///
64/// static SCHEMA: &[GroupDef] = &[GroupDef::new("SG1", "RFF")]; // unchanged
65///
66/// trait MessageSchema {
67///     //                          ↓ both lifetimes named
68///     fn groups(&self) -> &'static [GroupDef<'static>];
69/// }
70///
71/// struct Orders;
72/// impl MessageSchema for Orders {
73///     fn groups(&self) -> &'static [GroupDef<'static>] {
74///         SCHEMA
75///     }
76/// }
77/// assert_eq!(Orders.groups()[0].name, "SG1");
78/// ```
79#[derive(Debug, Clone, Copy)]
80pub struct GroupDef<'a> {
81    /// Human-readable group name, e.g. `"SG2"`.
82    pub name: &'a str,
83    /// The segment tag whose appearance starts a new instance of this group.
84    pub trigger: &'a str,
85    /// Nested child groups within this group.
86    ///
87    /// The first trigger encountered among `children` ends the current child
88    /// and starts a new one; a trigger that matches a sibling or ancestor group
89    /// ends this group entirely.
90    pub children: &'a [GroupDef<'a>],
91}
92
93impl<'a> GroupDef<'a> {
94    /// A leaf group: `name` is opened by `trigger` and has no nested groups.
95    #[must_use]
96    pub const fn new(name: &'a str, trigger: &'a str) -> Self {
97        Self {
98            name,
99            trigger,
100            children: &[],
101        }
102    }
103
104    /// A group with nested child groups.
105    #[must_use]
106    pub const fn with_children(
107        name: &'a str,
108        trigger: &'a str,
109        children: &'a [GroupDef<'a>],
110    ) -> Self {
111        Self {
112            name,
113            trigger,
114            children,
115        }
116    }
117}
118
119// ── SegmentGroupIndexed ───────────────────────────────────────────────────────
120
121/// Zero-copy segment group tree.  Stores index ranges into the original flat
122/// segment slice rather than cloning each segment.
123///
124/// Produced by [`group_segments_indexed`].  To access the actual segments use
125/// the original `&[Segment<'a>]` together with [`total_span`]:
126///
127/// ```rust,ignore
128/// let indexed = group_segments_indexed(&segments, MY_SCHEMA, "ROOT");
129/// for child in &indexed.children {
130///     let child_segs = &segments[child.total_span.clone()];
131/// }
132/// ```
133///
134/// [`total_span`]: SegmentGroupIndexed::total_span
135#[derive(Debug)]
136pub struct SegmentGroupIndexed<'a> {
137    /// Group name from the schema, e.g. `"SG2"`, or the root name.
138    ///
139    /// Borrows from the schema, so it lives exactly as long as the schema does.
140    pub definition: &'a str,
141    /// Contiguous span `[start, end)` of absolute indices into the original flat
142    /// segment slice covering **all** segments in this group instance — trigger
143    /// segment, direct segments, and all descendant groups combined.
144    ///
145    /// Use this to slice the original `&[Segment<'_>]` to get every segment
146    /// belonging to this group:
147    ///
148    /// ```rust,ignore
149    /// let all_sg2_segs = &segments[sg2.total_span.clone()];
150    /// ```
151    ///
152    /// To iterate over only the segments that belong *directly* to this group
153    /// (excluding descendants), use [`direct_segment_indices`].
154    ///
155    /// [`direct_segment_indices`]: SegmentGroupIndexed::direct_segment_indices
156    pub total_span: Range<usize>,
157    /// Child group instances, in message order.
158    pub children: Vec<SegmentGroupIndexed<'a>>,
159    /// Zero-based occurrence index of this group instance among all siblings
160    /// with the same `definition` at this level.
161    ///
162    /// For example, the first `SG5` child at a given level has `occurrence_index = 0`,
163    /// the second `SG5` has `occurrence_index = 1`, etc.  Siblings with a
164    /// *different* definition have independent counters.
165    ///
166    /// This field is essential for producing unambiguous rule-violation IDs
167    /// (e.g. `"SG5[2]/DTM"`) when the same group type repeats.
168    pub occurrence_index: usize,
169}
170
171impl SegmentGroupIndexed<'_> {
172    /// Iterate over the absolute indices of segments that belong *directly* to
173    /// this group — i.e. those within [`total_span`] that are **not** covered
174    /// by any child group's [`total_span`].
175    ///
176    /// Complexity: `O(total_span.len() × children.len())`.  For typical EDIFACT
177    /// message structures (≤ 8 children per group) this is negligible.
178    ///
179    /// [`total_span`]: SegmentGroupIndexed::total_span
180    pub fn direct_segment_indices(&self) -> impl Iterator<Item = usize> + '_ {
181        self.total_span.clone().filter(|i| {
182            !self
183                .children
184                .iter()
185                .any(|child| child.total_span.contains(i))
186        })
187    }
188}
189
190/// Partition `segments` into a [`SegmentGroupIndexed`] tree without cloning.
191///
192/// Stores `Range<usize>` indices into the original flat slice rather than
193/// copying each [`Segment`] into the tree.  Use the original slice together
194/// with [`SegmentGroupIndexed::total_span`] to access segments.
195///
196/// # Worked Example
197///
198/// Consider a simplified 3-level multi-level schema:
199///
200/// ```rust
201/// use edifact_rs::group::{GroupDef, group_segments_indexed};
202/// use edifact_rs::from_bytes;
203///
204/// // Schema: ROOT → SG1 (trigger: RFF) → SG5 (trigger: LOC) → SG6 (trigger: QTY)
205/// static SG6: &[GroupDef] = &[GroupDef::new("SG6", "QTY")];
206/// static SCHEMA: &[GroupDef] = &[
207///     GroupDef::new("SG1", "RFF"),
208///     GroupDef::with_children("SG5", "LOC", SG6),
209/// ];
210///
211/// // A small multi-level message fragment (no envelope for clarity).
212/// let input = b"RFF+Z13:REF1'LOC+172+DE123'DTM+163:20230101:102'QTY+220:100:KWH'";
213/// let segments: Vec<_> = from_bytes(input)
214///     .collect::<Result<_, _>>()
215///     .unwrap();
216///
217/// let tree = group_segments_indexed(&segments, SCHEMA, "ROOT");
218///
219/// // The root contains no direct segments (all consumed by SG1 / SG5).
220/// assert!(tree.direct_segment_indices().next().is_none());
221///
222/// // One SG1 group and one SG5 group at root level.
223/// let sg1 = tree.children.iter().find(|g| g.definition == "SG1").unwrap();
224/// let sg5 = tree.children.iter().find(|g| g.definition == "SG5").unwrap();
225///
226/// // SG1 spans the RFF segment only.
227/// assert_eq!(&segments[sg1.total_span.clone()].iter().map(|s| s.tag).collect::<Vec<_>>(),
228///            &["RFF"]);
229///
230/// // SG5 spans LOC + DTM + QTY (all three segments, including the SG6 child).
231/// let sg5_tags: Vec<_> = segments[sg5.total_span.clone()].iter().map(|s| s.tag).collect();
232/// assert_eq!(sg5_tags, &["LOC", "DTM", "QTY"]);
233///
234/// // SG5's direct segments (LOC + DTM) exclude the SG6 child (QTY).
235/// let sg5_direct: Vec<_> = sg5.direct_segment_indices()
236///     .map(|i| segments[i].tag)
237///     .collect();
238/// assert_eq!(sg5_direct, &["LOC", "DTM"]);
239///
240/// // SG6 contains only QTY.
241/// let sg6 = sg5.children.iter().find(|g| g.definition == "SG6").unwrap();
242/// assert_eq!(segments[sg6.total_span.clone()].iter().map(|s| s.tag).collect::<Vec<_>>(),
243///            &["QTY"]);
244/// ```
245///
246/// # Group validation
247///
248/// `group_segments_indexed` pairs naturally with
249/// [`crate::validator::ValidationContext::validate_lenient_grouped`] to enforce group-presence rules:
250///
251/// ```rust,ignore
252/// use edifact_rs::{ProfileRulePack, ValidationContext};
253///
254/// let pack = ProfileRulePack::new("MY-PROFILE")
255///     .require_segment_in_group("SG5", "DTM", "SG5-DTM-M")
256///     .forbid_segment_in_group("SG1", "LOC", "SG1-LOC-F");
257/// let ctx = ValidationContext::builder().with_profile_pack(pack).build();
258///
259/// let tree = group_segments_indexed(&segments, SCHEMA, "ORDERS");
260/// let report = ctx.validate_lenient_grouped(&tree, &segments);
261/// ```
262///
263/// # What a group spans
264///
265/// Grouping is driven purely by trigger tags: a group runs from its trigger to
266/// the next trigger belonging to a sibling or ancestor, or to the end of the
267/// slice.  Nothing stops the final group at `UNT`, because the trailer is not a
268/// trigger of anything — pass the message *body* when the group boundaries
269/// matter, or accept that the trailer lands inside the last group.
270///
271/// # Complexity
272///
273/// `O(n × schema_depth)` time, `O(tree_nodes)` space.  No `Segment` clones.
274pub fn group_segments_indexed<'g>(
275    segments: &[Segment<'_>],
276    schema: &'g [GroupDef<'g>],
277    root_name: &'g str,
278) -> SegmentGroupIndexed<'g> {
279    let mut root = SegmentGroupIndexed {
280        definition: root_name,
281        total_span: 0..0,
282        children: Vec::new(),
283        occurrence_index: 0,
284    };
285    group_recursive_indexed(segments, &mut root, schema, &[], 0);
286    root
287}
288
289/// Partition an owned-segment slice into a [`SegmentGroupIndexed`] tree according to `schema`.
290///
291/// Equivalent to [`group_segments_indexed`] but accepts `&[OwnedSegment]`.
292pub fn group_owned_segments_indexed<'g>(
293    segments: &[OwnedSegment],
294    schema: &'g [GroupDef<'g>],
295    root_name: &'g str,
296) -> SegmentGroupIndexed<'g> {
297    let borrowed: Vec<Segment<'_>> = segments.iter().map(|s| s.as_borrowed()).collect();
298    group_segments_indexed(&borrowed, schema, root_name)
299}
300
301/// Internal recursive indexed grouping.  Returns the number of segments consumed.
302fn group_recursive_indexed<'g>(
303    segments: &[Segment<'_>],
304    parent: &mut SegmentGroupIndexed<'g>,
305    schema: &'g [GroupDef<'g>],
306    stop_triggers: &[&'g str],
307    offset: usize,
308) -> usize {
309    let combined_stop: SmallVec<[&'g str; 16]> = {
310        let mut v: SmallVec<[&'g str; 16]> = SmallVec::from_slice(stop_triggers);
311        for d in schema {
312            if !v.contains(&d.trigger) {
313                v.push(d.trigger);
314            }
315        }
316        v
317    };
318
319    // `span_start` is the absolute index of the first segment in this group.
320    // For child groups the caller pre-seeds `parent.total_span.start` with the
321    // trigger segment position; for the root (or any group with no pre-seeded
322    // trigger) we start at `offset`.
323    let span_start = if !parent.total_span.is_empty() {
324        parent.total_span.start // pre-seeded trigger position
325    } else {
326        offset
327    };
328
329    let mut i = 0;
330    // Track how many children of each definition have been pushed at this level,
331    // so we can stamp `occurrence_index` on each new child.
332    let mut occ_counts: std::collections::HashMap<&'g str, usize> =
333        std::collections::HashMap::new();
334    while i < segments.len() {
335        let tag = segments[i].tag;
336
337        if stop_triggers.iter().copied().any(|t| t == tag) {
338            break;
339        }
340
341        if let Some(def) = schema.iter().find(|d| d.trigger == tag) {
342            let child_offset = offset + i;
343            let occ_idx = {
344                let c = occ_counts.entry(def.name).or_insert(0);
345                let idx = *c;
346                *c += 1;
347                idx
348            };
349            let mut child = SegmentGroupIndexed {
350                definition: def.name,
351                // Pre-seed the trigger segment; the recursive call extends
352                // total_span to cover the full child subtree.
353                total_span: child_offset..child_offset + 1,
354                children: Vec::new(),
355                occurrence_index: occ_idx,
356            };
357            i += 1;
358
359            let consumed = group_recursive_indexed(
360                &segments[i..],
361                &mut child,
362                def.children,
363                &combined_stop,
364                offset + i,
365            );
366            i += consumed;
367
368            parent.children.push(child);
369        } else {
370            i += 1;
371        }
372    }
373
374    // Total span covers everything from the first segment (trigger or first
375    // direct segment) to the last segment consumed in this call.
376    parent.total_span = span_start..(offset + i);
377
378    i
379}
380
381#[cfg(test)]
382mod tests {
383    use super::*;
384    use crate::Span;
385    use crate::model::Element;
386
387    fn seg(tag: &'static str) -> Segment<'static> {
388        Segment {
389            tag,
390            span: Span::new(0, 0),
391            tag_span: Span::new(0, 0),
392            elements: vec![Element::of(&["x"])],
393        }
394    }
395
396    static SCHEMA: &[GroupDef] = &[
397        GroupDef {
398            name: "SG1",
399            trigger: "NAD",
400            children: &[GroupDef {
401                name: "SG2",
402                trigger: "CTA",
403                children: &[],
404            }],
405        },
406        GroupDef {
407            name: "SG3",
408            trigger: "LIN",
409            children: &[],
410        },
411    ];
412
413    #[test]
414    fn root_segments_before_first_trigger() {
415        let segs = vec![seg("UNH"), seg("BGM"), seg("NAD")];
416        let tree = group_segments_indexed(&segs, SCHEMA, "ROOT");
417        // UNH (0) and BGM (1) are direct root segments; NAD (2) is in SG1.
418        let direct: Vec<_> = tree.direct_segment_indices().collect();
419        assert_eq!(direct, vec![0, 1], "UNH + BGM should be direct in root");
420        assert_eq!(tree.children.len(), 1);
421        assert_eq!(tree.children[0].definition, "SG1");
422    }
423
424    #[test]
425    fn repeated_trigger_creates_multiple_children() {
426        let segs = vec![seg("UNH"), seg("NAD"), seg("NAD"), seg("UNT")];
427        let tree = group_segments_indexed(&segs, SCHEMA, "ROOT");
428        // Two NAD triggers → two SG1 children
429        assert_eq!(
430            tree.children
431                .iter()
432                .filter(|c| c.definition == "SG1")
433                .count(),
434            2
435        );
436    }
437
438    #[test]
439    fn repeated_trigger_occurrence_index_is_stamped() {
440        let segs = vec![seg("NAD"), seg("NAD"), seg("NAD")];
441        let tree = group_segments_indexed(&segs, SCHEMA, "ROOT");
442        let indices: Vec<_> = tree.children.iter().map(|c| c.occurrence_index).collect();
443        assert_eq!(indices, vec![0, 1, 2]);
444    }
445
446    #[test]
447    fn nested_child_groups() {
448        let segs = vec![seg("NAD"), seg("CTA"), seg("CTA")];
449        let tree = group_segments_indexed(&segs, SCHEMA, "ROOT");
450        let sg1 = &tree.children[0];
451        assert_eq!(sg1.definition, "SG1");
452        // Two CTA triggers → two SG2 children inside SG1
453        assert_eq!(sg1.children.len(), 2);
454        assert!(sg1.children.iter().all(|c| c.definition == "SG2"));
455    }
456
457    #[test]
458    fn total_span_covers_all_segments() {
459        let segs = vec![seg("UNH"), seg("NAD"), seg("CTA")];
460        let tree = group_segments_indexed(&segs, SCHEMA, "ROOT");
461        // Root span covers all 3 segments
462        let all_tags: Vec<_> = segs[tree.total_span.clone()]
463            .iter()
464            .map(|s| s.tag)
465            .collect();
466        assert!(all_tags.contains(&"UNH"));
467        assert!(all_tags.contains(&"NAD"));
468        assert!(all_tags.contains(&"CTA"));
469    }
470}