axum_routing_htmx_macros/
lib.rs

1use compilation::CompiledRoute;
2use parsing::Route;
3use proc_macro::TokenStream;
4use proc_macro2::{Ident, Span, TokenStream as TokenStream2};
5use std::collections::HashMap;
6use syn::{
7    parse::{Parse, ParseStream},
8    punctuated::Punctuated,
9    token::{Colon, Comma, Slash},
10    FnArg, GenericArgument, ItemFn, LitStr, PathArguments, Type,
11};
12#[macro_use]
13extern crate quote;
14#[macro_use]
15extern crate syn;
16
17mod compilation;
18mod parsing;
19
20macro_rules! hx_route {
21    ($method:ident, $enum_verb:literal, $axum_method:literal) => {
22        #[doc = concat!("
23A macro that generates HTMX-compatible statically-typed ", $enum_verb, " routes for axum handlers.
24
25# Syntax
26```ignore
27#[", stringify!($method), "(\"<PATH>\" [with <STATE>])]
28```
29- `PATH` is the path of the route, with optional path parameters and query parameters,
30    e.g. `/item/:id?amount&offset`.
31- `STATE` is the type of axum-state, passed to the handler. This is optional, and if not
32    specified, the state type is guessed based on the parameters of the handler.
33
34# Example
35```
36use axum::extract::{State, Json};
37use axum_routing_htmx::", stringify!($method), ";
38
39#[", stringify!($method), "(\"/item/:id?amount&offset\")]
40async fn item_handler(
41    id: u32,
42    amount: Option<u32>,
43    offset: Option<u32>,
44    State(state): State<String>,
45    Json(json): Json<u32>,
46) -> String {
47    todo!(\"handle request\")
48}
49```
50
51# State type
52Normally, the state-type is guessed based on the parameters of the function:
53If the function has a parameter of type `[..]::State<T>`, then `T` is used as the state type.
54This should work for most cases, however when not sufficient, the state type can be specified
55explicitly using the `with` keyword:
56```ignore
57#[", stringify!($method), "(\"/item/:id?amount&offset\" with String)]
58```
59
60# Internals
61The macro expands to a function that returns an [`HtmxHandler<S>`].")]
62        #[proc_macro_attribute]
63        pub fn $method(attr: TokenStream, mut item: TokenStream) -> TokenStream {
64            match _route(attr, item.clone(), $enum_verb, $axum_method) {
65                Ok(tokens) => tokens.into(),
66                Err(err) => {
67                    let err: TokenStream = err.to_compile_error().into();
68                    item.extend(err);
69                    item
70                }
71            }
72        }
73    };
74}
75
76hx_route!(hx_get, "Get", "get");
77hx_route!(hx_post, "Post", "post");
78hx_route!(hx_delete, "Delete", "delete");
79hx_route!(hx_patch, "Patch", "patch");
80hx_route!(hx_put, "Put", "put");
81
82fn _route(
83    attr: TokenStream,
84    item: TokenStream,
85    enum_verb: &'static str,
86    axum_method: &'static str,
87) -> syn::Result<TokenStream2> {
88    // Parse the route and function
89    let route = syn::parse::<Route>(attr)?;
90    let function = syn::parse::<ItemFn>(item)?;
91
92    // Now we can compile the route
93    let route = CompiledRoute::from_route(route, &function)?;
94    let path_extractor = route.path_extractor();
95    let query_extractor = route.query_extractor();
96    let query_params_struct = route.query_params_struct();
97    let state_type = &route.state;
98    let axum_path = route.to_axum_path_string();
99    let format_path = route.to_format_path_string();
100    let remaining_numbered_pats = route.remaining_pattypes_numbered(&function.sig.inputs);
101    let extracted_idents = route.extracted_idents();
102    let remaining_numbered_idents = remaining_numbered_pats.iter().map(|pat_type| &pat_type.pat);
103    let route_docs = route.to_doc_comments();
104
105    // Get the variables we need for code generation
106    let fn_name = &function.sig.ident;
107    let fn_output = &function.sig.output;
108    let vis = &function.vis;
109    let asyncness = &function.sig.asyncness;
110    let (impl_generics, ty_generics, where_clause) = &function.sig.generics.split_for_impl();
111    let ty_generics = ty_generics.as_turbofish();
112    let fn_docs = function
113        .attrs
114        .iter()
115        .filter(|attr| attr.path().is_ident("doc"));
116    let enum_method = format_ident!("{}", enum_verb);
117    let http_method = format_ident!("{}", axum_method);
118    let htmx_struct = format_ident!("__HtmxHandler_{}", fn_name);
119
120    // Generate the code
121    Ok(quote! {
122        #[allow(non_camel_case_types)]
123        #vis struct #htmx_struct<S> {
124            /// The MethodRouter that must be consumed by axum.
125            method_router: ::axum::routing::MethodRouter<S>,
126        }
127
128        impl<S> #htmx_struct<S> {
129            /// Generates a path according to the expected fields of the handler.
130            fn htmx_path(
131                &self,
132                #(#extracted_idents: impl ::std::fmt::Display,)*
133            ) -> String {
134                format!(#format_path, #(#extracted_idents,)*)
135            }
136
137            /// Which HTMX method this corresponds with. The `Display` interface
138            /// can be used to generate the HTML attribute name.
139            fn htmx_method(&self) -> ::axum_routing_htmx::HtmxMethod {
140                ::axum_routing_htmx::HtmxMethod::#enum_method
141            }
142        }
143
144        impl<S> ::axum_routing_htmx::HtmxHandler<S> for #htmx_struct<S> {
145            fn axum_router(self) -> (&'static str, ::axum::routing::MethodRouter<S>) {
146                (#axum_path, self.method_router)
147            }
148        }
149
150        #(#fn_docs)*
151        #route_docs
152        #vis fn #fn_name #impl_generics() -> #htmx_struct<#state_type> #where_clause {
153
154            #query_params_struct
155
156            #asyncness fn __inner #impl_generics(
157                #path_extractor
158                #query_extractor
159                #remaining_numbered_pats
160            ) #fn_output #where_clause {
161                #function
162
163                #fn_name #ty_generics(#(#extracted_idents,)* #(#remaining_numbered_idents,)* ).await
164            }
165
166            #htmx_struct {
167                method_router: ::axum::routing::#http_method(__inner #ty_generics)
168            }
169        }
170    })
171}