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/// Every condition of a message type that answers "does the annotated thing
200/// exist?", which an absent element or group answers with no.
201///
202/// The [`presence_condition`], plus conditions that ask for one repetition per
203/// real-world object the message cannot see. UTILMD_Strom [2003] ("Einmal für
204/// jede ruhende Marktlokation, die der Marktlokation »Kundenanlage« …
205/// untergeordnet ist", FV2504–FV2610, absent in Gas) is `Soll` on the ruhende
206/// Marktlokation: zero ruhende MaLos means zero groups, and only the sender
207/// knows how many exist. The generated evaluators cannot decide it and answer
208/// `Unknown`, which reported every message without a ruhende MaLo.
209pub fn presence_conditions(message_type: &str) -> &'static [u32] {
210    match message_type {
211        "UTILMD_Strom" => &[166, 2003],
212        "UTILMD_Gas" => &[166],
213        "INVOIC" => &[22],
214        "ORDERS" => &[12],
215        _ => &[],
216    }
217}
218
219/// The condition of a message type that allows a group or segment once per
220/// transaction, and the transaction group: UTILMD's `[2061]` "Segment bzw.
221/// Segmentgruppe ist genau einmal je SG4 IDE (Vorgang) anzugeben" (Strom and
222/// Gas, FV2504–FV2610). The conditions evaluate it as `True` wherever there is
223/// a transaction, so what it limits — the count — is checked on its own; see
224/// [`binds_once`].
225pub fn once_per_transaction(message_type: &str) -> Option<(u32, &'static str)> {
226    transaction_group(message_type).map(|group| (2061, group))
227}
228
229/// The group a message type repeats once per transaction, whose conditions the
230/// AHB states per transaction: UTILMD's SG4 (`IDE+24`, Vorgang). The validator
231/// evaluates each repetition against the message as that transaction sees it
232/// (`mig_assembly::transaction_view`, #337).
233pub fn transaction_group(message_type: &str) -> Option<&'static str> {
234    match message_type {
235        "UTILMD_Strom" | "UTILMD_Gas" => Some("SG4"),
236        _ => None,
237    }
238}
239
240/// Whether `status` limits what it annotates to once per transaction: the
241/// condition `once` is a conjunct of every line (`Muss [483] ∧ [2061]`). Where
242/// it sits in one branch only (`Soll [165] ∧ (([2061] ∧ [583]) ∨ [584])`), the
243/// other branch allows more.
244pub fn binds_once(status: &str, once: u32) -> bool {
245    fn conjunct(expr: &crate::expr::ConditionExpr, once: u32) -> bool {
246        match expr {
247            crate::expr::ConditionExpr::Ref(id) => *id == once,
248            crate::expr::ConditionExpr::And(exprs) => exprs.iter().any(|e| conjunct(e, once)),
249            _ => false,
250        }
251    }
252    let lines: Vec<&str> = status
253        .lines()
254        .map(str::trim)
255        .filter(|l| !l.is_empty())
256        .collect();
257    !lines.is_empty()
258        && lines.iter().all(|line| {
259            matches!(
260                crate::expr::ConditionParser::parse(line),
261                Ok(Some(expr)) if conjunct(&expr, once)
262            )
263        })
264}
265
266/// Evaluates conditions for an element or group that is known to be absent.
267///
268/// Identical to the wrapped evaluator except that the message type's
269/// [`presence_conditions`] are `False`: whatever the condition annotates is not
270/// there. Use it wherever a status is evaluated to decide whether something
271/// missing is required.
272pub struct AbsentTarget<'a, E: ConditionEvaluator + ?Sized>(pub &'a E);
273
274impl<E: ConditionEvaluator + ?Sized> ConditionEvaluator for AbsentTarget<'_, E> {
275    fn evaluate(&self, condition: u32, ctx: &EvaluationContext) -> ConditionResult {
276        if presence_conditions(self.0.message_type()).contains(&condition) {
277            return ConditionResult::False;
278        }
279        self.0.evaluate(condition, ctx)
280    }
281
282    fn is_external(&self, condition: u32) -> bool {
283        self.0.is_external(condition)
284    }
285
286    fn is_known(&self, condition: u32) -> bool {
287        self.0.is_known(condition)
288    }
289
290    fn message_type(&self) -> &str {
291        self.0.message_type()
292    }
293
294    fn format_version(&self) -> &str {
295        self.0.format_version()
296    }
297}
298
299/// Evaluates conditions for an element or group that is known to be present,
300/// to decide whether it is allowed there.
301///
302/// The message type's [`presence_conditions`] are `True`: whatever the
303/// condition annotates is there. Only the AHB's conditions proper (`[1]` to
304/// `[499]`) decide; notes (`[500]`–`[899]`), formats (`[900]`–`[999]`) and
305/// repetition rules (`[2000]` and up: "genau einmal je SG8 …") say nothing about
306/// whether something may be sent, and are `Unknown` — so `[197] ∧ [2308]` is
307/// false when `[197]` is, and `[2317] ⊻ [2318]` is never.
308pub struct PresentTarget<'a, E: ConditionEvaluator + ?Sized>(pub &'a E);
309
310impl<E: ConditionEvaluator + ?Sized> ConditionEvaluator for PresentTarget<'_, E> {
311    fn evaluate(&self, condition: u32, ctx: &EvaluationContext) -> ConditionResult {
312        if presence_conditions(self.0.message_type()).contains(&condition) {
313            return ConditionResult::True;
314        }
315        if !(1..500).contains(&condition) {
316            return ConditionResult::Unknown;
317        }
318        self.0.evaluate(condition, ctx)
319    }
320
321    fn is_external(&self, condition: u32) -> bool {
322        self.0.is_external(condition)
323    }
324
325    fn is_known(&self, condition: u32) -> bool {
326        self.0.is_known(condition)
327    }
328
329    fn message_type(&self) -> &str {
330        self.0.message_type()
331    }
332
333    fn format_version(&self) -> &str {
334        self.0.format_version()
335    }
336}
337
338/// Whether something present under `status` is not allowed where `ctx` is: a
339/// `Muss`/`X` status with a condition that evaluates to false through
340/// [`PresentTarget`]. `Soll`/`Kann`, an unconditional status, an undecidable
341/// condition, a package (`[19P1..1]`, judged by its own check) and a status of
342/// several lines refuse nothing.
343pub fn refuses_presence<E: ConditionEvaluator + ?Sized>(
344    status: &str,
345    evaluator: &E,
346    ctx: &EvaluationContext,
347    ub_definitions: &std::collections::BTreeMap<String, crate::expr::ConditionExpr>,
348) -> bool {
349    refuses(status, &PresentTarget(evaluator), ctx, ub_definitions)
350}
351
352/// The repetition conditions of a message type whose evaluators count what
353/// they limit in the group they name, so that a false one means "sent too
354/// often there": ORDERS `[2005]` / `[2006]`, "Pro SG29 LIN ist die SG34
355/// RFF+Z09 (Gerätenummer) genau einmal / bis zu dreimal anzugeben". Most
356/// `[2000]`+ evaluators still count over the whole message; a condition joins
357/// this list once its evaluator is verified to count per instance.
358pub fn counted_repetitions(message_type: &str) -> &'static [u32] {
359    match message_type {
360        "ORDERS" => &[2005, 2006],
361        _ => &[],
362    }
363}
364
365/// The repetition conditions of a message type that set how often a segment
366/// is sent in its group instance where they apply, as `(condition, min,
367/// max)`: UTILMD Strom `[2317]` "… CAV+MIW/MPW/MUW vorhanden, ist das Segment
368/// mindestens zweimal anzugeben" and `[2318]` "… CAV+MBW vorhanden, … genau
369/// einmal" on the Wandler's CAV+Z30. Their evaluators say whether one applies
370/// there (which also makes the segment required); the count is judged by
371/// [`breaks_repetition_limit`].
372pub fn repetition_limits(message_type: &str) -> &'static [(u32, usize, Option<usize>)] {
373    match message_type {
374        "UTILMD_Strom" => &[(2317, 2, None), (2318, 1, Some(1))],
375        _ => &[],
376    }
377}
378
379/// Whether `count` segments under `status` break a [`repetition_limits`]
380/// condition the status names and that applies where `ctx` is.
381pub fn breaks_repetition_limit<E: ConditionEvaluator + ?Sized>(
382    status: &str,
383    evaluator: &E,
384    ctx: &EvaluationContext,
385    count: usize,
386) -> bool {
387    repetition_limits(evaluator.message_type())
388        .iter()
389        .filter(|(id, _, _)| status.contains(&format!("[{id}]")))
390        .any(|&(id, min, max)| {
391            evaluator.evaluate(id, ctx) == ConditionResult::True
392                && (count < min || max.is_some_and(|max| count > max))
393        })
394}
395
396/// [`PresentTarget`], except that the message type's
397/// [`counted_repetitions`] are evaluated.
398struct CountTarget<'a, E: ConditionEvaluator + ?Sized>(&'a E);
399
400impl<E: ConditionEvaluator + ?Sized> ConditionEvaluator for CountTarget<'_, E> {
401    fn evaluate(&self, condition: u32, ctx: &EvaluationContext) -> ConditionResult {
402        if counted_repetitions(self.0.message_type()).contains(&condition) {
403            return self.0.evaluate(condition, ctx);
404        }
405        PresentTarget(self.0).evaluate(condition, ctx)
406    }
407
408    fn is_external(&self, condition: u32) -> bool {
409        self.0.is_external(condition)
410    }
411
412    fn is_known(&self, condition: u32) -> bool {
413        self.0.is_known(condition)
414    }
415
416    fn message_type(&self) -> &str {
417        self.0.message_type()
418    }
419
420    fn format_version(&self) -> &str {
421        self.0.format_version()
422    }
423}
424
425/// Whether something present under `status` is sent more often than it allows
426/// where `ctx` is: [`refuses_presence`] does not refuse it, but does once the
427/// message type's [`counted_repetitions`] are evaluated. ORDERS 17009's SG34
428/// `Muss ([102] ∧ [2006]) ⊻ ([103] ∧ [2005])` holds for the second RFF+Z09
429/// of a Zähleinrichtung as far as presence goes; `[2005]` refuses it.
430pub fn refuses_count<E: ConditionEvaluator + ?Sized>(
431    status: &str,
432    evaluator: &E,
433    ctx: &EvaluationContext,
434    ub_definitions: &std::collections::BTreeMap<String, crate::expr::ConditionExpr>,
435) -> bool {
436    let counted = counted_repetitions(evaluator.message_type());
437    let names_one = status.split('[').skip(1).any(|term| {
438        term.split(']')
439            .next()
440            .and_then(|id| id.parse::<u32>().ok())
441            .is_some_and(|id| counted.contains(&id))
442    });
443    names_one
444        && !refuses_presence(status, evaluator, ctx, ub_definitions)
445        && refuses(status, &CountTarget(evaluator), ctx, ub_definitions)
446}
447
448/// Whether a `Muss`/`X` `status` evaluates to false through `target`; see
449/// [`refuses_presence`] for what refuses nothing.
450fn refuses<E: ConditionEvaluator>(
451    status: &str,
452    target: &E,
453    ctx: &EvaluationContext,
454    ub_definitions: &std::collections::BTreeMap<String, crate::expr::ConditionExpr>,
455) -> bool {
456    let status = status.trim();
457    let package = status.split('[').skip(1).any(|term| {
458        term.trim_start_matches(|c: char| c.is_ascii_digit())
459            .starts_with('P')
460    });
461    if status.contains('\n') || !status.contains('[') || package {
462        return false;
463    }
464    let keyword = &status[..status
465        .find(|c: char| !c.is_alphabetic())
466        .unwrap_or(status.len())];
467    if !matches!(keyword, "Muss" | "X") {
468        return false;
469    }
470    super::ConditionExprEvaluator::new(target).evaluate_status_with_ub(status, ctx, ub_definitions)
471        == ConditionResult::False
472}
473
474#[cfg(test)]
475mod tests {
476    use super::*;
477
478    struct AlwaysTrue(&'static str);
479
480    impl ConditionEvaluator for AlwaysTrue {
481        fn evaluate(&self, _: u32, _: &EvaluationContext) -> ConditionResult {
482            ConditionResult::True
483        }
484        fn is_external(&self, _: u32) -> bool {
485            false
486        }
487        fn message_type(&self) -> &str {
488            self.0
489        }
490        fn format_version(&self) -> &str {
491            "FV2604"
492        }
493    }
494
495    #[test]
496    fn once_per_transaction_binds_where_every_line_has_it() {
497        assert!(binds_once("Muss [483] ∧ [2061]", 2061));
498        assert!(binds_once("Muss [2061]", 2061));
499        assert!(binds_once(
500            "Muss ([77] ∧ [78]) ∧ [347] ∧ [2061]\r\nKann [2061]",
501            2061
502        ));
503        assert!(!binds_once("Soll [165] ∧ (([2061] ∧ [583]) ∨ [584])", 2061));
504        assert!(!binds_once(
505            "Muss ([32] ∧ [2061] ∧ [651]) ⊻ ([200] ∧ [601])",
506            2061
507        ));
508        assert!(!binds_once("Muss [483]", 2061));
509        assert!(!binds_once("", 2061));
510    }
511
512    #[test]
513    fn absent_target_answers_only_the_presence_condition_with_false() {
514        let external = NoOpExternalProvider;
515        let ctx = EvaluationContext::new("55043", &external, &[]);
516        let inner = AlwaysTrue("UTILMD_Strom");
517        let absent = AbsentTarget(&inner);
518        assert_eq!(absent.evaluate(166, &ctx), ConditionResult::False);
519        assert_eq!(absent.evaluate(674, &ctx), ConditionResult::True);
520        // [2003]: one group per ruhende Marktlokation the sender knows of.
521        assert_eq!(absent.evaluate(2003, &ctx), ConditionResult::False);
522
523        // [166] of another message type is an unrelated condition.
524        let invoic = AlwaysTrue("INVOIC");
525        assert_eq!(
526            AbsentTarget(&invoic).evaluate(166, &ctx),
527            ConditionResult::True
528        );
529        assert_eq!(
530            AbsentTarget(&invoic).evaluate(22, &ctx),
531            ConditionResult::False
532        );
533    }
534
535    #[test]
536    fn test_condition_result_is_methods() {
537        assert!(ConditionResult::True.is_true());
538        assert!(!ConditionResult::True.is_false());
539        assert!(!ConditionResult::True.is_unknown());
540
541        assert!(!ConditionResult::False.is_true());
542        assert!(ConditionResult::False.is_false());
543
544        assert!(ConditionResult::Unknown.is_unknown());
545    }
546
547    #[test]
548    fn three_valued_and_or_not() {
549        use ConditionResult::{False as F, True as T, Unknown as U};
550        assert_eq!(T.and(T), T);
551        assert_eq!(T.and(U), U);
552        assert_eq!(U.and(F), F);
553        assert_eq!(F.or(F), F);
554        assert_eq!(F.or(U), U);
555        assert_eq!(U.or(T), T);
556        assert_eq!(U.negate(), U);
557        assert_eq!(T.negate(), F);
558    }
559
560    #[test]
561    fn test_condition_result_to_option() {
562        assert_eq!(ConditionResult::True.to_option(), Some(true));
563        assert_eq!(ConditionResult::False.to_option(), Some(false));
564        assert_eq!(ConditionResult::Unknown.to_option(), None);
565    }
566
567    #[test]
568    fn test_condition_result_from_bool() {
569        assert_eq!(ConditionResult::from(true), ConditionResult::True);
570        assert_eq!(ConditionResult::from(false), ConditionResult::False);
571    }
572
573    #[test]
574    fn test_condition_result_display() {
575        assert_eq!(format!("{}", ConditionResult::True), "True");
576        assert_eq!(format!("{}", ConditionResult::False), "False");
577        assert_eq!(format!("{}", ConditionResult::Unknown), "Unknown");
578    }
579
580    #[test]
581    fn test_noop_external_provider() {
582        let provider = NoOpExternalProvider;
583        assert_eq!(
584            provider.evaluate("MessageSplitting"),
585            ConditionResult::Unknown
586        );
587        assert_eq!(provider.evaluate("anything"), ConditionResult::Unknown);
588    }
589}