Compare commits
6 Commits
57dd1da621
...
25a9005375
| Author | SHA1 | Date | |
|---|---|---|---|
|
25a9005375
|
|||
|
386eb59f84
|
|||
|
513cbc19a3
|
|||
|
995f3cc61a
|
|||
|
a5b42ab55a
|
|||
|
65007f6cd2
|
145
Cargo.lock
generated
145
Cargo.lock
generated
@@ -55,6 +55,8 @@ dependencies = [
|
|||||||
"axum",
|
"axum",
|
||||||
"axum-app-wrapper-macros",
|
"axum-app-wrapper-macros",
|
||||||
"futures",
|
"futures",
|
||||||
|
"tokio",
|
||||||
|
"tracing",
|
||||||
"type-map",
|
"type-map",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -92,6 +94,16 @@ version = "1.11.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3"
|
checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "errno"
|
||||||
|
version = "0.3.14"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
"windows-sys",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "fnv"
|
name = "fnv"
|
||||||
version = "1.0.7"
|
version = "1.0.7"
|
||||||
@@ -287,9 +299,9 @@ checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "libc"
|
name = "libc"
|
||||||
version = "0.2.177"
|
version = "0.2.186"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976"
|
checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "log"
|
name = "log"
|
||||||
@@ -317,13 +329,13 @@ checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "mio"
|
name = "mio"
|
||||||
version = "1.1.0"
|
version = "1.2.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "69d83b0086dc8ecf3ce9ae2874b2d1290252e2a30720bea58a5c6639b0092873"
|
checksum = "50b7e5b27aa02a74bac8c3f23f448f8d87ff11f92d3aac1a6ed369ee08cc56c1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"libc",
|
"libc",
|
||||||
"wasi",
|
"wasi",
|
||||||
"windows-sys 0.61.2",
|
"windows-sys",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -445,6 +457,16 @@ dependencies = [
|
|||||||
"serde",
|
"serde",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "signal-hook-registry"
|
||||||
|
version = "1.4.8"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b"
|
||||||
|
dependencies = [
|
||||||
|
"errno",
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "slab"
|
name = "slab"
|
||||||
version = "0.4.11"
|
version = "0.4.11"
|
||||||
@@ -459,12 +481,12 @@ checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "socket2"
|
name = "socket2"
|
||||||
version = "0.6.1"
|
version = "0.6.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "17129e116933cf371d018bb80ae557e889637989d8638274fb25622827b03881"
|
checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"libc",
|
"libc",
|
||||||
"windows-sys 0.60.2",
|
"windows-sys",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -486,23 +508,24 @@ checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tokio"
|
name = "tokio"
|
||||||
version = "1.48.0"
|
version = "1.52.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ff360e02eab121e0bc37a2d3b4d4dc622e6eda3a8e5253d5435ecf5bd4c68408"
|
checksum = "8fc7f01b389ac15039e4dc9531aa973a135d7a4135281b12d7c1bc79fd57fffe"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"libc",
|
"libc",
|
||||||
"mio",
|
"mio",
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
|
"signal-hook-registry",
|
||||||
"socket2",
|
"socket2",
|
||||||
"tokio-macros",
|
"tokio-macros",
|
||||||
"windows-sys 0.61.2",
|
"windows-sys",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tokio-macros"
|
name = "tokio-macros"
|
||||||
version = "2.6.0"
|
version = "2.7.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5"
|
checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
@@ -539,20 +562,32 @@ checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tracing"
|
name = "tracing"
|
||||||
version = "0.1.41"
|
version = "0.1.44"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0"
|
checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"log",
|
"log",
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
|
"tracing-attributes",
|
||||||
"tracing-core",
|
"tracing-core",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tracing-core"
|
name = "tracing-attributes"
|
||||||
version = "0.1.34"
|
version = "0.1.31"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678"
|
checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tracing-core"
|
||||||
|
version = "0.1.36"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"once_cell",
|
"once_cell",
|
||||||
]
|
]
|
||||||
@@ -584,15 +619,6 @@ version = "0.2.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
|
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "windows-sys"
|
|
||||||
version = "0.60.2"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb"
|
|
||||||
dependencies = [
|
|
||||||
"windows-targets",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows-sys"
|
name = "windows-sys"
|
||||||
version = "0.61.2"
|
version = "0.61.2"
|
||||||
@@ -601,68 +627,3 @@ checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"windows-link",
|
"windows-link",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "windows-targets"
|
|
||||||
version = "0.53.5"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3"
|
|
||||||
dependencies = [
|
|
||||||
"windows-link",
|
|
||||||
"windows_aarch64_gnullvm",
|
|
||||||
"windows_aarch64_msvc",
|
|
||||||
"windows_i686_gnu",
|
|
||||||
"windows_i686_gnullvm",
|
|
||||||
"windows_i686_msvc",
|
|
||||||
"windows_x86_64_gnu",
|
|
||||||
"windows_x86_64_gnullvm",
|
|
||||||
"windows_x86_64_msvc",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "windows_aarch64_gnullvm"
|
|
||||||
version = "0.53.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "windows_aarch64_msvc"
|
|
||||||
version = "0.53.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "windows_i686_gnu"
|
|
||||||
version = "0.53.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "windows_i686_gnullvm"
|
|
||||||
version = "0.53.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "windows_i686_msvc"
|
|
||||||
version = "0.53.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "windows_x86_64_gnu"
|
|
||||||
version = "0.53.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "windows_x86_64_gnullvm"
|
|
||||||
version = "0.53.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "windows_x86_64_msvc"
|
|
||||||
version = "0.53.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650"
|
|
||||||
|
|||||||
20
Cargo.toml
20
Cargo.toml
@@ -1,15 +1,23 @@
|
|||||||
[workspace]
|
|
||||||
members = [".", "crates/*"]
|
|
||||||
resolver = "2"
|
|
||||||
|
|
||||||
[package]
|
[package]
|
||||||
name = "axum-app-wrapper"
|
name = "axum-app-wrapper"
|
||||||
version = "0.1.1"
|
version = "0.1.2"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
|
|
||||||
|
[workspace]
|
||||||
|
resolver = "2"
|
||||||
|
members = [".", "crates/*"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
anyhow = "1.0"
|
anyhow = "1.0"
|
||||||
axum = "0.8"
|
axum = "0.8"
|
||||||
|
axum-app-wrapper-macros = { path = "crates/axum-app-wrapper-macros" }
|
||||||
futures = "0.3"
|
futures = "0.3"
|
||||||
type-map = "0.5"
|
type-map = "0.5"
|
||||||
axum-app-wrapper-macros = { path = "crates/axum-app-wrapper-macros" }
|
|
||||||
|
[dev-dependencies]
|
||||||
|
tokio = {
|
||||||
|
version = "1.52.3",
|
||||||
|
default-features = false,
|
||||||
|
features = ["net", "rt", "rt-multi-thread", "signal"]
|
||||||
|
}
|
||||||
|
tracing = "0.1.44"
|
||||||
|
|||||||
69
README.md
Normal file
69
README.md
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
# axum-app-wrapper
|
||||||
|
|
||||||
|
A small plugin layer for `axum` applications, inspired by [fastify](https://fastify.dev/) from the Node/JS ecosystem.
|
||||||
|
|
||||||
|
Plugins can:
|
||||||
|
|
||||||
|
- add startup state before the final axum state type exists
|
||||||
|
- add routes, services, and middleware after state is initialized
|
||||||
|
- run graceful shutdown work in reverse registration order
|
||||||
|
|
||||||
|
This crate is intentionally thin. It does not replace axum's router, extractors, or middleware. Rather, it organizes your server setup and teardown into plugins.
|
||||||
|
|
||||||
|
## Lifecycle
|
||||||
|
|
||||||
|
`App::init()` runs plugins in this order:
|
||||||
|
|
||||||
|
1. `on_init` in registration order, passing a `TypeMap` to build state.
|
||||||
|
2. `S::try_from(TypeMap)` to build the final typed app state.
|
||||||
|
3. `on_setup` in registration order, passing `Router<S>` and `&S`.
|
||||||
|
4. `on_shutdown` consecutively in reverse registration order: the last registered plugin shuts down first, and each hook finishes before the next one starts.
|
||||||
|
|
||||||
|
|
||||||
|
## Example
|
||||||
|
See the `examples` folder for full examples.
|
||||||
|
|
||||||
|
|
||||||
|
For `on_setup` closures that access typed state, construct the plugin as
|
||||||
|
`AdHocPlugin::<AppState>::new()`. That gives Rust enough context to infer the `state` parameter:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
let plugin = AdHocPlugin::<AppState>::new()
|
||||||
|
.on_setup(|router, state| {
|
||||||
|
let config = Arc::clone(&state.config);
|
||||||
|
Ok(router.layer(axum::Extension(config)))
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## Reusable Plugins
|
||||||
|
|
||||||
|
Implement `AppPlugin<S>` directly when setup should be reusable across apps:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
use axum::Router;
|
||||||
|
use axum_app_wrapper::{AppPlugin, TypeMap};
|
||||||
|
use futures::{future::BoxFuture, FutureExt};
|
||||||
|
|
||||||
|
struct ConfigPlugin {
|
||||||
|
config: Config,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AppPlugin<AppState> for ConfigPlugin {
|
||||||
|
fn on_init(&mut self, mut state: TypeMap) -> BoxFuture<'static, anyhow::Result<TypeMap>> {
|
||||||
|
let config = self.config.clone();
|
||||||
|
async move {
|
||||||
|
state.insert(config);
|
||||||
|
Ok(state)
|
||||||
|
}
|
||||||
|
.boxed()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_setup(
|
||||||
|
&mut self,
|
||||||
|
router: Router<AppState>,
|
||||||
|
_state: &AppState,
|
||||||
|
) -> anyhow::Result<Router<AppState>> {
|
||||||
|
Ok(router.route("/health", axum::routing::get(health)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
92
examples/app.rs
Normal file
92
examples/app.rs
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
use std::{net::SocketAddr, sync::Arc};
|
||||||
|
|
||||||
|
use axum::{Extension, extract::State, routing::get};
|
||||||
|
use axum_app_wrapper::{AdHocPlugin, App, AppState};
|
||||||
|
|
||||||
|
#[derive(Clone, AppState)]
|
||||||
|
struct AppState {
|
||||||
|
config: Arc<Config>,
|
||||||
|
metrics: Arc<Metrics>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
struct Config {
|
||||||
|
service_name: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Metrics;
|
||||||
|
|
||||||
|
impl Metrics {
|
||||||
|
fn new() -> Self {
|
||||||
|
Self
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn flush(&self) {
|
||||||
|
tracing::info!("flushed metrics");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn health(State(state): State<AppState>) -> String {
|
||||||
|
format!("{}:ok", state.config.service_name)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn metrics_handler(Extension(metrics): Extension<Arc<Metrics>>) -> &'static str {
|
||||||
|
let _metrics = metrics;
|
||||||
|
"metrics:ok"
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() -> anyhow::Result<()> {
|
||||||
|
let config = Arc::new(Config {
|
||||||
|
service_name: "api".to_owned(),
|
||||||
|
});
|
||||||
|
let metrics_registry = Arc::new(Metrics::new());
|
||||||
|
|
||||||
|
let config_plugin = AdHocPlugin::<AppState>::new()
|
||||||
|
.on_init(async move |mut state| {
|
||||||
|
state.insert(config);
|
||||||
|
Ok(state)
|
||||||
|
})
|
||||||
|
.on_setup(|router, state| {
|
||||||
|
tracing::info!(service = %state.config.service_name, "configuring routes");
|
||||||
|
Ok(router.route("/health", get(health)))
|
||||||
|
});
|
||||||
|
|
||||||
|
let metrics_plugin = AdHocPlugin::<AppState>::new()
|
||||||
|
.on_init(async move |mut state| {
|
||||||
|
state.insert(metrics_registry);
|
||||||
|
Ok(state)
|
||||||
|
})
|
||||||
|
.on_setup(|router, state| {
|
||||||
|
Ok(router
|
||||||
|
.route("/metrics", get(metrics_handler))
|
||||||
|
.layer(Extension(Arc::clone(&state.metrics))))
|
||||||
|
})
|
||||||
|
.on_shutdown(|state| {
|
||||||
|
let metrics = Arc::clone(&state.metrics);
|
||||||
|
async move {
|
||||||
|
metrics.flush().await;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let app = App::<AppState>::new()
|
||||||
|
.register(config_plugin)
|
||||||
|
.register(metrics_plugin);
|
||||||
|
|
||||||
|
let (router, state, on_shutdown) = app.init().await?;
|
||||||
|
let router = router.with_state(state);
|
||||||
|
|
||||||
|
let addr: SocketAddr = "127.0.0.1:3000".parse()?;
|
||||||
|
let listener = tokio::net::TcpListener::bind(addr).await?;
|
||||||
|
|
||||||
|
axum::serve(listener, router)
|
||||||
|
.with_graceful_shutdown(async {
|
||||||
|
tokio::signal::ctrl_c()
|
||||||
|
.await
|
||||||
|
.expect("failed to listen for ctrl-c");
|
||||||
|
on_shutdown.await;
|
||||||
|
})
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
227
src/lib.rs
227
src/lib.rs
@@ -1,13 +1,37 @@
|
|||||||
#![allow(clippy::type_complexity)]
|
#![allow(clippy::type_complexity)]
|
||||||
|
//! A small plugin layer for [`axum`](https://docs.rs/axum/latest/axum/) applications.
|
||||||
|
//!
|
||||||
|
//! `axum-app-wrapper` lets plugins contribute startup state, router setup, and shutdown work
|
||||||
|
//! around an `axum::Router`.
|
||||||
|
//!
|
||||||
|
//! Lifecycle:
|
||||||
|
//!
|
||||||
|
//! 1. [`AppPlugin::on_init`] runs in registration order and builds a [`TypeMap`].
|
||||||
|
//! 2. The `TypeMap` is converted into the app state `S` with `TryFrom<TypeMap>`.
|
||||||
|
//! 3. [`AppPlugin::on_setup`] runs in registration order and receives `Router<S>` plus `&S`.
|
||||||
|
//! 4. The returned shutdown future runs [`AppPlugin::on_shutdown`] consecutively in reverse
|
||||||
|
//! registration order.
|
||||||
|
//!
|
||||||
|
//! For one-off plugins, use [`AdHocPlugin`]. If the setup closure needs access to typed state,
|
||||||
|
//! spell the state type at construction time so Rust can infer the closure parameters:
|
||||||
|
//!
|
||||||
|
//! ```ignore
|
||||||
|
//! let plugin = AdHocPlugin::<AppState>::new()
|
||||||
|
//! .on_init(async |mut state| {
|
||||||
|
//! state.insert(config);
|
||||||
|
//! Ok(state)
|
||||||
|
//! })
|
||||||
|
//! .on_setup(|router, state| {
|
||||||
|
//! let config = state.config.clone();
|
||||||
|
//! Ok(router.layer(axum::Extension(config)))
|
||||||
|
//! });
|
||||||
|
//! ```
|
||||||
|
|
||||||
use std::{fmt::Display, sync::Arc};
|
use std::{fmt::Display, sync::Arc};
|
||||||
|
|
||||||
use anyhow::anyhow;
|
use anyhow::anyhow;
|
||||||
use axum::Router;
|
use axum::Router;
|
||||||
use futures::{
|
use futures::{FutureExt, future::BoxFuture};
|
||||||
FutureExt,
|
|
||||||
future::{BoxFuture, join_all},
|
|
||||||
};
|
|
||||||
|
|
||||||
// State extraction utilities
|
// State extraction utilities
|
||||||
|
|
||||||
@@ -25,7 +49,7 @@ pub fn extract_type_field<T: Send + Sync + 'static>(map: &mut TypeMap) -> anyhow
|
|||||||
|
|
||||||
// Plugin system
|
// Plugin system
|
||||||
|
|
||||||
/// A lightweight wrapper around axum that enables building plugins around the router
|
/// An axum app builder with plugin-managed state, router setup, and shutdown hooks.
|
||||||
pub struct App<S = Arc<TypeMap>> {
|
pub struct App<S = Arc<TypeMap>> {
|
||||||
base_router: Router<S>,
|
base_router: Router<S>,
|
||||||
plugins: Vec<Box<dyn AppPlugin<S>>>,
|
plugins: Vec<Box<dyn AppPlugin<S>>>,
|
||||||
@@ -36,7 +60,7 @@ impl<S> App<S>
|
|||||||
where
|
where
|
||||||
S: TryFrom<TypeMap> + Clone + Send + Sync + 'static,
|
S: TryFrom<TypeMap> + Clone + Send + Sync + 'static,
|
||||||
{
|
{
|
||||||
/// Create a new server with the given routes.
|
/// Create an empty app.
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
Self {
|
Self {
|
||||||
base_router: Router::new(),
|
base_router: Router::new(),
|
||||||
@@ -45,8 +69,10 @@ where
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Register a plugin. This can be considered analogous to axum/tower's `layer` function
|
/// Register a plugin.
|
||||||
/// and follows the same ordering (each plugin "wraps" the previous one).
|
///
|
||||||
|
/// `on_init` and `on_setup` run in registration order. Shutdown runs in reverse registration
|
||||||
|
/// order, awaiting each hook before starting the next.
|
||||||
pub fn register(mut self, plugin: impl AppPlugin<S> + 'static) -> Self {
|
pub fn register(mut self, plugin: impl AppPlugin<S> + 'static) -> Self {
|
||||||
self.plugins.push(Box::new(plugin));
|
self.plugins.push(Box::new(plugin));
|
||||||
self
|
self
|
||||||
@@ -68,8 +94,9 @@ where
|
|||||||
// self
|
// self
|
||||||
// }
|
// }
|
||||||
|
|
||||||
/// Build and initialize the server. Returns the base router, finalized state, and a future to run
|
/// Build the router, finalized state, and graceful shutdown future.
|
||||||
/// on graceful shutdown.
|
///
|
||||||
|
/// The returned shutdown future must be awaited by the caller when the server is shutting down.
|
||||||
pub async fn init(mut self) -> anyhow::Result<(Router<S>, S, impl Future + Send)>
|
pub async fn init(mut self) -> anyhow::Result<(Router<S>, S, impl Future + Send)>
|
||||||
where
|
where
|
||||||
S::Error: Display,
|
S::Error: Display,
|
||||||
@@ -86,44 +113,59 @@ where
|
|||||||
router = plugin.on_setup(router, &state)?;
|
router = plugin.on_setup(router, &state)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
let shutdown_fns = self
|
let shutdown_fns: Vec<_> = self
|
||||||
.plugins
|
.plugins
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.filter_map(|mut p| p.on_shutdown(&state));
|
.rev()
|
||||||
let on_shutdown = join_all(shutdown_fns);
|
.filter_map(|mut p| p.on_shutdown(&state))
|
||||||
|
.collect();
|
||||||
|
let on_shutdown = async move {
|
||||||
|
for shutdown_fn in shutdown_fns {
|
||||||
|
shutdown_fn.await;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
Ok((router, state, on_shutdown))
|
Ok((router, state, on_shutdown))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Trait for a plugin that can be attached to the server
|
/// A plugin that can participate in app initialization, router setup, and shutdown.
|
||||||
#[allow(unused_variables, reason = "trait functions with default no-op")]
|
#[allow(unused_variables, reason = "trait functions with default no-op")]
|
||||||
pub trait AppPlugin<S = Arc<TypeMap>> {
|
pub trait AppPlugin<S = Arc<TypeMap>> {
|
||||||
/// Init function that will run on server startup. Can add and manipulate state.
|
/// Run during startup before typed state exists.
|
||||||
|
///
|
||||||
|
/// Use this hook to insert or transform values in the shared [`TypeMap`].
|
||||||
fn on_init(&mut self, app_state: TypeMap) -> BoxFuture<'static, anyhow::Result<TypeMap>> {
|
fn on_init(&mut self, app_state: TypeMap) -> BoxFuture<'static, anyhow::Result<TypeMap>> {
|
||||||
async { Ok(app_state) }.boxed()
|
async { Ok(app_state) }.boxed()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Setup function that will run _after_ state is initialized (e.g. routes, middleware,
|
/// Run after typed state has been created.
|
||||||
/// and services should be added here)
|
///
|
||||||
|
/// Use this hook to add routes, services, middleware, or router state.
|
||||||
fn on_setup(&mut self, router: Router<S>, state: &S) -> anyhow::Result<Router<S>> {
|
fn on_setup(&mut self, router: Router<S>, state: &S) -> anyhow::Result<Router<S>> {
|
||||||
Ok(router)
|
Ok(router)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Teardown function that will run on server shutdown
|
/// Return shutdown work for this plugin.
|
||||||
|
///
|
||||||
|
/// Shutdown hooks are awaited consecutively in reverse registration order.
|
||||||
fn on_shutdown(&mut self, state: &S) -> Option<BoxFuture<'static, ()>> {
|
fn on_shutdown(&mut self, state: &S) -> Option<BoxFuture<'static, ()>> {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Utility to build a plugin on the fly
|
/// A closure-based plugin for application-local setup.
|
||||||
|
///
|
||||||
|
/// `on_init` and `on_setup` accept capturing closures. If `on_setup` uses typed state, prefer
|
||||||
|
/// `AdHocPlugin::<State>::new()` so the closure parameter type can be inferred.
|
||||||
pub struct AdHocPlugin<S = Arc<TypeMap>> {
|
pub struct AdHocPlugin<S = Arc<TypeMap>> {
|
||||||
on_init: Option<Box<dyn FnOnce(TypeMap) -> BoxFuture<'static, anyhow::Result<TypeMap>> + Send>>,
|
on_init: Option<Box<dyn FnOnce(TypeMap) -> BoxFuture<'static, anyhow::Result<TypeMap>> + Send>>,
|
||||||
on_setup: Option<fn(router: Router<S>, state: &S) -> anyhow::Result<Router<S>>>,
|
on_setup: Option<Box<dyn FnOnce(Router<S>, &S) -> anyhow::Result<Router<S>> + Send>>,
|
||||||
on_shutdown: Option<Box<dyn FnOnce(&S) -> BoxFuture<'static, ()>>>,
|
on_shutdown: Option<Box<dyn FnOnce(&S) -> BoxFuture<'static, ()>>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<S: 'static> AdHocPlugin<S> {
|
impl<S: 'static> AdHocPlugin<S> {
|
||||||
|
/// Create an empty ad-hoc plugin.
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
Self {
|
Self {
|
||||||
on_init: None,
|
on_init: None,
|
||||||
@@ -132,26 +174,26 @@ impl<S: 'static> AdHocPlugin<S> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Init function that will run on server startup. Can add and manipulate state.
|
/// Set startup state initialization for this plugin.
|
||||||
pub fn on_init<T>(mut self, on_init: fn(app_state: TypeMap) -> T) -> Self
|
pub fn on_init<F, T>(mut self, on_init: F) -> Self
|
||||||
where
|
where
|
||||||
|
F: FnOnce(TypeMap) -> T + Send + 'static,
|
||||||
T: Future<Output = anyhow::Result<TypeMap>> + Send + 'static,
|
T: Future<Output = anyhow::Result<TypeMap>> + Send + 'static,
|
||||||
{
|
{
|
||||||
self.on_init = Some(Box::new(move |s| Box::pin(on_init(s))));
|
self.on_init = Some(Box::new(move |s| Box::pin(on_init(s))));
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Setup function that will run _after_ state is initialized. (e.g. routes, middleware,
|
/// Set router setup for this plugin.
|
||||||
/// and services should be added here)
|
pub fn on_setup<F>(mut self, on_setup: F) -> Self
|
||||||
pub fn on_setup(
|
where
|
||||||
mut self,
|
F: FnOnce(Router<S>, &S) -> anyhow::Result<Router<S>> + Send + 'static,
|
||||||
on_setup: fn(router: Router<S>, state: &S) -> anyhow::Result<Router<S>>,
|
{
|
||||||
) -> Self {
|
self.on_setup = Some(Box::new(on_setup));
|
||||||
self.on_setup = Some(on_setup);
|
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Teardown function that will run on server shutdown
|
/// Set shutdown work for this plugin.
|
||||||
pub fn on_shutdown<T>(mut self, on_shutdown: fn(state: &S) -> T) -> Self
|
pub fn on_shutdown<T>(mut self, on_shutdown: fn(state: &S) -> T) -> Self
|
||||||
where
|
where
|
||||||
T: Future<Output = ()> + Send + 'static,
|
T: Future<Output = ()> + Send + 'static,
|
||||||
@@ -170,7 +212,7 @@ impl<S> AppPlugin<S> for AdHocPlugin<S> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn on_setup(&mut self, router: Router<S>, state: &S) -> anyhow::Result<Router<S>> {
|
fn on_setup(&mut self, router: Router<S>, state: &S) -> anyhow::Result<Router<S>> {
|
||||||
match self.on_setup {
|
match self.on_setup.take() {
|
||||||
Some(setup_fn) => setup_fn(router, state),
|
Some(setup_fn) => setup_fn(router, state),
|
||||||
None => Ok(router),
|
None => Ok(router),
|
||||||
}
|
}
|
||||||
@@ -183,3 +225,126 @@ impl<S> AppPlugin<S> for AdHocPlugin<S> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use std::{
|
||||||
|
convert::Infallible,
|
||||||
|
sync::{
|
||||||
|
Mutex,
|
||||||
|
atomic::{AtomicUsize, Ordering},
|
||||||
|
},
|
||||||
|
task::Poll,
|
||||||
|
};
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
struct TestState {
|
||||||
|
value: Arc<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryFrom<TypeMap> for TestState {
|
||||||
|
type Error = Infallible;
|
||||||
|
|
||||||
|
fn try_from(mut map: TypeMap) -> Result<Self, Self::Error> {
|
||||||
|
Ok(Self {
|
||||||
|
value: map.remove::<Arc<String>>().expect("missing state value"),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn adhoc_plugin_with_basic_state() {
|
||||||
|
let init_value = Arc::new(String::from("ready"));
|
||||||
|
let setup_value = Arc::clone(&init_value);
|
||||||
|
|
||||||
|
let plugin = AdHocPlugin::<TestState>::new()
|
||||||
|
.on_init(async |mut state| {
|
||||||
|
state.insert(init_value);
|
||||||
|
Ok(state)
|
||||||
|
})
|
||||||
|
.on_setup(move |router, state| {
|
||||||
|
assert_eq!(state.value.as_str(), setup_value.as_str());
|
||||||
|
Ok(router)
|
||||||
|
});
|
||||||
|
let app = App::<TestState>::new().register(plugin);
|
||||||
|
|
||||||
|
let (_router, state, _shutdown) =
|
||||||
|
futures::executor::block_on(app.init()).expect("app should initialize");
|
||||||
|
|
||||||
|
assert_eq!(state.value.as_str(), "ready");
|
||||||
|
}
|
||||||
|
|
||||||
|
struct ShutdownOrderPlugin {
|
||||||
|
name: &'static str,
|
||||||
|
events: Arc<Mutex<Vec<String>>>,
|
||||||
|
active_shutdowns: Arc<AtomicUsize>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AppPlugin<TestState> for ShutdownOrderPlugin {
|
||||||
|
fn on_shutdown(&mut self, _state: &TestState) -> Option<BoxFuture<'static, ()>> {
|
||||||
|
let name = self.name;
|
||||||
|
let events = Arc::clone(&self.events);
|
||||||
|
let active_shutdowns = Arc::clone(&self.active_shutdowns);
|
||||||
|
let mut yielded = false;
|
||||||
|
|
||||||
|
Some(Box::pin(futures::future::poll_fn(move |cx| {
|
||||||
|
if !yielded {
|
||||||
|
yielded = true;
|
||||||
|
let previously_active = active_shutdowns.fetch_add(1, Ordering::SeqCst);
|
||||||
|
assert_eq!(previously_active, 0, "shutdown hooks ran concurrently");
|
||||||
|
events
|
||||||
|
.lock()
|
||||||
|
.expect("events lock poisoned")
|
||||||
|
.push(format!("{name}:start"));
|
||||||
|
cx.waker().wake_by_ref();
|
||||||
|
return Poll::Pending;
|
||||||
|
}
|
||||||
|
|
||||||
|
events
|
||||||
|
.lock()
|
||||||
|
.expect("events lock poisoned")
|
||||||
|
.push(format!("{name}:finish"));
|
||||||
|
active_shutdowns.fetch_sub(1, Ordering::SeqCst);
|
||||||
|
Poll::Ready(())
|
||||||
|
})))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn shutdown_hooks_order() {
|
||||||
|
let events = Arc::new(Mutex::new(Vec::new()));
|
||||||
|
let active_shutdowns = Arc::new(AtomicUsize::new(0));
|
||||||
|
|
||||||
|
let app = App::<TestState>::new()
|
||||||
|
.register(AdHocPlugin::<TestState>::new().on_init(async |mut state| {
|
||||||
|
state.insert(Arc::new(String::from("ready")));
|
||||||
|
Ok(state)
|
||||||
|
}))
|
||||||
|
.register(ShutdownOrderPlugin {
|
||||||
|
name: "first",
|
||||||
|
events: Arc::clone(&events),
|
||||||
|
active_shutdowns: Arc::clone(&active_shutdowns),
|
||||||
|
})
|
||||||
|
.register(ShutdownOrderPlugin {
|
||||||
|
name: "second",
|
||||||
|
events: Arc::clone(&events),
|
||||||
|
active_shutdowns,
|
||||||
|
});
|
||||||
|
|
||||||
|
let (_router, _state, on_shutdown) =
|
||||||
|
futures::executor::block_on(app.init()).expect("app should initialize");
|
||||||
|
futures::executor::block_on(on_shutdown);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
*events.lock().expect("events lock poisoned"),
|
||||||
|
[
|
||||||
|
"second:start",
|
||||||
|
"second:finish",
|
||||||
|
"first:start",
|
||||||
|
"first:finish"
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user