Compare commits

..

No commits in common. "main" and "v0.2.3" have entirely different histories.
main ... v0.2.3

25 changed files with 375 additions and 991 deletions

1
.gitignore vendored
View file

@ -1,3 +1,2 @@
.direnv/ .direnv/
target/ target/
wip/

View file

@ -1,92 +1,15 @@
# Changelog # 0.2.3
- Refactored the detection of which methods exist,
we actually parse the file now instead of just checking that it contains `pub async #method_name`
All notable changes to this project will be documented in this file. # 0.2.2
- Re-licensed to MIT
## [Unreleased] # 0.2.1
- Documentation & test improvements
- Nothing yet # 0.2.0
- Generate module imports instead of `include!`ing, so rust-analyzer works.
## [0.3.6] - 2025-04-17 # 0.1.0
- MVP adapted from https://github.com/richardanaya/axum-folder-router-htmx
- Better error messages when having route.rs files with invalid code
## [0.3.5] - 2025-04-16
- Moved macrotest to dev deps
## [0.3.4] - 2025-04-16
- Refactored huge lib.rs into 3 seperate files.
- Downgraded edition to 2021 for better compatability
## [0.3.3] - 2025-04-15
### Added
- Add support for remaining HTTP methods
- we no support the full set as defined by rfc9110
- trace & connect were missing specifically
- Add support for `any` axum router method (default method router, others will take precedence)
## [0.3.2] - 2025-04-15
- Refactor internals
- Add solid testing
- explicitly test generated macro output using macrotest
- test error output using trybuilt
## [0.3.1] - 2025-04-15
- Fix invalid doc links
## [0.3.0] - 2025-04-15
After some experimentation, the API has begun to stabilize. This should likely be the last breaking change for some time.
### Breaking Changes
- **Reworked implementation into an attribute macro**
- Previous implementation required function calls:
```rust
folder_router!("./examples/simple/api", AppState);
// ...
let folder_router: Router<AppState> = folder_router();
```
- New implementation uses an attribute macro:
```rust
#[folder_router("./examples/simple/api", AppState)]
struct MyFolderRouter;
// ...
let folder_router: Router<AppState> = MyFolderRouter::into_router();
```
- This approach provides a cleaner API and allows for multiple separate folder-based Routers
## [0.2.3] - 2025-04-14
### Changed
- **Improved method detection** - Now properly parses files instead of using string matching
- Previous version checked if file contained ```pub async #method_name```
- New version properly parses the file using `syn` for more accurate detection
## [0.2.2] - 2025-04-14
### Changed
- **License changed to MIT**
## [0.2.1] - 2025-04-14
### Improved
- Enhanced documentation
- Added more comprehensive tests
## [0.2.0] - 2024-04-14
### Changed
- **Improved code integration**
- Generate module imports instead of using ```include!```
- Makes the code compatible with rust-analyzer
- Provides better IDE support
## [0.1.0] - 2024-04-14
### Added
- Initial release
- Minimum viable product adapted from https://github.com/richardanaya/axum-folder-router-htmx

214
Cargo.lock generated
View file

