Skip to main content

htmxology_macros/
lib.rs

1//! HTMX-SSR macros
2
3use syn::parse_macro_input;
4
5mod display_delegate;
6mod fragment;
7mod identity;
8mod named;
9mod route;
10mod routing_controller;
11mod utils;
12
13/// Derive a route type.
14///
15/// Route types are enum types that represent the possible routes in an HTMX application.
16#[proc_macro_derive(Route, attributes(route, subroute, catch_all, query, body))]
17pub fn derive_route(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
18    let mut input = parse_macro_input!(input as syn::DeriveInput);
19
20    route::derive(&mut input)
21        .unwrap_or_else(syn::Error::into_compile_error)
22        .into()
23}
24
25/// Implement the `Display` trait for an enum.
26///
27/// This derive macro simply implements the `Display` trait for the annotated enum type, by
28/// delegating it to the `Display` implementation of the inner variants.
29#[proc_macro_derive(DisplayDelegate)]
30pub fn derive_display_delegate(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
31    let mut input = parse_macro_input!(input as syn::DeriveInput);
32
33    display_delegate::derive(&mut input)
34        .unwrap_or_else(syn::Error::into_compile_error)
35        .into()
36}
37
38/// Implement the `RoutingController` trait for a controller.
39///
40/// This derive macro automatically implements sub-controller routing for a controller type
41/// by generating the necessary `Controller` and `HasSubcontroller` trait implementations.
42///
43/// # Attributes
44///
45/// - `#[controller(RouteType, args = ArgsType, pre_handler = "function", extra_derives = (Trait1, Trait2))]` - Specifies the route enum type and optional configuration:
46///   - `RouteType` - The route enum type for this controller (required)
47///   - `args = ArgsType` - The Args type passed to handle_request (optional, defaults to `()`)
48///   - `pre_handler = "function"` - Async function called before routing (optional)
49///     - Signature: `async fn(&self, &Route, &htmx::Request, &http::request::Parts, &ServerInfo, &mut Args) -> Option<Response>`
50///     - Returns `Some(response)` to short-circuit routing and return immediately
51///     - Returns `None` to proceed with normal routing
52///     - Use case: Authentication, rate limiting, request validation
53///   - `extra_derives = (Trait1, Trait2, ...)` - Additional derive traits for the generated route enum (optional)
54///     - Useful for adding `PartialEq`, `Eq`, `Hash`, `Serialize`, `Deserialize`, etc.
55/// - `#[subcontroller(...)]` - Defines a subcontroller with the following options:
56///   - `route = VariantName` - The route variant name (required)
57///   - `path = "path/"` - URL path for this subcontroller (optional)
58///   - `params(name: Type, ...)` - Path parameters to extract (optional)
59///   - `convert_with = "function"` - Custom function to create the subcontroller (optional)
60///   - `convert_response = "function"` - Custom function to convert the subcontroller's response (optional)
61///   - `doc = "description"` - Documentation for the route variant (optional)
62///
63/// # Response Type Conversion
64///
65/// The macro automatically handles converting subcontroller responses to parent controller responses
66/// in the generated `handle_request` method.
67///
68/// By default, the conversion uses `.into()`, assuming the parent's response type
69/// implements `From<SubcontrollerResponse>`. For custom conversions, use the `convert_response`
70/// attribute to specify a conversion function that will be called inline.
71///
72/// ```ignore
73/// #[subcontroller(
74///     MyController,
75///     route = MyRoute,
76///     path = "my-path/",
77///     convert_response = "Ok"
78/// )]
79/// ```
80///
81/// # Example
82///
83/// ```ignore
84/// use htmxology::{Controller, RoutingController, Route};
85///
86/// #[derive(RoutingController)]
87/// #[controller(AppRoute)]
88/// #[subcontroller(BlogController, route = Blog, path = "blog/")]
89/// #[subcontroller(
90///     AdminController,
91///     route = Admin,
92///     path = "admin/",
93///     convert_response = "Ok"
94/// )]
95/// struct MainController {
96///     // ... fields
97/// }
98/// ```
99///
100/// # Example with Pre-Handler
101///
102/// ```ignore
103/// use htmxology::{Controller, RoutingController, Route, htmx, ServerInfo};
104///
105/// #[derive(RoutingController)]
106/// #[controller(AppRoute, args = Session, pre_handler = "Self::authenticate")]
107/// #[subcontroller(DashboardController, route = Dashboard, path = "dashboard/")]
108/// struct MainController {
109///     // ... fields
110/// }
111///
112/// impl MainController {
113///     async fn authenticate(
114///         &self,
115///         route: &AppRoute,
116///         htmx: &htmx::Request,
117///         parts: &http::request::Parts,
118///         server_info: &ServerInfo,
119///         args: &mut Session,
120///     ) -> Option<Result<axum::response::Response, axum::response::Response>> {
121///         // Check authentication
122///         if !args.is_authenticated {
123///             // Return early with redirect to login
124///             return Some(Ok(axum::response::Redirect::to("/login").into_response()));
125///         }
126///         // Continue with normal routing
127///         None
128///     }
129/// }
130/// ```
131#[proc_macro_derive(RoutingController, attributes(controller, subcontroller))]
132pub fn derive_routing_controller(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
133    let mut input = parse_macro_input!(input as syn::DeriveInput);
134
135    routing_controller::derive(&mut input)
136        .unwrap_or_else(syn::Error::into_compile_error)
137        .into()
138}
139
140/// Derive the `Identity` trait for a type.
141///
142/// This macro implements the `Identity` trait, which provides a unique HTML ID for an element.
143/// The ID is validated at compile time to ensure it follows HTML5 rules.
144///
145/// # Examples
146///
147/// Using a static ID:
148///
149/// ```ignore
150/// use htmxology::htmx::Identity;
151///
152/// #[derive(Identity)]
153/// #[identity("my-element")]
154/// struct MyElement {
155///     content: String,
156/// }
157/// ```
158///
159/// Using a function to compute the ID dynamically:
160///
161/// ```ignore
162/// use htmxology::htmx::{Identity, HtmlId};
163///
164/// #[derive(Identity)]
165/// #[identity(with_fn = "get_id")]
166/// struct DynamicElement {
167///     index: usize,
168/// }
169///
170/// impl DynamicElement {
171///     fn get_id(&self) -> HtmlId {
172///         HtmlId::from_string(format!("element-{}", self.index))
173///             .expect("valid ID")
174///     }
175/// }
176/// ```
177#[proc_macro_derive(Identity, attributes(identity))]
178pub fn derive_identity(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
179    let mut input = parse_macro_input!(input as syn::DeriveInput);
180
181    identity::derive(&mut input)
182        .unwrap_or_else(syn::Error::into_compile_error)
183        .into()
184}
185
186/// Derive the `Named` trait for a type.
187///
188/// This macro implements the `Named` trait, which provides a unique HTML name attribute
189/// for a form element. The name is validated at compile time to ensure it follows HTML5 rules.
190///
191/// # Examples
192///
193/// Using a static name:
194///
195/// ```ignore
196/// use htmxology::htmx::Named;
197///
198/// #[derive(Named)]
199/// #[named("user-email")]
200/// struct EmailField {
201///     value: String,
202/// }
203/// ```
204///
205/// Using a function to compute the name dynamically:
206///
207/// ```ignore
208/// use htmxology::htmx::{Named, HtmlName};
209///
210/// #[derive(Named)]
211/// #[named(with_fn = "get_name")]
212/// struct DynamicField {
213///     field_type: String,
214/// }
215///
216/// impl DynamicField {
217///     fn get_name(&self) -> HtmlName {
218///         HtmlName::from_string(format!("field-{}", self.field_type))
219///             .expect("valid name")
220///     }
221/// }
222/// ```
223#[proc_macro_derive(Named, attributes(named))]
224pub fn derive_named(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
225    let mut input = parse_macro_input!(input as syn::DeriveInput);
226
227    named::derive(&mut input)
228        .unwrap_or_else(syn::Error::into_compile_error)
229        .into()
230}
231
232/// Derive the `Fragment` trait for a type.
233///
234/// This macro implements the `Fragment` trait, which extends `Identity` and specifies
235/// the HTMX swap strategy to use for out-of-band swaps.
236///
237/// Note: The type must also implement `Identity` (either manually or via derive).
238///
239/// # Examples
240///
241/// Using a static strategy:
242///
243/// ```ignore
244/// use htmxology::htmx::{Identity, Fragment};
245///
246/// #[derive(Identity, Fragment)]
247/// #[identity("notification")]
248/// #[fragment(strategy = "innerHTML")]
249/// struct Notification {
250///     message: String,
251/// }
252/// ```
253///
254/// Using a function to compute the strategy dynamically:
255///
256/// ```ignore
257/// use htmxology::htmx::{Identity, Fragment, InsertStrategy};
258///
259/// #[derive(Identity, Fragment)]
260/// #[identity("dynamic-element")]
261/// #[fragment(with_fn = "get_strategy")]
262/// struct DynamicElement {
263///     should_replace: bool,
264/// }
265///
266/// impl DynamicElement {
267///     fn get_strategy(&self) -> InsertStrategy {
268///         if self.should_replace {
269///             InsertStrategy::OuterHtml
270///         } else {
271///             InsertStrategy::InnerHtml
272///         }
273///     }
274/// }
275/// ```
276///
277/// # Supported strategies
278///
279/// The macro accepts HTMX-standard strategy strings:
280/// - `"innerHTML"` - Replace inner HTML
281/// - `"outerHTML"` - Replace outer HTML
282/// - `"textContent"` - Replace text content
283/// - `"beforebegin"` - Insert before element
284/// - `"afterbegin"` - Insert after opening tag
285/// - `"beforeend"` - Insert before closing tag
286/// - `"afterend"` - Insert after element
287/// - `"delete"` - Delete the element
288/// - `"none"` - Do nothing
289/// - Any other string will be treated as a custom strategy
290#[proc_macro_derive(Fragment, attributes(fragment))]
291pub fn derive_fragment(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
292    let mut input = parse_macro_input!(input as syn::DeriveInput);
293
294    fragment::derive(&mut input)
295        .unwrap_or_else(syn::Error::into_compile_error)
296        .into()
297}