acton_htmx/middleware/helpers.rs
1//! Middleware layer construction helpers
2//!
3//! This module provides helper utilities for creating middleware layers with
4//! consistent patterns across the framework. These helpers reduce boilerplate
5//! while maintaining clarity and idiomatic Rust code.
6//!
7//! # HTMX Request Detection
8//!
9//! The [`is_htmx_request`] function provides centralized HTMX request detection
10//! used by all middleware and extractors in the framework.
11
12use axum::http::HeaderMap;
13
14/// Check if the request is an HTMX request.
15///
16/// HTMX requests include the `HX-Request: true` header. This helper function
17/// provides a single source of truth for HTMX detection across all extractors
18/// and middleware in the framework.
19///
20/// # Arguments
21///
22/// * `headers` - The request headers to check
23///
24/// # Returns
25///
26/// `true` if the request is from HTMX, `false` otherwise.
27///
28/// # Example
29///
30/// ```rust
31/// use axum::http::HeaderMap;
32/// use acton_htmx::middleware::helpers::is_htmx_request;
33///
34/// let mut headers = HeaderMap::new();
35/// assert!(!is_htmx_request(&headers));
36///
37/// headers.insert("HX-Request", "true".parse().unwrap());
38/// assert!(is_htmx_request(&headers));
39/// ```
40#[must_use]
41#[inline]
42pub fn is_htmx_request(headers: &HeaderMap) -> bool {
43 headers
44 .get("HX-Request")
45 .and_then(|v| v.to_str().ok())
46 == Some("true")
47}
48
49/// Helper macro for creating standard middleware layer constructors
50///
51/// This macro generates the common constructor patterns that most middleware
52/// layers need: `new()`, `with_config()`, and `from_handle()`.
53///
54/// # Example
55///
56/// ```rust,ignore
57/// use acton_htmx::middleware::middleware_constructors;
58/// use acton_htmx::state::ActonHtmxState;
59/// use acton_reactive::prelude::AgentHandle;
60///
61/// pub struct MyMiddleware {
62/// agent_handle: AgentHandle<MyAgent>,
63/// }
64///
65/// middleware_constructors!(
66/// MyMiddleware, // Middleware type
67/// MyAgent, // Agent type
68/// agent_handle, // Field name for the handle
69/// MyConfig // Config type
70/// );
71/// ```
72///
73/// This generates:
74/// ```rust,ignore
75/// impl MyMiddleware {
76/// pub fn new(state: &ActonHtmxState) -> Self {
77/// Self {
78/// agent_handle: state.my_agent().clone(),
79/// }
80/// }
81///
82/// pub fn with_config(state: &ActonHtmxState, _config: MyConfig) -> Self {
83/// Self::new(state)
84/// }
85///
86/// pub fn from_handle(handle: AgentHandle<MyAgent>) -> Self {
87/// Self {
88/// agent_handle: handle,
89/// }
90/// }
91/// }
92/// ```
93#[macro_export]
94macro_rules! middleware_constructors {
95 ($middleware:ty, $agent:ty, $field:ident, $config:ty, $state_method:ident) => {
96 impl $middleware {
97 /// Create middleware from application state
98 ///
99 /// This is the standard way to create middleware when adding it to your router.
100 #[must_use]
101 pub fn new(state: &$crate::state::ActonHtmxState) -> Self {
102 Self {
103 $field: state.$state_method().clone(),
104 }
105 }
106
107 /// Create middleware with custom configuration
108 ///
109 /// Note: Most middleware layers ignore the config parameter and use
110 /// the configuration from the agent initialization. This method exists
111 /// for API consistency.
112 #[must_use]
113 pub fn with_config(state: &$crate::state::ActonHtmxState, _config: $config) -> Self {
114 Self::new(state)
115 }
116
117 /// Create middleware directly from an agent handle
118 ///
119 /// This is useful for testing or when you have a custom agent instance.
120 #[must_use]
121 pub const fn from_handle(handle: acton_reactive::prelude::AgentHandle<$agent>) -> Self {
122 Self { $field: handle }
123 }
124 }
125 };
126}
127
128#[cfg(test)]
129mod tests {
130 use super::*;
131
132 #[test]
133 fn test_is_htmx_request_with_header() {
134 let mut headers = HeaderMap::new();
135 headers.insert("HX-Request", "true".parse().unwrap());
136 assert!(is_htmx_request(&headers));
137 }
138
139 #[test]
140 fn test_is_htmx_request_without_header() {
141 let headers = HeaderMap::new();
142 assert!(!is_htmx_request(&headers));
143 }
144
145 #[test]
146 fn test_is_htmx_request_with_wrong_value() {
147 let mut headers = HeaderMap::new();
148 headers.insert("HX-Request", "false".parse().unwrap());
149 assert!(!is_htmx_request(&headers));
150 }
151
152 #[test]
153 fn test_is_htmx_request_with_empty_value() {
154 let mut headers = HeaderMap::new();
155 headers.insert("HX-Request", "".parse().unwrap());
156 assert!(!is_htmx_request(&headers));
157 }
158
159 #[test]
160 fn test_is_htmx_request_case_sensitive() {
161 let mut headers = HeaderMap::new();
162 headers.insert("HX-Request", "True".parse().unwrap());
163 assert!(!is_htmx_request(&headers));
164 }
165
166 // Macro usage is tested within the actual middleware implementations
167 // (session, csrf, auth) which use this macro.
168}