@ -17,15 +17,6 @@ version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" 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]] [[package]]
name = "anyhow" name = "anyhow"
version = "1.0.98" version = "1.0.98"
@ -94,18 +85,15 @@ dependencies = [
[[package]] [[package]]
name = "axum-folder-router" name = "axum-folder-router"
version = "0.3.6" version = "0.2.3"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"axum", "axum",
"glob", "glob",
"macrotest",
"proc-macro2", "proc-macro2",
"quote", "quote",
"regex",
"syn", "syn",
"tokio", "tokio",
"trybuild",
] ]
[[package]] [[package]]
@ -141,24 +129,6 @@ version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 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]] [[package]]
name = "fnv" name = "fnv"
version = "1.0.7" version = "1.0.7"
@ -219,12 +189,6 @@ version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a8d1add55171497b4705a648c6b583acafb01d58050a51727785f0b2c8e0a2b2" checksum = "a8d1add55171497b4705a648c6b583acafb01d58050a51727785f0b2c8e0a2b2"
[[package]]
name = "hashbrown"
version = "0.15.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289"
[[package]] [[package]]
name = "http" name = "http"
version = "1.3.1" version = "1.3.1"
@ -306,16 +270,6 @@ dependencies = [
"tower-service", "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]] [[package]]
name = "itoa" name = "itoa"
version = "1.0.15" version = "1.0.15"
@ -324,9 +278,9 @@ checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c"
[[package]] [[package]]
name = "libc" name = "libc"
version = "0.2.172" version = "0.2.171"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa" checksum = "c19937216e9d3aa9956d9bb8dfc0b0c8beb6058fc4f7a4dc4d850edf86a237d6"
[[package]] [[package]]
name = "lock_api" name = "lock_api"
@ -344,23 +298,6 @@ version = "0.4.27"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" 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]] [[package]]
name = "matchit" name = "matchit"
version = "0.8.4" version = "0.8.4"
@ -396,7 +333,7 @@ checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd"
dependencies = [ dependencies = [
"libc", "libc",
"wasi", "wasi",
"windows-sys 0.52.0", "windows-sys",
] ]
[[package]] [[package]]
@ -455,16 +392,6 @@ version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" 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]] [[package]]
name = "proc-macro2" name = "proc-macro2"
version = "1.0.94" version = "1.0.94"
@ -492,35 +419,6 @@ dependencies = [
"bitflags", "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]] [[package]]
name = "rustc-demangle" name = "rustc-demangle"
version = "0.1.24" version = "0.1.24"
@ -587,15 +485,6 @@ dependencies = [
"serde", "serde",
] ]
[[package]]
name = "serde_spanned"
version = "0.6.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "87607cb1398ed59d48732e575a4c28a7a8ebf2454b964fe3f224f2afc07909e1"
dependencies = [
"serde",
]
[[package]] [[package]]
name = "serde_urlencoded" name = "serde_urlencoded"
version = "0.7.1" version = "0.7.1"
@ -630,7 +519,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4f5fd57c80058a56cf5c777ab8a126398ece8e442983605d280a44ce79d0edef" checksum = "4f5fd57c80058a56cf5c777ab8a126398ece8e442983605d280a44ce79d0edef"
dependencies = [ dependencies = [
"libc", "libc",
"windows-sys 0.52.0", "windows-sys",
] ]
[[package]] [[package]]
@ -650,21 +539,6 @@ version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" 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]] [[package]]
name = "tokio" name = "tokio"
version = "1.44.2" version = "1.44.2"
@ -680,7 +554,7 @@ dependencies = [
"signal-hook-registry", "signal-hook-registry",
"socket2", "socket2",
"tokio-macros", "tokio-macros",
"windows-sys 0.52.0", "windows-sys",
] ]
[[package]] [[package]]
@ -694,40 +568,6 @@ dependencies = [
"syn", "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"
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]] [[package]]
name = "tower" name = "tower"
version = "0.5.2" version = "0.5.2"
@ -776,21 +616,6 @@ dependencies = [
"once_cell", "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]] [[package]]
name = "unicode-ident" name = "unicode-ident"
version = "1.0.18" version = "1.0.18"
@ -803,15 +628,6 @@ version = "0.11.0+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" 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 0.59.0",
]
[[package]] [[package]]
name = "windows-sys" name = "windows-sys"
version = "0.52.0" version = "0.52.0"
@ -821,15 +637,6 @@ dependencies = [
"windows-targets", "windows-targets",
] ]
[[package]]
name = "windows-sys"
version = "0.59.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b"
dependencies = [
"windows-targets",
]
[[package]] [[package]]
name = "windows-targets" name = "windows-targets"
version = "0.52.6" version = "0.52.6"
@ -893,12 +700,3 @@ name = "windows_x86_64_msvc"
version = "0.52.6" version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
[[package]]
name = "winnow"
version = "0.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "63d3fcd9bba44b03821e7d699eeee959f3126dcc4aa8e4ae18ec617c2a5cea10"
dependencies = [
"memchr",
]

View file

