Compare commits

...

16 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
e1440c6d57 Update ci.yml
Some checks failed
CI / build (push) Failing after 49s
2026-05-26 03:05:46 -04:00
e4d9af1e1c add CI 2026-05-26 03:05:13 -04:00
f28d9f5d14 add comments to example 2026-05-26 02:59:03 -04:00
25a9005375 bump version 2026-05-26 02:52:34 -04:00
386eb59f84 add example 2026-05-26 02:52:27 -04:00
513cbc19a3 Update README.md 2026-05-26 02:44:08 -04:00
995f3cc61a add docs 2026-05-26 02:14:41 -04:00
a5b42ab55a Run shutdown hooks sequentially in reverse 2026-05-26 01:58:56 -04:00
65007f6cd2 AdHocPlugin on_init and on_setup accept capturing closures 2026-05-26 01:47:53 -04:00
7 changed files with 760 additions and 248 deletions

26
.github/workflows/ci.yml vendored Normal file
View File

@@ -0,0 +1,26 @@
name: CI
on:
push:
env:
RUST_VERSION: "1.94"
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Install Rust toolchain
uses: actions-rust-lang/setup-rust-toolchain@v1
with:
toolchain: ${{ env.RUST_VERSION }}
- name: cargo check
run: cargo check --examples --locked
- name: cargo build
run: cargo build --examples --locked

281
Cargo.lock generated
View File

