Compare commits

..

No commits in common. "main" and "renovate/configure" have entirely different histories.

34 changed files with 258 additions and 1151 deletions

1
.gitignore vendored
View file

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

View file

@ -1,92 +0,0 @@
# Changelog
All notable changes to this project will be documented in this file.
## [Unreleased]
- Nothing yet
## [0.3.6] - 2025-04-17
- 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

220
Cargo.lock generated
View file

@ -17,15 +17,6 @@ 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,19 +84,16 @@ dependencies = [
]
[[package]]
name = "axum-folder-router"
version = "0.3.6"
name = "axum_folder_router"
version = "0.1.0"
dependencies = [
"anyhow",
"axum",
"glob",
"macrotest",
"proc-macro2",
"quote",
"regex",
"syn",
"tokio",
"trybuild",
]
[[package]]
@ -141,24 +129,6 @@ 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"
@ -219,12 +189,6 @@ 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"
@ -306,16 +270,6 @@ 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"
@ -324,9 +278,9 @@ checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c"
[[package]]
name = "libc"
version = "0.2.172"
version = "0.2.171"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa"
checksum = "c19937216e9d3aa9956d9bb8dfc0b0c8beb6058fc4f7a4dc4d850edf86a237d6"
[[package]]
name = "lock_api"
@ -344,23 +298,6 @@ 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"
@ -396,7 +333,7 @@ checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd"
dependencies = [
"libc",
"wasi",
"windows-sys 0.52.0",
"windows-sys",
]
[[package]]
@ -455,16 +392,6 @@ 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,42 +412,13 @@ dependencies = [
[[package]]
name = "redox_syscall"
version = "0.5.11"
version = "0.5.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d2f103c6d277498fbceb16e84d317e2a400f160f46904d5f5410848c829511a3"
checksum = "0b8c0c260b63a8219631167be35e6a988e9554dbd323f8bd08439c8ed1302bd1"
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"
@ -587,15 +485,6 @@ 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"
@ -630,7 +519,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4f5fd57c80058a56cf5c777ab8a126398ece8e442983605d280a44ce79d0edef"
dependencies = [
"libc",
"windows-sys 0.52.0",
"windows-sys",
]
[[package]]
@ -650,21 +539,6 @@ 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"
@ -680,7 +554,7 @@ dependencies = [
"signal-hook-registry",
"socket2",
"tokio-macros",
"windows-sys 0.52.0",
"windows-sys",
]
[[package]]
@ -694,40 +568,6 @@ 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"
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"
@ -776,21 +616,6 @@ 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"
@ -803,15 +628,6 @@ 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 0.59.0",
]
[[package]]
name = "windows-sys"
version = "0.52.0"
@ -821,15 +637,6 @@ dependencies = [
"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]]
name = "windows-targets"
version = "0.52.6"
@ -893,12 +700,3 @@ 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",
]

View file

@ -1,6 +1,6 @@
[package]
name = "axum-folder-router"
version = "0.3.6"
name = "axum_folder_router"
version = "0.1.0"
edition = "2021"
readme = "./README.md"
authors = ["Tristan Druyen <ek36g2vcc@mozmail.com>"]
@ -9,26 +9,24 @@ keywords = ["axum", "controller", "macro", "routing"]
description = "Helper macro for simple folder based routing of axum handlers"
homepage = "https://git.vlt81.de/vault81/axum-folder-router"
repository = "https://git.vlt81.de/vault81/axum-folder-router"
license = "MIT"
license = "AGPL-3.0-or-later"
[lib]
path = "./src/lib.rs"
proc-macro = true
[dependencies]
syn = { version = "2.0", features = ["full"] }
syn = "2.0"
quote = "1.0"
proc-macro2 = "1.0"
glob = "0.3"
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"
macrotest = "1.1.0"
[lints.clippy]
pedantic = { level = "warn", priority = -1 }
unused-async = { level = "allow", priority = 0 } # required for examples without unecessary noise
[badges]
github = { repository = "vault81/axum-folder-router" }
maintenance = { status = "actively-developed" }