@ -1,7 +1,7 @@
[package] [package]
name = "axum-folder-router" name = "axum-folder-router"
version = "0.3.6" version = "0.2.3"
edition = "2021" edition = "2024"
readme = "./README.md" readme = "./README.md"
authors = ["Tristan Druyen <ek36g2vcc@mozmail.com>"] authors = ["Tristan Druyen <ek36g2vcc@mozmail.com>"]
categories = ["web-programming"] categories = ["web-programming"]
@ -20,15 +20,8 @@ syn = { version = "2.0", features = ["full"] }
quote = "1.0" quote = "1.0"
proc-macro2 = "1.0" proc-macro2 = "1.0"
glob = "0.3" glob = "0.3"
regex = "1.11.1"
[dev-dependencies] [dev-dependencies]
anyhow = "1.0.98" anyhow = "1.0.98"
axum = "0.8.3" axum = "0.8.3"
tokio = { version = "1.44.2", features = ["full"] } tokio = { version = "1.44.2", features = ["full"] }
trybuild = "1.0.104"
macrotest = "1.1.0"
[lints.clippy]
pedantic = { level = "warn", priority = -1 }
unused-async = { level = "allow", priority = 0 } # required for examples without unecessary noise

View file

@ -4,21 +4,16 @@
![Maintenance](https://img.shields.io/badge/maintenance-actively--developed-brightgreen.svg) ![Maintenance](https://img.shields.io/badge/maintenance-actively--developed-brightgreen.svg)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
# axum-folder-router # axum-folder-router
```#[folder_router(...)]``` is a procedural attribute macro for the Axum web framework that automatically generates router boilerplate based on your direcory & file structure. ```folder_router``` is a procedural macro for the Axum web framework that automatically generates router boilerplate based on your file structure.
Inspired by popular frameworks like next.js. It simplifies route organization by using filesystem conventions to define your API routes.
## Features
- **File System-Based Routing**: Define your API routes using intuitive folder structures
- **Reduced Boilerplate**: Automatically generates route mapping code
- **IDE Support**: Generates proper module imports for better rust-analyzer integration
- **Multiple Routers**: Create separate folder-based routers in the same application
## Usage ## Usage
For detailed instructions see [the examples](./examples) or [docs.rs](https://docs.rs/axum-folder-router). See [the examples](./examples) or [docs.rs](https://docs.rs/axum-folder-router).
## License ## License

View file

@ -1,5 +1,5 @@
use axum::{extract::Path, response::IntoResponse}; use axum::{extract::Path, response::IntoResponse};
pub async fn get(Path(path): Path<String>) -> impl IntoResponse { pub async fn get(Path(path): Path<String>) -> impl IntoResponse {
format!("Requested file path: {path}") format!("Requested file path: {}", path)
} }

View file

@ -1,12 +0,0 @@
use axum::response::Html;
use axum::response::IntoResponse;
pub async fn get() -> impl IntoResponse {
Html("<h1>GET Pong!</h1>").into_response()
}
// This tests that our macro generates the routes in the correct order
// as any is only allowable as a first route.
pub async fn any() -> impl IntoResponse {
Html("<h1>ANY Pong!</h1>").into_response()
}

View file

@ -1,5 +1,5 @@
use axum::{extract::Path, response::IntoResponse}; use axum::{extract::Path, response::IntoResponse};
pub async fn get(Path(id): Path<String>) -> impl IntoResponse { pub async fn get(Path(id): Path<String>) -> impl IntoResponse {
format!("User ID: {id}") format!("User ID: {}", id)
} }

View file

@ -6,18 +6,17 @@ struct AppState {
_foo: String, _foo: String,
} }
// Imports route.rs files & generates an ::into_router() fn // Imports route.rs files & generates an init fn
#[folder_router("examples/advanced/api", AppState)] folder_router!("examples/advanced/api", AppState);
struct MyFolderRouter();
pub async fn server() -> anyhow::Result<()> { pub async fn server() -> anyhow::Result<()> {
// Create app state // Create app state
let app_state = AppState { let app_state = AppState {
_foo: String::new(), _foo: "".to_string(),
}; };
// Use the init fn generated above // Use the init fn generated above
let folder_router: Router<AppState> = MyFolderRouter::into_router(); 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);

View file

@ -1,20 +1,23 @@
use axum::Router; use axum::Router;
use axum_folder_router::folder_router; use axum_folder_router::folder_router;
#[derive(Clone)] #[derive(Clone, Debug)]
struct AppState; struct AppState {
_foo: String,
}
// Imports route.rs files & generates an ::into_router() fn // Imports route.rs files & generates an init fn
#[folder_router("./examples/simple/api", AppState)] folder_router!("./examples/simple/api", AppState);
struct MyFolderRouter();
#[tokio::main] #[tokio::main]
async fn main() -> anyhow::Result<()> { async fn main() -> anyhow::Result<()> {
// Create app state // Create app state
let app_state = AppState; let app_state = AppState {
_foo: "".to_string(),
};
// Use the init fn generated above // Use the init fn generated above
let folder_router: Router<AppState> = MyFolderRouter::into_router(); 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);

View file

@ -72,8 +72,7 @@
cargo-udeps cargo-udeps
cargo-outdated cargo-outdated
cargo-release cargo-release
cargo-readme cargo-readme
cargo-expand
calc calc
fish fish
inotify-tools inotify-tools
@ -94,8 +93,7 @@
packages = { packages = {
# default = pkgs.callPackage ./package.nix { }; # default = pkgs.callPackage ./package.nix { };
}; };
}) }) // {
// {
hydraJobs = hydraJobs =
let let
system = "x86_64-linux"; system = "x86_64-linux";

