automapper-validation 0.12.0

AHB condition expression parsing, evaluation, and EDIFACT validation
Documentation
//! Core condition evaluation traits.

use super::context::EvaluationContext;

/// Three-valued result of evaluating a single condition.
///
/// Unlike the C# implementation which uses `bool`, we use three-valued logic
/// to support partial evaluation when external conditions are unavailable.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum ConditionResult {
    /// The condition is satisfied.
    True,
    /// The condition is not satisfied.
    False,
    /// The condition cannot be determined (e.g., external condition without a provider).
    Unknown,
}

impl ConditionResult {
    /// Returns `true` if this is `ConditionResult::True`.
    pub fn is_true(self) -> bool {
        matches!(self, ConditionResult::True)
    }

    /// Returns `true` if this is `ConditionResult::False`.
    pub fn is_false(self) -> bool {
        matches!(self, ConditionResult::False)
    }

    /// Returns `true` if this is `ConditionResult::Unknown`.
    pub fn is_unknown(self) -> bool {
        matches!(self, ConditionResult::Unknown)
    }

    /// Three-valued AND: `False` if either is, `True` if both are, else `Unknown`.
    pub fn and(self, other: ConditionResult) -> ConditionResult {
        match (self, other) {
            (ConditionResult::False, _) | (_, ConditionResult::False) => ConditionResult::False,
            (ConditionResult::True, ConditionResult::True) => ConditionResult::True,
            _ => ConditionResult::Unknown,
        }
    }

    /// Three-valued OR: `True` if either is, `False` if both are, else `Unknown`.
    pub fn or(self, other: ConditionResult) -> ConditionResult {
        match (self, other) {
            (ConditionResult::True, _) | (_, ConditionResult::True) => ConditionResult::True,
            (ConditionResult::False, ConditionResult::False) => ConditionResult::False,
            _ => ConditionResult::Unknown,
        }
    }

    /// Three-valued NOT.
    pub fn negate(self) -> ConditionResult {
        match self {
            ConditionResult::True => ConditionResult::False,
            ConditionResult::False => ConditionResult::True,
            ConditionResult::Unknown => ConditionResult::Unknown,
        }
    }

    /// Converts to `Option<bool>`: True -> Some(true), False -> Some(false), Unknown -> None.
    pub fn to_option(self) -> Option<bool> {
        match self {
            ConditionResult::True => Some(true),
            ConditionResult::False => Some(false),
            ConditionResult::Unknown => None,
        }
    }
}

impl From<bool> for ConditionResult {
    fn from(value: bool) -> Self {
        if value {
            ConditionResult::True
        } else {
            ConditionResult::False
        }
    }
}

impl std::fmt::Display for ConditionResult {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            ConditionResult::True => write!(f, "True"),
            ConditionResult::False => write!(f, "False"),
            ConditionResult::Unknown => write!(f, "Unknown"),
        }
    }
}

/// Evaluates individual AHB conditions by number.
///
/// Implementations are typically generated from AHB XML schemas (one per
/// message type and format version). Each condition number maps to a
/// specific business rule check.
pub trait ConditionEvaluator: Send + Sync {
    /// Evaluate a single condition by number.
    ///
    /// Returns `ConditionResult::Unknown` for unrecognized condition numbers
    /// or conditions that require unavailable external context.
    fn evaluate(&self, condition: u32, ctx: &EvaluationContext) -> ConditionResult;

    /// Returns `true` if the given condition requires external context
    /// (i.e., cannot be determined from the EDIFACT message alone).
    fn is_external(&self, condition: u32) -> bool;

    /// Returns `true` if this evaluator has an implementation for the given
    /// condition number (whether internal or external). Conditions that fall
    /// through to the `_ => Unknown` wildcard return `false`.
    ///
    /// This allows distinguishing "implemented but returned Unknown because
    /// the relevant data isn't present in the message" from "not implemented
    /// at all".
    fn is_known(&self, _condition: u32) -> bool {
        false
    }

    /// Returns the message type this evaluator handles (e.g., "UTILMD").
    fn message_type(&self) -> &str;

    /// Returns the format version this evaluator handles (e.g., "FV2510").
    fn format_version(&self) -> &str;
}

impl<T: ConditionEvaluator + ?Sized> ConditionEvaluator for std::sync::Arc<T> {
    fn evaluate(&self, condition: u32, ctx: &EvaluationContext) -> ConditionResult {
        (**self).evaluate(condition, ctx)
    }

    fn is_external(&self, condition: u32) -> bool {
        (**self).is_external(condition)
    }

    fn is_known(&self, condition: u32) -> bool {
        (**self).is_known(condition)
    }

    fn message_type(&self) -> &str {
        (**self).message_type()
    }

    fn format_version(&self) -> &str {
        (**self).format_version()
    }
}

/// Provider for external conditions that depend on context outside the EDIFACT message.
///
/// External conditions are things like:
/// - [1] "Wenn Aufteilung vorhanden" (message splitting status)
/// - [14] "Wenn Datum bekannt" (whether a date is known)
/// - [30] "Wenn Antwort auf Aktivierung" (response to activation)
///
/// These cannot be determined from the EDIFACT content alone and require
/// business context from the calling system.
pub trait ExternalConditionProvider: Send + Sync {
    /// Evaluate an external condition by name.
    ///
    /// The `condition_name` corresponds to the speaking name from the
    /// generated external conditions constants (e.g., "MessageSplitting",
    /// "DateKnown").
    fn evaluate(&self, condition_name: &str) -> ConditionResult;
}

