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
4//! automatically generates router boilerplate based on your file structure. It
5//! simplifies route organization by using filesystem conventions to define your
6//! API routes.
7//!
8//! ## Installation
9//!
10//! Add the dependency to your ```Cargo.toml```:
11//!
12//! ```toml
13//! [dependencies]
14//! axum_folder_router = "0.1.0"
15//! axum = "0.8"
16//! ```
17//!
18//! ## Basic Usage
19//!
20//! The macro scans a directory for ```route.rs``` files and automatically
21//! creates an Axum router based on the file structure:
22//!
23//! ```rust,no_run
24#![doc = include_str!("../examples/simple/main.rs")]
25//! ```
26//! 
27//! ## File Structure Convention
28//!
29//! The macro converts your file structure into routes:
30//! ```text
31//! src/api/
32//! ├── route.rs                 -> "/"
33//! ├── hello/
34//! │   └── route.rs             -> "/hello"
35//! ├── users/
36//! │   ├── route.rs             -> "/users"
37//! │   └── [id]/
38//! │       └── route.rs         -> "/users/{id}"
39//! └── files/
40//!     └── [...path]/
41//!         └── route.rs         -> "/files/*path"
42//! ```
43//! 
44//! Each ```route.rs``` file can contain HTTP method handlers that are automatically mapped to the corresponding route.
45//!
46//! ## Route Handlers
47//!
48//! Inside each ```route.rs``` file, define async functions named after HTTP methods:
49//! ```rust
50#![doc = include_str!("../examples/simple/api/route.rs")]
51//! ```
52//! 
53//! ## Supported Features
54//!
55//! ### HTTP Methods
56//!
57//! The macro supports all standard HTTP methods:
58//! - ```get```
59//! - ```post```
60//! - ```put```
61//! - ```delete```
62//! - ```patch```
63//! - ```head```
64//! - ```options```
65//!
66//! ### Path Parameters
67//!
68//! Dynamic path segments are defined using brackets:
69//! ```text
70//! src/api/users/[id]/route.rs   -> "/users/{id}"
71//! ```
72//! 
73//! Inside the route handler:
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//! ```text
89//! src/api/files/[...path]/route.rs   -> "/files/*path"
90//! ```
91//! ```rust
92//! use axum::{
93//!   extract::Path,
94//!   response::IntoResponse
95//! };
96//!
97//! pub async fn get(Path(path): Path<String>) -> impl IntoResponse {
98//!     format!("Requested file path: {}", path)
99//! }
100//! ```
101//! 
102//! ### State Extraction
103//!
104//! The state type provided to the macro is available in all route handlers:
105//! All routes share the same state type, though you can use ```FromRef``` for more granular state extraction.
106//! ```rust
107//! use axum::{
108//!   extract::State,
109//!   response::IntoResponse
110//! };
111//!
112//! # #[derive(Debug, Clone)]
113//! # struct AppState ();
114//!
115//! pub async fn get(State(state): State<AppState>) -> impl IntoResponse {
116//!     format!("State: {:?}", state)
117//! }
118//! ```
119//! 
120//! ## Limitations
121//!
122//! - **Compile-time Only**: The routing is determined at compile time, so dynamic route registration isn't supported.
123use std::{
124    collections::HashMap,
125    fs,
126    path::{Path, PathBuf},
127};
128
129use proc_macro::TokenStream;
130use quote::{format_ident, quote};
131use syn::{
132    Ident,
133    LitStr,
134    Result,
135    Token,
136    parse::{Parse, ParseStream},
137    parse_macro_input,
138};
139
140struct FolderRouterArgs {
141    path: String,
142    state_type: Ident,
143}
144
145impl Parse for FolderRouterArgs {
146    fn parse(input: ParseStream) -> Result<Self> {
147        let path_lit = input.parse::<LitStr>()?;
148        input.parse::<Token![,]>()?;
149        let state_type = input.parse::<Ident>()?;
150
151        Ok(FolderRouterArgs {
152            path: path_lit.value(),
153            state_type,
154        })
155    }
156}
157
158// A struct representing a directory in the module tree
159#[derive(Debug)]
160struct ModuleDir {
161    name: String,
162    has_route: bool,
163    children: HashMap<String, ModuleDir>,
164}
165
166impl ModuleDir {
167    fn new(name: &str) -> Self {
168        ModuleDir {
169            name: name.to_string(),
170            has_route: false,
171            children: HashMap::new(),
172        }
173    }
174}
175
176/// Creates an Axum router module tree & creation function
177/// by scanning a directory for `route.rs` files.
178///
179/// # Parameters
180///
181/// * `path` - A string literal pointing to the API directory, relative to the
182///   Cargo manifest directory
183/// * `state_type` - The type name of your application state that will be shared
184///   across all routes
185///
186/// # Example
187///
188/// ```rust
189/// use axum_folder_router::folder_router;
190/// # #[derive(Debug, Clone)]
191/// # struct AppState ();
192/// #
193/// folder_router!("./src/api", AppState);
194/// #
195/// fn main() {
196///   let router = folder_router();
197/// }
198/// ```
199///
200/// This will scan all `route.rs` files in the `./src/api` directory and its
201/// subdirectories, automatically mapping their path structure to URL routes
202/// with the specified state type.
203#[proc_macro]
204pub fn folder_router(input: TokenStream) -> TokenStream {
205    let args = parse_macro_input!(input as FolderRouterArgs);
206    let base_path = args.path;
207    let state_type = args.state_type;
208
209    // Get the project root directory
210    let manifest_dir = std::env::var("CARGO_MANIFEST_DIR").unwrap();
211    let base_dir = Path::new(&manifest_dir).join(&base_path);
212
213    // Collect route files
214    let mut routes = Vec::new();
215    collect_route_files(&base_dir, &base_dir, &mut routes);
216
217    // Build module tree
218    let mut root = ModuleDir::new("__folder_router");
219    for (route_path, rel_path) in &routes {
220        add_to_module_tree(&mut root, rel_path, route_path);
221    }
222
223    // Generate module tree
224    let root_mod_ident = format_ident!("{}", root.name);
225
226    let base_path_lit = LitStr::new(base_dir.to_str().unwrap(), proc_macro2::Span::call_site());
227    let mod_hierarchy = generate_module_hierarchy(&root);
228
229    // Generate route registrations
230    let mut route_registrations = Vec::new();
231    for (route_path, rel_path) in routes {
232        // Generate module path and axum path
233        let (axum_path, mod_path) = path_to_module_path(&rel_path);
234
235        // Read the file content to find HTTP methods
236        let file_content = fs::read_to_string(&route_path).unwrap_or_default();
237        let methods = ["get", "post", "put", "delete", "patch", "head", "options"];
238
239        let mut method_registrations = Vec::new();
240        for method in &methods {
241            if file_content.contains(&format!("pub async fn {}(", method)) {
242                let method_ident = format_ident!("{}", method);
243                method_registrations.push((method, method_ident));
244            }
245        }
246
247        if !method_registrations.is_empty() {
248            let (_first_method, first_method_ident) = &method_registrations[0];
249            let mod_path_tokens = generate_mod_path_tokens(&mod_path);
250
251            let mut builder = quote! {
252                axum::routing::#first_method_ident(#root_mod_ident::#mod_path_tokens::#first_method_ident)
253            };
254
255            for (_method, method_ident) in &method_registrations[1..] {
256                builder = quote! {
257                    #builder.#method_ident(#root_mod_ident::#mod_path_tokens::#method_ident)
258                };
259            }
260
261            let registration = quote! {
262                router = router.route(#axum_path, #builder);
263            };
264            route_registrations.push(registration);
265        }
266    }
267
268    // Generate the final code
269    let expanded = quote! {
270            #[path = #base_path_lit]
271            mod #root_mod_ident {
272                #mod_hierarchy
273            }
274
275            fn folder_router() -> axum::Router::<#state_type> {
276                let mut router = axum::Router::<#state_type>::new();
277                #(#route_registrations)*
278                router
279            }
280    };
281
282    expanded.into()
283}
284
285// Add a path to the module tree
286fn add_to_module_tree(root: &mut ModuleDir, rel_path: &Path, _route_path: &Path) {
287    let mut current = root;
288
289    let components: Vec<_> = rel_path
290        .components()
291        .map(|c| c.as_os_str().to_string_lossy().to_string())
292        .collect();
293
294    // Handle special case for root route.rs
295    if components.is_empty() {
296        current.has_route = true;
297        return;
298    }
299
300    for (i, component) in components.iter().enumerate() {
301        // For the file itself (route.rs), we just mark the directory as having a route
302        if i == components.len() - 1 && component == "route.rs" {
303            current.has_route = true;
304            break;
305        }
306
307        // For directories, add them to the tree
308        let dir_name = component.clone();
309        if !current.children.contains_key(&dir_name) {
310            current
311                .children
312                .insert(dir_name.clone(), ModuleDir::new(&dir_name));
313        }
314
315        current = current.children.get_mut(&dir_name).unwrap();
316    }
317}
318
319// Generate module hierarchy code
320fn generate_module_hierarchy(dir: &ModuleDir) -> proc_macro2::TokenStream {
321    let mut result = proc_macro2::TokenStream::new();
322
323    // panic!("{:?}", dir);
324    // Add route.rs module if this directory has one
325    if dir.has_route {
326        let route_mod = quote! {
327            #[path = "route.rs"]
328            pub mod route;
329        };
330        result.extend(route_mod);
331    }
332
333    // Add subdirectories
334    for child in dir.children.values() {
335        let child_name = format_ident!("{}", normalize_module_name(&child.name));
336        let child_path_lit = LitStr::new(&child.name, proc_macro2::Span::call_site());
337        let child_content = generate_module_hierarchy(child);
338
339        let child_mod = quote! {
340            #[path = #child_path_lit]
341            pub mod #child_name {
342                #child_content
343            }
344        };
345
346        result.extend(child_mod);
347    }
348
349    result
350}
351
352// Generate tokens for a module path
353fn generate_mod_path_tokens(mod_path: &[String]) -> proc_macro2::TokenStream {
354    let mut result = proc_macro2::TokenStream::new();
355
356    for (i, segment) in mod_path.iter().enumerate() {
357        let segment_ident = format_ident!("{}", segment);
358
359        if i == 0 {
360            result = quote! { #segment_ident };
361        } else {
362            result = quote! { #result::#segment_ident };
363        }
364    }
365
366    result
367}
368
369// Normalize a path segment for use as a module name
370fn normalize_module_name(name: &str) -> String {
371    if name.starts_with('[') && name.ends_with(']') {
372        let inner = &name[1..name.len() - 1];
373        if let Some(stripped) = inner.strip_prefix("...") {
374            format!("___{}", stripped)
375        } else {
376            format!("__{}", inner)
377        }
378    } else {
379        name.replace(['-', '.'], "_")
380    }
381}
382
383// Convert a relative path to module path segments and axum route path
384fn path_to_module_path(rel_path: &Path) -> (String, Vec<String>) {
385    let mut axum_path = String::new();
386    let mut mod_path = Vec::new();
387
388    let components: Vec<_> = rel_path
389        .components()
390        .map(|c| c.as_os_str().to_string_lossy().to_string())
391        .collect();
392
393    // Handle root route
394    if components.is_empty() {
395        return ("/".to_string(), vec!["route".to_string()]);
396    }
397
398    for (i, segment) in components.iter().enumerate() {
399        if i == components.len() - 1 && segment == "route.rs" {
400            mod_path.push("route".to_string());
401        } else if segment.starts_with('[') && segment.ends_with(']') {
402            let inner = &segment[1..segment.len() - 1];
403            if let Some(param) = inner.strip_prefix("...") {
404                axum_path.push_str(&format!("/{{*{}}}", param));
405                mod_path.push(format!("___{}", param));
406            } else {
407                axum_path.push_str(&format!("/{{{}}}", inner));
408                mod_path.push(format!("__{}", inner));
409            }
410        } else if segment != "route.rs" {
411            // Skip the actual route.rs file
412            axum_path.push('/');
413            axum_path.push_str(segment);
414            mod_path.push(normalize_module_name(segment));
415        } else {
416            println!("blub");
417        }
418    }
419
420    if axum_path.is_empty() {
421        axum_path = "/".to_string();
422    }
423
424    (axum_path, mod_path)
425}
426
427// Recursively collect route.rs files (unchanged from your original)
428fn collect_route_files(base_dir: &Path, current_dir: &Path, routes: &mut Vec<(PathBuf, PathBuf)>) {
429    if let Ok(entries) = fs::read_dir(current_dir) {
430        for entry in entries.filter_map(std::result::Result::ok) {
431            let path = entry.path();
432
433            if path.is_dir() {
434                collect_route_files(base_dir, &path, routes);
435            } else if path.file_name().unwrap_or_default() == "route.rs" {
436                if let Ok(rel_dir) = path.strip_prefix(base_dir) {
437                    routes.push((path.clone(), rel_dir.to_path_buf()));
438                }
439
440                // if let Some(parent) = path.parent() {
441                //     if let Ok(rel_dir) = parent.strip_prefix(base_dir) {
442                //         routes.push((path.clone(), rel_dir.to_path_buf()));
443                //     }
444                // }
445            }
446        }
447    }
448}