1use std::fmt;
14
15use crate::context::RuntimeContext;
16use crate::envelope::CommandEnvelope;
17use crate::error::{RuntimeError, RuntimeResult};
18use crate::registry::NameRegistry;
19
20pub const MAX_DECISION_NODES: usize = 4096;
22pub const MAX_DECISION_NAME_BYTES: usize = 256;
24
25#[derive(Debug, Clone, PartialEq, Eq)]
27pub enum DecisionOutcome {
28 Allow,
30 Deny(String),
32 Defer(String),
34}
35
36pub trait DecisionNode: Send + Sync {
38 fn name(&self) -> &str;
40 fn decide(
42 &self,
43 command: &CommandEnvelope,
44 context: &dyn RuntimeContext,
45 ) -> RuntimeResult<DecisionOutcome>;
46}
47
48#[derive(Debug, Default)]
50pub struct DecisionRegistry {
51 names: NameRegistry<String>,
52}
53
54impl DecisionRegistry {
55 pub fn new() -> Self {
57 Self::default()
58 }
59
60 pub fn register<N>(&mut self, node: &N) -> RuntimeResult<()>
62 where
63 N: DecisionNode,
64 {
65 self.register_name(node.name())
66 }
67
68 pub fn register_name(&mut self, name: &str) -> RuntimeResult<()> {
70 validate_registration(name, self.names.list())?;
71 self.names.register(name.to_string(), "decision")
72 }
73
74 pub fn contains(&self, name: &str) -> bool {
76 self.names.list().iter().any(|item| item == name)
77 }
78
79 pub fn len(&self) -> usize {
81 self.names.len()
82 }
83
84 pub fn is_empty(&self) -> bool {
86 self.names.is_empty()
87 }
88
89 pub fn list(&self) -> &[String] {
91 self.names.list()
92 }
93}
94
95#[derive(Default)]
97pub struct DecisionEngine {
98 nodes: Vec<Box<dyn DecisionNode + Send + Sync>>,
99 node_names: Vec<String>,
100}
101
102impl fmt::Debug for DecisionEngine {
103 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
104 f.debug_struct("DecisionEngine")
105 .field("node_count", &self.nodes.len())
106 .field("node_names", &self.node_names)
107 .finish()
108 }
109}
110
111impl DecisionEngine {
112 pub fn new() -> Self {
114 Self::default()
115 }
116
117 pub fn register_node<N: DecisionNode + 'static>(&mut self, node: N) -> RuntimeResult<()> {
120 let name = node.name();
121 validate_registration(name, &self.node_names)?;
122 self.node_names.push(name.to_string());
123 self.nodes.push(Box::new(node));
124 Ok(())
125 }
126
127 pub fn len(&self) -> usize {
129 self.nodes.len()
130 }
131
132 pub fn is_empty(&self) -> bool {
134 self.nodes.is_empty()
135 }
136
137 pub fn node_names(&self) -> &[String] {
139 &self.node_names
140 }
141
142 pub fn evaluate(
144 &self,
145 command: &CommandEnvelope,
146 context: &dyn RuntimeContext,
147 ) -> RuntimeResult<DecisionOutcome> {
148 if self.nodes.is_empty() {
149 return Ok(DecisionOutcome::Allow);
150 }
151
152 for node in &self.nodes {
153 match node.decide(command, context)? {
154 DecisionOutcome::Allow => {}
155 DecisionOutcome::Deny(message) => return Ok(DecisionOutcome::Deny(message)),
156 DecisionOutcome::Defer(message) => return Ok(DecisionOutcome::Defer(message)),
157 }
158 }
159
160 Ok(DecisionOutcome::Allow)
161 }
162}
163
164fn validate_registration(name: &str, existing: &[String]) -> RuntimeResult<()> {
165 if name.is_empty() || name.len() > MAX_DECISION_NAME_BYTES {
166 return Err(RuntimeError::InvalidRequest {
167 kind: "decision",
168 reason: "decision name must contain 1..=256 UTF-8 bytes",
169 });
170 }
171 if existing.iter().any(|registered| registered == name) {
172 return Err(RuntimeError::DuplicateRegistryItem {
173 kind: "decision".into(),
174 });
175 }
176 if existing.len() >= MAX_DECISION_NODES {
177 return Err(RuntimeError::InvalidRequest {
178 kind: "decision",
179 reason: "decision registration capacity exceeded",
180 });
181 }
182 Ok(())
183}
184
185#[cfg(test)]
186#[path = "decision_tests.rs"]
187mod tests;