Skip to main content

crue_engine/
ir.rs

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