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
This commit is contained in:
parent
f0ce2b2737
commit
daf793eb06
20 changed files with 357 additions and 25 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -1,2 +1,3 @@
|
|||
.direnv/
|
||||
target/
|
||||
wip/
|
||||
|
|
91
Cargo.lock
generated
91
Cargo.lock
generated
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -5,4 +5,3 @@ async fn main() -> anyhow::Result<()> {
|
|||
server::server().await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
|
|
@ -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();
|
||||
|
||||
|
|
|
@ -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<AppState> = MyFolderRouter::into_router();
|
||||
|
|
44
src/lib.rs
44
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<String, ModuleDir>,
|
||||
children: BTreeMap<String, ModuleDir>,
|
||||
}
|
||||
|
||||
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)"
|
||||
));
|
||||
});
|
||||
}
|
||||
|
|
4
tests/expand.rs
Normal file
4
tests/expand.rs
Normal file
|
@ -0,0 +1,4 @@
|
|||
#[test]
|
||||
pub fn expand_snapshot_pass() {
|
||||
macrotest::expand("tests/expand/*.rs");
|
||||
}
|
131
tests/expand/advanced.expanded.rs
Normal file
131
tests/expand/advanced.expanded.rs
Normal file
|
@ -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("<h1>Hello World!</h1>").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("<h1>Hello World!</h1>").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<String>) -> 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("<h1>Hello World!</h1>").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<String>) -> 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<AppState> {
|
||||
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
|
||||
}
|
||||
}
|
9
tests/expand/advanced.rs
Normal file
9
tests/expand/advanced.rs
Normal file
|
@ -0,0 +1,9 @@
|
|||
use axum_folder_router::folder_router;
|
||||
|
||||
#[derive(Clone)]
|
||||
struct AppState {
|
||||
_foo: String,
|
||||
}
|
||||
|
||||
#[folder_router("examples/advanced/api", AppState)]
|
||||
struct MyFolderRouter();
|
|
@ -1 +0,0 @@
|
|||
../../examples
|
33
tests/expand/simple.expanded.rs
Normal file
33
tests/expand/simple.expanded.rs
Normal file
|
@ -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("<h1>Hello World!</h1>").into_response()
|
||||
}
|
||||
}
|
||||
}
|
||||
struct MyFolderRouter();
|
||||
impl MyFolderRouter {
|
||||
pub fn into_router() -> axum::Router<AppState> {
|
||||
let mut router = axum::Router::new();
|
||||
router = router
|
||||
.route(
|
||||
"/",
|
||||
axum::routing::get(
|
||||
__folder_router__myfolderrouter__examples_simple_api::route::get,
|
||||
),
|
||||
);
|
||||
router
|
||||
}
|
||||
}
|
7
tests/expand/simple.rs
Normal file
7
tests/expand/simple.rs
Normal file
|
@ -0,0 +1,7 @@
|
|||
use axum_folder_router::folder_router;
|
||||
|
||||
#[derive(Clone)]
|
||||
struct AppState;
|
||||
|
||||
#[folder_router("examples/simple/api", AppState)]
|
||||
struct MyFolderRouter();
|
5
tests/failures.rs
Normal file
5
tests/failures.rs
Normal file
|
@ -0,0 +1,5 @@
|
|||
#[test]
|
||||
fn ui() {
|
||||
let t = trybuild::TestCases::new();
|
||||
t.compile_fail("tests/failures/*.rs");
|
||||
}
|
9
tests/failures/no_files.rs
Normal file
9
tests/failures/no_files.rs
Normal file
|
@ -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() {}
|
7
tests/failures/no_files.stderr
Normal file
7
tests/failures/no_files.stderr
Normal file
|
@ -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)
|
9
tests/failures/no_routes.rs
Normal file
9
tests/failures/no_routes.rs
Normal file
|
@ -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() {}
|
7
tests/failures/no_routes.stderr
Normal file
7
tests/failures/no_routes.stderr
Normal file
|
@ -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)
|
0
tests/failures/no_routes/route.rs
Normal file
0
tests/failures/no_routes/route.rs
Normal file
|
@ -1,4 +0,0 @@
|
|||
#[test]
|
||||
pub fn expand_examples_pass() {
|
||||
macrotest::expand("test/expand/**/*.rs");
|
||||
}
|
Loading…
Add table
Reference in a new issue