Skip to main content

crue_engine/
ir.rs

1//! Typed rule IR for compiled-policy execution paths.
2
3use crate::error::EngineError;
4use crate::decision::Decision;
5use crue_dsl::ast::ActionNode;
6use crue_dsl::compiler::{ActionDecision as DslActionDecision, ActionInstruction as DslActionInstruction};
7use serde::{Deserialize, Serialize};
8
9/// Typed comparison operators for deterministic rule evaluation.
10#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
11pub enum Operator {
12    Eq,
13    Ne,
14    Gt,
15    Lt,
16    Gte,
17    Lte,
18}
19
20impl Operator {
21    pub fn parse(op: &str) -> Result<Self, EngineError> {
22        match op {
23            "==" => Ok(Self::Eq),
24            "!=" => Ok(Self::Ne),
25            ">" => Ok(Self::Gt),
26            "<" => Ok(Self::Lt),
27            ">=" => Ok(Self::Gte),
28            "<=" => Ok(Self::Lte),
29            _ => Err(EngineError::InvalidOperator(op.to_string())),
30        }
31    }
32}
33
34/// Typed legacy action kind to avoid stringly dispatch in the engine runtime.
35#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
36pub enum ActionKind {
37    Block,
38    Warn,
39    RequireApproval,
40    Log,
41}
42
43impl ActionKind {
44    pub fn parse(action: &str) -> Result<Self, EngineError> {
45        match action {
46            "BLOCK" => Ok(Self::Block),
47            "WARN" => Ok(Self::Warn),
48            "REQUIRE_APPROVAL" => Ok(Self::RequireApproval),
49            "LOG" => Ok(Self::Log),
50            _ => Err(EngineError::InvalidAction(action.to_string())),
51        }
52    }
53}
54
55/// Typed action/effect emitted by a compiled rule.
56#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
57pub enum RuleEffect {
58    Block {
59        code: String,
60        message: Option<String>,
61    },
62    Warn {
63        code: String,
64    },
65    RequireApproval {
66        code: String,
67        timeout_minutes: u32,
68    },
69    Log,
70    AlertSoc,
71}
72
73/// Explicit action VM instructions for compiled rule effects.
74#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
75pub enum ActionInstruction {
76    SetDecision(Decision),
77    SetErrorCode(String),
78    SetMessage(String),
79    SetApprovalTimeout(u32),
80    SetAlertSoc(bool),
81    Halt,
82}
83
84impl TryFrom<ActionNode> for RuleEffect {
85    type Error = EngineError;
86
87    fn try_from(value: ActionNode) -> Result<Self, Self::Error> {
88        Ok(match value {
89            ActionNode::Block { code, message } => Self::Block { code, message },
90            ActionNode::Warn { code } => Self::Warn { code },
91            ActionNode::RequireApproval {
92                code,
93                timeout_minutes,
94            } => Self::RequireApproval {
95                code,
96                timeout_minutes,
97            },
98            ActionNode::Log => Self::Log,
99            ActionNode::AlertSoc => Self::AlertSoc,
100        })
101    }
102}
103
104impl TryFrom<DslActionInstruction> for ActionInstruction {
105    type Error = EngineError;
106
107    fn try_from(value: DslActionInstruction) -> Result<Self, Self::Error> {
108        Ok(match value {
109            DslActionInstruction::SetDecision(d) => Self::SetDecision(match d {
110                DslActionDecision::Allow => Decision::Allow,
111                DslActionDecision::Block => Decision::Block,
112                DslActionDecision::Warn => Decision::Warn,
113                DslActionDecision::ApprovalRequired => Decision::ApprovalRequired,
114            }),
115            DslActionInstruction::SetErrorCode(code) => Self::SetErrorCode(code),
116            DslActionInstruction::SetMessage(msg) => Self::SetMessage(msg),
117            DslActionInstruction::SetApprovalTimeout(timeout) => Self::SetApprovalTimeout(timeout),
118            DslActionInstruction::SetAlertSoc(v) => Self::SetAlertSoc(v),
119            DslActionInstruction::Halt => Self::Halt,
120        })
121    }
122}
123
124impl RuleEffect {
125    pub fn is_alert_only(&self) -> bool {
126        matches!(self, Self::AlertSoc)
127    }
128}
129
130#[cfg(test)]
131mod tests {
132    use super::*;
133
134    #[test]
135    fn test_operator_parse() {
136        assert_eq!(Operator::parse(">=").unwrap(), Operator::Gte);
137        assert!(Operator::parse("contains").is_err());
138    }
139
140    #[test]
141    fn test_action_node_to_rule_effect() {
142        let effect = RuleEffect::try_from(ActionNode::RequireApproval {
143            code: "APPROVAL".to_string(),
144            timeout_minutes: 15,
145        })
146        .unwrap();
147
148        assert_eq!(
149            effect,
150            RuleEffect::RequireApproval {
151                code: "APPROVAL".to_string(),
152                timeout_minutes: 15,
153            }
154        );
155    }
156
157    #[test]
158    fn test_action_kind_parse() {
159        assert_eq!(ActionKind::parse("BLOCK").unwrap(), ActionKind::Block);
160        assert!(ActionKind::parse("DROP_TABLE").is_err());
161    }
162
163    #[test]
164    fn test_action_instruction_roundtrip_serde() {
165        let insn = ActionInstruction::SetDecision(Decision::Block);
166        let json = serde_json::to_string(&insn).unwrap();
167        let decoded: ActionInstruction = serde_json::from_str(&json).unwrap();
168        assert_eq!(decoded, insn);
169    }
170
171    #[test]
172    fn test_dsl_action_instruction_to_engine_action_instruction() {
173        let dsl = DslActionInstruction::SetDecision(DslActionDecision::Block);
174        let engine = ActionInstruction::try_from(dsl).unwrap();
175        assert_eq!(engine, ActionInstruction::SetDecision(Decision::Block));
176    }
177}