/// A no-op external condition provider that returns `Unknown` for everything.
///
/// Useful when no external context is available — conditions will propagate
/// as `Unknown` through the expression evaluator.
pub struct NoOpExternalProvider;

impl ExternalConditionProvider for NoOpExternalProvider {
    fn evaluate(&self, _condition_name: &str) -> ConditionResult {
        ConditionResult::Unknown
    }
}

/// The number of a message type's bare "Wenn vorhanden" condition, if it has one.
///
/// The condition is self-referential: its value is whether the element or
/// group it annotates is present. `Soll [166]` on a group means "send it if
/// you have it", which only the sender knows, so an absent group annotated
/// with it is never missing. The generated evaluators answer it with `True`
/// ("the rule applies wherever it is evaluated"), which is right for a present
/// element and wrong for an absent one — see [`AbsentTarget`].
///
/// The numbers are stable across every format version in
/// `xml-migs-and-ahbs/` (FV2410–FV2610); keyed by [`ConditionEvaluator::message_type`]
/// rather than by evaluator so aliased and regenerated evaluators need no edit.
pub fn presence_condition(message_type: &str) -> Option<u32> {
    match message_type {
        "UTILMD_Strom" | "UTILMD_Gas" => Some(166),
        "INVOIC" => Some(22),
        "ORDERS" => Some(12),
        _ => None,
    }
}

/// Evaluates conditions for an element or group that is known to be absent.
///
/// Identical to the wrapped evaluator except that the message type's
/// [`presence_condition`] is `False`: whatever the condition annotates is not
/// there. Use it wherever a status is evaluated to decide whether something
/// missing is required.
pub struct AbsentTarget<'a, E: ConditionEvaluator + ?Sized>(pub &'a E);

impl<E: ConditionEvaluator + ?Sized> ConditionEvaluator for AbsentTarget<'_, E> {
    fn evaluate(&self, condition: u32, ctx: &EvaluationContext) -> ConditionResult {
        if presence_condition(self.0.message_type()) == Some(condition) {
            return ConditionResult::False;
        }
        self.0.evaluate(condition, ctx)
    }

    fn is_external(&self, condition: u32) -> bool {
        self.0.is_external(condition)
    }

    fn is_known(&self, condition: u32) -> bool {
        self.0.is_known(condition)
    }

    fn message_type(&self) -> &str {
        self.0.message_type()
    }

    fn format_version(&self) -> &str {
        self.0.format_version()
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    struct AlwaysTrue(&'static str);

    impl ConditionEvaluator for AlwaysTrue {
        fn evaluate(&self, _: u32, _: &EvaluationContext) -> ConditionResult {
            ConditionResult::True
        }
        fn is_external(&self, _: u32) -> bool {
            false
        }
        fn message_type(&self) -> &str {
            self.0
        }
        fn format_version(&self) -> &str {
            "FV2604"
        }
    }

    #[test]
    fn absent_target_answers_only_the_presence_condition_with_false() {
        let external = NoOpExternalProvider;
        let ctx = EvaluationContext::new("55043", &external, &[]);
        let inner = AlwaysTrue("UTILMD_Strom");
        let absent = AbsentTarget(&inner);
        assert_eq!(absent.evaluate(166, &ctx), ConditionResult::False);
        assert_eq!(absent.evaluate(674, &ctx), ConditionResult::True);

        // [166] of another message type is an unrelated condition.
        let invoic = AlwaysTrue("INVOIC");
        assert_eq!(
            AbsentTarget(&invoic).evaluate(166, &ctx),
            ConditionResult::True
        );
        assert_eq!(
            AbsentTarget(&invoic).evaluate(22, &ctx),
            ConditionResult::False
        );
    }

    #[test]
    fn test_condition_result_is_methods() {
        assert!(ConditionResult::True.is_true());
        assert!(!ConditionResult::True.is_false());
        assert!(!ConditionResult::True.is_unknown());

        assert!(!ConditionResult::False.is_true());
        assert!(ConditionResult::False.is_false());

        assert!(ConditionResult::Unknown.is_unknown());
    }

    #[test]
    fn three_valued_and_or_not() {
        use ConditionResult::{False as F, True as T, Unknown as U};
        assert_eq!(T.and(T), T);
        assert_eq!(T.and(U), U);
        assert_eq!(U.and(F), F);
        assert_eq!(F.or(F), F);
        assert_eq!(F.or(U), U);
        assert_eq!(U.or(T), T);
        assert_eq!(U.negate(), U);
        assert_eq!(T.negate(), F);
    }

    #[test]
    fn test_condition_result_to_option() {
        assert_eq!(ConditionResult::True.to_option(), Some(true));
        assert_eq!(ConditionResult::False.to_option(), Some(false));
        assert_eq!(ConditionResult::Unknown.to_option(), None);
    }

    #[test]
    fn test_condition_result_from_bool() {
        assert_eq!(ConditionResult::from(true), ConditionResult::True);
        assert_eq!(ConditionResult::from(false), ConditionResult::False);
    }

    #[test]
    fn test_condition_result_display() {
        assert_eq!(format!("{}", ConditionResult::True), "True");
        assert_eq!(format!("{}", ConditionResult::False), "False");
        assert_eq!(format!("{}", ConditionResult::Unknown), "Unknown");
    }

    #[test]
    fn test_noop_external_provider() {
        let provider = NoOpExternalProvider;
        assert_eq!(
            provider.evaluate("MessageSplitting"),
            ConditionResult::Unknown
        );
        assert_eq!(provider.evaluate("anything"), ConditionResult::Unknown);
    }
}