axum_htmx/responders/
trigger.rs1use axum_core::response::{IntoResponseParts, ResponseParts};
2
3use crate::{HxError, headers};
4
5#[derive(Debug, Clone)]
7#[cfg_attr(feature = "serde", derive(serde::Serialize))]
8pub struct HxEvent {
9 pub name: String,
10 #[serde(skip_serializing_if = "Option::is_none")]
11 #[cfg(feature = "serde")]
12 #[cfg_attr(feature = "unstable", doc(cfg(feature = "serde")))]
13 pub data: Option<serde_json::Value>,
14}
15
16impl HxEvent {
17 pub fn new(name: impl AsRef<str>) -> Self {
19 Self {
20 name: name.as_ref().to_owned(),
21 #[cfg(feature = "serde")]
22 data: None,
23 }
24 }
25
26 #[cfg(feature = "serde")]
28 #[cfg_attr(feature = "unstable", doc(cfg(feature = "serde")))]
29 pub fn new_with_data<T: ::serde::Serialize>(
30 name: impl AsRef<str>,
31 data: T,
32 ) -> Result<Self, serde_json::Error> {
33 let data = serde_json::to_value(data)?;
34
35 Ok(Self {
36 name: name.as_ref().to_owned(),
37 #[cfg(feature = "serde")]
38 data: Some(data),
39 })
40 }
41}
42
43impl<N: AsRef<str>> From<N> for HxEvent {
44 fn from(name: N) -> Self {
45 Self {
46 name: name.as_ref().to_owned(),
47 #[cfg(feature = "serde")]
48 data: None,
49 }
50 }
51}
52
53#[cfg(not(feature = "serde"))]
54fn events_to_header_value(events: Vec<HxEvent>) -> Result<http::HeaderValue, HxError> {
55 let header = events
56 .into_iter()
57 .map(|HxEvent { name }| name)
58 .collect::<Vec<_>>()
59 .join(", ");
60
61 http::HeaderValue::from_str(&header).map_err(Into::into)
62}
63
64#[cfg(feature = "serde")]
65fn events_to_header_value(events: Vec<HxEvent>) -> Result<http::HeaderValue, HxError> {
66 use std::collections::HashMap;
67
68 use http::HeaderValue;
69 use serde_json::Value;
70
71 let with_data = events.iter().any(|e| e.data.is_some());
72
73 let header_value = if with_data {
74 let header_value = events
77 .into_iter()
78 .map(|e| (e.name, e.data.unwrap_or_default()))
79 .collect::<HashMap<String, Value>>();
80
81 serde_json::to_string(&header_value)?
82 } else {
83 events
86 .into_iter()
87 .map(|e| e.name)
88 .reduce(|acc, e| acc + ", " + &e)
89 .unwrap_or_default()
90 };
91
92 HeaderValue::from_maybe_shared(header_value).map_err(HxError::from)
93}
94
95#[derive(Debug, Clone, Copy, PartialEq, Eq)]
97#[non_exhaustive]
98pub enum TriggerMode {
99 Normal,
100 AfterSettle,
101 AfterSwap,
102}
103
104#[derive(Debug, Clone)]
118pub struct HxResponseTrigger {
119 pub mode: TriggerMode,
120 pub events: Vec<HxEvent>,
121}
122
123impl HxResponseTrigger {
124 pub fn new<T: Into<HxEvent>>(mode: TriggerMode, events: impl IntoIterator<Item = T>) -> Self {
127 Self {
128 mode,
129 events: events.into_iter().map(Into::into).collect(),
130 }
131 }
132
133 pub fn normal<T: Into<HxEvent>>(events: impl IntoIterator<Item = T>) -> Self {
138 Self::new(TriggerMode::Normal, events)
139 }
140
141 pub fn after_settle<T: Into<HxEvent>>(events: impl IntoIterator<Item = T>) -> Self {
146 Self::new(TriggerMode::AfterSettle, events)
147 }
148
149 pub fn after_swap<T: Into<HxEvent>>(events: impl IntoIterator<Item = T>) -> Self {
154 Self::new(TriggerMode::AfterSwap, events)
155 }
156}
157
158impl<T> From<(TriggerMode, T)> for HxResponseTrigger
159where
160 T: IntoIterator,
161 T::Item: Into<HxEvent>,
162{
163 fn from((mode, events): (TriggerMode, T)) -> Self {
164 Self {
165 mode,
166 events: events.into_iter().map(Into::into).collect(),
167 }
168 }
169}
170
171impl IntoResponseParts for HxResponseTrigger {
172 type Error = HxError;
173
174 fn into_response_parts(self, mut res: ResponseParts) -> Result<ResponseParts, Self::Error> {
175 if !self.events.is_empty() {
176 let header = match self.mode {
177 TriggerMode::Normal => headers::HX_TRIGGER,
178 TriggerMode::AfterSettle => headers::HX_TRIGGER_AFTER_SETTLE,
179 TriggerMode::AfterSwap => headers::HX_TRIGGER_AFTER_SETTLE,
180 };
181
182 res.headers_mut()
183 .insert(header, events_to_header_value(self.events)?);
184 }
185
186 Ok(res)
187 }
188}
189
190#[cfg(test)]
191mod tests {
192 use http::HeaderValue;
193 use serde_json::json;
194
195 use super::*;
196
197 #[test]
198 fn valid_event_to_header_encoding() {
199 let evt = HxEvent::new_with_data(
200 "my-event",
201 json!({"level": "info", "message": {
202 "body": "This is a test message.",
203 "title": "Hello, world!",
204 }}),
205 )
206 .unwrap();
207
208 let header_value = events_to_header_value(vec![evt]).unwrap();
209
210 let expected_value = r#"{"my-event":{"level":"info","message":{"body":"This is a test message.","title":"Hello, world!"}}}"#;
211
212 assert_eq!(header_value, HeaderValue::from_static(expected_value));
213
214 let value =
215 events_to_header_value(HxResponseTrigger::normal(["foo", "bar"]).events).unwrap();
216 assert_eq!(value, HeaderValue::from_static("foo, bar"));
217 }
218}