Skip to main content

mig_types/schema/
mig.rs

1use serde::{Deserialize, Serialize};
2
3use super::common::{Cardinality, CodeDefinition};
4
5/// Complete MIG schema for a message type.
6#[derive(Debug, Clone, Serialize, Deserialize)]
7pub struct MigSchema {
8    /// The EDIFACT message type (e.g., "UTILMD", "ORDERS").
9    pub message_type: String,
10    /// Optional variant (e.g., "Strom", "Gas").
11    pub variant: Option<String>,
12    /// Version number from the MIG (e.g., "S2.1", "1.4a").
13    pub version: String,
14    /// Publication date string.
15    pub publication_date: String,
16    /// Author (typically "BDEW").
17    pub author: String,
18    /// Format version directory (e.g., "FV2504").
19    pub format_version: String,
20    /// Path to the source XML file.
21    pub source_file: String,
22    /// Top-level segment definitions (not in groups).
23    pub segments: Vec<MigSegment>,
24    /// Segment group definitions (contain more segments).
25    pub segment_groups: Vec<MigSegmentGroup>,
26}
27
28impl MigSchema {
29    /// Whether this MIG includes the interchange envelope (UNA/UNB) as
30    /// top-level segments. UTILMD/MSCONS/INVOIC/REMADV do; UTILTS/PRICAT/
31    /// ORDERS/COMDIS start at UNH.
32    ///
33    /// Callers that assemble from raw EDIFACT need this to decide whether
34    /// to feed `MessageChunk::all_segments()` (envelope + UNH + body + UNT)
35    /// or `MessageChunk::message_segments()` (UNH + body + UNT) to the
36    /// assembler — feeding envelope segments to a UNH-start MIG aborts
37    /// assembly at the first segment.
38    pub fn includes_envelope(&self) -> bool {
39        self.segments.iter().any(|s| s.id == "UNA" || s.id == "UNB")
40    }
41}
42
43/// A segment (S_*) definition from the MIG.
44#[derive(Debug, Clone, Serialize, Deserialize)]
45pub struct MigSegment {
46    /// Segment identifier (e.g., "UNH", "BGM", "NAD").
47    pub id: String,
48    /// Human-readable name.
49    pub name: String,
50    /// Description of the segment.
51    pub description: Option<String>,
52    /// Position counter (e.g., "0010", "0020").
53    pub counter: Option<String>,
54    /// Nesting level (0=root, 1=first level, etc.).
55    pub level: i32,
56    /// Sequence number within the message.
57    pub number: Option<String>,
58    /// Standard maximum repetitions.
59    pub max_rep_std: i32,
60    /// Specification maximum repetitions.
61    pub max_rep_spec: i32,
62    /// Standard status (M=Mandatory, C=Conditional, etc.).
63    pub status_std: Option<String>,
64    /// Specification status (M, R, D, O, N).
65    pub status_spec: Option<String>,
66    /// Example EDIFACT string.
67    pub example: Option<String>,
68    /// Direct child data elements.
69    pub data_elements: Vec<MigDataElement>,
70    /// Child composite elements.
71    pub composites: Vec<MigComposite>,
72}
73
74impl MigSegment {
75    /// Returns the effective cardinality based on spec or std status.
76    pub fn cardinality(&self) -> Cardinality {
77        let status = self
78            .status_spec
79            .as_deref()
80            .or(self.status_std.as_deref())
81            .unwrap_or("C");
82        Cardinality::from_status(status)
83    }
84
85    /// Returns the effective max repetitions (spec overrides std).
86    pub fn max_rep(&self) -> i32 {
87        self.max_rep_spec.max(self.max_rep_std)
88    }
89}
90
91/// A segment group (G_SG*) definition from the MIG.
92#[derive(Debug, Clone, Serialize, Deserialize)]
93pub struct MigSegmentGroup {
94    /// Group identifier (e.g., "SG1", "SG2", "SG10").
95    pub id: String,
96    /// Human-readable name.
97    pub name: String,
98    /// Description of the segment group.
99    pub description: Option<String>,
100    /// Position counter (e.g., "0070", "0500").
101    pub counter: Option<String>,
102    /// Nesting level.
103    pub level: i32,
104    /// Standard maximum repetitions.
105    pub max_rep_std: i32,
106    /// Specification maximum repetitions.
107    pub max_rep_spec: i32,
108    /// Standard status.
109    pub status_std: Option<String>,
110    /// Specification status.
111    pub status_spec: Option<String>,
112    /// Segments directly in this group.
113    pub segments: Vec<MigSegment>,
114    /// Nested segment groups.
115    pub nested_groups: Vec<MigSegmentGroup>,
116    /// Optional variant qualifier code for the entry segment.
117    /// When set, the assembler only matches segments whose entry qualifier
118    /// equals this code (e.g., "Z98" for SEQ+Z98, "ZD5" for SEQ+ZD5).
119    #[serde(default, skip_serializing_if = "Option::is_none")]
120    pub variant_code: Option<String>,
121    /// Position of the variant qualifier in the entry segment:
122    /// (element_index, component_index). Defaults to (0, 0) when absent.
123    /// Some segments have the qualifier in a composite at a non-zero position
124    /// (e.g., CCI with qualifier in C240/D7037 at element index 2).
125    #[serde(default, skip_serializing_if = "Option::is_none")]
126    pub variant_qualifier_position: Option<(usize, usize)>,
127    /// All allowed qualifier codes for this variant (assembler matches ANY).
128    #[serde(default)]
129    pub variant_codes: Vec<String>,
130    /// Number of MIG XML variants that were merged into this group definition.
131    /// When multiple SG2 definitions (MS, MR, DP, etc.) are merged into one,
132    /// this holds the count of merged variants. Used to compute the correct
133    /// max_reps for PID schema generation (variant count, not max of individual max_reps).
134    #[serde(default, skip_serializing_if = "Option::is_none")]
135    pub merged_variant_count: Option<u32>,
136    /// Entry-segment qualifiers of the MIG variants merged into this group
137    /// definition, in MIG order (see `merge_group_variants` in
138    /// `mig-assembly::pid_filter`). Empty for a group that is not a merge — its
139    /// own [`entry_qualifier`](Self::entry_qualifier) applies.
140    ///
141    /// Merging unions the variants' segments, which loses which repetition
142    /// belongs to which variant. The reverse mapping needs exactly that to
143    /// emit repetitions in MIG variant order (`NAD+MS` before `NAD+MR`) rather
144    /// than in the order the BO4E JSON happens to list them.
145    #[serde(default, skip_serializing_if = "Vec::is_empty")]
146    pub variant_entry_qualifiers: Vec<EntryQualifier>,
147}
148
149/// The qualifier that identifies a group variant: the codes the MIG allows in
150/// the first coded data element of the group's entry segment.
151#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
152pub struct EntryQualifier {
153    /// Entry segment tag, e.g. `"NAD"`.
154    pub tag: String,
155    /// Element index within the segment (0-based).
156    pub element: usize,
157    /// Component index within the element (0 for a simple data element).
158    pub component: usize,
159    /// Allowed codes, in MIG order.
160    pub codes: Vec<String>,
161    /// IDs of the groups nested in this variant (e.g. `["SG3"]` for the SG2
162    /// `NAD+MS` variant that holds the sender's contact).
163    #[serde(default, skip_serializing_if = "Vec::is_empty")]
164    pub nested_group_ids: Vec<String>,
165}
166
167impl EntryQualifier {
168    /// Whether `other` identifies the same variant (nested groups aside).
169    pub fn same_qualifier(&self, other: &EntryQualifier) -> bool {
170        self.tag == other.tag
171            && self.element == other.element
172            && self.component == other.component
173            && self.codes == other.codes
174    }
175
176    /// Whether a segment's elements carry one of this qualifier's codes.
177    pub fn matches(&self, elements: &[Vec<String>]) -> bool {
178        elements
179            .get(self.element)
180            .and_then(|e| e.get(self.component))
181            .is_some_and(|v| self.codes.iter().any(|c| c.eq_ignore_ascii_case(v)))
182    }
183}
184
185impl MigSegmentGroup {
186    /// The qualifier of this group's entry segment: its first coded data
187    /// element by (element, component) position. `None` when the entry segment
188    /// has no coded element (the variant cannot be told apart by qualifier).
189    pub fn entry_qualifier(&self) -> Option<EntryQualifier> {
190        let entry = self.segments.first()?;
191        let simple = entry.data_elements.iter().map(|d| (d.position, 0, d));
192        let composite = entry.composites.iter().flat_map(|c| {
193            c.data_elements
194                .iter()
195                .map(move |d| (c.position, d.position, d))
196        });
197        simple
198            .chain(composite)
199            .filter(|(_, _, d)| d.codes.iter().any(|c| !c.value.is_empty()))
200            .min_by_key(|(e, c, _)| (*e, *c))
201            .map(|(element, component, d)| EntryQualifier {
202                tag: entry.id.clone(),
203                element,
204                component,
205                codes: d
206                    .codes
207                    .iter()
208                    .filter(|c| !c.value.is_empty())
209                    .map(|c| c.value.clone())
210                    .collect(),
211                nested_group_ids: self.nested_group_ids(),
212            })
213    }
214
215    /// IDs of the directly nested groups, deduplicated, in MIG order.
216    pub fn nested_group_ids(&self) -> Vec<String> {
217        let mut ids: Vec<String> = Vec::new();
218        for g in &self.nested_groups {
219            if !ids.contains(&g.id) {
220                ids.push(g.id.clone());
221            }
222        }
223        ids
224    }
225
226    /// Returns the effective cardinality.
227    pub fn cardinality(&self) -> Cardinality {
228        let status = self
229            .status_spec
230            .as_deref()
231            .or(self.status_std.as_deref())
232            .unwrap_or("C");
233        Cardinality::from_status(status)
234    }
235}
236
237/// A composite element (C_*) definition from the MIG.
238#[derive(Debug, Clone, Serialize, Deserialize)]
239pub struct MigComposite {
240    /// Composite identifier (e.g., "S009", "C002").
241    pub id: String,
242    /// Human-readable name.
243    pub name: String,
244    /// Description.
245    pub description: Option<String>,
246    /// Standard status.
247    pub status_std: Option<String>,
248    /// Specification status.
249    pub status_spec: Option<String>,
250    /// Child data elements within this composite.
251    pub data_elements: Vec<MigDataElement>,
252    /// Position of this composite within its parent segment (0-based).
253    pub position: usize,
254}
255
256/// A data element (D_*) definition from the MIG.
257#[derive(Debug, Clone, Serialize, Deserialize)]
258pub struct MigDataElement {
259    /// Element identifier (e.g., "0062", "3035").
260    pub id: String,
261    /// Human-readable name.
262    pub name: String,
263    /// Description.
264    pub description: Option<String>,
265    /// Standard status.
266    pub status_std: Option<String>,
267    /// Specification status.
268    pub status_spec: Option<String>,
269    /// Standard format (e.g., "an..14", "n13").
270    pub format_std: Option<String>,
271    /// Specification format.
272    pub format_spec: Option<String>,
273    /// Allowed code values, if restricted.
274    pub codes: Vec<CodeDefinition>,
275    /// Position within parent (0-based).
276    pub position: usize,
277}