Compare commits

...

7 Commits

Author SHA1 Message Date
b715796c56 cargo update
All checks were successful
CI / build (push) Successful in 38s
2026-05-27 01:46:03 -04:00
1ce2c25431 update examples 2026-05-27 01:43:59 -04:00
bcb5c31c31 add plugin naming 2026-05-27 01:39:42 -04:00
1d6708e23b API improvements
- Added public `Error`, `Result`, `InitFuture`, and `ShutdownFuture`
  aliases.
- Added `TypeMapState` as the default state instead of `Arc<TypeMap>`.
- Added `App::route`, `App::mount`, and `App::store`.
- Changed `App::init()` to return `InitializedApp<S>`.
- Added `InitializedApp::router()`, `state()`, `into_parts()`, and
  `shutdown()`.
- Made shutdown hooks fallible: `Result<()>`.
- Made `AdHocPlugin::on_shutdown` accept capturing closures.
- Added `Default` for `App` and `AdHocPlugin`.

Improved AppState macro:
- Uses `axum_app_wrapper::Error`.
- Supports generic state structs.
- Fixed the stale `Arc<AppState>` doc snippet.
2026-05-27 01:04:50 -04:00
958060e538 Update ci.yml
All checks were successful
CI / build (push) Successful in 18s
2026-05-26 03:22:30 -04:00
bda85091af ci: enforce frozen lockfile
Some checks failed
CI / build (push) Failing after 17s
2026-05-26 03:19:14 -04:00
e2d51e9e91 ci: update rust version
All checks were successful
CI / build (push) Successful in 42s
2026-05-26 03:12:02 -04:00
7 changed files with 368 additions and 180 deletions

View File

@@ -3,7 +3,7 @@ on:
push: push:
env: env:
RUST_VERSION: "1.90" RUST_VERSION: "1.94"
jobs: jobs:
build: build:
@@ -20,7 +20,7 @@ jobs:
toolchain: ${{ env.RUST_VERSION }} toolchain: ${{ env.RUST_VERSION }}
- name: cargo check - name: cargo check
run: cargo check --examples run: cargo check --examples --locked
- name: cargo build - name: cargo build
run: cargo build --examples run: cargo build --examples --locked

144
Cargo.lock generated
View File

