automapper-validation 0.16.0

AHB condition expression parsing, evaluation, and EDIFACT validation
Documentation
//! AHB status lines: the status word (`Muss`/`Soll`/`Kann`) and its condition.
//!
//! An AHB cell can carry several lines, each with a status word of its own:
//!
//! ```text
//! Muss [2119]
//! Soll [130]
//! ```
//!
//! "Required if [2119] holds, recommended if [130] holds." Each line is one
//! [`StatusBranch`]; `ConditionExprEvaluator::resolve_status` decides which
//! applies.

use super::ast::ConditionExpr;

/// The status word of one AHB status line. `X` is `Muss`.
///
/// Ordered by strictness: `Kann < Soll < Muss`.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum StatusKind {
    Kann,
    Soll,
    Muss,
}

impl StatusKind {
    /// The status word `line` starts with, if any.
    pub fn of(line: &str) -> Option<Self> {
        let line = line.trim_start();
        if line.starts_with("Muss") || line.starts_with('X') {
            Some(Self::Muss)
        } else if line.starts_with("Soll") {
            Some(Self::Soll)
        } else if line.starts_with("Kann") {
            Some(Self::Kann)
        } else {
            None
        }
    }
}

/// One line of an AHB status: its status word and its condition.
///
/// `kind` is `None` for a line without a status word (a bare `[1]`);
/// `condition` is `None` for a line without a condition (`Kann`).
#[derive(Debug, Clone, PartialEq)]
pub struct StatusBranch {
    pub kind: Option<StatusKind>,
    pub condition: Option<ConditionExpr>,
}

/// Split an AHB status into its status lines.
///
/// Lines are separated by line breaks, and also by a status word in the middle
/// of a line: IFTSTA writes some two-status cells as `Muss [56] ∧ [59] Soll [70]`.
///
/// Only a status word outside parentheses starts a line. The PID schema joins
/// nested group statuses into one parent status, which parenthesises such a
/// cell (`Muss ([525]) ∧ ([56] ∧ [59] Soll [70]) ∧ ([54])`); the word inside is
/// left to the tokenizer, which skips it.
pub(crate) fn status_lines(input: &str) -> Vec<&str> {
    let mut out = Vec::new();
    for line in input.lines() {
        let mut rest = line.trim();
        while !rest.is_empty() {
            let mut depth = 0usize;
            let split = rest
                .char_indices()
                .find(|&(i, c)| {
                    match c {
                        '(' => depth += 1,
                        ')' => depth = depth.saturating_sub(1),
                        _ => {}
                    }
                    i > 0
                        && depth == 0
                        && rest[..i].ends_with(char::is_whitespace)
                        && ["Muss", "Soll", "Kann"]
                            .iter()
                            .any(|w| rest[i..].starts_with(w))
                })
                .map_or(rest.len(), |(i, _)| i);
            out.push(rest[..split].trim());
            rest = rest[split..].trim_start();
        }
    }
    out
}