From daf793eb0645fefc6c78a8bac04c37761c3d9a1b Mon Sep 17 00:00:00 2001 From: Tristan Druyen Date: Tue, 15 Apr 2025 13:18:14 +0200 Subject: [PATCH] Vastly improve testing - simplify some examples - add macrotest for expanding/snapshotting macro expansion & add tests - add workaround to enable referencing relative dirs from macrotest files - ensure workaround doesn't get built in release - add trybuild for testing error messages & add tests - fix indeterministic macro output - sort routes after collecting - use BTreeMap instead of HashMap to preserve insertion order - fix compile_error syntax error --- .gitignore | 1 + Cargo.lock | 91 +++++++++++++++++++++ Cargo.toml | 5 +- examples/advanced/main.rs | 1 - examples/advanced/server.rs | 2 +- examples/simple/main.rs | 12 +-- src/lib.rs | 44 ++++++++-- tests/expand.rs | 4 + tests/expand/advanced.expanded.rs | 131 ++++++++++++++++++++++++++++++ tests/expand/advanced.rs | 9 ++ tests/expand/examples | 1 - tests/expand/simple.expanded.rs | 33 ++++++++ tests/expand/simple.rs | 7 ++ tests/failures.rs | 5 ++ tests/failures/no_files.rs | 9 ++ tests/failures/no_files.stderr | 7 ++ tests/failures/no_routes.rs | 9 ++ tests/failures/no_routes.stderr | 7 ++ tests/failures/no_routes/route.rs | 0 tests/tests.rs | 4 - 20 files changed, 357 insertions(+), 25 deletions(-) create mode 100644 tests/expand.rs create mode 100644 tests/expand/advanced.expanded.rs create mode 100644 tests/expand/advanced.rs delete mode 120000 tests/expand/examples create mode 100644 tests/expand/simple.expanded.rs create mode 100644 tests/expand/simple.rs create mode 100644 tests/failures.rs create mode 100644 tests/failures/no_files.rs create mode 100644 tests/failures/no_files.stderr create mode 100644 tests/failures/no_routes.rs create mode 100644 tests/failures/no_routes.stderr create mode 100644 tests/failures/no_routes/route.rs delete mode 100644 tests/tests.rs diff --git a/.gitignore b/.gitignore index 420bbe1..28ebd41 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ .direnv/ target/ +wip/ diff --git a/Cargo.lock b/Cargo.lock index a2ba4a8..b97d71e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -17,6 +17,15 @@ version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" +[[package]] +name = "aho-corasick" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +dependencies = [ + "memchr", +] + [[package]] name = "anyhow" version = "1.0.98" @@ -93,8 +102,10 @@ dependencies = [ "macrotest", "proc-macro2", "quote", + "regex", "syn", "tokio", + "trybuild", ] [[package]] @@ -481,6 +492,35 @@ dependencies = [ "bitflags", ] +[[package]] +name = "regex" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" + [[package]] name = "rustc-demangle" version = "0.1.24" @@ -610,6 +650,21 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +[[package]] +name = "target-triple" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ac9aa371f599d22256307c24a9d748c041e548cbf599f35d890f9d365361790" + +[[package]] +name = "termcolor" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" +dependencies = [ + "winapi-util", +] + [[package]] name = "tokio" version = "1.44.2" @@ -639,6 +694,18 @@ dependencies = [ "syn", ] +[[package]] +name = "toml" +version = "0.8.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd87a5cdd6ffab733b2f74bc4fd7ee5fff6634124999ac278c35fc78c6120148" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit", +] + [[package]] name = "toml_datetime" version = "0.6.8" @@ -709,6 +776,21 @@ dependencies = [ "once_cell", ] +[[package]] +name = "trybuild" +version = "1.0.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ae08be68c056db96f0e6c6dd820727cca756ced9e1f4cc7fdd20e2a55e23898" +dependencies = [ + "glob", + "serde", + "serde_derive", + "serde_json", + "target-triple", + "termcolor", + "toml", +] + [[package]] name = "unicode-ident" version = "1.0.18" @@ -721,6 +803,15 @@ version = "0.11.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" +[[package]] +name = "winapi-util" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" +dependencies = [ + "windows-sys", +] + [[package]] name = "windows-sys" version = "0.52.0" diff --git a/Cargo.toml b/Cargo.toml index c97b843..6998f67 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,11 +21,14 @@ quote = "1.0" proc-macro2 = "1.0" glob = "0.3" macrotest = "1.1.0" +regex = "1.11.1" [dev-dependencies] anyhow = "1.0.98" axum = "0.8.3" tokio = { version = "1.44.2", features = ["full"] } +trybuild = "1.0.104" [lints.clippy] -pedantic = "warn" +pedantic = { level = "warn", priority = -1 } +unused-async = { level = "allow", priority = 0 } # required for examples without unecessary noise diff --git a/examples/advanced/main.rs b/examples/advanced/main.rs index 95ca19e..3a656e9 100644 --- a/examples/advanced/main.rs +++ b/examples/advanced/main.rs @@ -5,4 +5,3 @@ async fn main() -> anyhow::Result<()> { server::server().await?; Ok(()) } - diff --git a/examples/advanced/server.rs b/examples/advanced/server.rs index 639203c..ea07eaf 100644 --- a/examples/advanced/server.rs +++ b/examples/advanced/server.rs @@ -6,7 +6,7 @@ struct AppState { _foo: String, } -// Imports route.rs files & generates an init fn +// Imports route.rs files & generates an ::into_router() fn #[folder_router("examples/advanced/api", AppState)] struct MyFolderRouter(); diff --git a/examples/simple/main.rs b/examples/simple/main.rs index e53e17b..23cf8c8 100644 --- a/examples/simple/main.rs +++ b/examples/simple/main.rs @@ -1,21 +1,17 @@ use axum::Router; use axum_folder_router::folder_router; -#[derive(Clone, Debug)] -struct AppState { - _foo: String, -} +#[derive(Clone)] +struct AppState; -// Imports route.rs files & generates an init fn +// Imports route.rs files & generates an ::into_router() fn #[folder_router("./examples/simple/api", AppState)] struct MyFolderRouter(); #[tokio::main] async fn main() -> anyhow::Result<()> { // Create app state - let app_state = AppState { - _foo: String::new(), - }; + let app_state = AppState; // Use the init fn generated above let folder_router: Router = MyFolderRouter::into_router(); diff --git a/src/lib.rs b/src/lib.rs index 9e66861..aaf19ea 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -123,7 +123,7 @@ //! - **Expects seperate directory**: To make rust-analyzer & co work correctly the macro imports all route.rs files inside the given directory tree. //! It is highly recommended to keep the route directory seperate from the rest of your module-tree. use std::{ - collections::HashMap, + collections::BTreeMap, fmt::Write, fs, path::{Path, PathBuf}, @@ -131,6 +131,7 @@ use std::{ use proc_macro::TokenStream; use quote::{format_ident, quote}; +use regex::Regex; use syn::{ Ident, Item, @@ -143,6 +144,7 @@ use syn::{ parse_macro_input, }; +#[derive(Debug)] struct FolderRouterArgs { path: String, state_type: Ident, @@ -166,7 +168,7 @@ impl Parse for FolderRouterArgs { struct ModuleDir { name: String, has_route: bool, - children: HashMap, + children: BTreeMap, } impl ModuleDir { @@ -174,7 +176,7 @@ impl ModuleDir { ModuleDir { name: name.to_string(), has_route: false, - children: HashMap::new(), + children: BTreeMap::new(), } } } @@ -192,6 +194,7 @@ impl ModuleDir { /// This will scan all `route.rs` files in the `./src/api` directory and its /// subdirectories, automatically mapping their path structure to URL routes /// with the specified state type. +#[allow(clippy::missing_panics_doc)] #[proc_macro_attribute] pub fn folder_router(attr: TokenStream, item: TokenStream) -> TokenStream { let args = parse_macro_input!(attr as FolderRouterArgs); @@ -201,19 +204,42 @@ pub fn folder_router(attr: TokenStream, item: TokenStream) -> TokenStream { let base_path = args.path; let state_type = args.state_type; - // Get the project root directory - let manifest_dir = std::env::var("CARGO_MANIFEST_DIR").unwrap_or("./".to_string()); + let manifest_dir = { + // Get the project root directory + let dir = std::env::var("CARGO_MANIFEST_DIR").unwrap_or("./".to_string()); + + // Create regex to match macrotest pattern with exactly 42 alphanumeric chars + // This is the only way to enable us to reference the example route folders in + // our macrotest::expand tests + let re = Regex::new(r"^(.+)/target/tests/axum-folder-router/[A-Za-z0-9]{42}$").unwrap(); + + // If the pattern matches, extract the real project root + // Being extra caucious to warn any users about this unexpected Workaround + if let Some(captures) = re.captures(&dir) { + #[cfg(not(debug_assertions))] + return TokenStream::from(quote! { + compile_error!("axum-folder-router: MACROTEST_WORKAROUND compiled in non-debug env, something is likely wrong!"); + }); + captures.get(1).unwrap().as_str().to_string() + } else { + dir + } + }; + let base_dir = Path::new(&manifest_dir).join(&base_path); // Collect route files let mut routes = Vec::new(); collect_route_files(&base_dir, &base_dir, &mut routes); + // ensures deterministic macro output + routes.sort(); + if routes.is_empty() { return TokenStream::from(quote! { - compile_error!(concat!("No route.rs files found in the specified directory: ", + compile_error!(concat!("No route.rs files found in the specified directory: '", #base_path, - ". Make sure the path is correct and contains route.rs files." + "'. Make sure the path is correct and contains route.rs files." )); }); } @@ -258,8 +284,8 @@ pub fn folder_router(attr: TokenStream, item: TokenStream) -> TokenStream { if method_registrations.is_empty() { return TokenStream::from(quote! { compile_error!(concat!("No routes defined in '", - #base_path - "', make sure to define at least one `pub async fn` named after an method. (E.g. get, post, put, delete)" + #base_path, + "', make sure to define at least one `pub async fn` named after an method. (e.g. get, post, put, delete)" )); }); } diff --git a/tests/expand.rs b/tests/expand.rs new file mode 100644 index 0000000..4ffadf5 --- /dev/null +++ b/tests/expand.rs @@ -0,0 +1,4 @@ +#[test] +pub fn expand_snapshot_pass() { + macrotest::expand("tests/expand/*.rs"); +} diff --git a/tests/expand/advanced.expanded.rs b/tests/expand/advanced.expanded.rs new file mode 100644 index 0000000..10d994b --- /dev/null +++ b/tests/expand/advanced.expanded.rs @@ -0,0 +1,131 @@ +use axum_folder_router::folder_router; +struct AppState { + _foo: String, +} +#[automatically_derived] +impl ::core::clone::Clone for AppState { + #[inline] + fn clone(&self) -> AppState { + AppState { + _foo: ::core::clone::Clone::clone(&self._foo), + } + } +} +#[path = "/home/tristand/code/axum-folder-router/examples/advanced/api"] +mod __folder_router__myfolderrouter__examples_advanced_api { + #[path = "route.rs"] + pub mod route { + use axum::response::{Html, IntoResponse}; + pub async fn get() -> impl IntoResponse { + Html("

Hello World!

").into_response() + } + pub async fn post() -> impl IntoResponse { + "Posted successfully".into_response() + } + } + #[path = "files"] + pub mod files { + #[path = "route.rs"] + pub mod route { + use axum::response::{Html, IntoResponse}; + pub async fn get() -> impl IntoResponse { + Html("

Hello World!

").into_response() + } + pub async fn post() -> impl IntoResponse { + "Posted successfully".into_response() + } + } + #[path = "[...path]"] + pub mod ___path { + #[path = "route.rs"] + pub mod route { + use axum::{extract::Path, response::IntoResponse}; + pub async fn get(Path(path): Path) -> impl IntoResponse { + ::alloc::__export::must_use({ + let res = ::alloc::fmt::format( + format_args!("Requested file path: {0}", path), + ); + res + }) + } + } + } + } + #[path = "users"] + pub mod users { + #[path = "route.rs"] + pub mod route { + use axum::response::{Html, IntoResponse}; + pub async fn get() -> impl IntoResponse { + Html("

Hello World!

").into_response() + } + pub async fn post() -> impl IntoResponse { + "Posted successfully".into_response() + } + } + #[path = "[id]"] + pub mod __id { + #[path = "route.rs"] + pub mod route { + use axum::{extract::Path, response::IntoResponse}; + pub async fn get(Path(id): Path) -> impl IntoResponse { + ::alloc::__export::must_use({ + let res = ::alloc::fmt::format(format_args!("User ID: {0}", id)); + res + }) + } + } + } + } +} +struct MyFolderRouter(); +impl MyFolderRouter { + pub fn into_router() -> axum::Router { + let mut router = axum::Router::new(); + router = router + .route( + "/files/{*path}", + axum::routing::get( + __folder_router__myfolderrouter__examples_advanced_api::files::___path::route::get, + ), + ); + router = router + .route( + "/files", + axum::routing::get( + __folder_router__myfolderrouter__examples_advanced_api::files::route::get, + ) + .post( + __folder_router__myfolderrouter__examples_advanced_api::files::route::post, + ), + ); + router = router + .route( + "/", + axum::routing::get( + __folder_router__myfolderrouter__examples_advanced_api::route::get, + ) + .post( + __folder_router__myfolderrouter__examples_advanced_api::route::post, + ), + ); + router = router + .route( + "/users/{:id}", + axum::routing::get( + __folder_router__myfolderrouter__examples_advanced_api::users::__id::route::get, + ), + ); + router = router + .route( + "/users", + axum::routing::get( + __folder_router__myfolderrouter__examples_advanced_api::users::route::get, + ) + .post( + __folder_router__myfolderrouter__examples_advanced_api::users::route::post, + ), + ); + router + } +} diff --git a/tests/expand/advanced.rs b/tests/expand/advanced.rs new file mode 100644 index 0000000..ff1303d --- /dev/null +++ b/tests/expand/advanced.rs @@ -0,0 +1,9 @@ +use axum_folder_router::folder_router; + +#[derive(Clone)] +struct AppState { + _foo: String, +} + +#[folder_router("examples/advanced/api", AppState)] +struct MyFolderRouter(); diff --git a/tests/expand/examples b/tests/expand/examples deleted file mode 120000 index d15735c..0000000 --- a/tests/expand/examples +++ /dev/null @@ -1 +0,0 @@ -../../examples \ No newline at end of file diff --git a/tests/expand/simple.expanded.rs b/tests/expand/simple.expanded.rs new file mode 100644 index 0000000..d81d96c --- /dev/null +++ b/tests/expand/simple.expanded.rs @@ -0,0 +1,33 @@ +use axum_folder_router::folder_router; +struct AppState; +#[automatically_derived] +impl ::core::clone::Clone for AppState { + #[inline] + fn clone(&self) -> AppState { + AppState + } +} +#[path = "/home/tristand/code/axum-folder-router/examples/simple/api"] +mod __folder_router__myfolderrouter__examples_simple_api { + #[path = "route.rs"] + pub mod route { + use axum::response::{Html, IntoResponse}; + pub async fn get() -> impl IntoResponse { + Html("

Hello World!

").into_response() + } + } +} +struct MyFolderRouter(); +impl MyFolderRouter { + pub fn into_router() -> axum::Router { + let mut router = axum::Router::new(); + router = router + .route( + "/", + axum::routing::get( + __folder_router__myfolderrouter__examples_simple_api::route::get, + ), + ); + router + } +} diff --git a/tests/expand/simple.rs b/tests/expand/simple.rs new file mode 100644 index 0000000..2aafb9b --- /dev/null +++ b/tests/expand/simple.rs @@ -0,0 +1,7 @@ +use axum_folder_router::folder_router; + +#[derive(Clone)] +struct AppState; + +#[folder_router("examples/simple/api", AppState)] +struct MyFolderRouter(); diff --git a/tests/failures.rs b/tests/failures.rs new file mode 100644 index 0000000..5f5371a --- /dev/null +++ b/tests/failures.rs @@ -0,0 +1,5 @@ +#[test] +fn ui() { + let t = trybuild::TestCases::new(); + t.compile_fail("tests/failures/*.rs"); +} diff --git a/tests/failures/no_files.rs b/tests/failures/no_files.rs new file mode 100644 index 0000000..e419115 --- /dev/null +++ b/tests/failures/no_files.rs @@ -0,0 +1,9 @@ +use axum_folder_router::folder_router; + +#[derive(Clone)] +struct AppState; + +#[folder_router("some/non/existing/directory", AppState)] +struct MyFolderRouter(); + +fn main() {} diff --git a/tests/failures/no_files.stderr b/tests/failures/no_files.stderr new file mode 100644 index 0000000..756b163 --- /dev/null +++ b/tests/failures/no_files.stderr @@ -0,0 +1,7 @@ +error: No route.rs files found in the specified directory: 'some/non/existing/directory'. Make sure the path is correct and contains route.rs files. + --> tests/failures/no_files.rs:6:1 + | +6 | #[folder_router("some/non/existing/directory", AppState)] + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + | + = note: this error originates in the attribute macro `folder_router` (in Nightly builds, run with -Z macro-backtrace for more info) diff --git a/tests/failures/no_routes.rs b/tests/failures/no_routes.rs new file mode 100644 index 0000000..4b81905 --- /dev/null +++ b/tests/failures/no_routes.rs @@ -0,0 +1,9 @@ +use axum_folder_router::folder_router; + +#[derive(Clone)] +struct AppState; + +#[folder_router("../../../../tests/failures/no_routes", AppState)] +struct MyFolderRouter(); + +fn main() {} diff --git a/tests/failures/no_routes.stderr b/tests/failures/no_routes.stderr new file mode 100644 index 0000000..95bc128 --- /dev/null +++ b/tests/failures/no_routes.stderr @@ -0,0 +1,7 @@ +error: No routes defined in '../../../../tests/failures/no_routes', make sure to define at least one `pub async fn` named after an method. (e.g. get, post, put, delete) + --> tests/failures/no_routes.rs:6:1 + | +6 | #[folder_router("../../../../tests/failures/no_routes", AppState)] + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + | + = note: this error originates in the attribute macro `folder_router` (in Nightly builds, run with -Z macro-backtrace for more info) diff --git a/tests/failures/no_routes/route.rs b/tests/failures/no_routes/route.rs new file mode 100644 index 0000000..e69de29 diff --git a/tests/tests.rs b/tests/tests.rs deleted file mode 100644 index 63bf38c..0000000 --- a/tests/tests.rs +++ /dev/null @@ -1,4 +0,0 @@ -#[test] -pub fn expand_examples_pass() { - macrotest::expand("test/expand/**/*.rs"); -}