htmx_types/headers/
response.rs

1//! htmx response headers.
2
3use std::collections::HashMap;
4
5use headers_core::{Header, HeaderValue};
6use http::{HeaderName, Uri};
7use serde::{Deserialize, Serialize};
8
9use super::{convert_header, define_header, string_header, true_header};
10use crate::Swap;
11
12/// ajax context for use with [`HxLocation`].
13#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
14pub struct AjaxContext {
15    /// the source element of the request
16    #[serde(skip_serializing_if = "Option::is_none")]
17    pub source: Option<String>,
18
19    /// an event that “triggered” the request
20    #[serde(skip_serializing_if = "Option::is_none")]
21    pub event: Option<String>,
22
23    /// a callback that will handle the response HTML
24    #[serde(skip_serializing_if = "Option::is_none")]
25    pub handler: Option<String>,
26
27    /// the target to swap the response into
28    #[serde(skip_serializing_if = "Option::is_none")]
29    pub target: Option<String>,
30
31    /// how the response will be swapped in relative to the target
32    #[serde(skip_serializing_if = "Option::is_none")]
33    pub swap: Option<String>,
34
35    /// values to submit with the request
36    #[serde(skip_serializing_if = "Option::is_none")]
37    pub values: Option<HashMap<String, String>>,
38
39    /// headers to submit with the request
40    #[serde(skip_serializing_if = "Option::is_none")]
41    pub headers: Option<HashMap<String, String>>,
42
43    /// allows you to select the content you want swapped from a response
44    #[serde(skip_serializing_if = "Option::is_none")]
45    pub select: Option<String>,
46}
47
48define_header! {
49    /// allows you to do a client-side redirect that does not do a full page reload
50    ///
51    /// [htmx docs](https://htmx.org/headers/hx-location/)
52    (HX_LOCATION, "hx-location")
53
54
55    #[derive(Serialize, Deserialize)]
56    pub struct HxLocation {
57        /// url to load the response from.
58        #[serde(with = "http_serde::uri")]
59        pub path: Uri,
60
61        /// other data, which mirrors the [ajax](https://htmx.org/api/#ajax) api context.
62        #[serde(flatten)]
63        pub context: Option<AjaxContext>,
64    }
65}
66
67impl Header for HxLocation {
68    fn name() -> &'static HeaderName {
69        &HX_LOCATION
70    }
71
72    fn decode<'i, I>(values: &mut I) -> Result<Self, headers_core::Error>
73    where
74        Self: Sized,
75        I: Iterator<Item = &'i HeaderValue>,
76    {
77        match (values.next(), values.next()) {
78            (Some(value), None) => {
79                serde_json::from_slice(value.as_bytes()).map_err(|_| headers_core::Error::invalid())
80            }
81            _ => Err(headers_core::Error::invalid()),
82        }
83    }
84
85    /// NOTE: Panics if the value cannot be converted to a header value.
86    fn encode<E: Extend<HeaderValue>>(&self, values: &mut E) {
87        let header = match self {
88            Self {
89                path,
90                context: None,
91            } => HeaderValue::from_str(&path.to_string()).unwrap(),
92            Self {
93                context: Some(_), ..
94            } => {
95                let s = serde_json::to_string(self).unwrap();
96                HeaderValue::from_str(&s).unwrap()
97            }
98        };
99
100        values.extend(std::iter::once(header));
101    }
102}
103
104/// to be used with [`HxPushUrl`] or [`HxReplaceUrl`].
105#[derive(Debug, Clone, PartialEq, Eq)]
106pub enum HxModifyHistory<M: HistoryModification> {
107    /// a url to modify the history with.
108    Uri(Uri),
109
110    /// do not change the history.
111    NoChange,
112    #[doc(hidden)]
113    #[allow(dead_code)]
114    Phantom(std::marker::PhantomData<M>),
115}
116
117/// history modification headers.
118pub trait HistoryModification {
119    /// the name of the header.
120    fn name() -> &'static HeaderName;
121}
122
123define_header! {
124    /// pushes a new url into the history stack
125    ///
126    /// [htmx docs](https://htmx.org/headers/hx-push-url/)
127    (HX_PUSH_URL, "hx-push-url")
128
129    #[derive(Copy)]
130    pub struct HxPushUrl;
131}
132
133impl HistoryModification for HxPushUrl {
134    fn name() -> &'static HeaderName {
135        &HX_PUSH_URL
136    }
137}
138
139define_header! {
140    /// replaces the current url in the history stack
141    ///
142    /// [htmx docs](https://htmx.org/headers/hx-replace-url/)
143    (HX_REPLACE_URL, "hx-replace-url")
144
145    #[derive(Copy)]
146    pub struct HxReplaceUrl;
147}
148
149impl HistoryModification for HxReplaceUrl {
150    fn name() -> &'static HeaderName {
151        &HX_REPLACE_URL
152    }
153}
154
155impl<M: HistoryModification> Header for HxModifyHistory<M> {
156    fn name() -> &'static HeaderName {
157        M::name()
158    }
159
160    fn decode<'i, I>(values: &mut I) -> Result<Self, headers_core::Error>
161    where
162        Self: Sized,
163        I: Iterator<Item = &'i HeaderValue>,
164    {
165        match (values.next(), values.next()) {
166            (Some(value), None) => {
167                if value == "false" {
168                    Ok(Self::NoChange)
169                } else {
170                    value
171                        .as_bytes()
172                        .try_into()
173                        .map(Self::Uri)
174                        .map_err(|_| headers_core::Error::invalid())
175                }
176            }
177            _ => Err(headers_core::Error::invalid()),
178        }
179    }
180
181    /// NOTE: Panics if the value cannot be converted to a header value.
182    fn encode<E: Extend<HeaderValue>>(&self, values: &mut E) {
183        let header = match self {
184            Self::Uri(uri) => HeaderValue::from_str(&uri.to_string()).unwrap(),
185            Self::NoChange => HeaderValue::from_static("false"),
186            Self::Phantom(_) => return,
187        };
188
189        values.extend(std::iter::once(header));
190    }
191}
192
193convert_header! {
194    /// can be used to do a client-side redirect to a new location
195    Uri => (HX_REDIRECT, HxRedirect, "hx-redirect")
196}
197
198true_header! {
199    /// if set to “true” the client-side will do a full refresh of the page
200    (HX_REFRESH, HxRefresh, "hx-refresh")
201}
202
203define_header! {
204    /// allows you to specify how the response will be swapped. See [hx-swap](https://htmx.org/attributes/hx-swap/) for possible values
205    (HX_RESWAP, "hx-reswap")
206
207    #[derive(Copy)]
208    pub struct HxReswap(pub Swap);
209}
210
211impl Header for HxReswap {
212    fn name() -> &'static HeaderName {
213        &HX_RESWAP
214    }
215
216    fn decode<'i, I>(values: &mut I) -> Result<Self, headers_core::Error>
217    where
218        Self: Sized,
219        I: Iterator<Item = &'i HeaderValue>,
220    {
221        match (values.next(), values.next()) {
222            (Some(value), None) => value
223                .as_bytes()
224                .try_into()
225                .map(Self)
226                .map_err(|()| headers_core::Error::invalid()),
227            _ => Err(headers_core::Error::invalid()),
228        }
229    }
230
231    /// NOTE: Panics if the value cannot be converted to a header value.
232    fn encode<E: Extend<HeaderValue>>(&self, values: &mut E) {
233        values.extend(std::iter::once(self.0.into()));
234    }
235}
236
237string_header! {
238    /// a CSS selector that updates the target of the content update to a different element on the page
239    (HX_RETARGET, HxRetarget, "hx-retarget")
240}
241
242string_header! {
243    /// a CSS selector that allows you to choose which part of the response is used to be swapped in. Overrides an existing [hx-select](https://htmx.org/attributes/hx-select/) on the triggering element
244    (HX_RESELECT, HxReselect, "hx-reselect")
245}
246
247define_header! {
248    /// allows you to trigger client-side events
249    ///
250    /// [htmx docs](https://htmx.org/headers/hx-trigger/)
251    (HX_TRIGGER, "hx-trigger")
252
253    pub enum HxTrigger<After: TriggerAfter = ()> {
254        /// a list of events to trigger
255        List(Vec<String>),
256
257        /// a map of events to trigger with details
258        WithDetails(HashMap<String, serde_json::Value>),
259        #[doc(hidden)]
260        #[allow(dead_code)]
261        Phantom(std::marker::PhantomData<After>),
262    }
263}
264
265/// trigger after headers.
266pub trait TriggerAfter {
267    /// the name of the header.
268    fn name() -> &'static HeaderName;
269}
270
271impl TriggerAfter for () {
272    fn name() -> &'static HeaderName {
273        &HX_TRIGGER
274    }
275}
276
277define_header! {
278    /// allows you to trigger client-side events after the settle step
279    ///
280    /// [htmx docs](https://htmx.org/headers/hx-trigger/)
281    (HX_TRIGGER_AFTER_SETTLE, "hx-trigger-after-settle")
282
283    #[derive(Copy)]
284    pub struct AfterSettle;
285}
286
287impl TriggerAfter for AfterSettle {
288    fn name() -> &'static HeaderName {
289        &HX_TRIGGER_AFTER_SETTLE
290    }
291}
292
293define_header! {
294    /// allows you to trigger client-side events after the swap step
295    ///
296    /// [htmx docs](https://htmx.org/headers/hx-trigger/)
297    (HX_TRIGGER_AFTER_SWAP, "hx-trigger-after-swap")
298
299    #[derive(Copy)]
300    pub struct AfterSwap;
301}
302
303impl TriggerAfter for AfterSwap {
304    fn name() -> &'static HeaderName {
305        &HX_TRIGGER_AFTER_SWAP
306    }
307}
308
309impl<After: TriggerAfter> Header for HxTrigger<After> {
310    fn name() -> &'static HeaderName {
311        After::name()
312    }
313
314    fn decode<'i, I>(values: &mut I) -> Result<Self, headers_core::Error>
315    where
316        Self: Sized,
317        I: Iterator<Item = &'i HeaderValue>,
318    {
319        match (values.next(), values.next()) {
320            (Some(value), None) => {
321                let bytes = value.as_bytes();
322                serde_json::from_slice(bytes)
323                    .map(Self::WithDetails)
324                    .or_else(|_| {
325                        let items = value
326                            .to_str()
327                            .map_err(|_| headers_core::Error::invalid())?
328                            .split(',')
329                            .map(|s| s.trim().to_owned())
330                            .collect();
331
332                        Ok(Self::List(items))
333                    })
334            }
335            _ => Err(headers_core::Error::invalid()),
336        }
337    }
338
339    /// NOTE: Panics if the value cannot be converted to a header value.
340    fn encode<E: Extend<HeaderValue>>(&self, values: &mut E) {
341        let val = match self {
342            Self::List(list) => {
343                let s = list.join(", ");
344                HeaderValue::from_str(&s).unwrap()
345            }
346            Self::WithDetails(details) => {
347                let s = serde_json::to_string(details).unwrap();
348                HeaderValue::from_str(&s).unwrap()
349            }
350            Self::Phantom(_) => return,
351        };
352
353        values.extend(std::iter::once(val));
354    }
355}
356
357#[cfg(test)]
358mod tests {
359    use super::*;
360
361    #[test]
362    fn trigger_works() {
363        let val = HeaderValue::from_static(r#"{"event1":"A message", "event2":"Another message"}"#);
364
365        claims::assert_ok_eq!(
366            HxTrigger::<()>::decode(&mut std::iter::once(&val)),
367            HxTrigger::WithDetails(
368                vec![
369                    ("event1".to_owned(), "A message".into()),
370                    ("event2".to_owned(), "Another message".into()),
371                ]
372                .into_iter()
373                .collect()
374            )
375        );
376
377        let val = HeaderValue::from_static("event1, event2");
378
379        claims::assert_ok_eq!(
380            HxTrigger::<()>::decode(&mut std::iter::once(&val)),
381            HxTrigger::List(vec!["event1".to_owned(), "event2".to_owned()])
382        );
383    }
384}