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}