View file

@ -1,234 +0,0 @@
use std::{collections::BTreeMap, fmt::Write, path::Path};
use proc_macro2::TokenStream;
use quote::{format_ident, quote};
use syn::LitStr;
use crate::parse::methods_for_route;
// A struct representing a directory in the module tree
#[derive(Debug)]
struct ModuleDir {
name: String,
has_route: bool,
children: BTreeMap<String, ModuleDir>,
}
impl ModuleDir {
fn new(name: &str) -> Self {
ModuleDir {
name: name.to_string(),
has_route: false,
children: BTreeMap::new(),
}
}
fn add_to_module_tree(&mut self, rel_path: &Path, _route_path: &Path) {
let components: Vec<_> = rel_path
.components()
.map(|c| c.as_os_str().to_string_lossy().to_string())
.collect();
if components.is_empty() {
self.has_route = true;
return;
}
let mut root = self;
for (i, segment) in components.iter().enumerate() {
if i == components.len() - 1 && segment == "route.rs" {
root.has_route = true;
break;
}
root = root
.children
.entry(segment.clone())
.or_insert_with(|| ModuleDir::new(segment));
}
}
}
// Add a route to the module tree
// 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 {
// Process directory name
let normalized = normalize_module_name(segment);
mod_path.push(normalized);
// Process URL path
if segment.starts_with('[') && segment.ends_with(']') {
let param = &segment[1..segment.len() - 1];
if let Some(stripped) = param.strip_prefix("...") {
write!(&mut axum_path, "/{{*{stripped}}}").unwrap();
} else {
write!(&mut axum_path, "/{{:{param}}}").unwrap();
}
} else {
write!(&mut axum_path, "/{segment}").unwrap();
}
}
}
if axum_path.is_empty() {
axum_path = "/".to_string();
}
(axum_path, mod_path)
}
// Generate tokens for a module path
fn generate_mod_path_tokens(mod_path: &[String]) -> TokenStream {
let mut result = 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
}
// Generate module hierarchy code
fn generate_module_hierarchy(dir: &ModuleDir) -> TokenStream {
let mut result = TokenStream::new();
// 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
}
pub fn route_registrations(
root_namespace_str: &str,
routes: &Vec<(std::path::PathBuf, std::path::PathBuf)>,
) -> TokenStream {
let root_namespace_ident = format_ident!("{}", root_namespace_str);
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);
let method_registrations = methods_for_route(route_path);
if !method_registrations.is_empty() {
let first_method = &method_registrations[0];
let first_method_ident = format_ident!("{}", first_method);
let mod_path_tokens = generate_mod_path_tokens(&mod_path);
let mut builder = quote! {
axum::routing::#first_method_ident(#root_namespace_ident::#mod_path_tokens::#first_method_ident)
};
for method in &method_registrations[1..] {
let method_ident = format_ident!("{}", method);
builder = quote! {
#builder.#method_ident(#root_namespace_ident::#mod_path_tokens::#method_ident)
};
}
let registration = quote! {
router = router.route(#axum_path, #builder);
};
route_registrations.push(registration);
}
}
if route_registrations.is_empty() {
return quote! {
compile_error!(concat!(
"No routes defined in your route.rs's !\n",
"Ensure that at least one `pub async fn` named after an HTTP verb is defined. (e.g. get, post, put, delete)"
));
};
}
TokenStream::from_iter(route_registrations)
}
pub fn module_tree(
root_namespace_str: &str,
base_dir: &Path,
routes: &Vec<(std::path::PathBuf, std::path::PathBuf)>,
) -> TokenStream {
let root_namespace_ident = format_ident!("{}", root_namespace_str);
let base_path_lit = LitStr::new(
base_dir.to_str().unwrap_or("./"),
proc_macro2::Span::call_site(),
);
let mut root = ModuleDir::new(root_namespace_str);
for (route_path, rel_path) in routes {
root.add_to_module_tree(rel_path, route_path);
}
let mod_hierarchy = generate_module_hierarchy(&root);
quote! {
#[path = #base_path_lit]
mod #root_namespace_ident {
#mod_hierarchy
}
}
}

