ahb_types/lib.rs
1//! Serializable AHB rulebook data types shared across the workspace.
2//!
3//! These are pure data types — no parsing or evaluation logic. The mapping crate
4//! (`mig-bo4e`) embeds [`AhbWorkflow`] in its distribution bundle, and the validation
5//! crate (`automapper-validation`) interprets it. Keeping the types here lets both
6//! depend on the data without a mapping ↔ validation crate edge. This mirrors how
7//! `PidRequirements` (the BO4E-side rulebook) already lives in the mapping crate.
8
9use std::collections::{BTreeMap, BTreeSet};
10
11use serde::{Deserialize, Serialize};
12
13/// A parsed AHB condition expression tree.
14///
15/// Represents boolean combinations of condition references like `[1] ∧ [2]` or
16/// `([3] ∨ [4]) ⊻ [5]`.
17///
18/// # Examples
19///
20/// A single condition reference:
21/// ```
22/// use ahb_types::ConditionExpr;
23/// let expr = ConditionExpr::Ref(931);
24/// assert_eq!(expr.condition_ids(), [931].into());
25/// ```
26#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
27pub enum ConditionExpr {
28 /// A leaf reference to a single condition by number, e.g., `[931]`.
29 Ref(u32),
30
31 /// Boolean AND of one or more expressions. All must be true.
32 /// Invariant: `exprs.len() >= 2`.
33 And(Vec<ConditionExpr>),
34
35 /// Boolean OR of one or more expressions. At least one must be true.
36 /// Invariant: `exprs.len() >= 2`.
37 Or(Vec<ConditionExpr>),
38
39 /// Boolean XOR of exactly two expressions. Exactly one must be true.
40 Xor(Box<ConditionExpr>, Box<ConditionExpr>),
41
42 /// Boolean NOT of an expression.
43 Not(Box<ConditionExpr>),
44
45 /// Package cardinality constraint: [NP_min..max]
46 Package { id: u32, min: u32, max: u32 },
47}
48
49impl ConditionExpr {
50 /// Extracts all condition IDs referenced in this expression tree.
51 pub fn condition_ids(&self) -> BTreeSet<u32> {
52 let mut ids = BTreeSet::new();
53 self.collect_ids(&mut ids);
54 ids
55 }
56
57 fn collect_ids(&self, ids: &mut BTreeSet<u32>) {
58 match self {
59 ConditionExpr::Ref(id) => {
60 ids.insert(*id);
61 }
62 ConditionExpr::And(exprs) | ConditionExpr::Or(exprs) => {
63 for expr in exprs {
64 expr.collect_ids(ids);
65 }
66 }
67 ConditionExpr::Xor(left, right) => {
68 left.collect_ids(ids);
69 right.collect_ids(ids);
70 }
71 ConditionExpr::Not(inner) => {
72 inner.collect_ids(ids);
73 }
74 ConditionExpr::Package { .. } => {
75 // Package constraints are structural, not condition references
76 }
77 }
78 }
79}
80
81impl std::fmt::Display for ConditionExpr {
82 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
83 match self {
84 ConditionExpr::Ref(id) => write!(f, "[{id}]"),
85 ConditionExpr::And(exprs) => {
86 let parts: Vec<String> = exprs.iter().map(|e| format!("{e}")).collect();
87 write!(f, "({})", parts.join(" ∧ "))
88 }
89 ConditionExpr::Or(exprs) => {
90 let parts: Vec<String> = exprs.iter().map(|e| format!("{e}")).collect();
91 write!(f, "({})", parts.join(" ∨ "))
92 }
93 ConditionExpr::Xor(left, right) => write!(f, "({left} ⊻ {right})"),
94 ConditionExpr::Not(inner) => write!(f, "NOT {inner}"),
95 ConditionExpr::Package { id, min, max } => write!(f, "[{id}P{min}..{max}]"),
96 }
97 }
98}
99
100/// An allowed code value within an AHB field rule.
101#[derive(Debug, Clone, Default, Serialize, Deserialize)]
102pub struct AhbCodeRule {
103 /// The code value (e.g., "E01", "Z33").
104 pub value: String,
105
106 /// Description of the code (e.g., "Anmeldung").
107 pub description: String,
108
109 /// AHB status for this code (e.g., "X", "Muss").
110 pub ahb_status: String,
111}
112
113/// AHB field definition for validation.
114///
115/// Represents a single field in an AHB rule table with its status
116/// and allowed codes for a specific Pruefidentifikator.
117#[derive(Debug, Clone, Default, Serialize, Deserialize)]
118pub struct AhbFieldRule {
119 /// Segment path (e.g., "SG2/NAD/C082/3039").
120 pub segment_path: String,
121
122 /// Human-readable field name (e.g., "MP-ID des MSB").
123 pub name: String,
124
125 /// AHB status (e.g., "Muss [182] ∧ [152]", "X", "Kann").
126 pub ahb_status: String,
127
128 /// Allowed code values with their AHB status.
129 pub codes: Vec<AhbCodeRule>,
130
131 /// AHB status of the innermost parent group (e.g., "Kann", "Muss", "Soll [46]").
132 ///
133 /// When the parent group is optional ("Kann") and its qualifier variant is
134 /// absent from the message, mandatory checks for child fields are skipped.
135 pub parent_group_ahb_status: Option<String>,
136
137 /// AHB status of the containing segment (e.g., "Kann", "Muss", "Muss [10]").
138 ///
139 /// When the containing segment is optional ("Kann") and absent from the
140 /// instance, AHB001 missing-field errors on its sub-fields are suppressed:
141 /// "X" on a Kann segment's sub-element means "required IF segment present".
142 pub segment_ahb_status: Option<String>,
143
144 /// Element index within the segment (0-based). Used to locate the correct
145 /// element when checking presence and code values. `None` defaults to 0.
146 pub element_index: Option<usize>,
147
148 /// Component sub-index within a composite element (0-based). Used to locate
149 /// the correct component. `None` defaults to 0.
150 pub component_index: Option<usize>,
151
152 /// MIG `Number` attribute of the parent segment. Links this AHB field to
153 /// the corresponding `AssembledSegment::mig_number` for tree-based joining.
154 pub mig_number: Option<String>,
155
156 /// The MIG data format, e.g. `"an..512"`, `"n13"`. `None` when unknown.
157 #[serde(default, skip_serializing_if = "Option::is_none")]
158 pub format: Option<String>,
159}
160
161/// AHB workflow definition for a specific Pruefidentifikator.
162#[derive(Debug, Clone, Serialize, Deserialize)]
163pub struct AhbWorkflow {
164 /// The Pruefidentifikator (e.g., "11001", "55001").
165 pub pruefidentifikator: String,
166
167 /// Description of the workflow.
168 pub description: String,
169
170 /// Communication direction (e.g., "NB an LF").
171 pub communication_direction: Option<String>,
172
173 /// All field rules for this workflow.
174 pub fields: Vec<AhbFieldRule>,
175
176 /// UB (Unterbedingung) definitions parsed from the AHB XML.
177 ///
178 /// Maps UB IDs (e.g., "UB1") to their parsed condition expressions.
179 /// These are expanded inline when evaluating condition expressions
180 /// that reference UB conditions.
181 pub ub_definitions: BTreeMap<String, ConditionExpr>,
182}
183
184#[cfg(test)]
185mod tests {
186 use super::*;
187
188 #[test]
189 fn test_ref_condition_ids() {
190 let expr = ConditionExpr::Ref(931);
191 assert_eq!(expr.condition_ids(), [931].into());
192 }
193
194 #[test]
195 fn test_nested_condition_ids() {
196 // (([1] ∧ [2]) ∨ ([3] ∧ [4])) ⊻ [5]
197 let expr = ConditionExpr::Xor(
198 Box::new(ConditionExpr::Or(vec![
199 ConditionExpr::And(vec![ConditionExpr::Ref(1), ConditionExpr::Ref(2)]),
200 ConditionExpr::And(vec![ConditionExpr::Ref(3), ConditionExpr::Ref(4)]),
201 ])),
202 Box::new(ConditionExpr::Ref(5)),
203 );
204 assert_eq!(expr.condition_ids(), [1, 2, 3, 4, 5].into());
205 }
206
207 #[test]
208 fn test_display_complex() {
209 let expr = ConditionExpr::Xor(
210 Box::new(ConditionExpr::And(vec![
211 ConditionExpr::Ref(102),
212 ConditionExpr::Ref(2006),
213 ])),
214 Box::new(ConditionExpr::And(vec![
215 ConditionExpr::Ref(103),
216 ConditionExpr::Ref(2005),
217 ])),
218 );
219 assert_eq!(format!("{expr}"), "(([102] ∧ [2006]) ⊻ ([103] ∧ [2005]))");
220 }
221
222 #[test]
223 fn test_package_condition_ids() {
224 let expr = ConditionExpr::Package {
225 id: 4,
226 min: 0,
227 max: 1,
228 };
229 assert!(expr.condition_ids().is_empty());
230 }
231
232 /// The whole point of this crate: the workflow must survive a serde round-trip
233 /// so it can be baked into the distribution bundle (bincode) and read back.
234 #[test]
235 fn ahb_workflow_json_roundtrip() {
236 let wf = AhbWorkflow {
237 pruefidentifikator: "55001".into(),
238 description: "Anmeldung MaLo".into(),
239 communication_direction: Some("NB an LF".into()),
240 fields: vec![AhbFieldRule {
241 segment_path: "SG2/NAD/3035".into(),
242 name: "Partnerrolle".into(),
243 ahb_status: "Muss [182] ∧ [152]".into(),
244 codes: vec![AhbCodeRule {
245 value: "MS".into(),
246 description: "Messstellenbetreiber".into(),
247 ahb_status: "X".into(),
248 }],
249 mig_number: Some("0042".into()),
250 ..Default::default()
251 }],
252 ub_definitions: BTreeMap::from([(
253 "UB1".to_string(),
254 ConditionExpr::Xor(
255 Box::new(ConditionExpr::Ref(931)),
256 Box::new(ConditionExpr::Ref(932)),
257 ),
258 )]),
259 };
260
261 let json = serde_json::to_string(&wf).unwrap();
262 let back: AhbWorkflow = serde_json::from_str(&json).unwrap();
263
264 assert_eq!(back.pruefidentifikator, "55001");
265 assert_eq!(back.fields.len(), 1);
266 assert_eq!(back.fields[0].codes[0].value, "MS");
267 assert_eq!(back.fields[0].ahb_status, "Muss [182] ∧ [152]");
268 assert_eq!(
269 back.ub_definitions["UB1"].condition_ids(),
270 [931, 932].into()
271 );
272 }
273}