diff --git a/Cargo.lock b/Cargo.lock index 2d4947d..a2ba4a8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -90,6 +90,7 @@ dependencies = [ "anyhow", "axum", "glob", + "macrotest", "proc-macro2", "quote", "syn", @@ -129,6 +130,24 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "diff" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8" + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "fastrand" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" + [[package]] name = "fnv" version = "1.0.7" @@ -189,6 +208,12 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a8d1add55171497b4705a648c6b583acafb01d58050a51727785f0b2c8e0a2b2" +[[package]] +name = "hashbrown" +version = "0.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" + [[package]] name = "http" version = "1.3.1" @@ -270,6 +295,16 @@ dependencies = [ "tower-service", ] +[[package]] +name = "indexmap" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cea70ddb795996207ad57735b50c5982d8844f38ba9ee5f1aedcfb708a2aa11e" +dependencies = [ + "equivalent", + "hashbrown", +] + [[package]] name = "itoa" version = "1.0.15" @@ -298,6 +333,23 @@ version = "0.4.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" +[[package]] +name = "macrotest" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0597a8d49ceeea5845b12d1970aa993261e68d4660b327eabab667b3e7ffd60" +dependencies = [ + "diff", + "fastrand", + "glob", + "prettyplease", + "serde", + "serde_derive", + "serde_json", + "syn", + "toml_edit", +] + [[package]] name = "matchit" version = "0.8.4" @@ -392,6 +444,16 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +[[package]] +name = "prettyplease" +version = "0.2.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "664ec5419c51e34154eec046ebcba56312d5a2fc3b09a06da188e1ad21afadf6" +dependencies = [ + "proc-macro2", + "syn", +] + [[package]] name = "proc-macro2" version = "1.0.94" @@ -485,6 +547,15 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_spanned" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87607cb1398ed59d48732e575a4c28a7a8ebf2454b964fe3f224f2afc07909e1" +dependencies = [ + "serde", +] + [[package]] name = "serde_urlencoded" version = "0.7.1" @@ -568,6 +639,28 @@ dependencies = [ "syn", ] +[[package]] +name = "toml_datetime" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_edit" +version = "0.22.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17b4795ff5edd201c7cd6dca065ae59972ce77d1b80fa0a84d94950ece7d1474" +dependencies = [ + "indexmap", + "serde", + "serde_spanned", + "toml_datetime", + "winnow", +] + [[package]] name = "tower" version = "0.5.2" @@ -700,3 +793,12 @@ name = "windows_x86_64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "winnow" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63d3fcd9bba44b03821e7d699eeee959f3126dcc4aa8e4ae18ec617c2a5cea10" +dependencies = [ + "memchr", +] diff --git a/Cargo.toml b/Cargo.toml index c6efafa..c97b843 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,8 +20,12 @@ syn = { version = "2.0", features = ["full"] } quote = "1.0" proc-macro2 = "1.0" glob = "0.3" +macrotest = "1.1.0" [dev-dependencies] anyhow = "1.0.98" axum = "0.8.3" tokio = { version = "1.44.2", features = ["full"] } + +[lints.clippy] +pedantic = "warn" diff --git a/examples/advanced/api/files/[...path]/route.rs b/examples/advanced/api/files/[...path]/route.rs index d88cae6..785166f 100644 --- a/examples/advanced/api/files/[...path]/route.rs +++ b/examples/advanced/api/files/[...path]/route.rs @@ -1,5 +1,5 @@ use axum::{extract::Path, response::IntoResponse}; pub async fn get(Path(path): Path) -> impl IntoResponse { - format!("Requested file path: {}", path) + format!("Requested file path: {path}") } diff --git a/examples/advanced/api/users/[id]/route.rs b/examples/advanced/api/users/[id]/route.rs index 1508802..410f216 100644 --- a/examples/advanced/api/users/[id]/route.rs +++ b/examples/advanced/api/users/[id]/route.rs @@ -1,5 +1,5 @@ use axum::{extract::Path, response::IntoResponse}; pub async fn get(Path(id): Path) -> impl IntoResponse { - format!("User ID: {}", id) + format!("User ID: {id}") } diff --git a/examples/advanced/main.rs b/examples/advanced/main.rs index 3a656e9..95ca19e 100644 --- a/examples/advanced/main.rs +++ b/examples/advanced/main.rs @@ -5,3 +5,4 @@ async fn main() -> anyhow::Result<()> { server::server().await?; Ok(()) } + diff --git a/examples/advanced/server.rs b/examples/advanced/server.rs index 6689b53..639203c 100644 --- a/examples/advanced/server.rs +++ b/examples/advanced/server.rs @@ -13,7 +13,7 @@ struct MyFolderRouter(); pub async fn server() -> anyhow::Result<()> { // Create app state let app_state = AppState { - _foo: "".to_string(), + _foo: String::new(), }; // Use the init fn generated above diff --git a/examples/simple/main.rs b/examples/simple/main.rs index 8f1be8e..e53e17b 100644 --- a/examples/simple/main.rs +++ b/examples/simple/main.rs @@ -14,7 +14,7 @@ struct MyFolderRouter(); async fn main() -> anyhow::Result<()> { // Create app state let app_state = AppState { - _foo: "".to_string(), + _foo: String::new(), }; // Use the init fn generated above diff --git a/flake.nix b/flake.nix index 7ff197f..b834fd3 100644 --- a/flake.nix +++ b/flake.nix @@ -72,7 +72,8 @@ cargo-udeps cargo-outdated cargo-release - cargo-readme + cargo-readme + cargo-expand calc fish inotify-tools @@ -93,7 +94,8 @@ packages = { # default = pkgs.callPackage ./package.nix { }; }; - }) // { + }) + // { hydraJobs = let system = "x86_64-linux"; diff --git a/src/lib.rs b/src/lib.rs index a706c60..9e66861 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -124,6 +124,7 @@ //! It is highly recommended to keep the route directory seperate from the rest of your module-tree. use std::{ collections::HashMap, + fmt::Write, fs, path::{Path, PathBuf}, }; @@ -177,6 +178,7 @@ impl ModuleDir { } } } + /// Creates an Axum router module tree & creation function /// by scanning a directory for `route.rs` files. /// @@ -200,7 +202,7 @@ pub fn folder_router(attr: TokenStream, item: TokenStream) -> TokenStream { let state_type = args.state_type; // Get the project root directory - let manifest_dir = std::env::var("CARGO_MANIFEST_DIR").unwrap(); + let manifest_dir = std::env::var("CARGO_MANIFEST_DIR").unwrap_or("./".to_string()); let base_dir = Path::new(&manifest_dir).join(&base_path); // Collect route files @@ -216,18 +218,22 @@ pub fn folder_router(attr: TokenStream, item: TokenStream) -> TokenStream { }); } - fn replace_special_chars(input: &str) -> String { - input + // Build module tree + // avoid conflicts by interpolating struct name & path + let mut root = ModuleDir::new(&format!( + "__folder_router__{}__{}", + struct_name + .to_string() .chars() .map(|c| if c.is_alphanumeric() { c } else { '_' }) - .collect() - } - - // Build module tree - let mut root = ModuleDir::new(&format!( - "__folder_router_{}", - replace_special_chars(&base_path) + .map(|c| c.to_ascii_lowercase()) + .collect::(), + base_path + .chars() + .map(|c| if c.is_alphanumeric() { c } else { '_' }) + .collect::() )); + for (route_path, rel_path) in &routes { add_to_module_tree(&mut root, rel_path, route_path); } @@ -235,7 +241,10 @@ pub fn folder_router(attr: TokenStream, item: TokenStream) -> TokenStream { // 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 base_path_lit = LitStr::new( + base_dir.to_str().unwrap_or("./"), + proc_macro2::Span::call_site(), + ); let mod_hierarchy = generate_module_hierarchy(&root); // Generate route registrations @@ -314,15 +323,13 @@ pub fn folder_router(attr: TokenStream, item: TokenStream) -> TokenStream { /// it returns: `vec!["get"]` fn methods_for_route(route_path: &PathBuf) -> Vec<&'static str> { // Read the file content - let file_content = match fs::read_to_string(route_path) { - Ok(content) => content, - Err(_) => return Vec::new(), + let Ok(file_content) = fs::read_to_string(route_path) else { + return Vec::new(); }; // Parse the file content into a syn syntax tree - let file = match parse_file(&file_content) { - Ok(file) => file, - Err(_) => return Vec::new(), + let Ok(file) = parse_file(&file_content) else { + return Vec::new(); }; // Define HTTP methods we're looking for @@ -433,9 +440,9 @@ 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) + format!("___{stripped}") } else { - format!("__{}", inner) + format!("__{inner}") } } else { name.replace(['-', '.'], "_") @@ -469,12 +476,12 @@ fn path_to_module_path(rel_path: &Path) -> (String, Vec) { if segment.starts_with('[') && segment.ends_with(']') { let param = &segment[1..segment.len() - 1]; if let Some(stripped) = param.strip_prefix("...") { - axum_path.push_str(&format!("/{{*{}}}", stripped)); + write!(&mut axum_path, "/{{*{stripped}}}").unwrap(); } else { - axum_path.push_str(&format!("/{{:{}}}", param)); + write!(&mut axum_path, "/{{:{param}}}").unwrap(); } } else { - axum_path.push_str(&format!("/{}", segment)); + write!(&mut axum_path, "/{segment}").unwrap(); } } } diff --git a/tests/expand/examples b/tests/expand/examples new file mode 120000 index 0000000..d15735c --- /dev/null +++ b/tests/expand/examples @@ -0,0 +1 @@ +../../examples \ No newline at end of file diff --git a/tests/tests.rs b/tests/tests.rs new file mode 100644 index 0000000..63bf38c --- /dev/null +++ b/tests/tests.rs @@ -0,0 +1,4 @@ +#[test] +pub fn expand_examples_pass() { + macrotest::expand("test/expand/**/*.rs"); +}