View file

@ -1,6 +1,6 @@
//! # ```axum_folder_router``` Macro Documentation //! # ```axum_folder_router``` Macro Documentation
//! //!
//! [macro@folder_router] is a procedural macro for the Axum web framework that //! [folder_router!] is a procedural macro for the Axum web framework that
//! automatically generates router boilerplate based on your file structure. It //! automatically generates router boilerplate based on your file structure. It
//! simplifies route organization by using filesystem conventions to define your //! simplifies route organization by using filesystem conventions to define your
//! API routes. //! API routes.
@ -11,7 +11,7 @@
//! //!
//! ```toml //! ```toml
//! [dependencies] //! [dependencies]
//! axum_folder_router = "0.3" //! axum_folder_router = "0.2"
//! axum = "0.8" //! axum = "0.8"
//! ``` //! ```
//! //!
@ -54,7 +54,7 @@
//! //!
//! ### HTTP Methods //! ### HTTP Methods
//! //!
//! The macro supports all standard HTTP methods as defined in RFC9110. //! The macro supports all standard HTTP methods:
//! - ```get``` //! - ```get```
//! - ```post``` //! - ```post```
//! - ```put``` //! - ```put```
@ -62,11 +62,6 @@
//! - ```patch``` //! - ```patch```
//! - ```head``` //! - ```head```
//! - ```options``` //! - ```options```
//! - ```trace```
//! - ```connect```
//!
//! And additionally
//! - ```any```, which maches all methods
//! //!
//! ### Path Parameters //! ### Path Parameters
//! //!
@ -127,96 +122,367 @@
//! - **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.
//! - **Expects seperate directory**: To make rust-analyzer & co work correctly the macro imports all route.rs files inside the given directory tree. //! - **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. //! It is highly recommended to keep the route directory seperate from the rest of your module-tree.
use std::path::Path; use std::{
collections::HashMap,
fs,
path::{Path, PathBuf},
};
use proc_macro::TokenStream; use proc_macro::TokenStream;
use quote::quote; use quote::{format_ident, quote};
use syn::parse_macro_input; use syn::{
Ident,
Item,
LitStr,
Result,
Token,
Visibility,
parse::{Parse, ParseStream},
parse_file,
parse_macro_input,
};
mod generate; struct FolderRouterArgs {
mod parse; path: String,
state_type: Ident,
}
impl Parse for FolderRouterArgs {
fn parse(input: ParseStream) -> Result<Self> {
let path_lit = input.parse::<LitStr>()?;
input.parse::<Token![,]>()?;
let state_type = input.parse::<Ident>()?;
Ok(FolderRouterArgs {
path: path_lit.value(),
state_type,
})
}
}
// 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 /// Creates an Axum router module tree & creation function
/// by scanning a directory for `route.rs` files. /// by scanning a directory for `route.rs` files.
/// ///
/// # Parameters /// # Parameters
/// ///
/// * `path` - A string literal pointing to the route directory, relative to the /// * `path` - A string literal pointing to the API directory, relative to the
/// Cargo manifest directory /// Cargo manifest directory
/// * `state_type` - The type name of your application state that will be shared /// * `state_type` - The type name of your application state that will be shared
/// across all routes /// across all routes
#[allow(clippy::missing_panics_doc)] ///
#[proc_macro_attribute] /// This will scan all `route.rs` files in the `./src/api` directory and its
pub fn folder_router(attr: TokenStream, item: TokenStream) -> TokenStream { /// subdirectories, automatically mapping their path structure to URL routes
let args = parse_macro_input!(attr as parse::FolderRouterArgs); /// with the specified state type.
let input_item = parse_macro_input!(item as syn::ItemStruct); #[proc_macro]
let struct_name = &input_item.ident; pub fn folder_router(input: TokenStream) -> TokenStream {
let args = parse_macro_input!(input as FolderRouterArgs);
let base_path = args.path; let base_path = args.path;
let state_type = args.state_type; let state_type = args.state_type;
let manifest_dir = get_manifest_dir(); // Get the project root directory
let manifest_dir = std::env::var("CARGO_MANIFEST_DIR").unwrap();
let base_dir = Path::new(&manifest_dir).join(&base_path); let base_dir = Path::new(&manifest_dir).join(&base_path);
let mod_namespace = format!( // Collect route files
"__folder_router__{}__{}", let mut routes = Vec::new();
struct_name collect_route_files(&base_dir, &base_dir, &mut routes);
.to_string()
.chars()
.map(|c| if c.is_alphanumeric() { c } else { '_' })
.map(|c| c.to_ascii_lowercase())
.collect::<String>(),
base_path
.chars()
.map(|c| if c.is_alphanumeric() { c } else { '_' })
.collect::<String>()
);
let routes = parse::collect_route_files(&base_dir, &base_dir);
if routes.is_empty() { if routes.is_empty() {
return TokenStream::from(quote! { 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, #base_path,
"'. Make sure the path is correct and contains route.rs files." ". Make sure the path is correct and contains route.rs files."
)); ));
}); });
} }
let module_tree = generate::module_tree(&mod_namespace, &base_dir, &routes); // Build module tree
let route_registrations = generate::route_registrations(&mod_namespace, &routes); let mut root = ModuleDir::new("__folder_router");
for (route_path, rel_path) in &routes {
quote! { add_to_module_tree(&mut root, rel_path, route_path);
#module_tree
#input_item
impl #struct_name {
pub fn into_router() -> axum::Router<#state_type> {
let mut router = axum::Router::new();
#route_registrations
router
}
}
} }
.into()
// 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);
let method_registrations = methods_for_route(&route_path);
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)"
));
});
}
let first_method = &method_registrations[0];
let first_method_ident = format_ident!("{}", first_method);
let mod_path_tokens = generate_mod_path_tokens(&mod_path);
let mut builder = quote! {
axum::routing::#first_method_ident(#root_mod_ident::#mod_path_tokens::#first_method_ident)
};
for method in &method_registrations[1..] {
let method_ident = format_ident!("{}", method);
builder = quote! {
#builder.#method_ident(#root_mod_ident::#mod_path_tokens::#method_ident)
};
}
let registration = quote! {
router = router.route(#axum_path, #builder);
};
route_registrations.push(registration);
}
// Generate the final code
let expanded = quote! {
#[path = #base_path_lit]
mod #root_mod_ident {
#mod_hierarchy
}
pub fn folder_router() -> axum::Router<#state_type> {
let mut router = axum::Router::new();
#(#route_registrations)*
router
}
};
expanded.into()
} }
// This is a workaround for macrotest behaviour /// parses the file at the specified location using syn
#[cfg(debug_assertions)] /// and returns a Vec<&'static str> of all used http verb fns
fn get_manifest_dir() -> String { /// e.g. for the file
use regex::Regex; ///
let dir = std::env::var("CARGO_MANIFEST_DIR").unwrap_or("./".to_string()); /// ```rust
let re = Regex::new(r"^(.+)/target/tests/axum-folder-router/[A-Za-z0-9]{42}$").unwrap(); /// pub async fn get() {} # ✅ => "get" be added to vec
/// pub fn post() {} # not async
/// async fn delete() {} # not pub
/// fn patch() {} # not pub nor async
/// pub fn non_verb() {} # not a http verb
/// ```
///
/// 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(),
};
if let Some(captures) = re.captures(&dir) { // Parse the file content into a syn syntax tree
captures.get(1).unwrap().as_str().to_string() let file = match parse_file(&file_content) {
Ok(file) => file,
Err(_) => return Vec::new(),
};
// Define HTTP methods we're looking for
let methods = ["get", "post", "put", "delete", "patch", "head", "options"];
let mut found_methods = Vec::new();
// Examine each item in the file
for item in &file.items {
if let Item::Fn(fn_item) = item {
let fn_name = fn_item.sig.ident.to_string();
// Check if the function name is one of our HTTP methods
if let Some(&method) = methods.iter().find(|&&m| m == fn_name) {
// Check if the function is public
let is_public = matches!(fn_item.vis, Visibility::Public(_));
// Check if the function is async
let is_async = fn_item.sig.asyncness.is_some();
if is_public && is_async {
found_methods.push(method);
}
}
}
}
found_methods
}
// Add a route to the module tree
fn add_to_module_tree(root: &mut ModuleDir, rel_path: &Path, _route_path: &Path) {
let components: Vec<_> = rel_path
.components()
.map(|c| c.as_os_str().to_string_lossy().to_string())
.collect();
if components.is_empty() {
root.has_route = true;
return;
}
let mut root = root;
for (i, segment) in components.iter().enumerate() {
if i == components.len() - 1 && segment == "route.rs" {
root.has_route = true;
break;
}
root = root
.children
.entry(segment.clone())
.or_insert_with(|| ModuleDir::new(segment));
}
}
// Generate module hierarchy code
fn generate_module_hierarchy(dir: &ModuleDir) -> proc_macro2::TokenStream {
let mut result = proc_macro2::TokenStream::new();
// 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 { } else {
dir name.replace(['-', '.'], "_")
} }
} }
#[cfg(not(debug_assertions))] // Convert a relative path to module path segments and axum route path
fn get_manifest_dir() -> String { fn path_to_module_path(rel_path: &Path) -> (String, Vec<String>) {
std::env::var("CARGO_MANIFEST_DIR").unwrap_or("./".to_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 {
// Process directory name
let normalized = normalize_module_name(segment);
mod_path.push(normalized);
// Process URL path
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));
} else {
axum_path.push_str(&format!("/{{:{}}}", param));
}
} else {
axum_path.push_str(&format!("/{}", segment));
}
}
}
if axum_path.is_empty() {
axum_path = "/".to_string();
}
(axum_path, mod_path)
}
// Collect route.rs files recursively
fn collect_route_files(base_dir: &Path, dir: &Path, routes: &mut Vec<(PathBuf, PathBuf)>) {
if let Ok(entries) = fs::read_dir(dir) {
for entry in entries.filter_map(std::result::Result::ok) {
let path = entry.path();
if path.is_dir() {
collect_route_files(base_dir, &path, routes);
} else if path.file_name().unwrap_or_default() == "route.rs" {
if let Ok(rel_dir) = path.strip_prefix(base_dir) {
routes.push((path.clone(), rel_dir.to_path_buf()));
}
}
}
}
} }

