Compare commits
16 Commits
57dd1da621
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
b715796c56
|
|||
|
1ce2c25431
|
|||
|
bcb5c31c31
|
|||
|
1d6708e23b
|
|||
|
958060e538
|
|||
|
bda85091af
|
|||
|
e2d51e9e91
|
|||
|
e1440c6d57
|
|||
|
e4d9af1e1c
|
|||
|
f28d9f5d14
|
|||
|
25a9005375
|
|||
|
386eb59f84
|
|||
|
513cbc19a3
|
|||
|
995f3cc61a
|
|||
|
a5b42ab55a
|
|||
|
65007f6cd2
|
26
.github/workflows/ci.yml
vendored
Normal file
26
.github/workflows/ci.yml
vendored
Normal 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
281
Cargo.lock
generated
@@ -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"
|
||||
|
||||
20
Cargo.toml
20
Cargo.toml
@@ -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
89
README.md
Normal 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)))
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -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
98
examples/app.rs
Normal 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(())
|
||||
}
|
||||
485
src/lib.rs
485
src/lib.rs
@@ -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"
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user