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 /// MIG `Number`s of this variant's own segments. A merged group keeps one
166 /// variant's segments, so without these the numbers of the others are
167 /// lost — ORDERS 17118's SG30 `CCI+Z39` (00100) beside `CCI+Z35`
168 /// (00097..00099) — and the rules of those variants never reach an
169 /// instance under the parent.
170 #[serde(default, skip_serializing_if = "Vec::is_empty")]
171 pub mig_numbers: Vec<String>,
172}
173
174impl EntryQualifier {
175 /// Whether `other` identifies the same variant (nested groups aside).
176 pub fn same_qualifier(&self, other: &EntryQualifier) -> bool {
177 self.tag == other.tag
178 && self.element == other.element
179 && self.component == other.component
180 && self.codes == other.codes
181 }
182
183 /// Whether a segment's elements carry one of this qualifier's codes.
184 pub fn matches(&self, elements: &[Vec<String>]) -> bool {
185 elements
186 .get(self.element)
187 .and_then(|e| e.get(self.component))
188 .is_some_and(|v| self.codes.iter().any(|c| c.eq_ignore_ascii_case(v)))
189 }
190}
191
192impl MigSegmentGroup {
193 /// The qualifier of this group's entry segment: its first coded data
194 /// element by (element, component) position. `None` when the entry segment
195 /// has no coded element (the variant cannot be told apart by qualifier).
196 pub fn entry_qualifier(&self) -> Option<EntryQualifier> {
197 let entry = self.segments.first()?;
198 let simple = entry.data_elements.iter().map(|d| (d.position, 0, d));
199 let composite = entry.composites.iter().flat_map(|c| {
200 c.data_elements
201 .iter()
202 .map(move |d| (c.position, d.position, d))
203 });
204 simple
205 .chain(composite)
206 .filter(|(_, _, d)| d.codes.iter().any(|c| !c.value.is_empty()))
207 .min_by_key(|(e, c, _)| (*e, *c))
208 .map(|(element, component, d)| EntryQualifier {
209 tag: entry.id.clone(),
210 element,
211 component,
212 codes: d
213 .codes
214 .iter()
215 .filter(|c| !c.value.is_empty())
216 .map(|c| c.value.clone())
217 .collect(),
218 nested_group_ids: self.nested_group_ids(),
219 mig_numbers: self
220 .segments
221 .iter()
222 .filter_map(|s| s.number.clone())
223 .collect(),
224 })
225 }
226
227 /// IDs of the directly nested groups, deduplicated, in MIG order.
228 pub fn nested_group_ids(&self) -> Vec<String> {
229 let mut ids: Vec<String> = Vec::new();
230 for g in &self.nested_groups {
231 if !ids.contains(&g.id) {
232 ids.push(g.id.clone());
233 }
234 }
235 ids
236 }
237
238 /// Returns the effective cardinality.
239 pub fn cardinality(&self) -> Cardinality {
240 let status = self
241 .status_spec
242 .as_deref()
243 .or(self.status_std.as_deref())
244 .unwrap_or("C");
245 Cardinality::from_status(status)
246 }
247}
248
249/// A composite element (C_*) definition from the MIG.
250#[derive(Debug, Clone, Serialize, Deserialize)]
251pub struct MigComposite {
252 /// Composite identifier (e.g., "S009", "C002").
253 pub id: String,
254 /// Human-readable name.
255 pub name: String,
256 /// Description.
257 pub description: Option<String>,
258 /// Standard status.
259 pub status_std: Option<String>,
260 /// Specification status.
261 pub status_spec: Option<String>,
262 /// Child data elements within this composite.
263 pub data_elements: Vec<MigDataElement>,
264 /// Position of this composite within its parent segment (0-based).
265 pub position: usize,
266}
267
268/// A data element (D_*) definition from the MIG.
269#[derive(Debug, Clone, Serialize, Deserialize)]
270pub struct MigDataElement {
271 /// Element identifier (e.g., "0062", "3035").
272 pub id: String,
273 /// Human-readable name.
274 pub name: String,
275 /// Description.
276 pub description: Option<String>,
277 /// Standard status.
278 pub status_std: Option<String>,
279 /// Specification status.
280 pub status_spec: Option<String>,
281 /// Standard format (e.g., "an..14", "n13").
282 pub format_std: Option<String>,
283 /// Specification format.
284 pub format_spec: Option<String>,
285 /// Allowed code values, if restricted.
286 pub codes: Vec<CodeDefinition>,
287 /// Position within parent (0-based).
288 pub position: usize,
289}