Skip to main content

hypen_engine/dispatch/
action.rs

1use serde::{Deserialize, Serialize};
2
3use crate::error::EngineError;
4
5/// An action dispatched from the UI via `@actions.<name>` in Hypen DSL.
6///
7/// Actions are the primary mechanism for UI-to-logic communication. When a
8/// user interacts with a component (e.g. clicks `Button("@actions.signIn")`),
9/// the renderer dispatches an `Action` to the engine, which routes it to the
10/// registered handler.
11///
12/// # Example
13///
14/// ```rust
15/// use hypen_engine::dispatch::Action;
16///
17/// let action = Action::new("signIn")
18///     .with_payload(serde_json::json!({"email": "alice@example.com"}));
19///
20/// assert_eq!(action.name, "signIn");
21/// ```
22#[derive(Debug, Clone, Serialize, Deserialize)]
23pub struct Action {
24    /// Action name (e.g. `"signIn"`, `"increment"`, `"addToCart"`)
25    pub name: String,
26
27    /// Optional payload data attached to the action.
28    pub payload: Option<serde_json::Value>,
29
30    /// Optional sender/source identifier (e.g. a node ID or component name).
31    pub sender: Option<String>,
32}
33
34impl Action {
35    /// Create a new action with the given name and no payload.
36    pub fn new(name: impl Into<String>) -> Self {
37        Self {
38            name: name.into(),
39            payload: None,
40            sender: None,
41        }
42    }
43
44    pub fn with_payload(mut self, payload: serde_json::Value) -> Self {
45        self.payload = Some(payload);
46        self
47    }
48
49    pub fn with_sender(mut self, sender: impl Into<String>) -> Self {
50        self.sender = Some(sender.into());
51        self
52    }
53}
54
55/// Callback signature for action handlers.
56///
57/// Handlers receive a shared reference to the [`Action`] being dispatched.
58pub type ActionHandler = Box<dyn Fn(&Action) + Send + Sync>;
59
60/// Routes actions to their registered handlers.
61///
62/// Used internally by [`Engine`](crate::Engine). SDK authors typically
63/// register handlers via `Engine::on_action()` or `WasmEngine::onAction()`
64/// rather than using this type directly.
65pub struct ActionDispatcher {
66    /// Map of action name -> handler
67    handlers: indexmap::IndexMap<String, ActionHandler>,
68}
69
70impl ActionDispatcher {
71    pub fn new() -> Self {
72        Self {
73            handlers: indexmap::IndexMap::new(),
74        }
75    }
76
77    /// Register a handler for an action
78    pub fn on<F>(&mut self, action_name: impl Into<String>, handler: F)
79    where
80        F: Fn(&Action) + Send + Sync + 'static,
81    {
82        self.handlers.insert(action_name.into(), Box::new(handler));
83    }
84
85    /// Dispatch an action to its handler
86    pub fn dispatch(&self, action: &Action) -> Result<(), EngineError> {
87        if let Some(handler) = self.handlers.get(&action.name) {
88            handler(action);
89            Ok(())
90        } else {
91            Err(EngineError::ActionNotFound(action.name.clone()))
92        }
93    }
94
95    /// Check if a handler is registered for an action
96    pub fn has_handler(&self, action_name: &str) -> bool {
97        self.handlers.contains_key(action_name)
98    }
99
100    /// Remove a handler
101    pub fn remove(&mut self, action_name: &str) {
102        self.handlers.shift_remove(action_name);
103    }
104
105    /// Clear all handlers
106    pub fn clear(&mut self) {
107        self.handlers.clear();
108    }
109}
110
111impl Default for ActionDispatcher {
112    fn default() -> Self {
113        Self::new()
114    }
115}
116
117#[cfg(test)]
118mod tests {
119    use super::*;
120    use std::sync::{Arc, Mutex};
121
122    #[test]
123    fn test_action_dispatch() {
124        let mut dispatcher = ActionDispatcher::new();
125        let called = Arc::new(Mutex::new(false));
126        let called_clone = called.clone();
127
128        dispatcher.on("test", move |_action| {
129            *called_clone.lock().unwrap() = true;
130        });
131
132        let action = Action::new("test");
133        dispatcher.dispatch(&action).unwrap();
134
135        assert!(*called.lock().unwrap());
136    }
137
138    #[test]
139    fn test_missing_handler() {
140        let dispatcher = ActionDispatcher::new();
141        let action = Action::new("unknown");
142        assert!(dispatcher.dispatch(&action).is_err());
143    }
144}