Skip to main content

automapper_validation/eval/
evaluator.rs

1//! Core condition evaluation traits.
2
3use super::context::EvaluationContext;
4
5/// Three-valued result of evaluating a single condition.
6///
7/// Unlike the C# implementation which uses `bool`, we use three-valued logic
8/// to support partial evaluation when external conditions are unavailable.
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
10pub enum ConditionResult {
11    /// The condition is satisfied.
12    True,
13    /// The condition is not satisfied.
14    False,
15    /// The condition cannot be determined (e.g., external condition without a provider).
16    Unknown,
17}
18
19impl ConditionResult {
20    /// Returns `true` if this is `ConditionResult::True`.
21    pub fn is_true(self) -> bool {
22        matches!(self, ConditionResult::True)
23    }
24
25    /// Returns `true` if this is `ConditionResult::False`.
26    pub fn is_false(self) -> bool {
27        matches!(self, ConditionResult::False)
28    }
29
30    /// Returns `true` if this is `ConditionResult::Unknown`.
31    pub fn is_unknown(self) -> bool {
32        matches!(self, ConditionResult::Unknown)
33    }
34
35    /// Three-valued AND: `False` if either is, `True` if both are, else `Unknown`.
36    pub fn and(self, other: ConditionResult) -> ConditionResult {
37        match (self, other) {
38            (ConditionResult::False, _) | (_, ConditionResult::False) => ConditionResult::False,
39            (ConditionResult::True, ConditionResult::True) => ConditionResult::True,
40            _ => ConditionResult::Unknown,
41        }
42    }
43
44    /// Three-valued OR: `True` if either is, `False` if both are, else `Unknown`.
45    pub fn or(self, other: ConditionResult) -> ConditionResult {
46        match (self, other) {
47            (ConditionResult::True, _) | (_, ConditionResult::True) => ConditionResult::True,
48            (ConditionResult::False, ConditionResult::False) => ConditionResult::False,
49            _ => ConditionResult::Unknown,
50        }
51    }
52
53    /// Three-valued NOT.
54    pub fn negate(self) -> ConditionResult {
55        match self {
56            ConditionResult::True => ConditionResult::False,
57            ConditionResult::False => ConditionResult::True,
58            ConditionResult::Unknown => ConditionResult::Unknown,
59        }
60    }
61
62    /// Converts to `Option<bool>`: True -> Some(true), False -> Some(false), Unknown -> None.
63    pub fn to_option(self) -> Option<bool> {
64        match self {
65            ConditionResult::True => Some(true),
66            ConditionResult::False => Some(false),
67            ConditionResult::Unknown => None,
68        }
69    }
70}
71
72impl From<bool> for ConditionResult {
73    fn from(value: bool) -> Self {
74        if value {
75            ConditionResult::True
76        } else {
77            ConditionResult::False
78        }
79    }
80}
81
82impl std::fmt::Display for ConditionResult {
83    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
84        match self {
85            ConditionResult::True => write!(f, "True"),
86            ConditionResult::False => write!(f, "False"),
87            ConditionResult::Unknown => write!(f, "Unknown"),
88        }
89    }
90}
91
92/// Evaluates individual AHB conditions by number.
93///
94/// Implementations are typically generated from AHB XML schemas (one per
95/// message type and format version). Each condition number maps to a
96/// specific business rule check.
97pub trait ConditionEvaluator: Send + Sync {
98    /// Evaluate a single condition by number.
99    ///
100    /// Returns `ConditionResult::Unknown` for unrecognized condition numbers
101    /// or conditions that require unavailable external context.
102    fn evaluate(&self, condition: u32, ctx: &EvaluationContext) -> ConditionResult;
103
104    /// Returns `true` if the given condition requires external context
105    /// (i.e., cannot be determined from the EDIFACT message alone).
106    fn is_external(&self, condition: u32) -> bool;
107
108    /// Returns `true` if this evaluator has an implementation for the given
109    /// condition number (whether internal or external). Conditions that fall
110    /// through to the `_ => Unknown` wildcard return `false`.
111    ///
112    /// This allows distinguishing "implemented but returned Unknown because
113    /// the relevant data isn't present in the message" from "not implemented
114    /// at all".
115    fn is_known(&self, _condition: u32) -> bool {
116        false
117    }
118
119    /// Returns the message type this evaluator handles (e.g., "UTILMD").
120    fn message_type(&self) -> &str;
121
122    /// Returns the format version this evaluator handles (e.g., "FV2510").
123    fn format_version(&self) -> &str;
124}
125
126impl<T: ConditionEvaluator + ?Sized> ConditionEvaluator for std::sync::Arc<T> {
127    fn evaluate(&self, condition: u32, ctx: &EvaluationContext) -> ConditionResult {
128        (**self).evaluate(condition, ctx)
129    }
130
131    fn is_external(&self, condition: u32) -> bool {
132        (**self).is_external(condition)
133    }
134
135    fn is_known(&self, condition: u32) -> bool {
136        (**self).is_known(condition)
137    }
138
139    fn message_type(&self) -> &str {
140        (**self).message_type()
141    }
142
143    fn format_version(&self) -> &str {
144        (**self).format_version()
145    }
146}
147
148/// Provider for external conditions that depend on context outside the EDIFACT message.
149///
150/// External conditions are things like:
151/// - [1] "Wenn Aufteilung vorhanden" (message splitting status)
152/// - [14] "Wenn Datum bekannt" (whether a date is known)
153/// - [30] "Wenn Antwort auf Aktivierung" (response to activation)
154///
155/// These cannot be determined from the EDIFACT content alone and require
156/// business context from the calling system.
157pub trait ExternalConditionProvider: Send + Sync {
158    /// Evaluate an external condition by name.
159    ///
160    /// The `condition_name` corresponds to the speaking name from the
161    /// generated external conditions constants (e.g., "MessageSplitting",
162    /// "DateKnown").
163    fn evaluate(&self, condition_name: &str) -> ConditionResult;
164}
165
166/// A no-op external condition provider that returns `Unknown` for everything.
167///
168/// Useful when no external context is available — conditions will propagate
169/// as `Unknown` through the expression evaluator.
170pub struct NoOpExternalProvider;
171
172impl ExternalConditionProvider for NoOpExternalProvider {
173    fn evaluate(&self, _condition_name: &str) -> ConditionResult {
174        ConditionResult::Unknown
175    }
176}
177
178/// The number of a message type's bare "Wenn vorhanden" condition, if it has one.
179///
180/// The condition is self-referential: its value is whether the element or
181/// group it annotates is present. `Soll [166]` on a group means "send it if
182/// you have it", which only the sender knows, so an absent group annotated
183/// with it is never missing. The generated evaluators answer it with `True`
184/// ("the rule applies wherever it is evaluated"), which is right for a present
185/// element and wrong for an absent one — see [`AbsentTarget`].
186///
187/// The numbers are stable across every format version in
188/// `xml-migs-and-ahbs/` (FV2410–FV2610); keyed by [`ConditionEvaluator::message_type`]
189/// rather than by evaluator so aliased and regenerated evaluators need no edit.
190pub fn presence_condition(message_type: &str) -> Option<u32> {
191    match message_type {
192        "UTILMD_Strom" | "UTILMD_Gas" => Some(166),
193        "INVOIC" => Some(22),
194        "ORDERS" => Some(12),
195        _ => None,
196    }
197}
198
199/// Evaluates conditions for an element or group that is known to be absent.
200///
201/// Identical to the wrapped evaluator except that the message type's
202/// [`presence_condition`] is `False`: whatever the condition annotates is not
203/// there. Use it wherever a status is evaluated to decide whether something
204/// missing is required.
205pub struct AbsentTarget<'a, E: ConditionEvaluator + ?Sized>(pub &'a E);
206
207impl<E: ConditionEvaluator + ?Sized> ConditionEvaluator for AbsentTarget<'_, E> {
208    fn evaluate(&self, condition: u32, ctx: &EvaluationContext) -> ConditionResult {
209        if presence_condition(self.0.message_type()) == Some(condition) {
210            return ConditionResult::False;
211        }
212        self.0.evaluate(condition, ctx)
213    }
214
215    fn is_external(&self, condition: u32) -> bool {
216        self.0.is_external(condition)
217    }
218
219    fn is_known(&self, condition: u32) -> bool {
220        self.0.is_known(condition)
221    }
222
223    fn message_type(&self) -> &str {
224        self.0.message_type()
225    }
226
227    fn format_version(&self) -> &str {
228        self.0.format_version()
229    }
230}
231
232#[cfg(test)]
233mod tests {
234    use super::*;
235
236    struct AlwaysTrue(&'static str);
237
238    impl ConditionEvaluator for AlwaysTrue {
239        fn evaluate(&self, _: u32, _: &EvaluationContext) -> ConditionResult {
240            ConditionResult::True
241        }
242        fn is_external(&self, _: u32) -> bool {
243            false
244        }
245        fn message_type(&self) -> &str {
246            self.0
247        }
248        fn format_version(&self) -> &str {
249            "FV2604"
250        }
251    }
252
253    #[test]
254    fn absent_target_answers_only_the_presence_condition_with_false() {
255        let external = NoOpExternalProvider;
256        let ctx = EvaluationContext::new("55043", &external, &[]);
257        let inner = AlwaysTrue("UTILMD_Strom");
258        let absent = AbsentTarget(&inner);
259        assert_eq!(absent.evaluate(166, &ctx), ConditionResult::False);
260        assert_eq!(absent.evaluate(674, &ctx), ConditionResult::True);
261
262        // [166] of another message type is an unrelated condition.
263        let invoic = AlwaysTrue("INVOIC");
264        assert_eq!(
265            AbsentTarget(&invoic).evaluate(166, &ctx),
266            ConditionResult::True
267        );
268        assert_eq!(
269            AbsentTarget(&invoic).evaluate(22, &ctx),
270            ConditionResult::False
271        );
272    }
273
274    #[test]
275    fn test_condition_result_is_methods() {
276        assert!(ConditionResult::True.is_true());
277        assert!(!ConditionResult::True.is_false());
278        assert!(!ConditionResult::True.is_unknown());
279
280        assert!(!ConditionResult::False.is_true());
281        assert!(ConditionResult::False.is_false());
282
283        assert!(ConditionResult::Unknown.is_unknown());
284    }
285
286    #[test]
287    fn three_valued_and_or_not() {
288        use ConditionResult::{False as F, True as T, Unknown as U};
289        assert_eq!(T.and(T), T);
290        assert_eq!(T.and(U), U);
291        assert_eq!(U.and(F), F);
292        assert_eq!(F.or(F), F);
293        assert_eq!(F.or(U), U);
294        assert_eq!(U.or(T), T);
295        assert_eq!(U.negate(), U);
296        assert_eq!(T.negate(), F);
297    }
298
299    #[test]
300    fn test_condition_result_to_option() {
301        assert_eq!(ConditionResult::True.to_option(), Some(true));
302        assert_eq!(ConditionResult::False.to_option(), Some(false));
303        assert_eq!(ConditionResult::Unknown.to_option(), None);
304    }
305
306    #[test]
307    fn test_condition_result_from_bool() {
308        assert_eq!(ConditionResult::from(true), ConditionResult::True);
309        assert_eq!(ConditionResult::from(false), ConditionResult::False);
310    }
311
312    #[test]
313    fn test_condition_result_display() {
314        assert_eq!(format!("{}", ConditionResult::True), "True");
315        assert_eq!(format!("{}", ConditionResult::False), "False");
316        assert_eq!(format!("{}", ConditionResult::Unknown), "Unknown");
317    }
318
319    #[test]
320    fn test_noop_external_provider() {
321        let provider = NoOpExternalProvider;
322        assert_eq!(
323            provider.evaluate("MessageSplitting"),
324            ConditionResult::Unknown
325        );
326        assert_eq!(provider.evaluate("anything"), ConditionResult::Unknown);
327    }
328}