1use std::fmt;
14
15use crate::context::RuntimeContext;
16use crate::envelope::CommandEnvelope;
17use crate::error::RuntimeResult;
18use crate::registry::NameRegistry;
19
20#[derive(Debug, Clone, PartialEq, Eq)]
22pub enum DecisionOutcome {
23 Allow,
25 Deny(String),
27 Defer(String),
29}
30
31pub trait DecisionNode: Send + Sync {
33 fn name(&self) -> &str;
35 fn decide(
37 &self,
38 command: &CommandEnvelope,
39 context: &dyn RuntimeContext,
40 ) -> RuntimeResult<DecisionOutcome>;
41}
42
43#[derive(Debug, Default)]
45pub struct DecisionRegistry {
46 names: NameRegistry<String>,
47}
48
49impl DecisionRegistry {
50 pub fn new() -> Self {
52 Self::default()
53 }
54
55 pub fn register<N>(&mut self, node: &N) -> RuntimeResult<()>
57 where
58 N: DecisionNode,
59 {
60 self.register_name(node.name())
61 }
62
63 pub fn register_name(&mut self, name: &str) -> RuntimeResult<()> {
65 self.names.register(name.to_string(), "decision")
66 }
67
68 pub fn contains(&self, name: &str) -> bool {
70 self.names.list().iter().any(|item| item == name)
71 }
72
73 pub fn len(&self) -> usize {
75 self.names.len()
76 }
77
78 pub fn is_empty(&self) -> bool {
80 self.names.is_empty()
81 }
82
83 pub fn list(&self) -> &[String] {
85 self.names.list()
86 }
87}
88
89#[derive(Default)]
91pub struct DecisionEngine {
92 nodes: Vec<Box<dyn DecisionNode + Send + Sync>>,
93 node_names: Vec<String>,
94}
95
96impl fmt::Debug for DecisionEngine {
97 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
98 f.debug_struct("DecisionEngine")
99 .field("node_count", &self.nodes.len())
100 .field("node_names", &self.node_names)
101 .finish()
102 }
103}
104
105impl DecisionEngine {
106 pub fn new() -> Self {
108 Self::default()
109 }
110
111 pub fn register_node<N: DecisionNode + 'static>(&mut self, node: N) -> RuntimeResult<()> {
113 self.node_names.push(node.name().to_string());
114 self.nodes.push(Box::new(node));
115 Ok(())
116 }
117
118 pub fn len(&self) -> usize {
120 self.nodes.len()
121 }
122
123 pub fn is_empty(&self) -> bool {
125 self.nodes.is_empty()
126 }
127
128 pub fn node_names(&self) -> &[String] {
130 &self.node_names
131 }
132
133 pub fn evaluate(
135 &self,
136 command: &CommandEnvelope,
137 context: &dyn RuntimeContext,
138 ) -> RuntimeResult<DecisionOutcome> {
139 if self.nodes.is_empty() {
140 return Ok(DecisionOutcome::Allow);
141 }
142
143 for node in &self.nodes {
144 match node.decide(command, context)? {
145 DecisionOutcome::Allow => {}
146 DecisionOutcome::Deny(message) => return Ok(DecisionOutcome::Deny(message)),
147 DecisionOutcome::Defer(message) => return Ok(DecisionOutcome::Defer(message)),
148 }
149 }
150
151 Ok(DecisionOutcome::Allow)
152 }
153}
154
155#[cfg(test)]
156#[path = "decision_tests.rs"]
157mod tests;