View file

@ -1,98 +0,0 @@
use std::{
fs,
path::{Path, PathBuf},
};
use syn::{
parse::{Parse, ParseStream},
parse_file,
Ident,
Item,
LitStr,
Result,
Token,
Visibility,
};
#[derive(Debug)]
pub struct FolderRouterArgs {
pub path: String,
pub state_type: Ident,
}
impl Parse for FolderRouterArgs {
fn parse(input: ParseStream) -> Result<Self> {
let path_lit = input.parse::<LitStr>()?;
input.parse::<Token![,]>()?;
let state_type = input.parse::<Ident>()?;
Ok(FolderRouterArgs {
path: path_lit.value(),
state_type,
})
}
}
/// Parses the file at the specified location and returns HTTP verb functions
pub fn methods_for_route(route_path: &PathBuf) -> Vec<&'static str> {
// Read the file content
let Ok(file_content) = fs::read_to_string(route_path) else {
return Vec::new();
};
// Parse the file content into a syn syntax tree
let Ok(file) = parse_file(&file_content) else {
return Vec::new();
};
// Define HTTP methods we're looking for
let allowed_methods = [
"any", "get", "post", "put", "delete", "patch", "head", "options", "trace", "connect",
];
let mut found_methods = Vec::new();
// Collect all pub & async fn's
for item in &file.items {
if let Item::Fn(fn_item) = item {
let fn_name = fn_item.sig.ident.to_string();
let is_public = matches!(fn_item.vis, Visibility::Public(_));
let is_async = fn_item.sig.asyncness.is_some();
if is_public && is_async {
found_methods.push(fn_name);
}
}
}
// Iterate through methods to ensure consistent order
allowed_methods
.into_iter()
.filter(|elem| {
found_methods
.clone()
.into_iter()
.any(|method| method == *elem)
})
.collect()
}
// Collect route.rs files recursively
pub fn collect_route_files(base_dir: &Path, dir: &Path) -> Vec<(PathBuf, PathBuf)> {
let mut routes = Vec::new();
if let Ok(entries) = fs::read_dir(dir) {
for entry in entries.filter_map(std::result::Result::ok) {
let path = entry.path();
if path.is_dir() {
let mut nested_routes = collect_route_files(base_dir, &path);
routes.append(&mut nested_routes);
} else if path.file_name().unwrap_or_default() == "route.rs" {
if let Ok(rel_dir) = path.strip_prefix(base_dir) {
routes.push((path.clone(), rel_dir.to_path_buf()));
}
}
}
}
routes.sort();
routes
}