View file

@ -1,26 +0,0 @@
The MIT License (MIT)
=====================
Copyright © `2025` `Tristan Druyen`
Permission is hereby granted, free of charge, to any person
obtaining a copy of this software and associated documentation
files (the “Software”), to deal in the Software without
restriction, including without limitation the rights to use,
copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following
conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
OTHER DEALINGS IN THE SOFTWARE.

View file

@ -1,33 +1,35 @@
[![Crates.io](https://img.shields.io/crates/v/axum-folder-router.svg)](https://crates.io/crates/axum-folder-router)
[![Workflow Status](https://github.com/vault81/axum-folder-router/workflows/main/badge.svg)](https://github.com/vault81/axum-folder-router/actions?query=workflow%3A%22main%22)
![Maintenance](https://img.shields.io/badge/maintenance-activly--developed-brightgreen.svg)
[![Crates.io](https://img.shields.io/crates/v/axum-folder-router)](https://crates.io/crates/axum-folder-router)
[![Documentation](https://docs.rs/axum-folder-router/badge.svg)](https://docs.rs/axum-folder-router)
![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)
# axum_folder_router
# axum-folder-router
## ```axum_folder_router``` Macro Documentation
```#[folder_router(...)]``` is a procedural attribute macro for the Axum web framework that automatically generates router boilerplate based on your direcory & file structure.
Inspired by popular frameworks like next.js.
```folder_router``` is a procedural macro for the Axum web framework that automatically generates router configurations based on your file structure. It simplifies route organization by using filesystem conventions to define your API routes.
## Features
### Installation
- **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
Add the dependency to your ```Cargo.toml```:
## Usage
```toml
[dependencies]
axum_folder_router = "0.1.0"
axum = "0.7"
```
For detailed instructions see [the examples](./examples) or [docs.rs](https://docs.rs/axum-folder-router).
### Basic Usage
The macro scans a directory for ```route.rs``` files and automatically creates an Axum router based on the file structure:
```rust
## License
This repository, is licensed permissively under the terms of the MIT license.
This repository, like all my personal projects, is licensed under the GNU Affero General Public License v3.0 or later (AGPL-3.0-or-later).
This ensures that modifications to the code remain open source when used in network services.
Contact me if this doesn't suit your needs.
### Contribution
Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, shall be licensed as above, without any additional terms or conditions.
### Attribution
This macro is based on the [build.rs template by @richardanaya](https://github.com/richardanaya/axum-folder-router-htmx)
Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the GNU Affero General Public License v3.0 or later (AGPL-3.0-or-later) license, shall be licensed as above, without any additional terms or conditions.

16
README.tpl Normal file
View file

@ -0,0 +1,16 @@
[![Crates.io](https://img.shields.io/crates/v/axum-folder-router.svg)](https://crates.io/crates/axum-folder-router)
{{badges}}
# {{crate}}
{{readme}}
## License
This repository, like all my personal projects, is licensed under the GNU Affero General Public License v3.0 or later (AGPL-3.0-or-later).
This ensures that modifications to the code remain open source when used in network services.
Contact me if this doesn't suit your needs.
### Contribution
Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the GNU Affero General Public License v3.0 or later (AGPL-3.0-or-later) license, shall be licensed as above, without any additional terms or conditions.

View file

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

View file

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

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,9 +0,0 @@
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()
}

View file

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

View file

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

View file

@ -1,7 +0,0 @@
mod server;
#[tokio::main]
async fn main() -> anyhow::Result<()> {
server::server().await?;
Ok(())
}

View file

@ -1,29 +0,0 @@
use axum::Router;
use axum_folder_router::folder_router;
#[derive(Clone, Debug)]
struct AppState {
_foo: String,
}
// Imports route.rs files & generates an ::into_router() fn
#[folder_router("examples/advanced/api", AppState)]
struct MyFolderRouter();
pub async fn server() -> anyhow::Result<()> {
// Create app state
let app_state = AppState {
_foo: String::new(),
};
// Use the init fn generated above
let folder_router: Router<AppState> = MyFolderRouter::into_router();
// Build the router and provide the state
let app: Router<()> = folder_router.with_state(app_state);
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await?;
println!("Listening on http://{}", listener.local_addr()?);
axum::serve(listener, app).await?;
Ok(())
}

View file

@ -3,3 +3,8 @@ 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()
}

View file

@ -1,26 +1,31 @@
use axum::Router;
use axum_folder_router::folder_router;
use tokio;
use axum::Router;
use anyhow;
#[derive(Clone)]
struct AppState;
// Imports route.rs files & generates an ::into_router() fn
#[folder_router("./examples/simple/api", AppState)]
struct MyFolderRouter();
#[derive(Clone, Debug)]
struct AppState {
_foo: String
}
#[tokio::main]
async fn main() -> anyhow::Result<()> {
// Create app state
let app_state = AppState;
let app_state = AppState {
_foo: "".to_string()
};
// Use the init fn generated above
let folder_router: Router<AppState> = MyFolderRouter::into_router();
// Generate the router using the macro
let folder_router: Router<AppState>= folder_router!("./examples/simple/api", AppState);
// 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);
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await?;
println!("Listening on http://{}", listener.local_addr()?);
axum::serve(listener, app).await?;
Ok(())
}

View file

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

3
renovate.json Normal file
View file

@ -0,0 +1,3 @@
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json"
}

View file

@ -1,23 +0,0 @@
edition = "2024"
max_width = 100
tab_spaces = 4
# unstable
format_macro_bodies = true
format_macro_matchers = true
format_strings = true
group_imports = "StdExternalCrate"
imports_granularity = "Crate"
imports_layout = "HorizontalVertical"
overflow_delimited_expr = true
reorder_impl_items = true
struct_field_align_threshold = 4
struct_lit_single_line = false
trailing_comma = "Vertical"
unstable_features = true
use_field_init_shorthand = true
use_try_shorthand = true
wrap_comments = true
# format_brace_macros = true

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,9 +1,6 @@
//! # ```axum_folder_router``` Macro Documentation
//!
//! [macro@folder_router] is a procedural macro for the Axum web framework that
//! automatically generates router boilerplate based on your file structure. It
//! simplifies route organization by using filesystem conventions to define your
//! API routes.
//! ```folder_router``` is a procedural macro for the Axum web framework that automatically generates router configurations based on your file structure. It simplifies route organization by using filesystem conventions to define your API routes.
//!
//! ## Installation
//!
@ -11,14 +8,13 @@
//!
//! ```toml
//! [dependencies]
//! axum_folder_router = "0.3"
//! axum = "0.8"
//! axum_folder_router = "0.1.0"
//! axum = "0.7"
//! ```
//!
//! ## Basic Usage
//!
//! The macro scans a directory for ```route.rs``` files and automatically
//! creates an Axum router based on the file structure:
//! The macro scans a directory for ```route.rs``` files and automatically creates an Axum router based on the file structure:
//!
//! ```rust,no_run
#![doc = include_str!("../examples/simple/main.rs")]
@ -27,6 +23,7 @@
//! ## File Structure Convention
//!
//! The macro converts your file structure into routes:
//!
//! ```text
//! src/api/
//! ├── route.rs -> "/"
@ -46,6 +43,7 @@
//! ## Route Handlers
//!
//! Inside each ```route.rs``` file, define async functions named after HTTP methods:
//!
//! ```rust
#![doc = include_str!("../examples/simple/api/route.rs")]
//! ```
@ -54,7 +52,7 @@
//!
//! ### HTTP Methods
//!
//! The macro supports all standard HTTP methods as defined in RFC9110.
//! The macro supports all standard HTTP methods:
//! - ```get```
//! - ```post```
//! - ```put```
@ -62,20 +60,17 @@
//! - ```patch```
//! - ```head```
//! - ```options```
//! - ```trace```
//! - ```connect```
//!
//! And additionally
//! - ```any```, which maches all methods
//!
//! ### Path Parameters
//!
//! Dynamic path segments are defined using brackets:
//!
//! ```text
//! src/api/users/[id]/route.rs -> "/users/{id}"
//! ```
//!
//! Inside the route handler:
//!
//! ```rust
//! use axum::{
//! extract::Path,
@ -90,9 +85,11 @@
//! ### Catch-all Parameters
//!
//! Use the spread syntax for catch-all segments:
//!
//! ```text
//! src/api/files/[...path]/route.rs -> "/files/*path"
//! ```
//!
//! ```rust
//! use axum::{
//! extract::Path,
@ -107,7 +104,7 @@
//! ### State Extraction
//!
//! The state type provided to the macro is available in all route handlers:
//! All routes share the same state type, though you can use ```FromRef``` for more granular state extraction.
//!
//! ```rust
//! use axum::{
//! extract::State,
@ -125,98 +122,188 @@
//! ## Limitations
//!
//! - **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.
//! It is highly recommended to keep the route directory seperate from the rest of your module-tree.
use std::path::Path;
//! - **File I/O**: The macro performs file I/O during compilation, which may have implications in certain build environments.
//! - **Single State Type**: All routes share the same state type, though you can use ```FromRef``` for more granular state extraction.
//!
//! ## Best Practices
//!
//! 1. **Consistent Structure**: Maintain a consistent file structure to make your API organization predictable.
//! 2. **Individual Route Files**: Use one ```route.rs``` file per route path for clarity.
//! 3. **Module Organization**: Consider grouping related functionality in directories.
//! 4. **Documentation**: Add comments to each route handler explaining its purpose.
use proc_macro::TokenStream;
use quote::quote;
use syn::parse_macro_input;
use quote::{format_ident, quote};
use std::fs;
use std::path::{Path, PathBuf};
use syn::{parse::Parse, parse::ParseStream, parse_macro_input, Ident, LitStr, Result, Token};
mod generate;
mod parse;
struct FolderRouterArgs {
path: String,
state_type: Ident,
}
/// Creates an Axum router module tree & creation function
/// by scanning a directory for `route.rs` files.
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,
})
}
}
/// Creates an Axum router by scanning a directory for `route.rs` files.
///
/// # Parameters
///
/// * `path` - A string literal pointing to the route directory, relative to the
/// Cargo manifest directory
/// * `state_type` - The type name of your application state that will be shared
/// across all routes
#[allow(clippy::missing_panics_doc)]
#[proc_macro_attribute]
pub fn folder_router(attr: TokenStream, item: TokenStream) -> TokenStream {
let args = parse_macro_input!(attr as parse::FolderRouterArgs);
let input_item = parse_macro_input!(item as syn::ItemStruct);
let struct_name = &input_item.ident;
/// * `path` - A string literal pointing to the API directory, relative to the Cargo manifest directory
/// * `state_type` - The type name of your application state that will be shared across all routes
///
/// # Example
///
/// ```rust
/// # use axum_folder_router::folder_router;
/// # #[derive(Debug, Clone)]
/// # struct AppState ();
/// #
/// let router = folder_router!("./src/api", AppState);
/// ```
///
/// 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.
#[proc_macro]
pub fn folder_router(input: TokenStream) -> TokenStream {
let args = parse_macro_input!(input as FolderRouterArgs);
let base_path = args.path;
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 mod_namespace = format!(
"__folder_router__{}__{}",
struct_name
.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>()
);
// Collect route files
let mut routes = Vec::new();
collect_route_files(&base_dir, &base_dir, &mut routes);
let routes = parse::collect_route_files(&base_dir, &base_dir);
// Generate module definitions and route registrations
let mut module_defs = Vec::new();
let mut route_registrations = Vec::new();
if routes.is_empty() {
return TokenStream::from(quote! {
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."
));
for (route_path, rel_path) in routes {
// Generate module name and axum path
let (axum_path, mod_name) = path_to_route_info(&rel_path);
let mod_ident = format_ident!("{}", mod_name);
// Create module path for include!
let rel_file_path = route_path.strip_prefix(&manifest_dir).unwrap();
let rel_file_str = rel_file_path.to_string_lossy().to_string();
// Add module definition
module_defs.push(quote! {
#[allow(warnings)]
mod #mod_ident {
include!(concat!(env!("CARGO_MANIFEST_DIR"), "/", #rel_file_str));
}
});
// Read the file content to find HTTP methods
let file_content = fs::read_to_string(&route_path).unwrap_or_default();
let methods = ["get", "post", "put", "delete", "patch", "head", "options"];
let mut method_registrations = Vec::new();
for method in &methods {
if file_content.contains(&format!("pub async fn {}(", method)) {
let method_ident = format_ident!("{}", method);
method_registrations.push((method, method_ident));
}
}
let module_tree = generate::module_tree(&mod_namespace, &base_dir, &routes);
let route_registrations = generate::route_registrations(&mod_namespace, &routes);
if !method_registrations.is_empty() {
let (_first_method, first_method_ident) = &method_registrations[0];
quote! {
#module_tree
let mut builder = quote! {
axum::routing::#first_method_ident(#mod_ident::#first_method_ident)
};
#input_item
for (_method, method_ident) in &method_registrations[1..] {
builder = quote! {
#builder.#method_ident(#mod_ident::#method_ident)
};
}
impl #struct_name {
pub fn into_router() -> axum::Router<#state_type> {
let mut router = axum::Router::new();
#route_registrations
let registration = quote! {
router = router.route(#axum_path, #builder);
};
route_registrations.push(registration);
}
}
// Generate the final code
let expanded = quote! {
{
#(#module_defs)*
let mut router = axum::Router::<#state_type>::new();
#(#route_registrations)*
router
}
}
}
.into()
};
expanded.into()
}
// This is a workaround for macrotest behaviour
#[cfg(debug_assertions)]
fn get_manifest_dir() -> String {
use regex::Regex;
let dir = std::env::var("CARGO_MANIFEST_DIR").unwrap_or("./".to_string());
let re = Regex::new(r"^(.+)/target/tests/axum-folder-router/[A-Za-z0-9]{42}$").unwrap();
// Recursively collect route.rs files
fn collect_route_files(base_dir: &Path, current_dir: &Path, routes: &mut Vec<(PathBuf, PathBuf)>) {
if let Ok(entries) = fs::read_dir(current_dir) {
for entry in entries.filter_map(std::result::Result::ok) {
let path = entry.path();
if let Some(captures) = re.captures(&dir) {
captures.get(1).unwrap().as_str().to_string()
if path.is_dir() {
collect_route_files(base_dir, &path, routes);
} else if path.file_name().unwrap_or_default() == "route.rs" {
if let Some(parent) = path.parent() {
if let Ok(rel_dir) = parent.strip_prefix(base_dir) {
routes.push((path.clone(), rel_dir.to_path_buf()));
}
}
}
}
}
}
// Convert a relative path to (axum_path, mod_name)
fn path_to_route_info(rel_path: &Path) -> (String, String) {
if rel_path.components().count() == 0 {
return ("/".to_string(), "root".to_string());
}
let mut axum_path = String::new();
let mut mod_name = String::new();
for segment in rel_path.iter() {
let s = segment.to_str().unwrap_or_default();
if s.starts_with('[') && s.ends_with(']') {
let inner = &s[1..s.len() - 1];
if inner.starts_with("...") {
let param = &inner[3..];
axum_path.push_str(&format!("/*{}", param));
mod_name.push_str(&format!("__{}", param));
} else {
dir
axum_path.push_str(&format!("/{{{}}}", inner));
mod_name.push_str(&format!("__{}", inner));
}
} else {
axum_path.push('/');
axum_path.push_str(s);
mod_name.push_str("__");
mod_name.push_str(s);
}
}
#[cfg(not(debug_assertions))]
fn get_manifest_dir() -> String {
std::env::var("CARGO_MANIFEST_DIR").unwrap_or("./".to_string())
(axum_path, mod_name.trim_start_matches('_').to_string())
}

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)