axum_routing_htmx_macros/
lib.rs1use 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 let route = syn::parse::<Route>(attr)?;
90 let function = syn::parse::<ItemFn>(item)?;
91
92 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 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 Ok(quote! {
122 #[allow(non_camel_case_types)]
123 #vis struct #htmx_struct<S> {
124 method_router: ::axum::routing::MethodRouter<S>,
126 }
127
128 impl<S> #htmx_struct<S> {
129 fn htmx_path(
131 &self,
132 #(#extracted_idents: impl ::std::fmt::Display,)*
133 ) -> String {
134 format!(#format_path, #(#extracted_idents,)*)
135 }
136
137 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}