@@ -4,9 +4,9 @@ version = 4
[[package]] [[package]]
name = "anyhow" name = "anyhow"
version = "1.0.100" version = "1.0.102"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c"
[[package]] [[package]]
name = "atomic-waker" name = "atomic-waker"
@@ -16,9 +16,9 @@ checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0"
[[package]] [[package]]
name = "axum" name = "axum"
version = "0.8.6" version = "0.8.9"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a18ed336352031311f4e0b4dd2ff392d4fbb370777c9d18d7fc9d7359f73871" checksum = "31b698c5f9a010f6573133b09e0de5408834d0c82f8d7475a89fc1867a71cd90"
dependencies = [ dependencies = [
"axum-core", "axum-core",
"bytes", "bytes",
@@ -71,9 +71,9 @@ dependencies = [
[[package]] [[package]]
name = "axum-core" name = "axum-core"
version = "0.5.5" version = "0.5.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "59446ce19cd142f8833f856eb31f3eb097812d1479ab224f54d72428ca21ea22" checksum = "08c78f31d7b1291f7ee735c1c6780ccde7785daae9a9206026862dab7d8792d1"
dependencies = [ dependencies = [
"bytes", "bytes",
"futures-core", "futures-core",
@@ -90,9 +90,9 @@ dependencies = [
[[package]] [[package]]
name = "bytes" name = "bytes"
version = "1.11.0" version = "1.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3" checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33"
[[package]] [[package]]
name = "errno" name = "errno"
@@ -104,12 +104,6 @@ dependencies = [
"windows-sys", "windows-sys",
] ]
[[package]]
name = "fnv"
version = "1.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
[[package]] [[package]]
name = "form_urlencoded" name = "form_urlencoded"
version = "1.2.2" version = "1.2.2"
@@ -121,9 +115,9 @@ dependencies = [
[[package]] [[package]]
name = "futures" name = "futures"
version = "0.3.31" version = "0.3.32"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d"
dependencies = [ dependencies = [
"futures-channel", "futures-channel",
"futures-core", "futures-core",
@@ -136,9 +130,9 @@ dependencies = [
[[package]] [[package]]
name = "futures-channel" name = "futures-channel"
version = "0.3.31" version = "0.3.32"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d"
dependencies = [ dependencies = [
"futures-core", "futures-core",
"futures-sink", "futures-sink",
@@ -146,15 +140,15 @@ dependencies = [
[[package]] [[package]]
name = "futures-core" name = "futures-core"
version = "0.3.31" version = "0.3.32"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d"
[[package]] [[package]]
name = "futures-executor" name = "futures-executor"
version = "0.3.31" version = "0.3.32"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d"
dependencies = [ dependencies = [
"futures-core", "futures-core",
"futures-task", "futures-task",
@@ -163,15 +157,15 @@ dependencies = [
[[package]] [[package]]
name = "futures-io" name = "futures-io"
version = "0.3.31" version = "0.3.32"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718"
[[package]] [[package]]
name = "futures-macro" name = "futures-macro"
version = "0.3.31" version = "0.3.32"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@@ -180,21 +174,21 @@ dependencies = [
[[package]] [[package]]
name = "futures-sink" name = "futures-sink"
version = "0.3.31" version = "0.3.32"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893"
[[package]] [[package]]
name = "futures-task" name = "futures-task"
version = "0.3.31" version = "0.3.32"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393"
[[package]] [[package]]
name = "futures-util" name = "futures-util"
version = "0.3.31" version = "0.3.32"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6"
dependencies = [ dependencies = [
"futures-channel", "futures-channel",
"futures-core", "futures-core",
@@ -204,18 +198,16 @@ dependencies = [
"futures-task", "futures-task",
"memchr", "memchr",
"pin-project-lite", "pin-project-lite",
"pin-utils",
"slab", "slab",
] ]
[[package]] [[package]]
name = "http" name = "http"
version = "1.3.1" version = "1.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565" checksum = "8be7462df143984c4598a256ef469b251d7d7f9e271135073e78fc535414f3d0"
dependencies = [ dependencies = [
"bytes", "bytes",
"fnv",
"itoa", "itoa",
] ]
@@ -256,9 +248,9 @@ checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"
[[package]] [[package]]
name = "hyper" name = "hyper"
version = "1.8.1" version = "1.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2ab2d4f250c3d7b1c9fcdff1cece94ea4e2dfbec68614f7b87cb205f24ca9d11" checksum = "6299f016b246a94207e63da54dbe807655bf9e00044f73ded42c3ac5305fbcca"
dependencies = [ dependencies = [
"atomic-waker", "atomic-waker",
"bytes", "bytes",
@@ -270,19 +262,17 @@ dependencies = [
"httpdate", "httpdate",
"itoa", "itoa",
"pin-project-lite", "pin-project-lite",
"pin-utils",
"smallvec", "smallvec",
"tokio", "tokio",
] ]
[[package]] [[package]]
name = "hyper-util" name = "hyper-util"
version = "0.1.18" version = "0.1.20"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "52e9a2a24dc5c6821e71a7030e1e14b7b632acac55c40e9d2e082c621261bb56" checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0"
dependencies = [ dependencies = [
"bytes", "bytes",
"futures-core",
"http", "http",
"http-body", "http-body",
"hyper", "hyper",
@@ -293,9 +283,9 @@ dependencies = [
[[package]] [[package]]
name = "itoa" name = "itoa"
version = "1.0.15" version = "1.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
[[package]] [[package]]
name = "libc" name = "libc"
@@ -305,9 +295,9 @@ checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66"
[[package]] [[package]]
name = "log" name = "log"
version = "0.4.28" version = "0.4.30"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432" checksum = "616ec5685824bcc94416c6d4a7a446eea774a31efd7062c8480ba6fd06d7a6e5"
[[package]] [[package]]
name = "matchit" name = "matchit"
@@ -317,9 +307,9 @@ checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3"
[[package]] [[package]]
name = "memchr" name = "memchr"
version = "2.7.6" version = "2.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" checksum = "6b947ae49db0d222b1dbc6b113ce7248a3fc3a6ca21b696717bfc000ba4484d8"
[[package]] [[package]]
name = "mime" name = "mime"
@@ -340,9 +330,9 @@ dependencies = [
[[package]] [[package]]
name = "once_cell" name = "once_cell"
version = "1.21.3" version = "1.21.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
[[package]] [[package]]
name = "percent-encoding" name = "percent-encoding"
@@ -352,45 +342,39 @@ checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"
[[package]] [[package]]
name = "pin-project-lite" name = "pin-project-lite"
version = "0.2.16" version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
[[package]]
name = "pin-utils"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
[[package]] [[package]]
name = "proc-macro2" name = "proc-macro2"
version = "1.0.103" version = "1.0.106"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8" checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
dependencies = [ dependencies = [
"unicode-ident", "unicode-ident",
] ]
[[package]] [[package]]
name = "quote" name = "quote"
version = "1.0.42" version = "1.0.45"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f" checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
] ]
[[package]] [[package]]
name = "rustc-hash" name = "rustc-hash"
version = "2.1.1" version = "2.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe"
[[package]] [[package]]
name = "ryu" name = "ryu"
version = "1.0.20" version = "1.0.23"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f"
[[package]] [[package]]
name = "serde" name = "serde"
@@ -423,15 +407,15 @@ dependencies = [
[[package]] [[package]]
name = "serde_json" name = "serde_json"
version = "1.0.145" version = "1.0.150"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c" checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9"
dependencies = [ dependencies = [
"itoa", "itoa",
"memchr", "memchr",
"ryu",
"serde", "serde",
"serde_core", "serde_core",
"zmij",
] ]
[[package]] [[package]]
@@ -469,9 +453,9 @@ dependencies = [
[[package]] [[package]]
name = "slab" name = "slab"
version = "0.4.11" version = "0.4.12"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5"
[[package]] [[package]]
name = "smallvec" name = "smallvec"
@@ -491,9 +475,9 @@ dependencies = [
[[package]] [[package]]
name = "syn" name = "syn"
version = "2.0.110" version = "2.0.117"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a99801b5bd34ede4cf3fc688c5919368fea4e4814a4664359503e6015b280aea" checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@@ -534,9 +518,9 @@ dependencies = [
[[package]] [[package]]
name = "tower" name = "tower"
version = "0.5.2" version = "0.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4"
dependencies = [ dependencies = [
"futures-core", "futures-core",
"futures-util", "futures-util",
@@ -603,9 +587,9 @@ dependencies = [
[[package]] [[package]]
name = "unicode-ident" name = "unicode-ident"
version = "1.0.22" version = "1.0.24"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
[[package]] [[package]]
name = "wasi" name = "wasi"
@@ -627,3 +611,9 @@ checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
dependencies = [ dependencies = [
"windows-link", "windows-link",
] ]
[[package]]
name = "zmij"
version = "1.0.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa"

View File

@@ -15,9 +15,9 @@ futures = "0.3"
type-map = "0.5" type-map = "0.5"
[dev-dependencies] [dev-dependencies]
tokio = {
version = "1.52.3",
default-features = false,
features = ["net", "rt", "rt-multi-thread", "signal"]
}
tracing = "0.1.44" tracing = "0.1.44"
[dev-dependencies.tokio]
version = "1.52.3"
default-features = false
features = ["net", "rt", "rt-multi-thread", "signal"]

View File

@@ -1,6 +1,6 @@
# axum-app-wrapper # axum-app-wrapper
A small plugin layer for `axum` applications, inspired by [fastify](https://fastify.dev/) from the Node/JS ecosystem. A small plugin layer for `axum` applications, inspired by [fastify](https://fastify.dev/) from the Node/JS ecosystem and the [rocket](https://rocket.rs/) framework.
Plugins can: Plugins can:
@@ -17,12 +17,32 @@ This crate is intentionally thin. It does not replace axum's router, extractors,
1. `on_init` in registration order, passing a `TypeMap` to build state. 1. `on_init` in registration order, passing a `TypeMap` to build state.
2. `S::try_from(TypeMap)` to build the final typed app state. 2. `S::try_from(TypeMap)` to build the final typed app state.
3. `on_setup` in registration order, passing `Router<S>` and `&S`. 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. 4. `InitializedApp::shutdown()` runs `on_shutdown` consecutively in reverse registration order: the last registered plugin shuts down first, and each hook finishes before the next one starts.
## Example ## Example
See the `examples` folder for full examples. See the `examples` folder for full examples.
`App::init()` returns an initialized app handle. Use `router()` to get a ready-to-serve `Router<()>`, `state()` to inspect the finalized state, and `shutdown()` from your graceful shutdown path:
```rust
let app = App::<AppState>::new()
.store(config)
.route("/health", axum::routing::get(health))
.register(metrics_plugin)
.init()
.await?;
let router = app.router();
let service_name = &app.state().config.service_name;
axum::serve(listener, router)
.with_graceful_shutdown(async move {
tokio::signal::ctrl_c().await.expect("failed to listen for ctrl-c");
app.shutdown().await.expect("failed to shut down");
})
.await?;
```
For `on_setup` closures that access typed state, construct the plugin as 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: `AdHocPlugin::<AppState>::new()`. That gives Rust enough context to infer the `state` parameter:
@@ -41,15 +61,15 @@ Implement `AppPlugin<S>` directly when setup should be reusable across apps:
```rust ```rust
use axum::Router; use axum::Router;
use axum_app_wrapper::{AppPlugin, TypeMap}; use axum_app_wrapper::{AppPlugin, InitFuture, Result, TypeMap};
use futures::{future::BoxFuture, FutureExt}; use futures::FutureExt;
struct ConfigPlugin { struct ConfigPlugin {
config: Config, config: Config,
} }
impl AppPlugin<AppState> for ConfigPlugin { impl AppPlugin<AppState> for ConfigPlugin {
fn on_init(&mut self, mut state: TypeMap) -> BoxFuture<'static, anyhow::Result<TypeMap>> { fn on_init(&mut self, mut state: TypeMap) -> InitFuture {
let config = self.config.clone(); let config = self.config.clone();
async move { async move {
state.insert(config); state.insert(config);
@@ -62,7 +82,7 @@ impl AppPlugin<AppState> for ConfigPlugin {
&mut self, &mut self,
router: Router<AppState>, router: Router<AppState>,
_state: &AppState, _state: &AppState,
) -> anyhow::Result<Router<AppState>> { ) -> Result<Router<AppState>> {
Ok(router.route("/health", axum::routing::get(health))) Ok(router.route("/health", axum::routing::get(health)))
} }
} }

View File

@@ -22,10 +22,10 @@ use syn::{Data, DeriveInput, Fields, parse_macro_input};
/// } /// }
/// // To wrap the whole state in `Arc`, implement `TryFrom<TypeMap>` for `Arc<AppState>`: /// // To wrap the whole state in `Arc`, implement `TryFrom<TypeMap>` for `Arc<AppState>`:
/// impl TryFrom<TypeMap> for Arc<AppState> { /// impl TryFrom<TypeMap> for Arc<AppState> {
/// type Error = anyhow::Error; /// type Error = axum_app_wrapper::Error;
/// ///
/// fn try_from(map: TypeMap) -> Result<Self, Self::Error> { /// fn try_from(map: TypeMap) -> Result<Self, Self::Error> {
/// Ok(Self(Arc::new(AppState::try_from(map)?))) /// Ok(Arc::new(AppState::try_from(map)?))
/// } /// }
/// } /// }
/// ``` /// ```
@@ -57,10 +57,11 @@ pub fn derive_app_state(input: TokenStream) -> TokenStream {
}; };
let field_names = fields.iter().map(|f| &f.ident); let field_names = fields.iter().map(|f| &f.ident);
let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl();
quote! { quote! {
impl ::std::convert::TryFrom<::axum_app_wrapper::TypeMap> for #name { impl #impl_generics ::std::convert::TryFrom<::axum_app_wrapper::TypeMap> for #name #ty_generics #where_clause {
type Error = ::anyhow::Error; type Error = ::axum_app_wrapper::Error;
fn try_from(mut map: ::axum_app_wrapper::TypeMap) -> ::std::result::Result<Self, Self::Error> { fn try_from(mut map: ::axum_app_wrapper::TypeMap) -> ::std::result::Result<Self, Self::Error> {
Ok(#name { Ok(#name {

View File

@@ -43,7 +43,7 @@ async fn main() -> anyhow::Result<()> {
let metrics_registry = Arc::new(Metrics::new()); let metrics_registry = Arc::new(Metrics::new());
// Create your plugins using AdHocPlugin // Create your plugins using AdHocPlugin
let config_plugin = AdHocPlugin::<AppState>::new() let config_plugin = AdHocPlugin::<AppState>::named("Config")
.on_init(async |mut state| { .on_init(async |mut state| {
state.insert(config); state.insert(config);
Ok(state) Ok(state)
@@ -53,7 +53,7 @@ async fn main() -> anyhow::Result<()> {
Ok(router.route("/health", get(health))) Ok(router.route("/health", get(health)))
}); });
let metrics_plugin = AdHocPlugin::<AppState>::new() let metrics_plugin = AdHocPlugin::<AppState>::named("Metrics")
.on_init(async |mut state| { .on_init(async |mut state| {
state.insert(metrics_registry); state.insert(metrics_registry);
Ok(state) Ok(state)
@@ -67,27 +67,30 @@ async fn main() -> anyhow::Result<()> {
let metrics = Arc::clone(&state.metrics); let metrics = Arc::clone(&state.metrics);
async move { async move {
metrics.flush().await; metrics.flush().await;
Ok(())
} }
}); });
// Register your plugins in the desired order // Register your plugins in the desired order, and initialize the app
let app = App::<AppState>::new() let app = App::<AppState>::new()
.register(config_plugin) .register(config_plugin)
.register(metrics_plugin); .register(metrics_plugin)
.init()
let (router, state, on_shutdown) = app.init().await?; .await?;
let router = router.with_state(state); tracing::info!(service = %app.state().config.service_name, "starting server");
let addr: SocketAddr = "127.0.0.1:3000".parse()?; let addr: SocketAddr = "127.0.0.1:3000".parse()?;
let listener = tokio::net::TcpListener::bind(addr).await?; let listener = tokio::net::TcpListener::bind(addr).await?;
// Start the axum server with graceful shutdown // Start the axum server with graceful shutdown
axum::serve(listener, router) axum::serve(listener, app.router())
.with_graceful_shutdown(async { .with_graceful_shutdown(async {
tokio::signal::ctrl_c() tokio::signal::ctrl_c()
.await .await
.expect("failed to listen for ctrl-c"); .expect("failed to listen for ctrl-c");
on_shutdown.await; // Run the on_shutdown future for graceful shutdown app.shutdown()
.await
.expect("failed to run graceful shutdown");
}) })
.await?; .await?;

View File

@@ -1,4 +1,3 @@
#![allow(clippy::type_complexity)]
//! A small plugin layer for [`axum`](https://docs.rs/axum/latest/axum/) applications. //! 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 //! `axum-app-wrapper` lets plugins contribute startup state, router setup, and shutdown work
@@ -9,7 +8,7 @@
//! 1. [`AppPlugin::on_init`] runs in registration order and builds a [`TypeMap`]. //! 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>`. //! 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`. //! 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 //! 4. [`InitializedApp::shutdown`] runs [`AppPlugin::on_shutdown`] consecutively in reverse
//! registration order. //! registration order.
//! //!
//! For one-off plugins, use [`AdHocPlugin`]. If the setup closure needs access to typed state, //! For one-off plugins, use [`AdHocPlugin`]. If the setup closure needs access to typed state,
@@ -27,30 +26,61 @@
//! }); //! });
//! ``` //! ```
use std::{fmt::Display, sync::Arc}; extern crate self as axum_app_wrapper;
use anyhow::anyhow; use std::{borrow::Cow, fmt::Display, sync::Arc};
use axum::Router;
use anyhow::Context;
use axum::{Router, routing::MethodRouter};
use futures::{FutureExt, future::BoxFuture}; use futures::{FutureExt, future::BoxFuture};
// State extraction utilities
pub use axum_app_wrapper_macros::AppState; pub use axum_app_wrapper_macros::AppState;
pub use type_map::concurrent::TypeMap; pub use type_map::concurrent::TypeMap;
pub use anyhow;
/// Error type used by this crate.
pub type Error = anyhow::Error;
/// Result type used by this crate.
pub type Result<T> = std::result::Result<T, Error>;
/// Startup initialization future returned by [`AppPlugin::on_init`].
pub type InitFuture = BoxFuture<'static, Result<TypeMap>>;
/// Shutdown future returned by [`AppPlugin::on_shutdown`].
pub type ShutdownFuture = BoxFuture<'static, Result<()>>;
/// Default app state that keeps the startup [`TypeMap`] available behind an [`Arc`].
#[derive(Clone)]
pub struct TypeMapState(Arc<TypeMap>);
impl TypeMapState {
/// Borrow the underlying [`TypeMap`].
pub fn type_map(&self) -> &TypeMap {
&self.0
}
}
impl From<TypeMap> for TypeMapState {
fn from(map: TypeMap) -> Self {
Self(Arc::new(map))
}
}
/// Extracts a value of type `T` from a [`TypeMap`], returning an Anyhow error if the type is not present. /// Extracts a value of type `T` from a [`TypeMap`], returning an Anyhow error if the type is not present.
/// ///
/// This is used internally by the [`AppState`] macro but is also available for manual /// This is used internally by the [`AppState`] macro but is also available for manual
/// [`TryFrom<TypeMap>`] implementations. /// [`TryFrom<TypeMap>`] implementations.
pub fn extract_type_field<T: Send + Sync + 'static>(map: &mut TypeMap) -> anyhow::Result<T> { pub fn extract_type_field<T: Send + Sync + 'static>(map: &mut TypeMap) -> Result<T> {
map.remove::<T>() map.remove::<T>()
.ok_or_else(|| anyhow!("Missing type in TypeMap: {}", std::any::type_name::<T>())) .ok_or_else(|| ::anyhow::anyhow!("Missing type in TypeMap: {}", std::any::type_name::<T>()))
} }
// Plugin system // Plugin system
/// An axum app builder with plugin-managed state, router setup, and shutdown hooks. /// An axum app builder with plugin-managed state, router setup, and shutdown hooks.
pub struct App<S = Arc<TypeMap>> { pub struct App<S = TypeMapState> {
base_router: Router<S>, base_router: Router<S>,
plugins: Vec<Box<dyn AppPlugin<S>>>, plugins: Vec<Box<dyn AppPlugin<S>>>,
state: TypeMap, state: TypeMap,
@@ -69,6 +99,28 @@ where
} }
} }
/// Add a route to the base router before plugins run setup.
pub fn route(mut self, path: &str, method_router: MethodRouter<S>) -> Self {
self.base_router = self.base_router.route(path, method_router);
self
}
/// Mount routes at the given path before any setup hooks run. Prefer mounting most
/// routes within plugins.
pub fn mount(mut self, path: &str, router: Router<S>) -> Self {
self.base_router = match path {
"" | "/" => self.base_router.merge(router),
_ => self.base_router.nest(path, router),
};
self
}
/// Store a value in router state.
pub fn store<T: Send + Sync + 'static>(mut self, state: T) -> Self {
self.state.insert(state);
self
}
/// Register a plugin. /// Register a plugin.
/// ///
/// `on_init` and `on_setup` run in registration order. Shutdown runs in reverse registration /// `on_init` and `on_setup` run in registration order. Shutdown runs in reverse registration
@@ -78,78 +130,120 @@ where
self self
} }
// /// Mount the routes at the given path. All layers / middleware from plugins /// Build the router, finalized state, and graceful shutdown handle.
// /// will be applied to these routes. pub async fn init(mut self) -> Result<InitializedApp<S>>
// pub fn mount(mut self, path: &str, router: Router<S>) -> Self {
// self.base_router = match path {
// "" | "/" => self.base_router.merge(router),
// _ => self.base_router.nest(path, router),
// };
// self
// }
// /// Store type T in state
// pub fn store<T: Send + Sync + 'static>(mut self, state: T) -> Self {
// self.state.insert(state);
// self
// }
/// Build the router, finalized state, and graceful shutdown future.
///
/// 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)>
where where
S::Error: Display, S::Error: Display,
{ {
let mut state = self.state; let mut state = self.state;
for plugin in self.plugins.iter_mut() { for plugin in self.plugins.iter_mut() {
state = plugin.on_init(state).await?; state = plugin
.on_init(state)
.await
.with_context(|| format!("Error in on_init hook of plugin '{}'", plugin.name()))?;
} }
let state = S::try_from(state).map_err(|err| anyhow!("Error creating state: {err}"))?; let state =
S::try_from(state).map_err(|err| ::anyhow::anyhow!("Error creating state: {err}"))?;
let mut router = self.base_router; let mut router = self.base_router;
for plugin in self.plugins.iter_mut() { for plugin in self.plugins.iter_mut() {
router = plugin.on_setup(router, &state)?; router = plugin
.on_setup(router, &state)
.with_context(|| format!("Error in on_setup hook of plugin '{}'", plugin.name()))?;
} }
let shutdown_fns: Vec<_> = self let shutdown_fns: Vec<_> = self
.plugins .plugins
.into_iter() .into_iter()
.rev() .rev()
.filter_map(|mut p| p.on_shutdown(&state)) .filter_map(|mut p| Some((p.name(), p.on_shutdown(&state)?)))
.collect(); .collect();
let on_shutdown = async move { let on_shutdown = async move {
for shutdown_fn in shutdown_fns { for (plugin_name, shutdown_fn) in shutdown_fns {
shutdown_fn.await; shutdown_fn.await.with_context(|| {
format!("Error in on_shutdown hook of plugin '{plugin_name}'")
})?;
} }
}; Ok(())
}
.boxed();
Ok((router, state, on_shutdown)) Ok(InitializedApp {
router,
state,
on_shutdown,
})
}
}
impl<S> Default for App<S>
where
S: TryFrom<TypeMap> + Clone + Send + Sync + 'static,
{
fn default() -> Self {
Self::new()
}
}
/// A fully initialized axum app.
pub struct InitializedApp<S> {
router: Router<S>,
state: S,
on_shutdown: ShutdownFuture,
}
impl<S> InitializedApp<S>
where
S: Clone + Send + Sync + 'static,
{
/// Build a ready-to-serve router by attaching the finalized state.
pub fn router(&self) -> Router<()> {
self.router.clone().with_state(self.state.clone())
}
/// Borrow the finalized app state.
pub fn state(&self) -> &S {
&self.state
}
/// Consume the initialized app and return its raw parts.
pub fn into_parts(self) -> (Router<S>, S, ShutdownFuture) {
(self.router, self.state, self.on_shutdown)
}
/// Run graceful shutdown work for all plugins.
pub async fn shutdown(self) -> Result<()> {
self.on_shutdown.await
} }
} }
/// A plugin that can participate in app initialization, router setup, and shutdown. /// 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 = TypeMapState> {
/// Plugin name
fn name(&self) -> Cow<'static, str> {
Cow::Borrowed(std::any::type_name::<Self>())
}
/// Run during startup before typed state exists. /// Run during startup before typed state exists.
/// ///
/// Use this hook to insert or transform values in the shared [`TypeMap`]. /// 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) -> InitFuture {
async { Ok(app_state) }.boxed() async { Ok(app_state) }.boxed()
} }
/// Run after typed state has been created. /// Run after typed state has been created.
/// ///
/// Use this hook to add routes, services, middleware, or router state. /// 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) -> Result<Router<S>> {
Ok(router) Ok(router)
} }
/// Return shutdown work for this plugin. /// Return shutdown work for this plugin.
/// ///
/// Shutdown hooks are awaited consecutively in reverse registration order. /// 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<ShutdownFuture> {
None None
} }
} }
@@ -158,16 +252,32 @@ pub trait AppPlugin<S = Arc<TypeMap>> {
/// ///
/// `on_init` and `on_setup` accept capturing closures. If `on_setup` uses typed state, prefer /// `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. /// `AdHocPlugin::<State>::new()` so the closure parameter type can be inferred.
pub struct AdHocPlugin<S = Arc<TypeMap>> { pub struct AdHocPlugin<S = TypeMapState> {
on_init: Option<Box<dyn FnOnce(TypeMap) -> BoxFuture<'static, anyhow::Result<TypeMap>> + Send>>, name: Cow<'static, str>,
on_setup: Option<Box<dyn FnOnce(Router<S>, &S) -> anyhow::Result<Router<S>> + Send>>, on_init: Option<InitFn>,
on_shutdown: Option<Box<dyn FnOnce(&S) -> BoxFuture<'static, ()>>>, on_setup: Option<SetupFn<S>>,
on_shutdown: Option<ShutdownFn<S>>,
} }
type InitFn = Box<dyn FnOnce(TypeMap) -> InitFuture + Send>;
type SetupFn<S> = Box<dyn FnOnce(Router<S>, &S) -> Result<Router<S>> + Send>;
type ShutdownFn<S> = Box<dyn FnOnce(&S) -> ShutdownFuture + Send>;
impl<S: 'static> AdHocPlugin<S> { impl<S: 'static> AdHocPlugin<S> {
/// Create an empty ad-hoc plugin. /// Create an ad-hoc plugin. Prefer `named()` to help with debugging.
pub fn new() -> Self { pub fn new() -> Self {
Self { Self {
name: Cow::Borrowed("adhoc"),
on_init: None,
on_setup: None,
on_shutdown: None,
}
}
/// Create a named ad-hoc plugin.
pub fn named(name: impl Into<Cow<'static, str>>) -> Self {
Self {
name: name.into(),
on_init: None, on_init: None,
on_setup: None, on_setup: None,
on_shutdown: None, on_shutdown: None,
@@ -178,7 +288,7 @@ impl<S: 'static> AdHocPlugin<S> {
pub fn on_init<F, T>(mut self, on_init: F) -> Self pub fn on_init<F, T>(mut self, on_init: F) -> Self
where where
F: FnOnce(TypeMap) -> T + Send + 'static, F: FnOnce(TypeMap) -> T + Send + 'static,
T: Future<Output = anyhow::Result<TypeMap>> + Send + 'static, T: Future<Output = 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
@@ -187,38 +297,49 @@ impl<S: 'static> AdHocPlugin<S> {
/// Set router setup for this plugin. /// Set router setup for this plugin.
pub fn on_setup<F>(mut self, on_setup: F) -> Self pub fn on_setup<F>(mut self, on_setup: F) -> Self
where where
F: FnOnce(Router<S>, &S) -> anyhow::Result<Router<S>> + Send + 'static, F: FnOnce(Router<S>, &S) -> Result<Router<S>> + Send + 'static,
{ {
self.on_setup = Some(Box::new(on_setup)); self.on_setup = Some(Box::new(on_setup));
self self
} }
/// Set shutdown work for this plugin. /// Set shutdown work for this plugin.
pub fn on_shutdown<T>(mut self, on_shutdown: fn(state: &S) -> T) -> Self pub fn on_shutdown<F, T>(mut self, on_shutdown: F) -> Self
where where
T: Future<Output = ()> + Send + 'static, F: FnOnce(&S) -> T + Send + 'static,
T: Future<Output = Result<()>> + Send + 'static,
{ {
self.on_shutdown = Some(Box::new(move |s| Box::pin(on_shutdown(s)))); self.on_shutdown = Some(Box::new(move |s| Box::pin(on_shutdown(s))));
self self
} }
} }
impl<S: 'static> Default for AdHocPlugin<S> {
fn default() -> Self {
Self::new()
}
}
impl<S> AppPlugin<S> for AdHocPlugin<S> { impl<S> AppPlugin<S> for AdHocPlugin<S> {
fn on_init(&mut self, app_state: TypeMap) -> BoxFuture<'static, anyhow::Result<TypeMap>> { fn name(&self) -> Cow<'static, str> {
self.name.clone()
}
fn on_init(&mut self, app_state: TypeMap) -> InitFuture {
match self.on_init.take() { match self.on_init.take() {
Some(init_fn) => async move { init_fn(app_state).await }.boxed(), Some(init_fn) => async move { init_fn(app_state).await }.boxed(),
None => async { Ok(app_state) }.boxed(), None => async { Ok(app_state) }.boxed(),
} }
} }
fn on_setup(&mut self, router: Router<S>, state: &S) -> anyhow::Result<Router<S>> { fn on_setup(&mut self, router: Router<S>, state: &S) -> Result<Router<S>> {
match self.on_setup.take() { 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),
} }
} }
fn on_shutdown(&mut self, state: &S) -> Option<BoxFuture<'static, ()>> { fn on_shutdown(&mut self, state: &S) -> Option<ShutdownFuture> {
match self.on_shutdown.take() { match self.on_shutdown.take() {
Some(shutdown_fn) => Some(shutdown_fn(state)), Some(shutdown_fn) => Some(shutdown_fn(state)),
None => None, None => None,
@@ -228,8 +349,9 @@ impl<S> AppPlugin<S> for AdHocPlugin<S> {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*;
use std::{ use std::{
convert::Infallible,
sync::{ sync::{
Mutex, Mutex,
atomic::{AtomicUsize, Ordering}, atomic::{AtomicUsize, Ordering},
@@ -237,23 +359,11 @@ mod tests {
task::Poll, task::Poll,
}; };
use super::*; #[derive(Clone, self::AppState)]
#[derive(Clone)]
struct TestState { struct TestState {
value: Arc<String>, 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] #[test]
fn adhoc_plugin_with_basic_state() { fn adhoc_plugin_with_basic_state() {
let init_value = Arc::new(String::from("ready")); let init_value = Arc::new(String::from("ready"));
@@ -270,10 +380,75 @@ mod tests {
}); });
let app = App::<TestState>::new().register(plugin); let app = App::<TestState>::new().register(plugin);
let (_router, state, _shutdown) = let app = futures::executor::block_on(app.init()).expect("app should initialize");
futures::executor::block_on(app.init()).expect("app should initialize");
assert_eq!(state.value.as_str(), "ready"); assert_eq!(app.state().value.as_str(), "ready");
}
#[test]
fn store_and_router_handle_expose_initialized_state() {
let app = App::<TestState>::new().store(Arc::new(String::from("stored")));
let app = futures::executor::block_on(app.init()).expect("app should initialize");
let _router = app.router();
assert_eq!(app.state().value.as_str(), "stored");
}
#[test]
fn default_state_keeps_type_map_available() {
let app: App = App::new().store(Arc::new(String::from("typemap")));
let app = futures::executor::block_on(app.init()).expect("app should initialize");
let value = app
.state()
.type_map()
.get::<Arc<String>>()
.expect("stored value should remain in typemap");
assert_eq!(value.as_str(), "typemap");
}
#[derive(Clone, AppState)]
struct GenericState<T: Send + Sync + 'static> {
value: Arc<T>,
}
#[test]
fn app_state_derive_supports_generics() {
let app = App::<GenericState<String>>::new().store(Arc::new(String::from("generic")));
let app = futures::executor::block_on(app.init()).expect("app should initialize");
assert_eq!(app.state().value.as_str(), "generic");
}
#[test]
fn adhoc_shutdown_accepts_capturing_fallible_closure() {
let events = Arc::new(Mutex::new(Vec::new()));
let shutdown_events = Arc::clone(&events);
let app = App::<TestState>::new()
.store(Arc::new(String::from("ready")))
.register(AdHocPlugin::<TestState>::new().on_shutdown(move |state| {
let events = Arc::clone(&shutdown_events);
let value = Arc::clone(&state.value);
async move {
events
.lock()
.expect("events lock poisoned")
.push(value.to_string());
Ok(())
}
}));
let app = futures::executor::block_on(app.init()).expect("app should initialize");
futures::executor::block_on(app.shutdown()).expect("shutdown should succeed");
assert_eq!(
*events.lock().expect("events lock poisoned"),
[String::from("ready")]
);
} }
struct ShutdownOrderPlugin { struct ShutdownOrderPlugin {
@@ -283,7 +458,7 @@ mod tests {
} }
impl AppPlugin<TestState> for ShutdownOrderPlugin { impl AppPlugin<TestState> for ShutdownOrderPlugin {
fn on_shutdown(&mut self, _state: &TestState) -> Option<BoxFuture<'static, ()>> { fn on_shutdown(&mut self, _state: &TestState) -> Option<ShutdownFuture> {
let name = self.name; let name = self.name;
let events = Arc::clone(&self.events); let events = Arc::clone(&self.events);
let active_shutdowns = Arc::clone(&self.active_shutdowns); let active_shutdowns = Arc::clone(&self.active_shutdowns);
@@ -307,7 +482,7 @@ mod tests {
.expect("events lock poisoned") .expect("events lock poisoned")
.push(format!("{name}:finish")); .push(format!("{name}:finish"));
active_shutdowns.fetch_sub(1, Ordering::SeqCst); active_shutdowns.fetch_sub(1, Ordering::SeqCst);
Poll::Ready(()) Poll::Ready(Ok(()))
}))) })))
} }
} }
@@ -333,9 +508,8 @@ mod tests {
active_shutdowns, active_shutdowns,
}); });
let (_router, _state, on_shutdown) = let app = futures::executor::block_on(app.init()).expect("app should initialize");
futures::executor::block_on(app.init()).expect("app should initialize"); futures::executor::block_on(app.shutdown()).expect("shutdown should succeed");
futures::executor::block_on(on_shutdown);
assert_eq!( assert_eq!(
*events.lock().expect("events lock poisoned"), *events.lock().expect("events lock poisoned"),