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
14macro_rules! collection {
15 ($($k:expr => $v:expr),* $(,)?) => {{
16 use std::iter::{Iterator, IntoIterator};
17 Iterator::collect(IntoIterator::into_iter([$(($k, $v),)*]))
18 }};
19}
20
21#[derive(Clone)]
22pub struct Htmx {
23 inner: Rc<RefCell<HtmxInner>>,
24 pub is_htmx: bool,
25 pub boosted: bool,
26 pub history_restore_request: bool,
27}
28
29#[derive(Clone, PartialEq, Eq, Hash)]
30pub enum TriggerType {
31 Standard,
32 AfterSettle,
33 AfterSwap,
34}
35
36pub enum SwapType {
37 InnerHtml,
38 OuterHtml,
39 BeforeBegin,
40 AfterBegin,
41 BeforeEnd,
42 AfterEnd,
43 Delete,
44 None,
45}
46
47enum DataType {
48 String(Option<String>),
49 Bool(bool),
50}
51
52impl fmt::Display for SwapType {
53 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
54 match self {
55 SwapType::InnerHtml => write!(f, "innerHTML"),
56 SwapType::OuterHtml => write!(f, "outerHTML"),
57 SwapType::BeforeBegin => write!(f, "beforebegin"),
58 SwapType::AfterBegin => write!(f, "afterbegin"),
59 SwapType::BeforeEnd => write!(f, "beforeend"),
60 SwapType::AfterEnd => write!(f, "afterend"),
61 SwapType::Delete => write!(f, "delete"),
62 SwapType::None => write!(f, "none"),
63 }
64 }
65}
66
67struct HtmxInner {
68 standard_triggers: IndexMap<String, Option<String>>,
69 after_settle_triggers: IndexMap<String, Option<String>>,
70 after_swap_triggers: IndexMap<String, Option<String>>,
71 response_headers: IndexMap<String, String>,
72 request_headers: IndexMap<String, DataType>,
73 simple_trigger: HashMap<TriggerType, bool>,
74}
75
76impl HtmxInner {
77 pub fn new(req: &HttpRequest) -> HtmxInner {
78 let request_headers = collection![
79 RequestHeaders::HX_REQUEST.to_string() => DataType::Bool(req.headers().get(RequestHeaders::HX_REQUEST).as_bool()),
80 RequestHeaders::HX_BOOSTED.to_string() => DataType::Bool(req.headers().get(RequestHeaders::HX_BOOSTED).as_bool()),
81 RequestHeaders::HX_CURRENT_URL.to_string() => DataType::String(req.headers().get(RequestHeaders::HX_CURRENT_URL).as_option_string()),
82 RequestHeaders::HX_HISTORY_RESTORE_REQUEST.to_string() => DataType::Bool(req.headers().get(RequestHeaders::HX_HISTORY_RESTORE_REQUEST).as_bool()),
83 RequestHeaders::HX_PROMPT.to_string() => DataType::String(req.headers().get(RequestHeaders::HX_PROMPT).as_option_string()),
84 RequestHeaders::HX_TARGET.to_string() => DataType::String(req.headers().get(RequestHeaders::HX_TARGET).as_option_string()),
85 RequestHeaders::HX_TRIGGER.to_string() => DataType::String(req.headers().get(RequestHeaders::HX_TRIGGER).as_option_string()),
86 RequestHeaders::HX_TRIGGER_NAME.to_string() => DataType::String(req.headers().get(RequestHeaders::HX_TRIGGER_NAME).as_option_string()),
87 ];
88
89 HtmxInner {
90 request_headers,
91 response_headers: IndexMap::new(),
92 standard_triggers: IndexMap::new(),
93 after_settle_triggers: IndexMap::new(),
94 after_swap_triggers: IndexMap::new(),
95 simple_trigger: HashMap::new(),
96 }
97 }
98
99 fn get_bool_header(&self, header_name: &str) -> bool {
100 self.request_headers
101 .get(header_name)
102 .map(|data_type| match data_type {
103 DataType::Bool(b) => *b,
104 _ => false,
105 })
106 .unwrap_or(false)
107 }
108
109 fn get_string_header(&self, header_name: &str) -> Option<String> {
110 self.request_headers
111 .get(header_name)
112 .map(|data_type| match data_type {
113 DataType::String(s) => {
114 if let Some(s) = s {
115 Some(s.clone())
116 } else {
117 None
118 }
119 },
120 _ => None,
121 })
122 .unwrap_or(None)
123 }
124}
125
126impl Htmx {
127 fn from_inner(inner: Rc<RefCell<HtmxInner>>) -> Htmx {
128 let is_htmx = inner.borrow().get_bool_header(RequestHeaders::HX_REQUEST);
129 let boosted = inner.borrow().get_bool_header(RequestHeaders::HX_BOOSTED);
130 let history_restore_request = inner.borrow().get_bool_header(RequestHeaders::HX_HISTORY_RESTORE_REQUEST);
131
132 Htmx {
133 inner,
134 is_htmx,
135 boosted,
136 history_restore_request,
137 }
138 }
139
140 pub fn new(req: &ServiceRequest) -> Htmx {
141 let req = req.request();
142 let inner = Rc::new(RefCell::new(HtmxInner::new(req)));
143 Htmx::from_inner(inner)
144 }
145
146 pub fn current_url(&self) -> Option<String> {
147 self.inner.borrow().get_string_header(RequestHeaders::HX_CURRENT_URL)
148 }
149
150 pub fn prompt(&self) -> Option<String> {
151 self.inner.borrow().get_string_header(RequestHeaders::HX_PROMPT)
152 }
153
154 pub fn target(&self) -> Option<String> {
155 self.inner.borrow().get_string_header(RequestHeaders::HX_TARGET)
156 }
157
158 pub fn trigger(&self) -> Option<String> {
159 self.inner.borrow().get_string_header(RequestHeaders::HX_TRIGGER)
160 }
161
162 pub fn trigger_name(&self) -> Option<String> {
163 self.inner.borrow().get_string_header(RequestHeaders::HX_TRIGGER_NAME)
164 }
165
166 pub fn trigger_event(&self, name: String, message: Option<String>, trigger_type: Option<TriggerType>) {
167 let trigger_type = trigger_type.unwrap_or(TriggerType::Standard);
168 match trigger_type {
169 TriggerType::Standard => {
170 if message != None {
171 _ = self.inner.borrow_mut().simple_trigger.entry(TriggerType::Standard).or_insert(false);
172 }
173 self.inner.borrow_mut().standard_triggers.insert(name, message);
174 }
175 TriggerType::AfterSettle => {
176 if message != None {
177 _ = self.inner.borrow_mut().simple_trigger.entry(TriggerType::AfterSettle).or_insert(false);
178 }
179 self.inner
180 .borrow_mut()
181 .after_settle_triggers
182 .insert(name, message);
183 }
184 TriggerType::AfterSwap => {
185 if message != None {
186 _ = self.inner.borrow_mut().simple_trigger.entry(TriggerType::AfterSwap).or_insert(false);
187 }
188 self.inner
189 .borrow_mut()
190 .after_swap_triggers
191 .insert(name, message);
192 }
193 }
194 }
195
196 pub fn redirect(&self, path: String) {
197 self.inner
198 .borrow_mut()
199 .response_headers
200 .insert(ResponseHeaders::HX_REDIRECT.to_string(), path);
201 }
202
203 pub fn redirect_with_swap(&self, path: String) {
204 self.inner
205 .borrow_mut()
206 .response_headers
207 .insert(ResponseHeaders::HX_LOCATION.to_string(), path);
208 }
209
210 pub fn refresh(&self) {
211 self.inner
212 .borrow_mut()
213 .response_headers
214 .insert(ResponseHeaders::HX_REFRESH.to_string(), "true".to_string());
215 }
216
217 pub fn push_url(&self, path: String) {
218 self.inner
219 .borrow_mut()
220 .response_headers
221 .insert(ResponseHeaders::HX_PUSH_URL.to_string(), path);
222 }
223
224 pub fn replace_url(&self, path: String) {
225 self.inner
226 .borrow_mut()
227 .response_headers
228 .insert(ResponseHeaders::HX_REPLACE_URL.to_string(), path);
229 }
230
231 pub fn reswap(&self, swap_type: SwapType) {
232 self.inner.borrow_mut().response_headers.insert(
233 ResponseHeaders::HX_RESWAP.to_string(),
234 swap_type.to_string(),
235 );
236 }
237
238 pub fn retarget(&self, selector: String) {
239 self.inner.borrow_mut().response_headers.insert(
240 ResponseHeaders::HX_RETARGET.to_string(),
241 selector.to_string(),
242 );
243 }
244
245 pub fn reselect(&self, selector: String) {
246 self.inner.borrow_mut().response_headers.insert(
247 ResponseHeaders::HX_RESELECT.to_string(),
248 selector.to_string(),
249 );
250 }
251
252 pub(crate) fn get_triggers(&self, trigger_type: TriggerType) -> IndexMap<String, Option<String>> {
253 match trigger_type {
254 TriggerType::Standard => self.inner.borrow().standard_triggers.clone(),
255 TriggerType::AfterSettle => self.inner.borrow().after_settle_triggers.clone(),
256 TriggerType::AfterSwap => self.inner.borrow().after_swap_triggers.clone(),
257 }
258 }
259
260 pub(crate) fn is_simple_trigger(&self, trigger_type: TriggerType) -> bool {
261 match trigger_type {
262 TriggerType::Standard => *self.inner.borrow().simple_trigger.get(&TriggerType::Standard).unwrap_or(&true),
263 TriggerType::AfterSettle => *self.inner.borrow().simple_trigger.get(&TriggerType::AfterSettle).unwrap_or(&true),
264 TriggerType::AfterSwap => *self.inner.borrow().simple_trigger.get(&TriggerType::AfterSwap).unwrap_or(&true),
265 }
266 }
267
268 pub(crate) fn get_response_headers(&self) -> IndexMap<String, String> {
269 self.inner.borrow().response_headers.clone()
270 }
271}
272
273impl FromRequest for Htmx {
274 type Error = Error;
275 type Future = Ready<Result<Htmx, Error>>;
276
277 #[inline]
278 fn from_request(req: &actix_web::HttpRequest, _: &mut Payload) -> Self::Future {
279 if let Some(htmx) = req.extensions_mut().get::<Htmx>() {
280 return ready(Ok(htmx.clone()));
281 }
282
283 let inner = Rc::new(RefCell::new(HtmxInner::new(req)));
284
285 ready(Ok(Htmx::from_inner(inner)))
286 }
287}
288
289trait AsBool {
290 fn as_bool(&self) -> bool;
291}
292
293trait AsOptionString {
294 fn as_option_string(&self) -> Option<String>;
295}
296
297impl AsBool for Option<&HeaderValue> {
298 fn as_bool(&self) -> bool {
299 match self {
300 Some(header) => {
301 if let Ok(header) = header.to_str() {
302 header.parse::<bool>().unwrap_or(false)
303 } else {
304 false
305 }
306 }
307 None => false,
308 }
309 }
310}
311
312impl AsOptionString for Option<&HeaderValue> {
313 fn as_option_string(&self) -> Option<String> {
314 match self {
315 Some(header) => {
316 if let Ok(header) = header.to_str() {
317 Some(header.to_string())
318 } else {
319 None
320 }
321 }
322 None => None,
323 }
324 }
325}