1use actix_web::dev::{Payload, ServiceRequest};
2use actix_web::error::Error;
3use actix_web::http::header::HeaderValue;
4use actix_web::{FromRequest, HttpMessage, HttpRequest};
5use futures_util::future::{ready, Ready};
6use indexmap::IndexMap;
7use std::cell::RefCell;
8use std::collections::HashMap;
9use std::fmt;
10use std::rc::Rc;
11
12use crate::{
13 headers::{RequestHeaders, ResponseHeaders},
14 trigger_payload::TriggerPayload,
15 HxLocation,
16};
17
18#[derive(Clone)]
73pub struct Htmx {
74 inner: Rc<RefCell<HtmxInner>>,
75 pub is_htmx: bool,
77 pub boosted: bool,
79 pub history_restore_request: bool,
81}
82
83macro_rules! collection {
84 ($($k:expr => $v:expr),* $(,)?) => {{
85 use std::iter::{Iterator, IntoIterator};
86 Iterator::collect(IntoIterator::into_iter([$(($k, $v),)*]))
87 }};
88}
89
90#[derive(Clone, PartialEq, Eq, Hash)]
94pub enum TriggerType {
95 Standard,
96 AfterSettle,
97 AfterSwap,
98}
99
100pub enum SwapType {
104 InnerHtml,
106 OuterHtml,
108 BeforeBegin,
110 AfterBegin,
112 BeforeEnd,
114 AfterEnd,
116 Delete,
118 None,
120}
121
122enum DataType {
123 String(Option<String>),
124 Bool(bool),
125}
126
127impl fmt::Display for SwapType {
128 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
129 match self {
130 SwapType::InnerHtml => write!(f, "innerHTML"),
131 SwapType::OuterHtml => write!(f, "outerHTML"),
132 SwapType::BeforeBegin => write!(f, "beforebegin"),
133 SwapType::AfterBegin => write!(f, "afterbegin"),
134 SwapType::BeforeEnd => write!(f, "beforeend"),
135 SwapType::AfterEnd => write!(f, "afterend"),
136 SwapType::Delete => write!(f, "delete"),
137 SwapType::None => write!(f, "none"),
138 }
139 }
140}
141
142struct HtmxInner {
143 standard_triggers: IndexMap<String, Option<TriggerPayload>>,
144 after_settle_triggers: IndexMap<String, Option<TriggerPayload>>,
145 after_swap_triggers: IndexMap<String, Option<TriggerPayload>>,
146 response_headers: IndexMap<String, String>,
147 request_headers: IndexMap<String, DataType>,
148 simple_trigger: HashMap<TriggerType, bool>,
149}
150
151impl HtmxInner {
152 pub fn new(req: &HttpRequest) -> HtmxInner {
153 let request_headers = collection![
154 RequestHeaders::HX_REQUEST.to_string() => DataType::Bool(req.headers().get(RequestHeaders::HX_REQUEST).as_bool()),
155 RequestHeaders::HX_BOOSTED.to_string() => DataType::Bool(req.headers().get(RequestHeaders::HX_BOOSTED).as_bool()),
156 RequestHeaders::HX_CURRENT_URL.to_string() => DataType::String(req.headers().get(RequestHeaders::HX_CURRENT_URL).as_option_string()),
157 RequestHeaders::HX_HISTORY_RESTORE_REQUEST.to_string() => DataType::Bool(req.headers().get(RequestHeaders::HX_HISTORY_RESTORE_REQUEST).as_bool()),
158 RequestHeaders::HX_PROMPT.to_string() => DataType::String(req.headers().get(RequestHeaders::HX_PROMPT).as_option_string()),
159 RequestHeaders::HX_TARGET.to_string() => DataType::String(req.headers().get(RequestHeaders::HX_TARGET).as_option_string()),
160 RequestHeaders::HX_TRIGGER.to_string() => DataType::String(req.headers().get(RequestHeaders::HX_TRIGGER).as_option_string()),
161 RequestHeaders::HX_TRIGGER_NAME.to_string() => DataType::String(req.headers().get(RequestHeaders::HX_TRIGGER_NAME).as_option_string()),
162 ];
163
164 HtmxInner {
165 request_headers,
166 response_headers: IndexMap::new(),
167 standard_triggers: IndexMap::new(),
168 after_settle_triggers: IndexMap::new(),
169 after_swap_triggers: IndexMap::new(),
170 simple_trigger: HashMap::new(),
171 }
172 }
173
174 fn get_bool_header(&self, header_name: &str) -> bool {
175 self.request_headers
176 .get(header_name)
177 .map(|data_type| match data_type {
178 DataType::Bool(b) => *b,
179 _ => false,
180 })
181 .unwrap_or(false)
182 }
183
184 fn get_string_header(&self, header_name: &str) -> Option<String> {
185 self.request_headers
186 .get(header_name)
187 .map(|data_type| match data_type {
188 DataType::String(s) => s.clone(),
189 _ => None,
190 })
191 .unwrap_or(None)
192 }
193}
194
195impl Htmx {
196 fn from_inner(inner: Rc<RefCell<HtmxInner>>) -> Htmx {
197 let is_htmx = inner.borrow().get_bool_header(RequestHeaders::HX_REQUEST);
198 let boosted = inner.borrow().get_bool_header(RequestHeaders::HX_BOOSTED);
199 let history_restore_request = inner
200 .borrow()
201 .get_bool_header(RequestHeaders::HX_HISTORY_RESTORE_REQUEST);
202
203 Htmx {
204 inner,
205 is_htmx,
206 boosted,
207 history_restore_request,
208 }
209 }
210
211 pub fn new(req: &ServiceRequest) -> Htmx {
212 let req = req.request();
213 let inner = Rc::new(RefCell::new(HtmxInner::new(req)));
214 Htmx::from_inner(inner)
215 }
216
217 pub fn current_url(&self) -> Option<String> {
221 self.inner
222 .borrow()
223 .get_string_header(RequestHeaders::HX_CURRENT_URL)
224 }
225
226 pub fn prompt(&self) -> Option<String> {
230 self.inner
231 .borrow()
232 .get_string_header(RequestHeaders::HX_PROMPT)
233 }
234
235 pub fn target(&self) -> Option<String> {
239 self.inner
240 .borrow()
241 .get_string_header(RequestHeaders::HX_TARGET)
242 }
243
244 pub fn trigger(&self) -> Option<String> {
248 self.inner
249 .borrow()
250 .get_string_header(RequestHeaders::HX_TRIGGER)
251 }
252
253 pub fn trigger_name(&self) -> Option<String> {
257 self.inner
258 .borrow()
259 .get_string_header(RequestHeaders::HX_TRIGGER_NAME)
260 }
261
262 pub fn trigger_event(
296 &self,
297 name: impl Into<String>,
298 payload: Option<TriggerPayload>,
299 trigger_type: Option<TriggerType>,
300 ) {
301 let name = name.into();
302 let trigger_type = trigger_type.unwrap_or(TriggerType::Standard);
303 let mut inner = self.inner.borrow_mut();
304
305 if payload.is_some() {
306 inner.simple_trigger.insert(trigger_type.clone(), false);
307 }
308
309 let target_map = match trigger_type {
310 TriggerType::Standard => &mut inner.standard_triggers,
311 TriggerType::AfterSettle => &mut inner.after_settle_triggers,
312 TriggerType::AfterSwap => &mut inner.after_swap_triggers,
313 };
314
315 target_map.insert(name, payload);
316 }
317
318 pub fn redirect(&self, path: impl Into<String>) {
323 self.inner
324 .borrow_mut()
325 .response_headers
326 .insert(ResponseHeaders::HX_REDIRECT.to_string(), path.into());
327 }
328
329 pub fn redirect_with_swap(&self, path: impl Into<String>) {
334 self.inner
335 .borrow_mut()
336 .response_headers
337 .insert(ResponseHeaders::HX_LOCATION.to_string(), path.into());
338 }
339
340 pub fn redirect_with_location(&self, location: HxLocation) {
346 self.inner.borrow_mut().response_headers.insert(
347 ResponseHeaders::HX_LOCATION.to_string(),
348 location.into_header_value(),
349 );
350 }
351
352 pub fn refresh(&self) {
356 self.inner
357 .borrow_mut()
358 .response_headers
359 .insert(ResponseHeaders::HX_REFRESH.to_string(), "true".to_string());
360 }
361
362 pub fn push_url(&self, path: impl Into<String>) {
367 self.inner
368 .borrow_mut()
369 .response_headers
370 .insert(ResponseHeaders::HX_PUSH_URL.to_string(), path.into());
371 }
372
373 pub fn replace_url(&self, path: impl Into<String>) {
378 self.inner
379 .borrow_mut()
380 .response_headers
381 .insert(ResponseHeaders::HX_REPLACE_URL.to_string(), path.into());
382 }
383
384 pub fn reswap(&self, swap_type: SwapType) {
389 self.inner.borrow_mut().response_headers.insert(
390 ResponseHeaders::HX_RESWAP.to_string(),
391 swap_type.to_string(),
392 );
393 }
394
395 pub fn retarget(&self, selector: impl Into<String>) {
400 self.inner
401 .borrow_mut()
402 .response_headers
403 .insert(ResponseHeaders::HX_RETARGET.to_string(), selector.into());
404 }
405
406 pub fn reselect(&self, selector: impl Into<String>) {
411 self.inner
412 .borrow_mut()
413 .response_headers
414 .insert(ResponseHeaders::HX_RESELECT.to_string(), selector.into());
415 }
416
417 pub(crate) fn get_triggers(
418 &self,
419 trigger_type: TriggerType,
420 ) -> IndexMap<String, Option<TriggerPayload>> {
421 match trigger_type {
422 TriggerType::Standard => self.inner.borrow().standard_triggers.clone(),
423 TriggerType::AfterSettle => self.inner.borrow().after_settle_triggers.clone(),
424 TriggerType::AfterSwap => self.inner.borrow().after_swap_triggers.clone(),
425 }
426 }
427
428 pub(crate) fn is_simple_trigger(&self, trigger_type: TriggerType) -> bool {
429 match trigger_type {
430 TriggerType::Standard => *self
431 .inner
432 .borrow()
433 .simple_trigger
434 .get(&TriggerType::Standard)
435 .unwrap_or(&true),
436 TriggerType::AfterSettle => *self
437 .inner
438 .borrow()
439 .simple_trigger
440 .get(&TriggerType::AfterSettle)
441 .unwrap_or(&true),
442 TriggerType::AfterSwap => *self
443 .inner
444 .borrow()
445 .simple_trigger
446 .get(&TriggerType::AfterSwap)
447 .unwrap_or(&true),
448 }
449 }
450
451 pub(crate) fn get_response_headers(&self) -> IndexMap<String, String> {
452 self.inner.borrow().response_headers.clone()
453 }
454}
455
456impl FromRequest for Htmx {
457 type Error = Error;
458 type Future = Ready<Result<Htmx, Error>>;
459
460 #[inline]
461 fn from_request(req: &actix_web::HttpRequest, _: &mut Payload) -> Self::Future {
462 if let Some(htmx) = req.extensions_mut().get::<Htmx>() {
463 return ready(Ok(htmx.clone()));
464 }
465
466 let inner = Rc::new(RefCell::new(HtmxInner::new(req)));
467
468 ready(Ok(Htmx::from_inner(inner)))
469 }
470}
471
472trait AsBool {
473 fn as_bool(&self) -> bool;
474}
475
476trait AsOptionString {
477 fn as_option_string(&self) -> Option<String>;
478}
479
480impl AsBool for Option<&HeaderValue> {
481 fn as_bool(&self) -> bool {
482 match self {
483 Some(header) => {
484 if let Ok(header) = header.to_str() {
485 header.parse::<bool>().unwrap_or(false)
486 } else {
487 false
488 }
489 }
490 None => false,
491 }
492 }
493}
494
495impl AsOptionString for Option<&HeaderValue> {
496 fn as_option_string(&self) -> Option<String> {
497 match self {
498 Some(header) => {
499 if let Ok(header) = header.to_str() {
500 Some(header.to_string())
501 } else {
502 None
503 }
504 }
505 None => None,
506 }
507 }
508}