axum_folder_router/
lib.rs1#![doc = include_str!("../examples/simple/main.rs")]
25#![doc = include_str!("../examples/simple/api/route.rs")]
51use 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#[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#[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 let manifest_dir = std::env::var("CARGO_MANIFEST_DIR").unwrap();
211 let base_dir = Path::new(&manifest_dir).join(&base_path);
212
213 let mut routes = Vec::new();
215 collect_route_files(&base_dir, &base_dir, &mut routes);
216
217 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 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 let mut route_registrations = Vec::new();
231 for (route_path, rel_path) in routes {
232 let (axum_path, mod_path) = path_to_module_path(&rel_path);
234
235 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 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
285fn 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 if components.is_empty() {
296 current.has_route = true;
297 return;
298 }
299
300 for (i, component) in components.iter().enumerate() {
301 if i == components.len() - 1 && component == "route.rs" {
303 current.has_route = true;
304 break;
305 }
306
307 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
319fn generate_module_hierarchy(dir: &ModuleDir) -> proc_macro2::TokenStream {
321 let mut result = proc_macro2::TokenStream::new();
322
323 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 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
352fn 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
369fn 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
383fn 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 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 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
427fn 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 }
446 }
447 }
448}