View file

@ -1,4 +0,0 @@
#[test]
pub fn expand_snapshot_pass() {
macrotest::expand("tests/expand/*.rs");
}

View file

@ -1,155 +0,0 @@
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 = "ping"]
pub mod ping {
#[path = "route.rs"]
pub mod route {
use axum::response::Html;
use axum::response::IntoResponse;
pub async fn get() -> impl IntoResponse {
Html("<h1>GET Pong!</h1>").into_response()
}
pub async fn any() -> impl IntoResponse {
Html("<h1>ANY Pong!</h1>").into_response()
}
}
}
#[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(
"/ping",
axum::routing::any(
__folder_router__myfolderrouter__examples_advanced_api::ping::route::any,
)
.get(
__folder_router__myfolderrouter__examples_advanced_api::ping::route::get,
),
);
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
}
}

View file

@ -1,9 +0,0 @@
use axum_folder_router::folder_router;
#[derive(Clone)]
struct AppState {
_foo: String,
}
#[folder_router("examples/advanced/api", AppState)]
struct MyFolderRouter();

View file

@ -1,33 +0,0 @@
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
}
}

View file

@ -1,7 +0,0 @@
use axum_folder_router::folder_router;
#[derive(Clone)]
struct AppState;
#[folder_router("examples/simple/api", AppState)]
struct MyFolderRouter();

View file

@ -1,5 +0,0 @@
#[test]
fn ui() {
let t = trybuild::TestCases::new();
t.compile_fail("tests/failures/*.rs");
}

View file

@ -1,9 +0,0 @@
use axum_folder_router::folder_router;
#[derive(Clone)]
struct AppState;
#[folder_router("some/non/existing/directory", AppState)]
struct MyFolderRouter();
fn main() {}

View file

@ -1,7 +0,0 @@
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)

View file

@ -1,9 +0,0 @@
use axum_folder_router::folder_router;
#[derive(Clone)]
struct AppState;
#[folder_router("../../../../tests/failures/no_routes", AppState)]
struct MyFolderRouter();
fn main() {}

View file

@ -1,8 +0,0 @@
error: No routes defined in your route.rs's !
Ensure that at least one `pub async fn` named after an HTTP verb is defined. (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)