@@ -4,9 +4,9 @@ version = 4
[[package]]
name = "anyhow"
version = "1.0.100"
version = "1.0.102"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61"
checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c"
[[package]]
name = "atomic-waker"
@@ -16,9 +16,9 @@ checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0"
[[package]]
name = "axum"
version = "0.8.6"
version = "0.8.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a18ed336352031311f4e0b4dd2ff392d4fbb370777c9d18d7fc9d7359f73871"
checksum = "31b698c5f9a010f6573133b09e0de5408834d0c82f8d7475a89fc1867a71cd90"
dependencies = [
"axum-core",
"bytes",
@@ -49,12 +49,14 @@ dependencies = [
[[package]]
name = "axum-app-wrapper"
version = "0.1.1"
version = "0.1.2"
dependencies = [
"anyhow",
"axum",
"axum-app-wrapper-macros",
"futures",
"tokio",
"tracing",
"type-map",
]
@@ -69,9 +71,9 @@ dependencies = [
[[package]]
name = "axum-core"
version = "0.5.5"
version = "0.5.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "59446ce19cd142f8833f856eb31f3eb097812d1479ab224f54d72428ca21ea22"
checksum = "08c78f31d7b1291f7ee735c1c6780ccde7785daae9a9206026862dab7d8792d1"
dependencies = [
"bytes",
"futures-core",
@@ -88,15 +90,19 @@ dependencies = [
[[package]]
name = "bytes"
version = "1.11.0"
version = "1.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3"
checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33"
[[package]]
name = "fnv"
version = "1.0.7"
name = "errno"
version = "0.3.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
dependencies = [
"libc",
"windows-sys",
]
[[package]]
name = "form_urlencoded"
@@ -109,9 +115,9 @@ dependencies = [
[[package]]
name = "futures"
version = "0.3.31"
version = "0.3.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876"
checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d"
dependencies = [
"futures-channel",
"futures-core",
@@ -124,9 +130,9 @@ dependencies = [
[[package]]
name = "futures-channel"
version = "0.3.31"
version = "0.3.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10"
checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d"
dependencies = [
"futures-core",
"futures-sink",
@@ -134,15 +140,15 @@ dependencies = [
[[package]]
name = "futures-core"
version = "0.3.31"
version = "0.3.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e"
checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d"
[[package]]
name = "futures-executor"
version = "0.3.31"
version = "0.3.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f"
checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d"
dependencies = [
"futures-core",
"futures-task",
@@ -151,15 +157,15 @@ dependencies = [
[[package]]
name = "futures-io"
version = "0.3.31"
version = "0.3.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6"
checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718"
[[package]]
name = "futures-macro"
version = "0.3.31"
version = "0.3.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650"
checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b"
dependencies = [
"proc-macro2",
"quote",
@@ -168,21 +174,21 @@ dependencies = [
[[package]]
name = "futures-sink"
version = "0.3.31"
version = "0.3.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7"
checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893"
[[package]]
name = "futures-task"
version = "0.3.31"
version = "0.3.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988"
checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393"
[[package]]
name = "futures-util"
version = "0.3.31"
version = "0.3.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81"
checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6"
dependencies = [
"futures-channel",
"futures-core",
@@ -192,18 +198,16 @@ dependencies = [
"futures-task",
"memchr",
"pin-project-lite",
"pin-utils",
"slab",
]
[[package]]
name = "http"
version = "1.3.1"
version = "1.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565"
checksum = "8be7462df143984c4598a256ef469b251d7d7f9e271135073e78fc535414f3d0"
dependencies = [
"bytes",
"fnv",
"itoa",
]
@@ -244,9 +248,9 @@ checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"
[[package]]
name = "hyper"
version = "1.8.1"
version = "1.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2ab2d4f250c3d7b1c9fcdff1cece94ea4e2dfbec68614f7b87cb205f24ca9d11"
checksum = "6299f016b246a94207e63da54dbe807655bf9e00044f73ded42c3ac5305fbcca"
dependencies = [
"atomic-waker",
"bytes",
@@ -258,19 +262,17 @@ dependencies = [
"httpdate",
"itoa",
"pin-project-lite",
"pin-utils",
"smallvec",
"tokio",
]
[[package]]
name = "hyper-util"
version = "0.1.18"
version = "0.1.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "52e9a2a24dc5c6821e71a7030e1e14b7b632acac55c40e9d2e082c621261bb56"
checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0"
dependencies = [
"bytes",
"futures-core",
"http",
"http-body",
"hyper",
@@ -281,21 +283,21 @@ dependencies = [
[[package]]
name = "itoa"
version = "1.0.15"
version = "1.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c"
checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
[[package]]
name = "libc"
version = "0.2.177"
version = "0.2.186"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976"
checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66"
[[package]]
name = "log"
version = "0.4.28"
version = "0.4.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432"
checksum = "616ec5685824bcc94416c6d4a7a446eea774a31efd7062c8480ba6fd06d7a6e5"
[[package]]
name = "matchit"
@@ -305,9 +307,9 @@ checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3"
[[package]]
name = "memchr"
version = "2.7.6"
version = "2.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273"
checksum = "6b947ae49db0d222b1dbc6b113ce7248a3fc3a6ca21b696717bfc000ba4484d8"
[[package]]
name = "mime"
@@ -317,20 +319,20 @@ checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
[[package]]
name = "mio"
version = "1.1.0"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "69d83b0086dc8ecf3ce9ae2874b2d1290252e2a30720bea58a5c6639b0092873"
checksum = "50b7e5b27aa02a74bac8c3f23f448f8d87ff11f92d3aac1a6ed369ee08cc56c1"
dependencies = [
"libc",
"wasi",
"windows-sys 0.61.2",
"windows-sys",
]
[[package]]
name = "once_cell"
version = "1.21.3"
version = "1.21.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
[[package]]
name = "percent-encoding"
@@ -340,45 +342,39 @@ checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"
[[package]]
name = "pin-project-lite"
version = "0.2.16"
version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b"
[[package]]
name = "pin-utils"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
[[package]]
name = "proc-macro2"
version = "1.0.103"
version = "1.0.106"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8"
checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.42"
version = "1.0.45"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f"
checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924"
dependencies = [
"proc-macro2",
]
[[package]]
name = "rustc-hash"
version = "2.1.1"
version = "2.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d"
checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe"
[[package]]
name = "ryu"
version = "1.0.20"
version = "1.0.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f"
checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f"
[[package]]
name = "serde"
@@ -411,15 +407,15 @@ dependencies = [
[[package]]
name = "serde_json"
version = "1.0.145"
version = "1.0.150"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c"
checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9"
dependencies = [
"itoa",
"memchr",
"ryu",
"serde",
"serde_core",
"zmij",
]
[[package]]
@@ -446,10 +442,20 @@ dependencies = [
]
[[package]]
name = "slab"
version = "0.4.11"
name = "signal-hook-registry"
version = "1.4.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589"
checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b"
dependencies = [
"errno",
"libc",
]
[[package]]
name = "slab"
version = "0.4.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5"
[[package]]
name = "smallvec"
@@ -459,19 +465,19 @@ checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
[[package]]
name = "socket2"
version = "0.6.1"
version = "0.6.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "17129e116933cf371d018bb80ae557e889637989d8638274fb25622827b03881"
checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e"
dependencies = [
"libc",
"windows-sys 0.60.2",
"windows-sys",
]
[[package]]
name = "syn"
version = "2.0.110"
version = "2.0.117"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a99801b5bd34ede4cf3fc688c5919368fea4e4814a4664359503e6015b280aea"
checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99"
dependencies = [
"proc-macro2",
"quote",
@@ -486,23 +492,24 @@ checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263"
[[package]]
name = "tokio"
version = "1.48.0"
version = "1.52.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ff360e02eab121e0bc37a2d3b4d4dc622e6eda3a8e5253d5435ecf5bd4c68408"
checksum = "8fc7f01b389ac15039e4dc9531aa973a135d7a4135281b12d7c1bc79fd57fffe"
dependencies = [
"libc",
"mio",
"pin-project-lite",
"signal-hook-registry",
"socket2",
"tokio-macros",
"windows-sys 0.61.2",
"windows-sys",
]
[[package]]
name = "tokio-macros"
version = "2.6.0"
version = "2.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5"
checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496"
dependencies = [
"proc-macro2",
"quote",
@@ -511,9 +518,9 @@ dependencies = [
[[package]]
name = "tower"
version = "0.5.2"
version = "0.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9"
checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4"
dependencies = [
"futures-core",
"futures-util",
@@ -539,20 +546,32 @@ checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3"
[[package]]
name = "tracing"
version = "0.1.41"
version = "0.1.44"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0"
checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100"
dependencies = [
"log",
"pin-project-lite",
"tracing-attributes",
"tracing-core",
]
[[package]]
name = "tracing-core"
version = "0.1.34"
name = "tracing-attributes"
version = "0.1.31"
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 = [
"once_cell",
]
@@ -568,9 +587,9 @@ dependencies = [
[[package]]
name = "unicode-ident"
version = "1.0.22"
version = "1.0.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5"
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
[[package]]
name = "wasi"
@@ -584,15 +603,6 @@ version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
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]]
name = "windows-sys"
version = "0.61.2"
@@ -603,66 +613,7 @@ dependencies = [
]
[[package]]
name = "windows-targets"
version = "0.53.5"
name = "zmij"
version = "1.0.21"
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"
checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa"

View File

@@ -1,15 +1,23 @@
[workspace]
members = [".", "crates/*"]
resolver = "2"
[package]
name = "axum-app-wrapper"
version = "0.1.1"
version = "0.1.2"
edition = "2024"
[workspace]
resolver = "2"
members = [".", "crates/*"]
[dependencies]
anyhow = "1.0"
axum = "0.8"
axum-app-wrapper-macros = { path = "crates/axum-app-wrapper-macros" }
futures = "0.3"
type-map = "0.5"
axum-app-wrapper-macros = { path = "crates/axum-app-wrapper-macros" }
[dev-dependencies]
tracing = "0.1.44"
[dev-dependencies.tokio]
version = "1.52.3"
default-features = false
features = ["net", "rt", "rt-multi-thread", "signal"]

89
README.md Normal file
View File

@@ -0,0 +1,89 @@
# axum-app-wrapper
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:
- 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. `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
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
`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, InitFuture, Result, TypeMap};
use futures::FutureExt;
struct ConfigPlugin {
config: Config,
}
impl AppPlugin<AppState> for ConfigPlugin {
fn on_init(&mut self, mut state: TypeMap) -> InitFuture {
let config = self.config.clone();
async move {
state.insert(config);
Ok(state)
}
.boxed()
}
fn on_setup(
&mut self,
router: Router<AppState>,
_state: &AppState,
) -> Result<Router<AppState>> {
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>`:
/// 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> {
/// 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 (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl();
quote! {
impl ::std::convert::TryFrom<::axum_app_wrapper::TypeMap> for #name {
type Error = ::anyhow::Error;
impl #impl_generics ::std::convert::TryFrom<::axum_app_wrapper::TypeMap> for #name #ty_generics #where_clause {
type Error = ::axum_app_wrapper::Error;
fn try_from(mut map: ::axum_app_wrapper::TypeMap) -> ::std::result::Result<Self, Self::Error> {
Ok(#name {

98
examples/app.rs Normal file
View File

@@ -0,0 +1,98 @@
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());
// Create your plugins using AdHocPlugin
let config_plugin = AdHocPlugin::<AppState>::named("Config")
.on_init(async |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>::named("Metrics")
.on_init(async |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;
Ok(())
}
});
// Register your plugins in the desired order, and initialize the app
let app = App::<AppState>::new()
.register(config_plugin)
.register(metrics_plugin)
.init()
.await?;
tracing::info!(service = %app.state().config.service_name, "starting server");
let addr: SocketAddr = "127.0.0.1:3000".parse()?;
let listener = tokio::net::TcpListener::bind(addr).await?;
// Start the axum server with graceful shutdown
axum::serve(listener, app.router())
.with_graceful_shutdown(async {
tokio::signal::ctrl_c()
.await
.expect("failed to listen for ctrl-c");
app.shutdown()
.await
.expect("failed to run graceful shutdown");
})
.await?;
Ok(())
}

View File

@@ -1,32 +1,86 @@
#![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. [`InitializedApp::shutdown`] 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};
extern crate self as axum_app_wrapper;
use anyhow::anyhow;
use axum::Router;
use futures::{
FutureExt,
future::{BoxFuture, join_all},
};
use std::{borrow::Cow, fmt::Display, sync::Arc};
// State extraction utilities
use anyhow::Context;
use axum::{Router, routing::MethodRouter};
use futures::{FutureExt, future::BoxFuture};
pub use axum_app_wrapper_macros::AppState;
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.
///
/// This is used internally by the [`AppState`] macro but is also available for manual
/// [`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>()
.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
/// A lightweight wrapper around axum that enables building plugins around the router
pub struct App<S = Arc<TypeMap>> {
/// An axum app builder with plugin-managed state, router setup, and shutdown hooks.
pub struct App<S = TypeMapState> {
base_router: Router<S>,
plugins: Vec<Box<dyn AppPlugin<S>>>,
state: TypeMap,
@@ -36,7 +90,7 @@ impl<S> App<S>
where
S: TryFrom<TypeMap> + Clone + Send + Sync + 'static,
{
/// Create a new server with the given routes.
/// Create an empty app.
pub fn new() -> Self {
Self {
base_router: Router::new(),
@@ -45,141 +99,426 @@ where
}
}
/// Register a plugin. This can be considered analogous to axum/tower's `layer` function
/// and follows the same ordering (each plugin "wraps" the previous one).
/// 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.
///
/// `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 {
self.plugins.push(Box::new(plugin));
self
}
// /// Mount the routes at the given path. All layers / middleware from plugins
// /// will be applied to these routes.
// 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 and initialize the server. Returns the base router, finalized state, and a future to run
/// on graceful shutdown.
pub async fn init(mut self) -> anyhow::Result<(Router<S>, S, impl Future + Send)>
/// Build the router, finalized state, and graceful shutdown handle.
pub async fn init(mut self) -> Result<InitializedApp<S>>
where
S::Error: Display,
{
let mut state = self.state;
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;
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 = self
let shutdown_fns: Vec<_> = self
.plugins
.into_iter()
.filter_map(|mut p| p.on_shutdown(&state));
let on_shutdown = join_all(shutdown_fns);
.rev()
.filter_map(|mut p| Some((p.name(), p.on_shutdown(&state)?)))
.collect();
let on_shutdown = async move {
for (plugin_name, shutdown_fn) in shutdown_fns {
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,
})
}
}
/// Trait for a plugin that can be attached to the server
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.
#[allow(unused_variables, reason = "trait functions with default no-op")]
pub trait AppPlugin<S = Arc<TypeMap>> {
/// Init function that will run on server startup. Can add and manipulate state.
fn on_init(&mut self, app_state: TypeMap) -> BoxFuture<'static, anyhow::Result<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.
///
/// Use this hook to insert or transform values in the shared [`TypeMap`].
fn on_init(&mut self, app_state: TypeMap) -> InitFuture {
async { Ok(app_state) }.boxed()
}
/// Setup function that will run _after_ state is initialized (e.g. routes, middleware,
/// and services should be added here)
fn on_setup(&mut self, router: Router<S>, state: &S) -> anyhow::Result<Router<S>> {
/// Run after typed state has been created.
///
/// Use this hook to add routes, services, middleware, or router state.
fn on_setup(&mut self, router: Router<S>, state: &S) -> Result<Router<S>> {
Ok(router)
}
/// Teardown function that will run on server shutdown
fn on_shutdown(&mut self, state: &S) -> Option<BoxFuture<'static, ()>> {
/// Return shutdown work for this plugin.
///
/// Shutdown hooks are awaited consecutively in reverse registration order.
fn on_shutdown(&mut self, state: &S) -> Option<ShutdownFuture> {
None
}
}
/// Utility to build a plugin on the fly
pub struct AdHocPlugin<S = Arc<TypeMap>> {
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_shutdown: Option<Box<dyn FnOnce(&S) -> BoxFuture<'static, ()>>>,
/// 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 = TypeMapState> {
name: Cow<'static, str>,
on_init: Option<InitFn>,
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> {
/// Create an ad-hoc plugin. Prefer `named()` to help with debugging.
pub fn new() -> Self {
Self {
name: Cow::Borrowed("adhoc"),
on_init: None,
on_setup: None,
on_shutdown: None,
}
}
/// Init function that will run on server startup. Can add and manipulate state.
pub fn on_init<T>(mut self, on_init: fn(app_state: TypeMap) -> T) -> Self
/// Create a named ad-hoc plugin.
pub fn named(name: impl Into<Cow<'static, str>>) -> Self {
Self {
name: name.into(),
on_init: None,
on_setup: None,
on_shutdown: None,
}
}
/// Set startup state initialization for this plugin.
pub fn on_init<F, T>(mut self, on_init: F) -> Self
where
T: Future<Output = anyhow::Result<TypeMap>> + Send + 'static,
F: FnOnce(TypeMap) -> T + Send + 'static,
T: Future<Output = Result<TypeMap>> + Send + 'static,
{
self.on_init = Some(Box::new(move |s| Box::pin(on_init(s))));
self
}
/// Setup function that will run _after_ state is initialized. (e.g. routes, middleware,
/// and services should be added here)
pub fn on_setup(
mut self,
on_setup: fn(router: Router<S>, state: &S) -> anyhow::Result<Router<S>>,
) -> Self {
self.on_setup = Some(on_setup);
/// Set router setup for this plugin.
pub fn on_setup<F>(mut self, on_setup: F) -> Self
where
F: FnOnce(Router<S>, &S) -> Result<Router<S>> + Send + 'static,
{
self.on_setup = Some(Box::new(on_setup));
self
}
/// Teardown function that will run on server shutdown
pub fn on_shutdown<T>(mut self, on_shutdown: fn(state: &S) -> T) -> Self
/// Set shutdown work for this plugin.
pub fn on_shutdown<F, T>(mut self, on_shutdown: F) -> Self
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
}
}
impl<S: 'static> Default for AdHocPlugin<S> {
fn default() -> Self {
Self::new()
}
}
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() {
Some(init_fn) => async move { init_fn(app_state).await }.boxed(),
None => async { Ok(app_state) }.boxed(),
}
}
fn on_setup(&mut self, router: Router<S>, state: &S) -> anyhow::Result<Router<S>> {
match self.on_setup {
fn on_setup(&mut self, router: Router<S>, state: &S) -> Result<Router<S>> {
match self.on_setup.take() {
Some(setup_fn) => setup_fn(router, state),
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() {
Some(shutdown_fn) => Some(shutdown_fn(state)),
None => None,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::{
sync::{
Mutex,
atomic::{AtomicUsize, Ordering},
},
task::Poll,
};
#[derive(Clone, self::AppState)]
struct TestState {
value: Arc<String>,
}
#[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 app = futures::executor::block_on(app.init()).expect("app should initialize");
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 {
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<ShutdownFuture> {
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(Ok(()))
})))
}
}
#[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 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"),
[
"second:start",
"second:finish",
"first:start",
"first:finish"
]
);
}
}