Skip to main content

hypen_server/
action.rs

1use serde::de::DeserializeOwned;
2use serde_json::Value;
3
4/// Context for a dispatched action, providing metadata and raw payload access.
5///
6/// While `on_action::<T>()` handles payload deserialization automatically,
7/// `ActionContext` is available for internal dispatch and middleware use cases.
8pub struct ActionContext {
9    /// The action name (e.g., `"increment"`, `"setName"`).
10    pub name: String,
11
12    /// The raw JSON payload (if any).
13    raw_payload: Option<Value>,
14
15    /// Who dispatched this action.
16    pub sender: ActionSender,
17}
18
19/// Identifies who dispatched an action.
20#[derive(Debug, Clone, PartialEq, Eq)]
21pub enum ActionSender {
22    /// Dispatched from UI (e.g., `@actions.increment`).
23    Ui,
24    /// Dispatched programmatically from code.
25    Programmatic,
26    /// Custom sender identifier.
27    Custom(String),
28}
29
30impl ActionContext {
31    pub(crate) fn new(name: String, payload: Option<Value>, sender: ActionSender) -> Self {
32        Self {
33            name,
34            raw_payload: payload,
35            sender,
36        }
37    }
38
39    /// Deserialize the action payload into `T`.
40    ///
41    /// Returns `None` if no payload was provided or deserialization fails.
42    pub fn payload<T: DeserializeOwned>(&self) -> Option<T> {
43        self.raw_payload
44            .as_ref()
45            .and_then(|v| serde_json::from_value(v.clone()).ok())
46    }
47
48    /// Get the raw JSON payload.
49    pub fn raw_payload(&self) -> Option<&Value> {
50        self.raw_payload.as_ref()
51    }
52
53    /// Check if this action has a payload.
54    pub fn has_payload(&self) -> bool {
55        self.raw_payload.is_some()
56    }
57}
58
59impl From<&hypen_engine::dispatch::Action> for ActionContext {
60    fn from(action: &hypen_engine::dispatch::Action) -> Self {
61        let sender = match action.sender.as_deref() {
62            Some("ui") | None => ActionSender::Ui,
63            Some("programmatic") => ActionSender::Programmatic,
64            Some(other) => ActionSender::Custom(other.to_string()),
65        };
66        Self::new(action.name.clone(), action.payload.clone(), sender)
67    }
68}
69
70#[cfg(test)]
71mod tests {
72    use super::*;
73    use serde_json::json;
74
75    #[test]
76    fn test_action_context_payload_deserialization() {
77        let ctx = ActionContext::new(
78            "test".into(),
79            Some(json!({"name": "Alice", "age": 30})),
80            ActionSender::Ui,
81        );
82
83        #[derive(serde::Deserialize, PartialEq, Debug)]
84        struct Payload {
85            name: String,
86            age: i32,
87        }
88
89        let payload: Option<Payload> = ctx.payload();
90        assert_eq!(
91            payload,
92            Some(Payload {
93                name: "Alice".into(),
94                age: 30
95            })
96        );
97    }
98
99    #[test]
100    fn test_action_context_scalar_payload() {
101        let ctx = ActionContext::new("test".into(), Some(json!(42)), ActionSender::Programmatic);
102
103        let val: Option<i32> = ctx.payload();
104        assert_eq!(val, Some(42));
105    }
106
107    #[test]
108    fn test_action_context_no_payload() {
109        let ctx = ActionContext::new("test".into(), None, ActionSender::Ui);
110
111        let val: Option<i32> = ctx.payload();
112        assert_eq!(val, None);
113        assert!(!ctx.has_payload());
114    }
115
116    #[test]
117    fn test_action_context_wrong_type_returns_none() {
118        let ctx = ActionContext::new("test".into(), Some(json!("hello")), ActionSender::Ui);
119
120        let val: Option<i32> = ctx.payload();
121        assert_eq!(val, None);
122    }
123
124    #[test]
125    fn test_from_engine_action() {
126        let engine_action = hypen_engine::dispatch::Action::new("click")
127            .with_payload(json!({"x": 10}))
128            .with_sender("ui");
129
130        let ctx = ActionContext::from(&engine_action);
131        assert_eq!(ctx.name, "click");
132        assert_eq!(ctx.sender, ActionSender::Ui);
133        assert!(ctx.has_payload());
134    }
135}