Skip to main content

haystack_server/
actions.rs

1//! Action registry for the `invokeAction` op.
2//!
3//! Provides a trait-based dispatch mechanism so that custom action handlers
4//! can be registered at startup and invoked by name at runtime.
5
6use std::collections::HashMap;
7
8use haystack_core::data::{HDict, HGrid};
9
10/// A handler for a named action that can be invoked on entities.
11///
12/// Implementors provide:
13/// - `name()` — the action name used for dispatch (e.g. `"reboot"`)
14/// - `invoke()` — the logic that executes the action
15pub trait ActionHandler: Send + Sync {
16    /// The action name used for dispatch.
17    fn name(&self) -> &str;
18
19    /// Invoke the action on the given entity with the supplied arguments.
20    ///
21    /// - `entity` — the resolved entity dict from the graph
22    /// - `action` — the action name (same as `self.name()`)
23    /// - `args`   — additional arguments from the request grid row
24    ///
25    /// Returns a result grid on success, or a human-readable error string.
26    fn invoke(&self, entity: &HDict, action: &str, args: &HDict) -> Result<HGrid, String>;
27}
28
29/// Registry that maps action names to their handlers.
30///
31/// Thread-safe because every stored handler is `Send + Sync`.
32pub struct ActionRegistry {
33    handlers: HashMap<String, Box<dyn ActionHandler>>,
34}
35
36impl ActionRegistry {
37    /// Create an empty registry.
38    pub fn new() -> Self {
39        Self {
40            handlers: HashMap::new(),
41        }
42    }
43
44    /// Register a handler, keyed by `handler.name()`.
45    ///
46    /// If a handler with the same name already exists it is replaced.
47    pub fn register(&mut self, handler: Box<dyn ActionHandler>) {
48        let name = handler.name().to_string();
49        self.handlers.insert(name, handler);
50    }
51
52    /// Dispatch an action invocation to the matching handler.
53    ///
54    /// Returns `Err` with a descriptive message if no handler is registered
55    /// for the requested action name.
56    pub fn invoke(&self, entity: &HDict, action: &str, args: &HDict) -> Result<HGrid, String> {
57        match self.handlers.get(action) {
58            Some(handler) => handler.invoke(entity, action, args),
59            None => Err(format!("unknown action: {action}")),
60        }
61    }
62
63    /// List all registered action names (in arbitrary order).
64    pub fn list_actions(&self) -> Vec<String> {
65        self.handlers.keys().cloned().collect()
66    }
67}
68
69impl Default for ActionRegistry {
70    fn default() -> Self {
71        Self::new()
72    }
73}
74
75#[cfg(test)]
76mod tests {
77    use super::*;
78    use haystack_core::data::HCol;
79    use haystack_core::kinds::{HRef, Kind};
80
81    /// A trivial test handler that always succeeds and returns a single-row grid.
82    struct EchoAction;
83
84    impl ActionHandler for EchoAction {
85        fn name(&self) -> &str {
86            "echo"
87        }
88
89        fn invoke(&self, _entity: &HDict, action: &str, _args: &HDict) -> Result<HGrid, String> {
90            let mut row = HDict::new();
91            row.set("action", Kind::Str(action.to_string()));
92            row.set("result", Kind::Str("ok".to_string()));
93            let cols = vec![HCol::new("action"), HCol::new("result")];
94            Ok(HGrid::from_parts(HDict::new(), cols, vec![row]))
95        }
96    }
97
98    /// Another trivial handler for testing list_actions.
99    struct RebootAction;
100
101    impl ActionHandler for RebootAction {
102        fn name(&self) -> &str {
103            "reboot"
104        }
105
106        fn invoke(&self, _entity: &HDict, _action: &str, _args: &HDict) -> Result<HGrid, String> {
107            Ok(HGrid::new())
108        }
109    }
110
111    #[test]
112    fn invoke_known_action() {
113        let mut registry = ActionRegistry::new();
114        registry.register(Box::new(EchoAction));
115
116        let mut entity = HDict::new();
117        entity.set("id", Kind::Ref(HRef::from_val("equip-1")));
118        entity.set("equip", Kind::Marker);
119
120        let result = registry.invoke(&entity, "echo", &HDict::new());
121        assert!(result.is_ok());
122
123        let grid = result.unwrap();
124        assert_eq!(grid.len(), 1);
125        let row = grid.row(0).unwrap();
126        assert_eq!(row.get("action"), Some(&Kind::Str("echo".to_string())));
127        assert_eq!(row.get("result"), Some(&Kind::Str("ok".to_string())));
128    }
129
130    #[test]
131    fn invoke_unknown_action_returns_error() {
132        let registry = ActionRegistry::new();
133
134        let entity = HDict::new();
135        let result = registry.invoke(&entity, "nonexistent", &HDict::new());
136
137        assert!(result.is_err());
138        let err = result.unwrap_err();
139        assert!(err.contains("unknown action"));
140        assert!(err.contains("nonexistent"));
141    }
142
143    #[test]
144    fn list_actions_returns_registered_names() {
145        let mut registry = ActionRegistry::new();
146        registry.register(Box::new(EchoAction));
147        registry.register(Box::new(RebootAction));
148
149        let mut names = registry.list_actions();
150        names.sort();
151        assert_eq!(names, vec!["echo".to_string(), "reboot".to_string()]);
152    }
153
154    #[test]
155    fn empty_registry_has_no_actions() {
156        let registry = ActionRegistry::new();
157        assert!(registry.list_actions().is_empty());
158    }
159
160    #[test]
161    fn register_replaces_existing_handler() {
162        let mut registry = ActionRegistry::new();
163        registry.register(Box::new(EchoAction));
164        // Register another handler with the same name
165        registry.register(Box::new(EchoAction));
166
167        let names = registry.list_actions();
168        assert_eq!(names.len(), 1);
169    }
170}