automapper_validation/expr/status.rs
1//! AHB status lines: the status word (`Muss`/`Soll`/`Kann`) and its condition.
2//!
3//! An AHB cell can carry several lines, each with a status word of its own:
4//!
5//! ```text
6//! Muss [2119]
7//! Soll [130]
8//! ```
9//!
10//! "Required if [2119] holds, recommended if [130] holds." Each line is one
11//! [`StatusBranch`]; `ConditionExprEvaluator::resolve_status` decides which
12//! applies.
13
14use super::ast::ConditionExpr;
15
16/// The status word of one AHB status line. `X` is `Muss`.
17///
18/// Ordered by strictness: `Kann < Soll < Muss`.
19#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
20pub enum StatusKind {
21 Kann,
22 Soll,
23 Muss,
24}
25
26impl StatusKind {
27 /// The status word `line` starts with, if any.
28 pub fn of(line: &str) -> Option<Self> {
29 let line = line.trim_start();
30 if line.starts_with("Muss") || line.starts_with('X') {
31 Some(Self::Muss)
32 } else if line.starts_with("Soll") {
33 Some(Self::Soll)
34 } else if line.starts_with("Kann") {
35 Some(Self::Kann)
36 } else {
37 None
38 }
39 }
40}
41
42/// One line of an AHB status: its status word and its condition.
43///
44/// `kind` is `None` for a line without a status word (a bare `[1]`);
45/// `condition` is `None` for a line without a condition (`Kann`).
46#[derive(Debug, Clone, PartialEq)]
47pub struct StatusBranch {
48 pub kind: Option<StatusKind>,
49 pub condition: Option<ConditionExpr>,
50}
51
52/// Split an AHB status into its status lines.
53///
54/// Lines are separated by line breaks, and also by a status word in the middle
55/// of a line: IFTSTA writes some two-status cells as `Muss [56] ∧ [59] Soll [70]`.
56///
57/// Only a status word outside parentheses starts a line. The PID schema joins
58/// nested group statuses into one parent status, which parenthesises such a
59/// cell (`Muss ([525]) ∧ ([56] ∧ [59] Soll [70]) ∧ ([54])`); the word inside is
60/// left to the tokenizer, which skips it.
61pub(crate) fn status_lines(input: &str) -> Vec<&str> {
62 let mut out = Vec::new();
63 for line in input.lines() {
64 let mut rest = line.trim();
65 while !rest.is_empty() {
66 let mut depth = 0usize;
67 let split = rest
68 .char_indices()
69 .find(|&(i, c)| {
70 match c {
71 '(' => depth += 1,
72 ')' => depth = depth.saturating_sub(1),
73 _ => {}
74 }
75 i > 0
76 && depth == 0
77 && rest[..i].ends_with(char::is_whitespace)
78 && ["Muss", "Soll", "Kann"]
79 .iter()
80 .any(|w| rest[i..].starts_with(w))
81 })
82 .map_or(rest.len(), |(i, _)| i);
83 out.push(rest[..split].trim());
84 rest = rest[split..].trim_start();
85 }
86 }
87 out
88}