Rewrite for better module handling
This commit is contained in:
parent
b3771ba87a
commit
73672d8c96
11 changed files with 332 additions and 96 deletions
|
@ -7,7 +7,8 @@
|
||||||
|
|
||||||
# axum-folder-router
|
# axum-folder-router
|
||||||
|
|
||||||
```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.
|
```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.
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
|
|
5
examples/advanced/api/files/[...path]/route.rs
Normal file
5
examples/advanced/api/files/[...path]/route.rs
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
use axum::{extract::Path, response::IntoResponse};
|
||||||
|
|
||||||
|
pub async fn get(Path(path): Path<String>) -> impl IntoResponse {
|
||||||
|
format!("Requested file path: {}", path)
|
||||||
|
}
|
9
examples/advanced/api/files/route.rs
Normal file
9
examples/advanced/api/files/route.rs
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
use axum::response::{Html, IntoResponse};
|
||||||
|
|
||||||
|
pub async fn get() -> impl IntoResponse {
|
||||||
|
Html("<h1>Hello World!</h1>").into_response()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn post() -> impl IntoResponse {
|
||||||
|
"Posted successfully".into_response()
|
||||||
|
}
|
9
examples/advanced/api/route.rs
Normal file
9
examples/advanced/api/route.rs
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
use axum::response::{Html, IntoResponse};
|
||||||
|
|
||||||
|
pub async fn get() -> impl IntoResponse {
|
||||||
|
Html("<h1>Hello World!</h1>").into_response()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn post() -> impl IntoResponse {
|
||||||
|
"Posted successfully".into_response()
|
||||||
|
}
|
5
examples/advanced/api/users/[id]/route.rs
Normal file
5
examples/advanced/api/users/[id]/route.rs
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
use axum::{extract::Path, response::IntoResponse};
|
||||||
|
|
||||||
|
pub async fn get(Path(id): Path<String>) -> impl IntoResponse {
|
||||||
|
format!("User ID: {}", id)
|
||||||
|
}
|
9
examples/advanced/api/users/route.rs
Normal file
9
examples/advanced/api/users/route.rs
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
use axum::response::{Html, IntoResponse};
|
||||||
|
|
||||||
|
pub async fn get() -> impl IntoResponse {
|
||||||
|
Html("<h1>Hello World!</h1>").into_response()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn post() -> impl IntoResponse {
|
||||||
|
"Posted successfully".into_response()
|
||||||
|
}
|
28
examples/advanced/main.rs
Normal file
28
examples/advanced/main.rs
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
use axum::Router;
|
||||||
|
use axum_folder_router::folder_router;
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
struct AppState {
|
||||||
|
_foo: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
folder_router!("examples/advanced/api", AppState);
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() -> anyhow::Result<()> {
|
||||||
|
// Create app state
|
||||||
|
let app_state = AppState {
|
||||||
|
_foo: "".to_string(),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Generate the router using the macro
|
||||||
|
let folder_router: Router<AppState> = folder_router();
|
||||||
|
|
||||||
|
// Build the router and provide the state
|
||||||
|
let app: Router<()> = folder_router.with_state(app_state);
|
||||||
|
|
||||||
|
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await?;
|
||||||
|
println!("Listening on http://{}", listener.local_addr()?);
|
||||||
|
axum::serve(listener, app).await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
|
@ -7,4 +7,3 @@ pub async fn get() -> impl IntoResponse {
|
||||||
pub async fn post() -> impl IntoResponse {
|
pub async fn post() -> impl IntoResponse {
|
||||||
"Posted successfully".into_response()
|
"Posted successfully".into_response()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,13 +1,13 @@
|
||||||
use anyhow;
|
|
||||||
use axum::Router;
|
use axum::Router;
|
||||||
use axum_folder_router::folder_router;
|
use axum_folder_router::folder_router;
|
||||||
use tokio;
|
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
struct AppState {
|
struct AppState {
|
||||||
_foo: String,
|
_foo: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
folder_router!("./examples/simple/api", AppState);
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() -> anyhow::Result<()> {
|
async fn main() -> anyhow::Result<()> {
|
||||||
// Create app state
|
// Create app state
|
||||||
|
@ -16,7 +16,7 @@ async fn main() -> anyhow::Result<()> {
|
||||||
};
|
};
|
||||||
|
|
||||||
// Generate the router using the macro
|
// Generate the router using the macro
|
||||||
let folder_router: Router<AppState> = folder_router!("./examples/simple/api", AppState);
|
let folder_router: Router<AppState> = folder_router();
|
||||||
|
|
||||||
// Build the router and provide the state
|
// Build the router and provide the state
|
||||||
let app: Router<()> = folder_router.with_state(app_state);
|
let app: Router<()> = folder_router.with_state(app_state);
|
||||||
|
|
23
rustfmt.toml
Normal file
23
rustfmt.toml
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
|
||||||
|
edition = "2024"
|
||||||
|
max_width = 100
|
||||||
|
tab_spaces = 4
|
||||||
|
|
||||||
|
|
||||||
|
# unstable
|
||||||
|
format_macro_bodies = true
|
||||||
|
format_macro_matchers = true
|
||||||
|
format_strings = true
|
||||||
|
group_imports = "StdExternalCrate"
|
||||||
|
imports_granularity = "Crate"
|
||||||
|
imports_layout = "HorizontalVertical"
|
||||||
|
overflow_delimited_expr = true
|
||||||
|
reorder_impl_items = true
|
||||||
|
struct_field_align_threshold = 4
|
||||||
|
struct_lit_single_line = false
|
||||||
|
trailing_comma = "Vertical"
|
||||||
|
unstable_features = true
|
||||||
|
use_field_init_shorthand = true
|
||||||
|
use_try_shorthand = true
|
||||||
|
wrap_comments = true
|
||||||
|
# format_brace_macros = true
|
306
src/lib.rs
306
src/lib.rs
|
@ -1,6 +1,9 @@
|
||||||
//! # ```axum_folder_router``` Macro Documentation
|
//! # ```axum_folder_router``` Macro Documentation
|
||||||
//!
|
//!
|
||||||
//! [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.
|
//! [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.
|
||||||
//!
|
//!
|
||||||
//! ## Installation
|
//! ## Installation
|
||||||
//!
|
//!
|
||||||
|
@ -9,12 +12,13 @@
|
||||||
//! ```toml
|
//! ```toml
|
||||||
//! [dependencies]
|
//! [dependencies]
|
||||||
//! axum_folder_router = "0.1.0"
|
//! axum_folder_router = "0.1.0"
|
||||||
//! axum = "0.7"
|
//! axum = "0.8"
|
||||||
//! ```
|
//! ```
|
||||||
//!
|
//!
|
||||||
//! ## Basic Usage
|
//! ## Basic Usage
|
||||||
//!
|
//!
|
||||||
//! The macro scans a directory for ```route.rs``` files and automatically creates an Axum router based on the file structure:
|
//! The macro scans a directory for ```route.rs``` files and automatically
|
||||||
|
//! creates an Axum router based on the file structure:
|
||||||
//!
|
//!
|
||||||
//! ```rust,no_run
|
//! ```rust,no_run
|
||||||
#![doc = include_str!("../examples/simple/main.rs")]
|
#![doc = include_str!("../examples/simple/main.rs")]
|
||||||
|
@ -23,7 +27,6 @@
|
||||||
//! ## File Structure Convention
|
//! ## File Structure Convention
|
||||||
//!
|
//!
|
||||||
//! The macro converts your file structure into routes:
|
//! The macro converts your file structure into routes:
|
||||||
//!
|
|
||||||
//! ```text
|
//! ```text
|
||||||
//! src/api/
|
//! src/api/
|
||||||
//! ├── route.rs -> "/"
|
//! ├── route.rs -> "/"
|
||||||
|
@ -43,7 +46,6 @@
|
||||||
//! ## Route Handlers
|
//! ## Route Handlers
|
||||||
//!
|
//!
|
||||||
//! Inside each ```route.rs``` file, define async functions named after HTTP methods:
|
//! Inside each ```route.rs``` file, define async functions named after HTTP methods:
|
||||||
//!
|
|
||||||
//! ```rust
|
//! ```rust
|
||||||
#![doc = include_str!("../examples/simple/api/route.rs")]
|
#![doc = include_str!("../examples/simple/api/route.rs")]
|
||||||
//! ```
|
//! ```
|
||||||
|
@ -64,13 +66,11 @@
|
||||||
//! ### Path Parameters
|
//! ### Path Parameters
|
||||||
//!
|
//!
|
||||||
//! Dynamic path segments are defined using brackets:
|
//! Dynamic path segments are defined using brackets:
|
||||||
//!
|
|
||||||
//! ```text
|
//! ```text
|
||||||
//! src/api/users/[id]/route.rs -> "/users/{id}"
|
//! src/api/users/[id]/route.rs -> "/users/{id}"
|
||||||
//! ```
|
//! ```
|
||||||
//!
|
//!
|
||||||
//! Inside the route handler:
|
//! Inside the route handler:
|
||||||
//!
|
|
||||||
//! ```rust
|
//! ```rust
|
||||||
//! use axum::{
|
//! use axum::{
|
||||||
//! extract::Path,
|
//! extract::Path,
|
||||||
|
@ -85,11 +85,9 @@
|
||||||
//! ### Catch-all Parameters
|
//! ### Catch-all Parameters
|
||||||
//!
|
//!
|
||||||
//! Use the spread syntax for catch-all segments:
|
//! Use the spread syntax for catch-all segments:
|
||||||
//!
|
|
||||||
//! ```text
|
//! ```text
|
||||||
//! src/api/files/[...path]/route.rs -> "/files/*path"
|
//! src/api/files/[...path]/route.rs -> "/files/*path"
|
||||||
//! ```
|
//! ```
|
||||||
//!
|
|
||||||
//! ```rust
|
//! ```rust
|
||||||
//! use axum::{
|
//! use axum::{
|
||||||
//! extract::Path,
|
//! extract::Path,
|
||||||
|
@ -105,7 +103,6 @@
|
||||||
//!
|
//!
|
||||||
//! The state type provided to the macro is available in all route handlers:
|
//! The state type provided to the macro is available in all route handlers:
|
||||||
//! All routes share the same state type, though you can use ```FromRef``` for more granular state extraction.
|
//! All routes share the same state type, though you can use ```FromRef``` for more granular state extraction.
|
||||||
//!
|
|
||||||
//! ```rust
|
//! ```rust
|
||||||
//! use axum::{
|
//! use axum::{
|
||||||
//! extract::State,
|
//! extract::State,
|
||||||
|
@ -123,13 +120,22 @@
|
||||||
//! ## Limitations
|
//! ## Limitations
|
||||||
//!
|
//!
|
||||||
//! - **Compile-time Only**: The routing is determined at compile time, so dynamic route registration isn't supported.
|
//! - **Compile-time Only**: The routing is determined at compile time, so dynamic route registration isn't supported.
|
||||||
//!
|
use std::{
|
||||||
|
collections::HashMap,
|
||||||
|
fs,
|
||||||
|
path::{Path, PathBuf},
|
||||||
|
};
|
||||||
|
|
||||||
use proc_macro::TokenStream;
|
use proc_macro::TokenStream;
|
||||||
use quote::{format_ident, quote};
|
use quote::{format_ident, quote};
|
||||||
use std::fs;
|
use syn::{
|
||||||
use std::path::{Path, PathBuf};
|
Ident,
|
||||||
use syn::{Ident, LitStr, Result, Token, parse::Parse, parse::ParseStream, parse_macro_input};
|
LitStr,
|
||||||
|
Result,
|
||||||
|
Token,
|
||||||
|
parse::{Parse, ParseStream},
|
||||||
|
parse_macro_input,
|
||||||
|
};
|
||||||
|
|
||||||
struct FolderRouterArgs {
|
struct FolderRouterArgs {
|
||||||
path: String,
|
path: String,
|
||||||
|
@ -149,25 +155,51 @@ impl Parse for FolderRouterArgs {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Creates an Axum router by scanning a directory for `route.rs` files.
|
// A struct representing a directory in the module tree
|
||||||
|
#[derive(Debug)]
|
||||||
|
struct ModuleDir {
|
||||||
|
name: String,
|
||||||
|
has_route: bool,
|
||||||
|
children: HashMap<String, ModuleDir>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ModuleDir {
|
||||||
|
fn new(name: &str) -> Self {
|
||||||
|
ModuleDir {
|
||||||
|
name: name.to_string(),
|
||||||
|
has_route: false,
|
||||||
|
children: HashMap::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates an Axum router module tree & creation function
|
||||||
|
/// by scanning a directory for `route.rs` files.
|
||||||
///
|
///
|
||||||
/// # Parameters
|
/// # Parameters
|
||||||
///
|
///
|
||||||
/// * `path` - A string literal pointing to the API directory, relative to the Cargo manifest directory
|
/// * `path` - A string literal pointing to the API directory, relative to the
|
||||||
/// * `state_type` - The type name of your application state that will be shared across all routes
|
/// Cargo manifest directory
|
||||||
|
/// * `state_type` - The type name of your application state that will be shared
|
||||||
|
/// across all routes
|
||||||
///
|
///
|
||||||
/// # Example
|
/// # Example
|
||||||
///
|
///
|
||||||
/// ```rust
|
/// ```rust
|
||||||
/// # use axum_folder_router::folder_router;
|
/// use axum_folder_router::folder_router;
|
||||||
/// # #[derive(Debug, Clone)]
|
/// # #[derive(Debug, Clone)]
|
||||||
/// # struct AppState ();
|
/// # struct AppState ();
|
||||||
/// #
|
/// #
|
||||||
/// let router = folder_router!("./src/api", AppState);
|
/// folder_router!("./src/api", AppState);
|
||||||
|
/// #
|
||||||
|
/// fn main() {
|
||||||
|
/// let router = folder_router();
|
||||||
|
/// }
|
||||||
/// ```
|
/// ```
|
||||||
///
|
///
|
||||||
/// This will scan all `route.rs` files in the `./src/api` directory and its subdirectories,
|
/// This will scan all `route.rs` files in the `./src/api` directory and its
|
||||||
/// automatically mapping their path structure to URL routes with the specified state type.
|
/// subdirectories, automatically mapping their path structure to URL routes
|
||||||
|
/// with the specified state type.
|
||||||
#[proc_macro]
|
#[proc_macro]
|
||||||
pub fn folder_router(input: TokenStream) -> TokenStream {
|
pub fn folder_router(input: TokenStream) -> TokenStream {
|
||||||
let args = parse_macro_input!(input as FolderRouterArgs);
|
let args = parse_macro_input!(input as FolderRouterArgs);
|
||||||
|
@ -182,26 +214,23 @@ pub fn folder_router(input: TokenStream) -> TokenStream {
|
||||||
let mut routes = Vec::new();
|
let mut routes = Vec::new();
|
||||||
collect_route_files(&base_dir, &base_dir, &mut routes);
|
collect_route_files(&base_dir, &base_dir, &mut routes);
|
||||||
|
|
||||||
// Generate module definitions and route registrations
|
// Build module tree
|
||||||
let mut module_defs = Vec::new();
|
let mut root = ModuleDir::new("__folder_router");
|
||||||
let mut route_registrations = Vec::new();
|
for (route_path, rel_path) in &routes {
|
||||||
|
add_to_module_tree(&mut root, rel_path, route_path);
|
||||||
for (route_path, rel_path) in routes {
|
|
||||||
// Generate module name and axum path
|
|
||||||
let (axum_path, mod_name) = path_to_route_info(&rel_path);
|
|
||||||
let mod_ident = format_ident!("{}", mod_name);
|
|
||||||
|
|
||||||
// Create module path for include!
|
|
||||||
let rel_file_path = route_path.strip_prefix(&manifest_dir).unwrap();
|
|
||||||
let rel_file_str = rel_file_path.to_string_lossy().to_string();
|
|
||||||
|
|
||||||
// Add module definition
|
|
||||||
module_defs.push(quote! {
|
|
||||||
#[allow(warnings)]
|
|
||||||
mod #mod_ident {
|
|
||||||
include!(concat!(env!("CARGO_MANIFEST_DIR"), "/", #rel_file_str));
|
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
// Generate module tree
|
||||||
|
let root_mod_ident = format_ident!("{}", root.name);
|
||||||
|
|
||||||
|
let base_path_lit = LitStr::new(base_dir.to_str().unwrap(), proc_macro2::Span::call_site());
|
||||||
|
let mod_hierarchy = generate_module_hierarchy(&root);
|
||||||
|
|
||||||
|
// Generate route registrations
|
||||||
|
let mut route_registrations = Vec::new();
|
||||||
|
for (route_path, rel_path) in routes {
|
||||||
|
// Generate module path and axum path
|
||||||
|
let (axum_path, mod_path) = path_to_module_path(&rel_path);
|
||||||
|
|
||||||
// Read the file content to find HTTP methods
|
// Read the file content to find HTTP methods
|
||||||
let file_content = fs::read_to_string(&route_path).unwrap_or_default();
|
let file_content = fs::read_to_string(&route_path).unwrap_or_default();
|
||||||
|
@ -217,14 +246,15 @@ pub fn folder_router(input: TokenStream) -> TokenStream {
|
||||||
|
|
||||||
if !method_registrations.is_empty() {
|
if !method_registrations.is_empty() {
|
||||||
let (_first_method, first_method_ident) = &method_registrations[0];
|
let (_first_method, first_method_ident) = &method_registrations[0];
|
||||||
|
let mod_path_tokens = generate_mod_path_tokens(&mod_path);
|
||||||
|
|
||||||
let mut builder = quote! {
|
let mut builder = quote! {
|
||||||
axum::routing::#first_method_ident(#mod_ident::#first_method_ident)
|
axum::routing::#first_method_ident(#root_mod_ident::#mod_path_tokens::#first_method_ident)
|
||||||
};
|
};
|
||||||
|
|
||||||
for (_method, method_ident) in &method_registrations[1..] {
|
for (_method, method_ident) in &method_registrations[1..] {
|
||||||
builder = quote! {
|
builder = quote! {
|
||||||
#builder.#method_ident(#mod_ident::#method_ident)
|
#builder.#method_ident(#root_mod_ident::#mod_path_tokens::#method_ident)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -237,9 +267,12 @@ pub fn folder_router(input: TokenStream) -> TokenStream {
|
||||||
|
|
||||||
// Generate the final code
|
// Generate the final code
|
||||||
let expanded = quote! {
|
let expanded = quote! {
|
||||||
{
|
#[path = #base_path_lit]
|
||||||
#(#module_defs)*
|
mod #root_mod_ident {
|
||||||
|
#mod_hierarchy
|
||||||
|
}
|
||||||
|
|
||||||
|
fn folder_router() -> axum::Router::<#state_type> {
|
||||||
let mut router = axum::Router::<#state_type>::new();
|
let mut router = axum::Router::<#state_type>::new();
|
||||||
#(#route_registrations)*
|
#(#route_registrations)*
|
||||||
router
|
router
|
||||||
|
@ -249,7 +282,149 @@ pub fn folder_router(input: TokenStream) -> TokenStream {
|
||||||
expanded.into()
|
expanded.into()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Recursively collect route.rs files
|
// Add a path to the module tree
|
||||||
|
fn add_to_module_tree(root: &mut ModuleDir, rel_path: &Path, _route_path: &Path) {
|
||||||
|
let mut current = root;
|
||||||
|
|
||||||
|
let components: Vec<_> = rel_path
|
||||||
|
.components()
|
||||||
|
.map(|c| c.as_os_str().to_string_lossy().to_string())
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
// Handle special case for root route.rs
|
||||||
|
if components.is_empty() {
|
||||||
|
current.has_route = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (i, component) in components.iter().enumerate() {
|
||||||
|
// For the file itself (route.rs), we just mark the directory as having a route
|
||||||
|
if i == components.len() - 1 && component == "route.rs" {
|
||||||
|
current.has_route = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// For directories, add them to the tree
|
||||||
|
let dir_name = component.clone();
|
||||||
|
if !current.children.contains_key(&dir_name) {
|
||||||
|
current
|
||||||
|
.children
|
||||||
|
.insert(dir_name.clone(), ModuleDir::new(&dir_name));
|
||||||
|
}
|
||||||
|
|
||||||
|
current = current.children.get_mut(&dir_name).unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate module hierarchy code
|
||||||
|
fn generate_module_hierarchy(dir: &ModuleDir) -> proc_macro2::TokenStream {
|
||||||
|
let mut result = proc_macro2::TokenStream::new();
|
||||||
|
|
||||||
|
// panic!("{:?}", dir);
|
||||||
|
// Add route.rs module if this directory has one
|
||||||
|
if dir.has_route {
|
||||||
|
let route_mod = quote! {
|
||||||
|
#[path = "route.rs"]
|
||||||
|
pub mod route;
|
||||||
|
};
|
||||||
|
result.extend(route_mod);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add subdirectories
|
||||||
|
for child in dir.children.values() {
|
||||||
|
let child_name = format_ident!("{}", normalize_module_name(&child.name));
|
||||||
|
let child_path_lit = LitStr::new(&child.name, proc_macro2::Span::call_site());
|
||||||
|
let child_content = generate_module_hierarchy(child);
|
||||||
|
|
||||||
|
let child_mod = quote! {
|
||||||
|
#[path = #child_path_lit]
|
||||||
|
pub mod #child_name {
|
||||||
|
#child_content
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
result.extend(child_mod);
|
||||||
|
}
|
||||||
|
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate tokens for a module path
|
||||||
|
fn generate_mod_path_tokens(mod_path: &[String]) -> proc_macro2::TokenStream {
|
||||||
|
let mut result = proc_macro2::TokenStream::new();
|
||||||
|
|
||||||
|
for (i, segment) in mod_path.iter().enumerate() {
|
||||||
|
let segment_ident = format_ident!("{}", segment);
|
||||||
|
|
||||||
|
if i == 0 {
|
||||||
|
result = quote! { #segment_ident };
|
||||||
|
} else {
|
||||||
|
result = quote! { #result::#segment_ident };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
|
// Normalize a path segment for use as a module name
|
||||||
|
fn normalize_module_name(name: &str) -> String {
|
||||||
|
if name.starts_with('[') && name.ends_with(']') {
|
||||||
|
let inner = &name[1..name.len() - 1];
|
||||||
|
if let Some(stripped) = inner.strip_prefix("...") {
|
||||||
|
format!("___{}", stripped)
|
||||||
|
} else {
|
||||||
|
format!("__{}", inner)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
name.replace(['-', '.'], "_")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert a relative path to module path segments and axum route path
|
||||||
|
fn path_to_module_path(rel_path: &Path) -> (String, Vec<String>) {
|
||||||
|
let mut axum_path = String::new();
|
||||||
|
let mut mod_path = Vec::new();
|
||||||
|
|
||||||
|
let components: Vec<_> = rel_path
|
||||||
|
.components()
|
||||||
|
.map(|c| c.as_os_str().to_string_lossy().to_string())
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
// Handle root route
|
||||||
|
if components.is_empty() {
|
||||||
|
return ("/".to_string(), vec!["route".to_string()]);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (i, segment) in components.iter().enumerate() {
|
||||||
|
if i == components.len() - 1 && segment == "route.rs" {
|
||||||
|
mod_path.push("route".to_string());
|
||||||
|
} else if segment.starts_with('[') && segment.ends_with(']') {
|
||||||
|
let inner = &segment[1..segment.len() - 1];
|
||||||
|
if let Some(param) = inner.strip_prefix("...") {
|
||||||
|
axum_path.push_str(&format!("/{{*{}}}", param));
|
||||||
|
mod_path.push(format!("___{}", param));
|
||||||
|
} else {
|
||||||
|
axum_path.push_str(&format!("/{{{}}}", inner));
|
||||||
|
mod_path.push(format!("__{}", inner));
|
||||||
|
}
|
||||||
|
} else if segment != "route.rs" {
|
||||||
|
// Skip the actual route.rs file
|
||||||
|
axum_path.push('/');
|
||||||
|
axum_path.push_str(segment);
|
||||||
|
mod_path.push(normalize_module_name(segment));
|
||||||
|
} else {
|
||||||
|
println!("blub");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if axum_path.is_empty() {
|
||||||
|
axum_path = "/".to_string();
|
||||||
|
}
|
||||||
|
|
||||||
|
(axum_path, mod_path)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Recursively collect route.rs files (unchanged from your original)
|
||||||
fn collect_route_files(base_dir: &Path, current_dir: &Path, routes: &mut Vec<(PathBuf, PathBuf)>) {
|
fn collect_route_files(base_dir: &Path, current_dir: &Path, routes: &mut Vec<(PathBuf, PathBuf)>) {
|
||||||
if let Ok(entries) = fs::read_dir(current_dir) {
|
if let Ok(entries) = fs::read_dir(current_dir) {
|
||||||
for entry in entries.filter_map(std::result::Result::ok) {
|
for entry in entries.filter_map(std::result::Result::ok) {
|
||||||
|
@ -258,43 +433,16 @@ fn collect_route_files(base_dir: &Path, current_dir: &Path, routes: &mut Vec<(Pa
|
||||||
if path.is_dir() {
|
if path.is_dir() {
|
||||||
collect_route_files(base_dir, &path, routes);
|
collect_route_files(base_dir, &path, routes);
|
||||||
} else if path.file_name().unwrap_or_default() == "route.rs" {
|
} else if path.file_name().unwrap_or_default() == "route.rs" {
|
||||||
if let Some(parent) = path.parent() {
|
if let Ok(rel_dir) = path.strip_prefix(base_dir) {
|
||||||
if let Ok(rel_dir) = parent.strip_prefix(base_dir) {
|
|
||||||
routes.push((path.clone(), rel_dir.to_path_buf()));
|
routes.push((path.clone(), rel_dir.to_path_buf()));
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Convert a relative path to (axum_path, mod_name)
|
// if let Some(parent) = path.parent() {
|
||||||
fn path_to_route_info(rel_path: &Path) -> (String, String) {
|
// if let Ok(rel_dir) = parent.strip_prefix(base_dir) {
|
||||||
if rel_path.components().count() == 0 {
|
// routes.push((path.clone(), rel_dir.to_path_buf()));
|
||||||
return ("/".to_string(), "root".to_string());
|
// }
|
||||||
}
|
// }
|
||||||
|
}
|
||||||
let mut axum_path = String::new();
|
|
||||||
let mut mod_name = String::new();
|
|
||||||
|
|
||||||
for segment in rel_path.iter() {
|
|
||||||
let s = segment.to_str().unwrap_or_default();
|
|
||||||
if s.starts_with('[') && s.ends_with(']') {
|
|
||||||
let inner = &s[1..s.len() - 1];
|
|
||||||
if let Some(param) = inner.strip_prefix("...") {
|
|
||||||
axum_path.push_str(&format!("/*{}", param));
|
|
||||||
mod_name.push_str(&format!("__{}", param));
|
|
||||||
} else {
|
|
||||||
axum_path.push_str(&format!("/{{{}}}", inner));
|
|
||||||
mod_name.push_str(&format!("__{}", inner));
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
axum_path.push('/');
|
|
||||||
axum_path.push_str(s);
|
|
||||||
mod_name.push_str("__");
|
|
||||||
mod_name.push_str(s);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
(axum_path, mod_name.trim_start_matches('_').to_string())
|
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Reference in a new issue