1use http::HeaderValue;
2use http::header::HeaderName;
3use http::response::Parts;
4use serde::Serialize;
5use serde_json::{Map, Value};
6use topcoat_core::{context::Cx, error::Result};
7use topcoat_router::IntoResponseParts;
8
9use crate::header;
10
11#[derive(Debug, Clone, PartialEq, Eq)]
13pub struct HxEvent {
14 pub name: String,
16 pub data: Option<Value>,
18}
19
20impl HxEvent {
21 #[must_use]
23 pub fn new(name: impl Into<String>) -> Self {
24 Self {
25 name: name.into(),
26 data: None,
27 }
28 }
29
30 pub fn with_data(name: impl Into<String>, data: impl Serialize) -> Result<Self> {
36 Ok(Self {
37 name: name.into(),
38 data: Some(serde_json::to_value(data)?),
39 })
40 }
41}
42
43impl From<&str> for HxEvent {
44 fn from(name: &str) -> Self {
45 Self::new(name)
46 }
47}
48
49impl From<String> for HxEvent {
50 fn from(name: String) -> Self {
51 Self::new(name)
52 }
53}
54
55#[derive(Debug, Clone, Copy, PartialEq, Eq)]
57pub enum TriggerTiming {
58 Receive,
60 AfterSettle,
62 AfterSwap,
64}
65
66impl TriggerTiming {
67 fn header(self) -> HeaderName {
68 match self {
69 Self::Receive => header::HX_TRIGGER,
70 Self::AfterSettle => header::HX_TRIGGER_AFTER_SETTLE,
71 Self::AfterSwap => header::HX_TRIGGER_AFTER_SWAP,
72 }
73 }
74}
75
76#[derive(Debug, Clone, PartialEq, Eq)]
95pub struct HxResponseTrigger {
96 pub timing: TriggerTiming,
98 pub events: Vec<HxEvent>,
100}
101
102impl HxResponseTrigger {
103 pub fn new(
105 timing: TriggerTiming,
106 events: impl IntoIterator<Item = impl Into<HxEvent>>,
107 ) -> Self {
108 Self {
109 timing,
110 events: events.into_iter().map(Into::into).collect(),
111 }
112 }
113
114 pub fn receive(events: impl IntoIterator<Item = impl Into<HxEvent>>) -> Self {
116 Self::new(TriggerTiming::Receive, events)
117 }
118
119 pub fn after_settle(events: impl IntoIterator<Item = impl Into<HxEvent>>) -> Self {
121 Self::new(TriggerTiming::AfterSettle, events)
122 }
123
124 pub fn after_swap(events: impl IntoIterator<Item = impl Into<HxEvent>>) -> Self {
126 Self::new(TriggerTiming::AfterSwap, events)
127 }
128
129 fn header_value(self) -> Result<HeaderValue> {
132 if self.events.iter().all(|event| event.data.is_none()) {
133 let names = self
134 .events
135 .iter()
136 .map(|event| event.name.as_str())
137 .collect::<Vec<_>>()
138 .join(", ");
139 return Ok(HeaderValue::from_str(&names)?);
140 }
141
142 let object = self
143 .events
144 .into_iter()
145 .map(|event| (event.name, event.data.unwrap_or(Value::Null)))
146 .collect::<Map<_, _>>();
147 Ok(HeaderValue::from_str(&serde_json::to_string(&object)?)?)
148 }
149}
150
151impl IntoResponseParts for HxResponseTrigger {
152 fn into_response_parts(self, _cx: &Cx, parts: &mut Parts) -> Result<()> {
153 let name = self.timing.header();
154 parts.headers.insert(name, self.header_value()?);
155 Ok(())
156 }
157}
158
159#[cfg(test)]
160mod tests {
161 use super::*;
162
163 fn header_value(trigger: HxResponseTrigger) -> (HeaderName, String) {
164 let name = trigger.timing.header();
165 let mut parts = http::Response::new(()).into_parts().0;
166 trigger
167 .into_response_parts(&Cx::default(), &mut parts)
168 .unwrap();
169 let value = parts
170 .headers
171 .get(&name)
172 .unwrap()
173 .to_str()
174 .unwrap()
175 .to_owned();
176 (name, value)
177 }
178
179 #[test]
180 fn name_only_events_join_with_commas() {
181 let (name, value) = header_value(HxResponseTrigger::receive(["refresh", "close"]));
182 assert_eq!(name, header::HX_TRIGGER);
183 assert_eq!(value, "refresh, close");
184 }
185
186 #[test]
187 fn events_with_data_serialize_as_json() {
188 let trigger = HxResponseTrigger::after_swap([
189 HxEvent::with_data("show-toast", "Saved!").unwrap(),
190 HxEvent::new("refresh"),
191 ]);
192 let (name, value) = header_value(trigger);
193 assert_eq!(name, header::HX_TRIGGER_AFTER_SWAP);
194 let json: Value = serde_json::from_str(&value).unwrap();
195 assert_eq!(json["show-toast"], "Saved!");
196 assert_eq!(json["refresh"], Value::Null);
197 }
198
199 #[test]
200 fn timing_selects_header() {
201 let (name, _) = header_value(HxResponseTrigger::after_settle(["x"]));
202 assert_eq!(name, header::HX_TRIGGER_AFTER_SETTLE);
203 }
204}