1use serde::de::DeserializeOwned;
2use serde_json::Value;
3
4pub struct ActionContext {
9 pub name: String,
11
12 raw_payload: Option<Value>,
14
15 pub sender: ActionSender,
17}
18
19#[derive(Debug, Clone, PartialEq, Eq)]
21pub enum ActionSender {
22 Ui,
24 Programmatic,
26 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 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 pub fn raw_payload(&self) -> Option<&Value> {
50 self.raw_payload.as_ref()
51 }
52
53 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}