axum_folder_router/lib.rs
1//! # ```axum_folder_router``` Macro Documentation
2//!
3//! [folder_router!] is a procedural macro for the Axum web framework that automatically generates router boilerplate based on your file structure. It simplifies route organization by using filesystem conventions to define your API routes.
4//!
5//! ## Installation
6//!
7//! Add the dependency to your ```Cargo.toml```:
8//!
9//! ```toml
10//! [dependencies]
11//! axum_folder_router = "0.1.0"
12//! axum = "0.7"
13//! ```
14//!
15//! ## Basic Usage
16//!
17//! The macro scans a directory for ```route.rs``` files and automatically creates an Axum router based on the file structure:
18//!
19//! ```rust,no_run
20#![doc = include_str!("../examples/simple/main.rs")]
21//! ```
22//!
23//! ## File Structure Convention
24//!
25//! The macro converts your file structure into routes:
26//!
27//! ```text
28//! src/api/
29//! ├── route.rs -> "/"
30//! ├── hello/
31//! │ └── route.rs -> "/hello"
32//! ├── users/
33//! │ ├── route.rs -> "/users"
34//! │ └── [id]/
35//! │ └── route.rs -> "/users/{id}"
36//! └── files/
37//! └── [...path]/
38//! └── route.rs -> "/files/*path"
39//! ```
40//!
41//! Each ```route.rs``` file can contain HTTP method handlers that are automatically mapped to the corresponding route.
42//!
43//! ## Route Handlers
44//!
45//! Inside each ```route.rs``` file, define async functions named after HTTP methods:
46//!
47//! ```rust
48#![doc = include_str!("../examples/simple/api/route.rs")]
49//! ```
50//!
51//! ## Supported Features
52//!
53//! ### HTTP Methods
54//!
55//! The macro supports all standard HTTP methods:
56//! - ```get```
57//! - ```post```
58//! - ```put```
59//! - ```delete```
60//! - ```patch```
61//! - ```head```
62//! - ```options```
63//!
64//! ### Path Parameters
65//!
66//! Dynamic path segments are defined using brackets:
67//!
68//! ```text
69//! src/api/users/[id]/route.rs -> "/users/{id}"
70//! ```
71//!
72//! Inside the route handler:
73//!
74//! ```rust
75//! use axum::{
76//! extract::Path,
77//! response::IntoResponse
78//! };
79//!
80//! pub async fn get(Path(id): Path<String>) -> impl IntoResponse {
81//! format!("User ID: {}", id)
82//! }
83//! ```
84//!
85//! ### Catch-all Parameters
86//!
87//! Use the spread syntax for catch-all segments:
88//!
89//! ```text
90//! src/api/files/[...path]/route.rs -> "/files/*path"
91//! ```
92//!
93//! ```rust
94//! use axum::{
95//! extract::Path,
96//! response::IntoResponse
97//! };
98//!
99//! pub async fn get(Path(path): Path<String>) -> impl IntoResponse {
100//! format!("Requested file path: {}", path)
101//! }
102//! ```
103//!
104//! ### State Extraction
105//!
106//! The state type provided to the macro is available in all route handlers:
107//!
108//! ```rust
109//! use axum::{
110//! extract::State,
111//! response::IntoResponse
112//! };
113//!
114//! # #[derive(Debug, Clone)]
115//! # struct AppState ();
116//!
117//! pub async fn get(State(state): State<AppState>) -> impl IntoResponse {
118//! format!("State: {:?}", state)
119//! }
120//! ```
121//!
122//! ## Limitations
123//!
124//! - **Compile-time Only**: The routing is determined at compile time, so dynamic route registration isn't supported.
125//! - **File I/O**: The macro performs file I/O during compilation, which may have implications in certain build environments.
126//! - **Single State Type**: All routes share the same state type, though you can use ```FromRef``` for more granular state extraction.
127//!
128//! ## Best Practices
129//!
130//! 1. **Consistent Structure**: Maintain a consistent file structure to make your API organization predictable.
131//! 2. **Individual Route Files**: Use one ```route.rs``` file per route path for clarity.
132//! 3. **Module Organization**: Consider grouping related functionality in directories.
133//! 4. **Documentation**: Add comments to each route handler explaining its purpose.
134
135use proc_macro::TokenStream;
136use quote::{format_ident, quote};
137use std::fs;
138use std::path::{Path, PathBuf};
139use syn::{Ident, LitStr, Result, Token, parse::Parse, parse::ParseStream, parse_macro_input};
140
141struct FolderRouterArgs {
142 path: String,
143 state_type: Ident,
144}
145
146impl Parse for FolderRouterArgs {
147 fn parse(input: ParseStream) -> Result<Self> {
148 let path_lit = input.parse::<LitStr>()?;
149 input.parse::<Token![,]>()?;
150 let state_type = input.parse::<Ident>()?;
151
152 Ok(FolderRouterArgs {
153 path: path_lit.value(),
154 state_type,
155 })
156 }
157}
158
159/// Creates an Axum router by scanning a directory for `route.rs` files.
160///
161/// # Parameters
162///
163/// * `path` - A string literal pointing to the API directory, relative to the Cargo manifest directory
164/// * `state_type` - The type name of your application state that will be shared across all routes
165///
166/// # Example
167///
168/// ```rust
169/// # use axum_folder_router::folder_router;
170/// # #[derive(Debug, Clone)]
171/// # struct AppState ();
172/// #
173/// let router = folder_router!("./src/api", AppState);
174/// ```
175///
176/// This will scan all `route.rs` files in the `./src/api` directory and its subdirectories,
177/// automatically mapping their path structure to URL routes with the specified state type.
178#[proc_macro]
179pub fn folder_router(input: TokenStream) -> TokenStream {
180 let args = parse_macro_input!(input as FolderRouterArgs);
181 let base_path = args.path;
182 let state_type = args.state_type;
183
184 // Get the project root directory
185 let manifest_dir = std::env::var("CARGO_MANIFEST_DIR").unwrap();
186 let base_dir = Path::new(&manifest_dir).join(&base_path);
187
188 // Collect route files
189 let mut routes = Vec::new();
190 collect_route_files(&base_dir, &base_dir, &mut routes);
191
192 // Generate module definitions and route registrations
193 let mut module_defs = Vec::new();
194 let mut route_registrations = Vec::new();
195
196 for (route_path, rel_path) in routes {
197 // Generate module name and axum path
198 let (axum_path, mod_name) = path_to_route_info(&rel_path);
199 let mod_ident = format_ident!("{}", mod_name);
200
201 // Create module path for include!
202 let rel_file_path = route_path.strip_prefix(&manifest_dir).unwrap();
203 let rel_file_str = rel_file_path.to_string_lossy().to_string();
204
205 // Add module definition
206 module_defs.push(quote! {
207 #[allow(warnings)]
208 mod #mod_ident {
209 include!(concat!(env!("CARGO_MANIFEST_DIR"), "/", #rel_file_str));
210 }
211 });
212
213 // Read the file content to find HTTP methods
214 let file_content = fs::read_to_string(&route_path).unwrap_or_default();
215 let methods = ["get", "post", "put", "delete", "patch", "head", "options"];
216
217 let mut method_registrations = Vec::new();
218 for method in &methods {
219 if file_content.contains(&format!("pub async fn {}(", method)) {
220 let method_ident = format_ident!("{}", method);
221 method_registrations.push((method, method_ident));
222 }
223 }
224
225 if !method_registrations.is_empty() {
226 let (_first_method, first_method_ident) = &method_registrations[0];
227
228 let mut builder = quote! {
229 axum::routing::#first_method_ident(#mod_ident::#first_method_ident)
230 };
231
232 for (_method, method_ident) in &method_registrations[1..] {
233 builder = quote! {
234 #builder.#method_ident(#mod_ident::#method_ident)
235 };
236 }
237
238 let registration = quote! {
239 router = router.route(#axum_path, #builder);
240 };
241 route_registrations.push(registration);
242 }
243 }
244
245 // Generate the final code
246 let expanded = quote! {
247 {
248 #(#module_defs)*
249
250 let mut router = axum::Router::<#state_type>::new();
251 #(#route_registrations)*
252 router
253 }
254 };
255
256 expanded.into()
257}
258
259// Recursively collect route.rs files
260fn collect_route_files(base_dir: &Path, current_dir: &Path, routes: &mut Vec<(PathBuf, PathBuf)>) {
261 if let Ok(entries) = fs::read_dir(current_dir) {
262 for entry in entries.filter_map(std::result::Result::ok) {
263 let path = entry.path();
264
265 if path.is_dir() {
266 collect_route_files(base_dir, &path, routes);
267 } else if path.file_name().unwrap_or_default() == "route.rs" {
268 if let Some(parent) = path.parent() {
269 if let Ok(rel_dir) = parent.strip_prefix(base_dir) {
270 routes.push((path.clone(), rel_dir.to_path_buf()));
271 }
272 }
273 }
274 }
275 }
276}
277
278// Convert a relative path to (axum_path, mod_name)
279fn path_to_route_info(rel_path: &Path) -> (String, String) {
280 if rel_path.components().count() == 0 {
281 return ("/".to_string(), "root".to_string());
282 }
283
284 let mut axum_path = String::new();
285 let mut mod_name = String::new();
286
287 for segment in rel_path.iter() {
288 let s = segment.to_str().unwrap_or_default();
289 if s.starts_with('[') && s.ends_with(']') {
290 let inner = &s[1..s.len() - 1];
291 if let Some(param) = inner.strip_prefix("...") {
292 axum_path.push_str(&format!("/*{}", param));
293 mod_name.push_str(&format!("__{}", param));
294 } else {
295 axum_path.push_str(&format!("/{{{}}}", inner));
296 mod_name.push_str(&format!("__{}", inner));
297 }
298 } else {
299 axum_path.push('/');
300 axum_path.push_str(s);
301 mod_name.push_str("__");
302 mod_name.push_str(s);
303 }
304 }
305
306 (axum_path, mod_name.trim_start_matches('_').to_string())
307}