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}