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