axum_htmx/
extractors.rs

1//! Axum extractors for htmx request headers.
2
3use axum_core::extract::FromRequestParts;
4use http::request::Parts;
5
6use crate::{
7    HX_BOOSTED, HX_CURRENT_URL, HX_HISTORY_RESTORE_REQUEST, HX_PROMPT, HX_REQUEST, HX_TARGET,
8    HX_TRIGGER, HX_TRIGGER_NAME,
9};
10
11/// The `HX-Boosted` header.
12///
13/// This is set when a request is made from an element where its parent has the
14/// `hx-boost` attribute set to `true`.
15///
16/// This extractor will always return a value. If the header is not present, it
17/// will return `false`.
18///
19/// See <https://htmx.org/attributes/hx-boost/> for more information.
20#[derive(Debug, Clone, Copy)]
21pub struct HxBoosted(pub bool);
22
23impl<S> FromRequestParts<S> for HxBoosted
24where
25    S: Send + Sync,
26{
27    type Rejection = std::convert::Infallible;
28
29    async fn from_request_parts(parts: &mut Parts, _: &S) -> Result<Self, Self::Rejection> {
30        if parts.headers.contains_key(HX_BOOSTED) {
31            Ok(HxBoosted(true))
32        } else {
33            Ok(HxBoosted(false))
34        }
35    }
36}
37
38/// The `HX-Current-Url` header.
39///
40/// This is set on every request made by htmx itself. As its name implies, it
41/// just contains the current url.
42///
43/// This extractor will always return a value. If the header is not present, or
44/// extractor fails to parse the url it will return `None`.
45#[derive(Debug, Clone)]
46pub struct HxCurrentUrl(pub Option<http::Uri>);
47
48impl<S> FromRequestParts<S> for HxCurrentUrl
49where
50    S: Send + Sync,
51{
52    type Rejection = std::convert::Infallible;
53
54    async fn from_request_parts(parts: &mut Parts, _: &S) -> Result<Self, Self::Rejection> {
55        if let Some(url) = parts.headers.get(HX_CURRENT_URL) {
56            let url = url
57                .to_str()
58                .ok()
59                .and_then(|url| url.parse::<http::Uri>().ok());
60
61            return Ok(HxCurrentUrl(url));
62        }
63
64        Ok(HxCurrentUrl(None))
65    }
66}
67
68/// The `HX-History-Restore-Request` header.
69///
70/// This extractor will always return a value. If the header is not present, it
71/// will return `false`.
72#[derive(Debug, Clone, Copy)]
73pub struct HxHistoryRestoreRequest(pub bool);
74
75impl<S> FromRequestParts<S> for HxHistoryRestoreRequest
76where
77    S: Send + Sync,
78{
79    type Rejection = std::convert::Infallible;
80
81    async fn from_request_parts(parts: &mut Parts, _: &S) -> Result<Self, Self::Rejection> {
82        if parts.headers.contains_key(HX_HISTORY_RESTORE_REQUEST) {
83            Ok(HxHistoryRestoreRequest(true))
84        } else {
85            Ok(HxHistoryRestoreRequest(false))
86        }
87    }
88}
89
90/// The `HX-Prompt` header.
91///
92/// This is set when a request is made from an element that has the `hx-prompt`
93/// attribute set. The value will contain the string input by the user.
94///
95/// This extractor will always return a value. If the header is not present, it
96/// will return `None`.
97#[derive(Debug, Clone)]
98pub struct HxPrompt(pub Option<String>);
99
100impl<S> FromRequestParts<S> for HxPrompt
101where
102    S: Send + Sync,
103{
104    type Rejection = std::convert::Infallible;
105
106    async fn from_request_parts(parts: &mut Parts, _: &S) -> Result<Self, Self::Rejection> {
107        if let Some(prompt) = parts.headers.get(HX_PROMPT) {
108            if let Ok(prompt) = prompt.to_str() {
109                return Ok(HxPrompt(Some(prompt.to_string())));
110            }
111        }
112
113        Ok(HxPrompt(None))
114    }
115}
116
117/// The `HX-Request` header.
118///
119/// This is set on every request made by htmx itself. It won't be present on
120/// requests made manually, or by other libraries.
121///
122/// This extractor will always return a value. If the header is not present, it
123/// will return `false`.
124#[derive(Debug, Clone, Copy)]
125pub struct HxRequest(pub bool);
126
127impl<S> FromRequestParts<S> for HxRequest
128where
129    S: Send + Sync,
130{
131    type Rejection = std::convert::Infallible;
132
133    async fn from_request_parts(parts: &mut Parts, _: &S) -> Result<Self, Self::Rejection> {
134        #[cfg(feature = "auto-vary")]
135        parts
136            .extensions
137            .get_mut::<crate::auto_vary::HxRequestExtracted>()
138            .map(crate::auto_vary::Notifier::notify);
139
140        if parts.headers.contains_key(HX_REQUEST) {
141            Ok(HxRequest(true))
142        } else {
143            Ok(HxRequest(false))
144        }
145    }
146}
147
148/// The `HX-Target` header.
149///
150/// This is set when a request is made from an element that has the `hx-target`
151/// attribute set. The value will contain the target element's id. If the id
152/// does not exist on the page, the value will be None.
153///
154/// This extractor will always return a value. If the header is not present, it
155/// will return `None`.
156#[derive(Debug, Clone)]
157pub struct HxTarget(pub Option<String>);
158
159impl<S> FromRequestParts<S> for HxTarget
160where
161    S: Send + Sync,
162{
163    type Rejection = std::convert::Infallible;
164
165    async fn from_request_parts(parts: &mut Parts, _: &S) -> Result<Self, Self::Rejection> {
166        #[cfg(feature = "auto-vary")]
167        parts
168            .extensions
169            .get_mut::<crate::auto_vary::HxTargetExtracted>()
170            .map(crate::auto_vary::Notifier::notify);
171
172        if let Some(target) = parts.headers.get(HX_TARGET) {
173            if let Ok(target) = target.to_str() {
174                return Ok(HxTarget(Some(target.to_string())));
175            }
176        }
177
178        Ok(HxTarget(None))
179    }
180}
181
182/// The `HX-Trigger-Name` header.
183///
184/// This is set when a request is made from an element that has the `hx-trigger`
185/// attribute set. The value will contain the trigger element's name. If the
186/// name does not exist on the page, the value will be None.
187///
188/// This extractor will always return a value. If the header is not present, it
189/// will return `None`.
190#[derive(Debug, Clone)]
191pub struct HxTriggerName(pub Option<String>);
192
193impl<S> FromRequestParts<S> for HxTriggerName
194where
195    S: Send + Sync,
196{
197    type Rejection = std::convert::Infallible;
198
199    async fn from_request_parts(parts: &mut Parts, _: &S) -> Result<Self, Self::Rejection> {
200        #[cfg(feature = "auto-vary")]
201        parts
202            .extensions
203            .get_mut::<crate::auto_vary::HxTriggerNameExtracted>()
204            .map(crate::auto_vary::Notifier::notify);
205
206        if let Some(trigger_name) = parts.headers.get(HX_TRIGGER_NAME) {
207            if let Ok(trigger_name) = trigger_name.to_str() {
208                return Ok(HxTriggerName(Some(trigger_name.to_string())));
209            }
210        }
211
212        Ok(HxTriggerName(None))
213    }
214}
215
216/// The `HX-Trigger` header.
217///
218/// This is set when a request is made from an element that has the `hx-trigger`
219/// attribute set. The value will contain the trigger element's id. If the id
220/// does not exist on the page, the value will be None.
221///
222/// This extractor will always return a value. If the header is not present, it
223/// will return `None`.
224#[derive(Debug, Clone)]
225pub struct HxTrigger(pub Option<String>);
226
227impl<S> FromRequestParts<S> for HxTrigger
228where
229    S: Send + Sync,
230{
231    type Rejection = std::convert::Infallible;
232
233    async fn from_request_parts(parts: &mut Parts, _: &S) -> Result<Self, Self::Rejection> {
234        #[cfg(feature = "auto-vary")]
235        parts
236            .extensions
237            .get_mut::<crate::auto_vary::HxTriggerExtracted>()
238            .map(crate::auto_vary::Notifier::notify);
239
240        if let Some(trigger) = parts.headers.get(HX_TRIGGER) {
241            if let Ok(trigger) = trigger.to_str() {
242                return Ok(HxTrigger(Some(trigger.to_string())));
243            }
244        }
245
246        Ok(HxTrigger(None))
247    }
248}