Skip to main content

soma_som_ring/
dispatch.rs

1// SPDX-License-Identifier: LGPL-3.0-only
2#![allow(missing_docs)]
3
4//! Abstract command dispatch and policy provider traits.
5//!
6//! These traits decouple the ring engine from application-layer organ
7//! implementations. The application provides concrete implementations
8//! that wire its organs to these abstractions.
9//!
10//! ## Design
11//!
12//! The ring engine defines abstractions; applications provide implementations.
13//! This preserves Dependency Inversion: the foundation layer does not
14//! import application-layer types.
15
16use std::fmt;
17
18// ── CommandDispatcher ──────────────────────────────────────────────
19
20/// Error type for command dispatch failures.
21#[derive(Debug)]
22#[non_exhaustive]
23pub enum CommandDispatchError {
24    /// Command type not recognized by any organ.
25    UnknownCommand(String),
26    /// Organ returned an error during execution.
27    ExecutionFailed(String),
28    /// Serialization/deserialization failure.
29    SerializationError(String),
30}
31
32impl fmt::Display for CommandDispatchError {
33    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
34        match self {
35            Self::UnknownCommand(cmd) => write!(f, "unknown command: {cmd}"),
36            Self::ExecutionFailed(msg) => write!(f, "dispatch failed: {msg}"),
37            Self::SerializationError(msg) => write!(f, "serde error: {msg}"),
38        }
39    }
40}
41
42impl std::error::Error for CommandDispatchError {}
43
44/// Abstract command dispatch — OU delegates here instead of
45/// instantiating organ managers directly.
46///
47/// The ring engine calls `dispatch()` with a command type string
48/// and serialized payload. The application layer routes to the
49/// correct organ and returns the serialized result.
50///
51/// # Example
52///
53/// ```ignore
54/// struct AppDispatcher { /* application-specific state */ }
55///
56/// impl CommandDispatcher for AppDispatcher {
57///     fn dispatch(&self, cmd: &str, payload: &[u8]) -> Result<Vec<u8>, CommandDispatchError> {
58///         match cmd {
59///             "resource.create" => { /* deserialize, call organ handler */ }
60///             _ => Err(CommandDispatchError::UnknownCommand(cmd.into())),
61///         }
62///     }
63/// }
64/// ```
65pub trait CommandDispatcher: Send + Sync {
66    /// Dispatch a command identified by type string and JSON payload.
67    /// Returns a serialized JSON result or error.
68    fn dispatch(&self, command_type: &str, payload: &[u8])
69    -> Result<Vec<u8>, CommandDispatchError>;
70}
71
72// ── PolicyProvider ─────────────────────────────────────────────────
73
74/// Abstract RBAC policy lookup — CU evaluates authorization through
75/// this trait instead of calling organ-specific functions.
76///
77/// Each method returns `None` if the command type has no policy
78/// constraint (i.e., the command is unrestricted for that dimension).
79///
80/// # AEQ dimensions
81///
82/// - **Activity Maximum**: highest activity level this command allows
83/// - **Security Floor**: minimum security level required
84/// - **Decision Tier**: how significant the action is (Routine/Significant/Critical)
85///
86/// # Example
87///
88/// ```ignore
89/// struct AppPolicyProvider { /* application-specific policy table */ }
90///
91/// impl PolicyProvider for AppPolicyProvider {
92///     fn required_permission(&self, cmd: &str) -> Option<String> {
93///         match cmd {
94///             "resource.delete" => Some("role.admin".into()),
95///             _ => None,
96///         }
97///     }
98///     // ... implement remaining methods per application policy
99/// }
100/// ```
101pub trait PolicyProvider: Send + Sync {
102    /// Required permission string for this command type, if any.
103    fn required_permission(&self, command_type: &str) -> Option<String>;
104
105    /// AEQ: maximum activity level for this command (0-4).
106    fn activity_maximum(&self, command_type: &str) -> Option<u8>;
107
108    /// AEQ: minimum security floor for this command (0-4).
109    fn security_floor(&self, command_type: &str) -> Option<u8>;
110
111    /// AEQ: decision tier for this command ("Routine", "Significant", "Critical").
112    fn decision_tier(&self, command_type: &str) -> Option<String>;
113
114    /// Map a role string to its authorization level (0-4).
115    /// Returns `None` if the role is not recognized.
116    fn role_authorization_level(&self, role: &str) -> Option<u8>;
117}
118
119// ── Tests ──────────────────────────────────────────────────────────
120
121// inline: exercises module-private items via super::*
122#[cfg(test)]
123mod tests {
124    use super::*;
125
126    struct EchoDispatcher;
127
128    impl CommandDispatcher for EchoDispatcher {
129        fn dispatch(
130            &self,
131            command_type: &str,
132            payload: &[u8],
133        ) -> Result<Vec<u8>, CommandDispatchError> {
134            if command_type == "unknown" {
135                return Err(CommandDispatchError::UnknownCommand(command_type.into()));
136            }
137            Ok(payload.to_vec())
138        }
139    }
140
141    struct TestPolicyProvider;
142
143    impl PolicyProvider for TestPolicyProvider {
144        fn required_permission(&self, command_type: &str) -> Option<String> {
145            match command_type {
146                "user.create" => Some("users:manage".into()),
147                _ => None,
148            }
149        }
150
151        fn activity_maximum(&self, command_type: &str) -> Option<u8> {
152            match command_type {
153                "user.create" => Some(3),
154                _ => None,
155            }
156        }
157
158        fn security_floor(&self, command_type: &str) -> Option<u8> {
159            match command_type {
160                "user.create" => Some(2),
161                _ => None,
162            }
163        }
164
165        fn decision_tier(&self, command_type: &str) -> Option<String> {
166            match command_type {
167                "user.create" => Some("Significant".into()),
168                _ => None,
169            }
170        }
171
172        fn role_authorization_level(&self, role: &str) -> Option<u8> {
173            match role {
174                "Admin" => Some(4),
175                "Operator" => Some(3),
176                "Viewer" => Some(1),
177                _ => None,
178            }
179        }
180    }
181
182    #[test]
183    fn dispatcher_echo_returns_payload() {
184        let d = EchoDispatcher;
185        let result = d.dispatch("user.create", b"test-payload").unwrap();
186        assert_eq!(result, b"test-payload");
187    }
188
189    #[test]
190    fn dispatcher_unknown_command_errors() {
191        let d = EchoDispatcher;
192        let err = d.dispatch("unknown", b"").unwrap_err();
193        assert!(matches!(err, CommandDispatchError::UnknownCommand(_)));
194        assert!(err.to_string().contains("unknown"));
195    }
196
197    #[test]
198    fn policy_required_permission() {
199        let p = TestPolicyProvider;
200        assert_eq!(
201            p.required_permission("user.create"),
202            Some("users:manage".into())
203        );
204        assert_eq!(p.required_permission("noop"), None);
205    }
206
207    #[test]
208    fn policy_aeq_dimensions() {
209        let p = TestPolicyProvider;
210        assert_eq!(p.activity_maximum("user.create"), Some(3));
211        assert_eq!(p.security_floor("user.create"), Some(2));
212        assert_eq!(p.decision_tier("user.create"), Some("Significant".into()));
213    }
214
215    #[test]
216    fn policy_role_authorization_level() {
217        let p = TestPolicyProvider;
218        assert_eq!(p.role_authorization_level("Admin"), Some(4));
219        assert_eq!(p.role_authorization_level("Viewer"), Some(1));
220        assert_eq!(p.role_authorization_level("Ghost"), None);
221    }
222
223    #[test]
224    fn dispatch_error_display() {
225        let e1 = CommandDispatchError::UnknownCommand("foo".into());
226        let e2 = CommandDispatchError::ExecutionFailed("bar".into());
227        let e3 = CommandDispatchError::SerializationError("baz".into());
228        assert_eq!(e1.to_string(), "unknown command: foo");
229        assert_eq!(e2.to_string(), "dispatch failed: bar");
230        assert_eq!(e3.to_string(), "serde error: baz");
231    }
232}