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::headers::{RequestHeaders, ResponseHeaders};
13
14#[derive(Clone)]
67pub struct Htmx {
68 inner: Rc<RefCell<HtmxInner>>,
69 pub is_htmx: bool,
71 pub boosted: bool,
73 pub history_restore_request: bool,
75}
76
77macro_rules! collection {
78 ($($k:expr => $v:expr),* $(,)?) => {{
79 use std::iter::{Iterator, IntoIterator};
80 Iterator::collect(IntoIterator::into_iter([$(($k, $v),)*]))
81 }};
82}
83
84#[derive(Clone, PartialEq, Eq, Hash)]
88pub enum TriggerType {
89 Standard,
90 AfterSettle,
91 AfterSwap,
92}
93
94pub enum SwapType {
98 InnerHtml,
100 OuterHtml,
102 BeforeBegin,
104 AfterBegin,
106 BeforeEnd,
108 AfterEnd,
110 Delete,
112 None,
114}
115
116enum DataType {
117 String(Option<String>),
118 Bool(bool),
119}
120
121impl fmt::Display for SwapType {
122 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
123 match self {
124 SwapType::InnerHtml => write!(f, "innerHTML"),
125 SwapType::OuterHtml => write!(f, "outerHTML"),
126 SwapType::BeforeBegin => write!(f, "beforebegin"),
127 SwapType::AfterBegin => write!(f, "afterbegin"),
128 SwapType::BeforeEnd => write!(f, "beforeend"),
129 SwapType::AfterEnd => write!(f, "afterend"),
130 SwapType::Delete => write!(f, "delete"),
131 SwapType::None => write!(f, "none"),
132 }
133 }
134}
135
136struct HtmxInner {
137 standard_triggers: IndexMap<String, Option<String>>,
138 after_settle_triggers: IndexMap<String, Option<String>>,
139 after_swap_triggers: IndexMap<String, Option<String>>,
140 response_headers: IndexMap<String, String>,
141 request_headers: IndexMap<String, DataType>,
142 simple_trigger: HashMap<TriggerType, bool>,
143}
144
145impl HtmxInner {
146 pub fn new(req: &HttpRequest) -> HtmxInner {
147 let request_headers = collection![
148 RequestHeaders::HX_REQUEST.to_string() => DataType::Bool(req.headers().get(RequestHeaders::HX_REQUEST).as_bool()),
149 RequestHeaders::HX_BOOSTED.to_string() => DataType::Bool(req.headers().get(RequestHeaders::HX_BOOSTED).as_bool()),
150 RequestHeaders::HX_CURRENT_URL.to_string() => DataType::String(req.headers().get(RequestHeaders::HX_CURRENT_URL).as_option_string()),
151 RequestHeaders::HX_HISTORY_RESTORE_REQUEST.to_string() => DataType::Bool(req.headers().get(RequestHeaders::HX_HISTORY_RESTORE_REQUEST).as_bool()),
152 RequestHeaders::HX_PROMPT.to_string() => DataType::String(req.headers().get(RequestHeaders::HX_PROMPT).as_option_string()),
153 RequestHeaders::HX_TARGET.to_string() => DataType::String(req.headers().get(RequestHeaders::HX_TARGET).as_option_string()),
154 RequestHeaders::HX_TRIGGER.to_string() => DataType::String(req.headers().get(RequestHeaders::HX_TRIGGER).as_option_string()),
155 RequestHeaders::HX_TRIGGER_NAME.to_string() => DataType::String(req.headers().get(RequestHeaders::HX_TRIGGER_NAME).as_option_string()),
156 ];
157
158 HtmxInner {
159 request_headers,
160 response_headers: IndexMap::new(),
161 standard_triggers: IndexMap::new(),
162 after_settle_triggers: IndexMap::new(),
163 after_swap_triggers: IndexMap::new(),
164 simple_trigger: HashMap::new(),
165 }
166 }
167
168 fn get_bool_header(&self, header_name: &str) -> bool {
169 self.request_headers
170 .get(header_name)
171 .map(|data_type| match data_type {
172 DataType::Bool(b) => *b,
173 _ => false,
174 })
175 .unwrap_or(false)
176 }
177
178 fn get_string_header(&self, header_name: &str) -> Option<String> {
179 self.request_headers
180 .get(header_name)
181 .map(|data_type| match data_type {
182 DataType::String(s) => s.clone(),
183 _ => None,
184 })
185 .unwrap_or(None)
186 }
187}
188
189impl Htmx {
190 fn from_inner(inner: Rc<RefCell<HtmxInner>>) -> Htmx {
191 let is_htmx = inner.borrow().get_bool_header(RequestHeaders::HX_REQUEST);
192 let boosted = inner.borrow().get_bool_header(RequestHeaders::HX_BOOSTED);
193 let history_restore_request = inner
194 .borrow()
195 .get_bool_header(RequestHeaders::HX_HISTORY_RESTORE_REQUEST);
196
197 Htmx {
198 inner,
199 is_htmx,
200 boosted,
201 history_restore_request,
202 }
203 }
204
205 pub fn new(req: &ServiceRequest) -> Htmx {
206 let req = req.request();
207 let inner = Rc::new(RefCell::new(HtmxInner::new(req)));
208 Htmx::from_inner(inner)
209 }
210
211 pub fn current_url(&self) -> Option<String> {
215 self.inner
216 .borrow()
217 .get_string_header(RequestHeaders::HX_CURRENT_URL)
218 }
219
220 pub fn prompt(&self) -> Option<String> {
224 self.inner
225 .borrow()
226 .get_string_header(RequestHeaders::HX_PROMPT)
227 }
228
229 pub fn target(&self) -> Option<String> {
233 self.inner
234 .borrow()
235 .get_string_header(RequestHeaders::HX_TARGET)
236 }
237
238 pub fn trigger(&self) -> Option<String> {
242 self.inner
243 .borrow()
244 .get_string_header(RequestHeaders::HX_TRIGGER)
245 }
246
247 pub fn trigger_name(&self) -> Option<String> {
251 self.inner
252 .borrow()
253 .get_string_header(RequestHeaders::HX_TRIGGER_NAME)
254 }
255
256 pub fn trigger_event(
285 &self,
286 name: String,
287 message: Option<String>,
288 trigger_type: Option<TriggerType>,
289 ) {
290 let trigger_type = trigger_type.unwrap_or(TriggerType::Standard);
291 match trigger_type {
292 TriggerType::Standard => {
293 if message.is_some() {
294 _ = self
295 .inner
296 .borrow_mut()
297 .simple_trigger
298 .entry(TriggerType::Standard)
299 .or_insert(false);
300 }
301 self.inner
302 .borrow_mut()
303 .standard_triggers
304 .insert(name, message);
305 }
306 TriggerType::AfterSettle => {
307 if message.is_some() {
308 _ = self
309 .inner
310 .borrow_mut()
311 .simple_trigger
312 .entry(TriggerType::AfterSettle)
313 .or_insert(false);
314 }
315 self.inner
316 .borrow_mut()
317 .after_settle_triggers
318 .insert(name, message);
319 }
320 TriggerType::AfterSwap => {
321 if message.is_some() {
322 _ = self
323 .inner
324 .borrow_mut()
325 .simple_trigger
326 .entry(TriggerType::AfterSwap)
327 .or_insert(false);
328 }
329 self.inner
330 .borrow_mut()
331 .after_swap_triggers
332 .insert(name, message);
333 }
334 }
335 }
336
337 pub fn redirect(&self, path: String) {
342 self.inner
343 .borrow_mut()
344 .response_headers
345 .insert(ResponseHeaders::HX_REDIRECT.to_string(), path);
346 }
347
348 pub fn redirect_with_swap(&self, path: String) {
353 self.inner
354 .borrow_mut()
355 .response_headers
356 .insert(ResponseHeaders::HX_LOCATION.to_string(), path);
357 }
358
359 pub fn refresh(&self) {
363 self.inner
364 .borrow_mut()
365 .response_headers
366 .insert(ResponseHeaders::HX_REFRESH.to_string(), "true".to_string());
367 }
368
369 pub fn push_url(&self, path: String) {
374 self.inner
375 .borrow_mut()
376 .response_headers
377 .insert(ResponseHeaders::HX_PUSH_URL.to_string(), path);
378 }
379
380 pub fn replace_url(&self, path: String) {
385 self.inner
386 .borrow_mut()
387 .response_headers
388 .insert(ResponseHeaders::HX_REPLACE_URL.to_string(), path);
389 }
390
391 pub fn reswap(&self, swap_type: SwapType) {
396 self.inner.borrow_mut().response_headers.insert(
397 ResponseHeaders::HX_RESWAP.to_string(),
398 swap_type.to_string(),
399 );
400 }
401
402 pub fn retarget(&self, selector: String) {
407 self.inner.borrow_mut().response_headers.insert(
408 ResponseHeaders::HX_RETARGET.to_string(),
409 selector.to_string(),
410 );
411 }
412
413 pub fn reselect(&self, selector: String) {
418 self.inner.borrow_mut().response_headers.insert(
419 ResponseHeaders::HX_RESELECT.to_string(),
420 selector.to_string(),
421 );
422 }
423
424 pub(crate) fn get_triggers(
425 &self,
426 trigger_type: TriggerType,
427 ) -> IndexMap<String, Option<String>> {
428 match trigger_type {
429 TriggerType::Standard => self.inner.borrow().standard_triggers.clone(),
430 TriggerType::AfterSettle => self.inner.borrow().after_settle_triggers.clone(),
431 TriggerType::AfterSwap => self.inner.borrow().after_swap_triggers.clone(),
432 }
433 }
434
435 pub(crate) fn is_simple_trigger(&self, trigger_type: TriggerType) -> bool {
436 match trigger_type {
437 TriggerType::Standard => *self
438 .inner
439 .borrow()
440 .simple_trigger
441 .get(&TriggerType::Standard)
442 .unwrap_or(&true),
443 TriggerType::AfterSettle => *self
444 .inner
445 .borrow()
446 .simple_trigger
447 .get(&TriggerType::AfterSettle)
448 .unwrap_or(&true),
449 TriggerType::AfterSwap => *self
450 .inner
451 .borrow()
452 .simple_trigger
453 .get(&TriggerType::AfterSwap)
454 .unwrap_or(&true),
455 }
456 }
457
458 pub(crate) fn get_response_headers(&self) -> IndexMap<String, String> {
459 self.inner.borrow().response_headers.clone()
460 }
461}
462
463impl FromRequest for Htmx {
464 type Error = Error;
465 type Future = Ready<Result<Htmx, Error>>;
466
467 #[inline]
468 fn from_request(req: &actix_web::HttpRequest, _: &mut Payload) -> Self::Future {
469 if let Some(htmx) = req.extensions_mut().get::<Htmx>() {
470 return ready(Ok(htmx.clone()));
471 }
472
473 let inner = Rc::new(RefCell::new(HtmxInner::new(req)));
474
475 ready(Ok(Htmx::from_inner(inner)))
476 }
477}
478
479trait AsBool {
480 fn as_bool(&self) -> bool;
481}
482
483trait AsOptionString {
484 fn as_option_string(&self) -> Option<String>;
485}
486
487impl AsBool for Option<&HeaderValue> {
488 fn as_bool(&self) -> bool {
489 match self {
490 Some(header) => {
491 if let Ok(header) = header.to_str() {
492 header.parse::<bool>().unwrap_or(false)
493 } else {
494 false
495 }
496 }
497 None => false,
498 }
499 }
500}
501
502impl AsOptionString for Option<&HeaderValue> {
503 fn as_option_string(&self) -> Option<String> {
504 match self {
505 Some(header) => {
506 if let Ok(header) = header.to_str() {
507 Some(header.to_string())
508 } else {
509 None
510 }
511 }
512 None => None,
513 }
514 }
515}