├── .github └── workflows │ ├── cross_compile.yml │ └── main.yml ├── .gitignore ├── CHANGELOG.md ├── Cargo.lock ├── Cargo.toml ├── LICENSE-APACHE ├── LICENSE-MIT ├── Makefile ├── README.md ├── bacon.toml └── src └── lib.rs /.github/workflows/cross_compile.yml: -------------------------------------------------------------------------------- 1 | # We could use `@actions-rs/cargo` Action ability to automatically install `cross` tool 2 | # in order to compile our application for some unusual targets. 3 | 4 | on: [push, pull_request] 5 | 6 | name: Cross-compile 7 | 8 | jobs: 9 | build: 10 | name: Build 11 | runs-on: ubuntu-latest 12 | strategy: 13 | matrix: 14 | target: 15 | - armv7-unknown-linux-gnueabihf 16 | # - mips-unknown-linux-musl 17 | - x86_64-unknown-linux-musl 18 | - aarch64-unknown-linux-gnu 19 | steps: 20 | - uses: actions/checkout@v2 21 | - uses: actions-rs/toolchain@v1 22 | with: 23 | toolchain: stable 24 | target: ${{ matrix.target }} 25 | override: true 26 | - uses: actions-rs/cargo@v1 27 | with: 28 | use-cross: true 29 | command: build 30 | args: --target=${{ matrix.target }} --no-default-features --features json,default-json,cbor -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | # Based on https://github.com/actions-rs/meta/blob/master/recipes/quickstart.md 2 | # 3 | # While our "example" application has the platform-specific code, 4 | # for simplicity we are compiling and testing everything on the Ubuntu environment only. 5 | # For multi-OS testing see the `cross.yml` workflow. 6 | 7 | on: [push, pull_request] 8 | 9 | name: Quickstart 10 | 11 | jobs: 12 | lints: 13 | name: Lints 14 | runs-on: ubuntu-latest 15 | steps: 16 | - name: Checkout sources 17 | uses: actions/checkout@v2 18 | 19 | - name: Install stable toolchain 20 | uses: actions-rs/toolchain@v1 21 | with: 22 | profile: minimal 23 | toolchain: stable 24 | override: true 25 | components: rustfmt 26 | # components: rustfmt, clippy 27 | 28 | - name: Run cargo fmt 29 | uses: actions-rs/cargo@v1 30 | with: 31 | command: fmt 32 | args: --all -- --check 33 | 34 | # TODO enable clippy when unsafe calls are fixed 35 | # - name: Run cargo clippy 36 | # run: cargo clippy -- -D warnings 37 | 38 | check: 39 | name: Check 40 | runs-on: ${{matrix.os}} 41 | strategy: 42 | matrix: 43 | os: [ubuntu-latest, windows-latest, macos-latest] 44 | steps: 45 | - name: Checkout sources 46 | uses: actions/checkout@v2 47 | 48 | - name: Install stable toolchain 49 | uses: actions-rs/toolchain@v1 50 | with: 51 | profile: minimal 52 | toolchain: stable 53 | override: true 54 | 55 | - name: Run cargo check 56 | uses: actions-rs/cargo@v1 57 | with: 58 | command: check 59 | args: --examples 60 | 61 | test: 62 | name: Test Suite 63 | runs-on: ${{matrix.os}} 64 | strategy: 65 | matrix: 66 | os: [ubuntu-latest, windows-latest, macos-latest] 67 | steps: 68 | - name: Checkout sources 69 | uses: actions/checkout@v2 70 | 71 | - name: Install stable toolchain 72 | uses: actions-rs/toolchain@v1 73 | with: 74 | profile: minimal 75 | toolchain: stable 76 | override: true 77 | 78 | - name: Run cargo build 79 | uses: actions-rs/cargo@v1 80 | with: 81 | command: build 82 | args: --examples 83 | 84 | - name: Run cargo test 85 | uses: actions-rs/cargo@v1 86 | with: 87 | command: test 88 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /.bin/ 3 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 2025-05-16, Version v0.1.3 2 | ### Commits 3 | - [[`e6eca94052`](https://github.com/bltavares/axum-content-negotiation/commit/e6eca940521d44df9dfc799f0934fdd291c88615)] chore: Release axum-content-negotiation version 0.1.3 (Bruno Tavares) 4 | - [[`29117d515d`](https://github.com/bltavares/axum-content-negotiation/commit/29117d515df83893dd2fd306b830eae51e1326ea)] Merge pull request #6 from bltavares/optmize-multiple-q-entries (Bruno Tavares) 5 | - [[`fa78b21684`](https://github.com/bltavares/axum-content-negotiation/commit/fa78b2168419e047f0e13d8b37ec7aeb72b9899a)] Optmize the handling of multiple encoding formats (Bruno Tavares) 6 | - [[`c1d44f0014`](https://github.com/bltavares/axum-content-negotiation/commit/c1d44f00146fa7f3cf5e406872d27757effde519)] Merge pull request #5 from notNotDaniel/accept-multiple-mime-types (Bruno Tavares) 7 | - [[`c01536d73c`](https://github.com/bltavares/axum-content-negotiation/commit/c01536d73c45b9addc8ba6afad167c96aca6ba66)] run cargo fmt (Daniel Keller) 8 | - [[`a9848e163d`](https://github.com/bltavares/axum-content-negotiation/commit/a9848e163d64cb2a8d5eb2d7403ea49b93ff71e0)] remove unneeded return statement, to satisfy clippy (Daniel Keller) 9 | - [[`0d1b154520`](https://github.com/bltavares/axum-content-negotiation/commit/0d1b1545201ed28cf40a5140b8f287772e13ad00)] select the correct mime type given equal q values (Daniel Keller) 10 | - [[`d931cffa72`](https://github.com/bltavares/axum-content-negotiation/commit/d931cffa72ea2ca372dc7127e7cff28e50a0f4b6)] support multiple mime-types in the Accept header, and honor q= values (Daniel Keller) 11 | - [[`7be677598b`](https://github.com/bltavares/axum-content-negotiation/commit/7be677598b81de24c111b6adf0d8dcadc5f10321)] Update the changelog (Bruno Tavares) 12 | 13 | ### Stats 14 | ```diff 15 | CHANGELOG.md | 18 +++- 16 | Cargo.lock | 2 +- 17 | Cargo.toml | 2 +- 18 | src/lib.rs | 386 ++++++++++++++++++++++++++++++++++++++++++++++++++++-------- 19 | 4 files changed, 356 insertions(+), 52 deletions(-) 20 | ``` 21 | 22 | 23 | ## 2025-01-05, Version v0.1.2 24 | ### Commits 25 | - [[`8a91d5b6e6`](https://github.com/bltavares/axum-content-negotiation/commit/8a91d5b6e6237bb8037cf7a1f1da973368ec7c56)] chore: Release axum-content-negotiation version 0.1.2 (Bruno Tavares) 26 | - [[`bc151c3f71`](https://github.com/bltavares/axum-content-negotiation/commit/bc151c3f716fbb30722c7d10e02d4195251b02e2)] dev: Include dependency on semver checks for dev tasks (Bruno Tavares) 27 | - [[`724886ce56`](https://github.com/bltavares/axum-content-negotiation/commit/724886ce56610a7c813e0d36408f1460c5e67d56)] Merge pull request #4 from bltavares/upgrade-deps (Bruno Tavares) 28 | - [[`951b3ae8da`](https://github.com/bltavares/axum-content-negotiation/commit/951b3ae8dae00958d99ccc7bc0a5004b7d117938)] Upgrade axum to 0.8.x (Bruno Tavares) 29 | - [[`4352aa5098`](https://github.com/bltavares/axum-content-negotiation/commit/4352aa509899d711f3aac875786459987133fb7c)] Update the changelog (Bruno Tavares) 30 | 31 | ### Stats 32 | ```diff 33 | CHANGELOG.md | 34 ++++++++- 34 | Cargo.lock | 252 ++++++++---------------------------------------------------- 35 | Cargo.toml | 12 +-- 36 | src/lib.rs | 14 +-- 37 | 4 files changed, 85 insertions(+), 227 deletions(-) 38 | ``` 39 | 40 | 41 | ## 2024-04-27, Version v0.1.1 42 | ### Commits 43 | - [[`04cc447c30`](https://github.com/bltavares/axum-content-negotiation/commit/04cc447c30a74e552f31723f6a9845aa8e4251f6)] chore: Release axum-content-negotiation version 0.1.1 (Bruno Tavares) 44 | - [[`4c8aa1eaa5`](https://github.com/bltavares/axum-content-negotiation/commit/4c8aa1eaa5e19e22df38d98f0d941c88f220799f)] Merge pull request #2 from jbourassa/reset-content-length (Bruno Tavares) 45 | - [[`fcdbb2f365`](https://github.com/bltavares/axum-content-negotiation/commit/fcdbb2f36591a0ec55602dd9edc7f2f8677c7f36)] Reset content length (Jimmy Bourassa) 46 | - [[`d88bb45a5c`](https://github.com/bltavares/axum-content-negotiation/commit/d88bb45a5cd9becd834efcec753ce5a428bc0bb5)] Rename variable into a more meaninful name (leftover of lint fixes) (Bruno Tavares) 47 | - [[`3082caf72c`](https://github.com/bltavares/axum-content-negotiation/commit/3082caf72cb4df2ecc05573a90f491f53f4172e9)] Fix branch name on README (Bruno Tavares) 48 | - [[`876826e37d`](https://github.com/bltavares/axum-content-negotiation/commit/876826e37da5b69d7b65089294ceedb20ae7df05)] Merge branch 'Testing' (Bruno Tavares) 49 | - [[`b39117530b`](https://github.com/bltavares/axum-content-negotiation/commit/b39117530b5725b1d7913aa661ced045c441fa13)] README (Bruno Tavares) 50 | - [[`9307225220`](https://github.com/bltavares/axum-content-negotiation/commit/9307225220617a4268f25787c9ab845143b41d0a)] ARMv7 does not have simd (Bruno Tavares) 51 | - [[`e7505419c5`](https://github.com/bltavares/axum-content-negotiation/commit/e7505419c5090619ebc83057308ca541451d3828)] Remove MIPs from CI as it's not stable anymore (Bruno Tavares) 52 | - [[`fb7a940e97`](https://github.com/bltavares/axum-content-negotiation/commit/fb7a940e9726a6546582d0c11a428426153fbcbc)] Initial GH Actions setup (Bruno Tavares) 53 | - [[`55238224b1`](https://github.com/bltavares/axum-content-negotiation/commit/55238224b19977f1a2894a67a6e5a29ad70d0839)] Documentation (Bruno Tavares) 54 | - [[`cd83102e32`](https://github.com/bltavares/axum-content-negotiation/commit/cd83102e32e3be714e00b7a193e0ad4cf1c140e5)] Preparation for release (Bruno Tavares) 55 | - [[`730c0dec4c`](https://github.com/bltavares/axum-content-negotiation/commit/730c0dec4ccdc44316a28dce65def0b4831abf28)] More tests (Bruno Tavares) 56 | - [[`63acdfba38`](https://github.com/bltavares/axum-content-negotiation/commit/63acdfba38f4e75efdb049ca3e369bcd414f3326)] Initial commit (Bruno Tavares) 57 | 58 | ### Stats 59 | ```diff 60 | .github/workflows/cross_compile.yml | 30 +- 61 | .github/workflows/main.yml | 87 +++- 62 | .gitignore | 2 +- 63 | Cargo.lock | 1145 ++++++++++++++++++++++++++++++++++++- 64 | Cargo.toml | 45 +- 65 | LICENSE-APACHE | 201 ++++++- 66 | LICENSE-MIT | 21 +- 67 | Makefile | 48 ++- 68 | README.md | 120 ++++- 69 | bacon.toml | 81 +++- 70 | src/lib.rs | 940 ++++++++++++++++++++++++++++++- 71 | 11 files changed, 2720 insertions(+) 72 | ``` 73 | 74 | 75 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 4 4 | 5 | [[package]] 6 | name = "addr2line" 7 | version = "0.21.0" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "8a30b2e23b9e17a9f90641c7ab1549cd9b44f296d3ccbf309d2863cfe398a0cb" 10 | dependencies = [ 11 | "gimli", 12 | ] 13 | 14 | [[package]] 15 | name = "adler" 16 | version = "1.0.2" 17 | source = "registry+https://github.com/rust-lang/crates.io-index" 18 | checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" 19 | 20 | [[package]] 21 | name = "ahash" 22 | version = "0.8.7" 23 | source = "registry+https://github.com/rust-lang/crates.io-index" 24 | checksum = "77c3a9648d43b9cd48db467b3f87fdd6e146bcc88ab0180006cef2179fe11d01" 25 | dependencies = [ 26 | "cfg-if", 27 | "once_cell", 28 | "version_check", 29 | "zerocopy", 30 | ] 31 | 32 | [[package]] 33 | name = "autocfg" 34 | version = "1.1.0" 35 | source = "registry+https://github.com/rust-lang/crates.io-index" 36 | checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" 37 | 38 | [[package]] 39 | name = "axum" 40 | version = "0.8.1" 41 | source = "registry+https://github.com/rust-lang/crates.io-index" 42 | checksum = "6d6fd624c75e18b3b4c6b9caf42b1afe24437daaee904069137d8bab077be8b8" 43 | dependencies = [ 44 | "axum-core", 45 | "axum-macros", 46 | "bytes", 47 | "form_urlencoded", 48 | "futures-util", 49 | "http", 50 | "http-body", 51 | "http-body-util", 52 | "hyper", 53 | "hyper-util", 54 | "itoa", 55 | "matchit", 56 | "memchr", 57 | "mime", 58 | "percent-encoding", 59 | "pin-project-lite", 60 | "rustversion", 61 | "serde", 62 | "serde_json", 63 | "serde_path_to_error", 64 | "serde_urlencoded", 65 | "sync_wrapper", 66 | "tokio", 67 | "tower", 68 | "tower-layer", 69 | "tower-service", 70 | "tracing", 71 | ] 72 | 73 | [[package]] 74 | name = "axum-content-negotiation" 75 | version = "0.1.3" 76 | dependencies = [ 77 | "axum", 78 | "cbor4ii", 79 | "erased-serde", 80 | "http-body-util", 81 | "serde", 82 | "serde_json", 83 | "simd-json", 84 | "tokio", 85 | "tower", 86 | "tracing", 87 | ] 88 | 89 | [[package]] 90 | name = "axum-core" 91 | version = "0.5.0" 92 | source = "registry+https://github.com/rust-lang/crates.io-index" 93 | checksum = "df1362f362fd16024ae199c1970ce98f9661bf5ef94b9808fee734bc3698b733" 94 | dependencies = [ 95 | "bytes", 96 | "futures-util", 97 | "http", 98 | "http-body", 99 | "http-body-util", 100 | "mime", 101 | "pin-project-lite", 102 | "rustversion", 103 | "sync_wrapper", 104 | "tower-layer", 105 | "tower-service", 106 | "tracing", 107 | ] 108 | 109 | [[package]] 110 | name = "axum-macros" 111 | version = "0.5.0" 112 | source = "registry+https://github.com/rust-lang/crates.io-index" 113 | checksum = "604fde5e028fea851ce1d8570bbdc034bec850d157f7569d10f347d06808c05c" 114 | dependencies = [ 115 | "proc-macro2", 116 | "quote", 117 | "syn", 118 | ] 119 | 120 | [[package]] 121 | name = "backtrace" 122 | version = "0.3.69" 123 | source = "registry+https://github.com/rust-lang/crates.io-index" 124 | checksum = "2089b7e3f35b9dd2d0ed921ead4f6d318c27680d4a5bd167b3ee120edb105837" 125 | dependencies = [ 126 | "addr2line", 127 | "cc", 128 | "cfg-if", 129 | "libc", 130 | "miniz_oxide", 131 | "object", 132 | "rustc-demangle", 133 | ] 134 | 135 | [[package]] 136 | name = "bitflags" 137 | version = "1.3.2" 138 | source = "registry+https://github.com/rust-lang/crates.io-index" 139 | checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" 140 | 141 | [[package]] 142 | name = "bumpalo" 143 | version = "3.14.0" 144 | source = "registry+https://github.com/rust-lang/crates.io-index" 145 | checksum = "7f30e7476521f6f8af1a1c4c0b8cc94f0bee37d91763d0ca2665f299b6cd8aec" 146 | 147 | [[package]] 148 | name = "bytes" 149 | version = "1.9.0" 150 | source = "registry+https://github.com/rust-lang/crates.io-index" 151 | checksum = "325918d6fe32f23b19878fe4b34794ae41fc19ddbe53b10571a4874d44ffd39b" 152 | 153 | [[package]] 154 | name = "cbor4ii" 155 | version = "0.3.2" 156 | source = "registry+https://github.com/rust-lang/crates.io-index" 157 | checksum = "59b4c883b9cc4757b061600d39001d4d0232bece4a3174696cf8f58a14db107d" 158 | dependencies = [ 159 | "serde", 160 | ] 161 | 162 | [[package]] 163 | name = "cc" 164 | version = "1.0.83" 165 | source = "registry+https://github.com/rust-lang/crates.io-index" 166 | checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0" 167 | dependencies = [ 168 | "libc", 169 | ] 170 | 171 | [[package]] 172 | name = "cfg-if" 173 | version = "1.0.0" 174 | source = "registry+https://github.com/rust-lang/crates.io-index" 175 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 176 | 177 | [[package]] 178 | name = "erased-serde" 179 | version = "0.4.2" 180 | source = "registry+https://github.com/rust-lang/crates.io-index" 181 | checksum = "55d05712b2d8d88102bc9868020c9e5c7a1f5527c452b9b97450a1d006140ba7" 182 | dependencies = [ 183 | "serde", 184 | ] 185 | 186 | [[package]] 187 | name = "float-cmp" 188 | version = "0.10.0" 189 | source = "registry+https://github.com/rust-lang/crates.io-index" 190 | checksum = "b09cf3155332e944990140d967ff5eceb70df778b34f77d8075db46e4704e6d8" 191 | dependencies = [ 192 | "num-traits", 193 | ] 194 | 195 | [[package]] 196 | name = "fnv" 197 | version = "1.0.7" 198 | source = "registry+https://github.com/rust-lang/crates.io-index" 199 | checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" 200 | 201 | [[package]] 202 | name = "form_urlencoded" 203 | version = "1.2.1" 204 | source = "registry+https://github.com/rust-lang/crates.io-index" 205 | checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" 206 | dependencies = [ 207 | "percent-encoding", 208 | ] 209 | 210 | [[package]] 211 | name = "futures-channel" 212 | version = "0.3.30" 213 | source = "registry+https://github.com/rust-lang/crates.io-index" 214 | checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78" 215 | dependencies = [ 216 | "futures-core", 217 | ] 218 | 219 | [[package]] 220 | name = "futures-core" 221 | version = "0.3.30" 222 | source = "registry+https://github.com/rust-lang/crates.io-index" 223 | checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" 224 | 225 | [[package]] 226 | name = "futures-task" 227 | version = "0.3.30" 228 | source = "registry+https://github.com/rust-lang/crates.io-index" 229 | checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004" 230 | 231 | [[package]] 232 | name = "futures-util" 233 | version = "0.3.30" 234 | source = "registry+https://github.com/rust-lang/crates.io-index" 235 | checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" 236 | dependencies = [ 237 | "futures-core", 238 | "futures-task", 239 | "pin-project-lite", 240 | "pin-utils", 241 | ] 242 | 243 | [[package]] 244 | name = "getrandom" 245 | version = "0.2.12" 246 | source = "registry+https://github.com/rust-lang/crates.io-index" 247 | checksum = "190092ea657667030ac6a35e305e62fc4dd69fd98ac98631e5d3a2b1575a12b5" 248 | dependencies = [ 249 | "cfg-if", 250 | "js-sys", 251 | "libc", 252 | "wasi", 253 | "wasm-bindgen", 254 | ] 255 | 256 | [[package]] 257 | name = "gimli" 258 | version = "0.28.1" 259 | source = "registry+https://github.com/rust-lang/crates.io-index" 260 | checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253" 261 | 262 | [[package]] 263 | name = "halfbrown" 264 | version = "0.2.4" 265 | source = "registry+https://github.com/rust-lang/crates.io-index" 266 | checksum = "5681137554ddff44396e5f149892c769d45301dd9aa19c51602a89ee214cb0ec" 267 | dependencies = [ 268 | "hashbrown", 269 | "serde", 270 | ] 271 | 272 | [[package]] 273 | name = "hashbrown" 274 | version = "0.13.2" 275 | source = "registry+https://github.com/rust-lang/crates.io-index" 276 | checksum = "43a3c133739dddd0d2990f9a4bdf8eb4b21ef50e4851ca85ab661199821d510e" 277 | dependencies = [ 278 | "ahash", 279 | ] 280 | 281 | [[package]] 282 | name = "hermit-abi" 283 | version = "0.3.4" 284 | source = "registry+https://github.com/rust-lang/crates.io-index" 285 | checksum = "5d3d0e0f38255e7fa3cf31335b3a56f05febd18025f4db5ef7a0cfb4f8da651f" 286 | 287 | [[package]] 288 | name = "http" 289 | version = "1.0.0" 290 | source = "registry+https://github.com/rust-lang/crates.io-index" 291 | checksum = "b32afd38673a8016f7c9ae69e5af41a58f81b1d31689040f2f1959594ce194ea" 292 | dependencies = [ 293 | "bytes", 294 | "fnv", 295 | "itoa", 296 | ] 297 | 298 | [[package]] 299 | name = "http-body" 300 | version = "1.0.0" 301 | source = "registry+https://github.com/rust-lang/crates.io-index" 302 | checksum = "1cac85db508abc24a2e48553ba12a996e87244a0395ce011e62b37158745d643" 303 | dependencies = [ 304 | "bytes", 305 | "http", 306 | ] 307 | 308 | [[package]] 309 | name = "http-body-util" 310 | version = "0.1.0" 311 | source = "registry+https://github.com/rust-lang/crates.io-index" 312 | checksum = "41cb79eb393015dadd30fc252023adb0b2400a0caee0fa2a077e6e21a551e840" 313 | dependencies = [ 314 | "bytes", 315 | "futures-util", 316 | "http", 317 | "http-body", 318 | "pin-project-lite", 319 | ] 320 | 321 | [[package]] 322 | name = "httparse" 323 | version = "1.8.0" 324 | source = "registry+https://github.com/rust-lang/crates.io-index" 325 | checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904" 326 | 327 | [[package]] 328 | name = "httpdate" 329 | version = "1.0.3" 330 | source = "registry+https://github.com/rust-lang/crates.io-index" 331 | checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" 332 | 333 | [[package]] 334 | name = "hyper" 335 | version = "1.5.2" 336 | source = "registry+https://github.com/rust-lang/crates.io-index" 337 | checksum = "256fb8d4bd6413123cc9d91832d78325c48ff41677595be797d90f42969beae0" 338 | dependencies = [ 339 | "bytes", 340 | "futures-channel", 341 | "futures-util", 342 | "http", 343 | "http-body", 344 | "httparse", 345 | "httpdate", 346 | "itoa", 347 | "pin-project-lite", 348 | "smallvec", 349 | "tokio", 350 | ] 351 | 352 | [[package]] 353 | name = "hyper-util" 354 | version = "0.1.10" 355 | source = "registry+https://github.com/rust-lang/crates.io-index" 356 | checksum = "df2dcfbe0677734ab2f3ffa7fa7bfd4706bfdc1ef393f2ee30184aed67e631b4" 357 | dependencies = [ 358 | "bytes", 359 | "futures-util", 360 | "http", 361 | "http-body", 362 | "hyper", 363 | "pin-project-lite", 364 | "tokio", 365 | "tower-service", 366 | ] 367 | 368 | [[package]] 369 | name = "itoa" 370 | version = "1.0.10" 371 | source = "registry+https://github.com/rust-lang/crates.io-index" 372 | checksum = "b1a46d1a171d865aa5f83f92695765caa047a9b4cbae2cbf37dbd613a793fd4c" 373 | 374 | [[package]] 375 | name = "js-sys" 376 | version = "0.3.67" 377 | source = "registry+https://github.com/rust-lang/crates.io-index" 378 | checksum = "9a1d36f1235bc969acba30b7f5990b864423a6068a10f7c90ae8f0112e3a59d1" 379 | dependencies = [ 380 | "wasm-bindgen", 381 | ] 382 | 383 | [[package]] 384 | name = "libc" 385 | version = "0.2.152" 386 | source = "registry+https://github.com/rust-lang/crates.io-index" 387 | checksum = "13e3bf6590cbc649f4d1a3eefc9d5d6eb746f5200ffb04e5e142700b8faa56e7" 388 | 389 | [[package]] 390 | name = "lock_api" 391 | version = "0.4.11" 392 | source = "registry+https://github.com/rust-lang/crates.io-index" 393 | checksum = "3c168f8615b12bc01f9c17e2eb0cc07dcae1940121185446edc3744920e8ef45" 394 | dependencies = [ 395 | "autocfg", 396 | "scopeguard", 397 | ] 398 | 399 | [[package]] 400 | name = "log" 401 | version = "0.4.20" 402 | source = "registry+https://github.com/rust-lang/crates.io-index" 403 | checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" 404 | 405 | [[package]] 406 | name = "matchit" 407 | version = "0.8.4" 408 | source = "registry+https://github.com/rust-lang/crates.io-index" 409 | checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" 410 | 411 | [[package]] 412 | name = "memchr" 413 | version = "2.7.1" 414 | source = "registry+https://github.com/rust-lang/crates.io-index" 415 | checksum = "523dc4f511e55ab87b694dc30d0f820d60906ef06413f93d4d7a1385599cc149" 416 | 417 | [[package]] 418 | name = "mime" 419 | version = "0.3.17" 420 | source = "registry+https://github.com/rust-lang/crates.io-index" 421 | checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" 422 | 423 | [[package]] 424 | name = "miniz_oxide" 425 | version = "0.7.1" 426 | source = "registry+https://github.com/rust-lang/crates.io-index" 427 | checksum = "e7810e0be55b428ada41041c41f32c9f1a42817901b4ccf45fa3d4b6561e74c7" 428 | dependencies = [ 429 | "adler", 430 | ] 431 | 432 | [[package]] 433 | name = "mio" 434 | version = "0.8.10" 435 | source = "registry+https://github.com/rust-lang/crates.io-index" 436 | checksum = "8f3d0b296e374a4e6f3c7b0a1f5a51d748a0d34c85e7dc48fc3fa9a87657fe09" 437 | dependencies = [ 438 | "libc", 439 | "wasi", 440 | "windows-sys", 441 | ] 442 | 443 | [[package]] 444 | name = "num-traits" 445 | version = "0.2.17" 446 | source = "registry+https://github.com/rust-lang/crates.io-index" 447 | checksum = "39e3200413f237f41ab11ad6d161bc7239c84dcb631773ccd7de3dfe4b5c267c" 448 | dependencies = [ 449 | "autocfg", 450 | ] 451 | 452 | [[package]] 453 | name = "num_cpus" 454 | version = "1.16.0" 455 | source = "registry+https://github.com/rust-lang/crates.io-index" 456 | checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" 457 | dependencies = [ 458 | "hermit-abi", 459 | "libc", 460 | ] 461 | 462 | [[package]] 463 | name = "object" 464 | version = "0.32.2" 465 | source = "registry+https://github.com/rust-lang/crates.io-index" 466 | checksum = "a6a622008b6e321afc04970976f62ee297fdbaa6f95318ca343e3eebb9648441" 467 | dependencies = [ 468 | "memchr", 469 | ] 470 | 471 | [[package]] 472 | name = "once_cell" 473 | version = "1.19.0" 474 | source = "registry+https://github.com/rust-lang/crates.io-index" 475 | checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" 476 | 477 | [[package]] 478 | name = "parking_lot" 479 | version = "0.12.1" 480 | source = "registry+https://github.com/rust-lang/crates.io-index" 481 | checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" 482 | dependencies = [ 483 | "lock_api", 484 | "parking_lot_core", 485 | ] 486 | 487 | [[package]] 488 | name = "parking_lot_core" 489 | version = "0.9.9" 490 | source = "registry+https://github.com/rust-lang/crates.io-index" 491 | checksum = "4c42a9226546d68acdd9c0a280d17ce19bfe27a46bf68784e4066115788d008e" 492 | dependencies = [ 493 | "cfg-if", 494 | "libc", 495 | "redox_syscall", 496 | "smallvec", 497 | "windows-targets", 498 | ] 499 | 500 | [[package]] 501 | name = "percent-encoding" 502 | version = "2.3.1" 503 | source = "registry+https://github.com/rust-lang/crates.io-index" 504 | checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" 505 | 506 | [[package]] 507 | name = "pin-project-lite" 508 | version = "0.2.13" 509 | source = "registry+https://github.com/rust-lang/crates.io-index" 510 | checksum = "8afb450f006bf6385ca15ef45d71d2288452bc3683ce2e2cacc0d18e4be60b58" 511 | 512 | [[package]] 513 | name = "pin-utils" 514 | version = "0.1.0" 515 | source = "registry+https://github.com/rust-lang/crates.io-index" 516 | checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" 517 | 518 | [[package]] 519 | name = "proc-macro2" 520 | version = "1.0.76" 521 | source = "registry+https://github.com/rust-lang/crates.io-index" 522 | checksum = "95fc56cda0b5c3325f5fbbd7ff9fda9e02bb00bb3dac51252d2f1bfa1cb8cc8c" 523 | dependencies = [ 524 | "unicode-ident", 525 | ] 526 | 527 | [[package]] 528 | name = "quote" 529 | version = "1.0.35" 530 | source = "registry+https://github.com/rust-lang/crates.io-index" 531 | checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef" 532 | dependencies = [ 533 | "proc-macro2", 534 | ] 535 | 536 | [[package]] 537 | name = "redox_syscall" 538 | version = "0.4.1" 539 | source = "registry+https://github.com/rust-lang/crates.io-index" 540 | checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" 541 | dependencies = [ 542 | "bitflags", 543 | ] 544 | 545 | [[package]] 546 | name = "ref-cast" 547 | version = "1.0.22" 548 | source = "registry+https://github.com/rust-lang/crates.io-index" 549 | checksum = "c4846d4c50d1721b1a3bef8af76924eef20d5e723647333798c1b519b3a9473f" 550 | dependencies = [ 551 | "ref-cast-impl", 552 | ] 553 | 554 | [[package]] 555 | name = "ref-cast-impl" 556 | version = "1.0.22" 557 | source = "registry+https://github.com/rust-lang/crates.io-index" 558 | checksum = "5fddb4f8d99b0a2ebafc65a87a69a7b9875e4b1ae1f00db265d300ef7f28bccc" 559 | dependencies = [ 560 | "proc-macro2", 561 | "quote", 562 | "syn", 563 | ] 564 | 565 | [[package]] 566 | name = "rustc-demangle" 567 | version = "0.1.23" 568 | source = "registry+https://github.com/rust-lang/crates.io-index" 569 | checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" 570 | 571 | [[package]] 572 | name = "rustversion" 573 | version = "1.0.14" 574 | source = "registry+https://github.com/rust-lang/crates.io-index" 575 | checksum = "7ffc183a10b4478d04cbbbfc96d0873219d962dd5accaff2ffbd4ceb7df837f4" 576 | 577 | [[package]] 578 | name = "ryu" 579 | version = "1.0.16" 580 | source = "registry+https://github.com/rust-lang/crates.io-index" 581 | checksum = "f98d2aa92eebf49b69786be48e4477826b256916e84a57ff2a4f21923b48eb4c" 582 | 583 | [[package]] 584 | name = "scopeguard" 585 | version = "1.2.0" 586 | source = "registry+https://github.com/rust-lang/crates.io-index" 587 | checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" 588 | 589 | [[package]] 590 | name = "serde" 591 | version = "1.0.195" 592 | source = "registry+https://github.com/rust-lang/crates.io-index" 593 | checksum = "63261df402c67811e9ac6def069e4786148c4563f4b50fd4bf30aa370d626b02" 594 | dependencies = [ 595 | "serde_derive", 596 | ] 597 | 598 | [[package]] 599 | name = "serde_derive" 600 | version = "1.0.195" 601 | source = "registry+https://github.com/rust-lang/crates.io-index" 602 | checksum = "46fe8f8603d81ba86327b23a2e9cdf49e1255fb94a4c5f297f6ee0547178ea2c" 603 | dependencies = [ 604 | "proc-macro2", 605 | "quote", 606 | "syn", 607 | ] 608 | 609 | [[package]] 610 | name = "serde_json" 611 | version = "1.0.111" 612 | source = "registry+https://github.com/rust-lang/crates.io-index" 613 | checksum = "176e46fa42316f18edd598015a5166857fc835ec732f5215eac6b7bdbf0a84f4" 614 | dependencies = [ 615 | "itoa", 616 | "ryu", 617 | "serde", 618 | ] 619 | 620 | [[package]] 621 | name = "serde_path_to_error" 622 | version = "0.1.15" 623 | source = "registry+https://github.com/rust-lang/crates.io-index" 624 | checksum = "ebd154a240de39fdebcf5775d2675c204d7c13cf39a4c697be6493c8e734337c" 625 | dependencies = [ 626 | "itoa", 627 | "serde", 628 | ] 629 | 630 | [[package]] 631 | name = "serde_urlencoded" 632 | version = "0.7.1" 633 | source = "registry+https://github.com/rust-lang/crates.io-index" 634 | checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" 635 | dependencies = [ 636 | "form_urlencoded", 637 | "itoa", 638 | "ryu", 639 | "serde", 640 | ] 641 | 642 | [[package]] 643 | name = "signal-hook-registry" 644 | version = "1.4.1" 645 | source = "registry+https://github.com/rust-lang/crates.io-index" 646 | checksum = "d8229b473baa5980ac72ef434c4415e70c4b5e71b423043adb4ba059f89c99a1" 647 | dependencies = [ 648 | "libc", 649 | ] 650 | 651 | [[package]] 652 | name = "simd-json" 653 | version = "0.14.3" 654 | source = "registry+https://github.com/rust-lang/crates.io-index" 655 | checksum = "aa2bcf6c6e164e81bc7a5d49fc6988b3d515d9e8c07457d7b74ffb9324b9cd40" 656 | dependencies = [ 657 | "getrandom", 658 | "halfbrown", 659 | "ref-cast", 660 | "serde", 661 | "serde_json", 662 | "simdutf8", 663 | "value-trait", 664 | ] 665 | 666 | [[package]] 667 | name = "simdutf8" 668 | version = "0.1.4" 669 | source = "registry+https://github.com/rust-lang/crates.io-index" 670 | checksum = "f27f6278552951f1f2b8cf9da965d10969b2efdea95a6ec47987ab46edfe263a" 671 | 672 | [[package]] 673 | name = "smallvec" 674 | version = "1.13.1" 675 | source = "registry+https://github.com/rust-lang/crates.io-index" 676 | checksum = "e6ecd384b10a64542d77071bd64bd7b231f4ed5940fba55e98c3de13824cf3d7" 677 | 678 | [[package]] 679 | name = "socket2" 680 | version = "0.5.5" 681 | source = "registry+https://github.com/rust-lang/crates.io-index" 682 | checksum = "7b5fac59a5cb5dd637972e5fca70daf0523c9067fcdc4842f053dae04a18f8e9" 683 | dependencies = [ 684 | "libc", 685 | "windows-sys", 686 | ] 687 | 688 | [[package]] 689 | name = "syn" 690 | version = "2.0.48" 691 | source = "registry+https://github.com/rust-lang/crates.io-index" 692 | checksum = "0f3531638e407dfc0814761abb7c00a5b54992b849452a0646b7f65c9f770f3f" 693 | dependencies = [ 694 | "proc-macro2", 695 | "quote", 696 | "unicode-ident", 697 | ] 698 | 699 | [[package]] 700 | name = "sync_wrapper" 701 | version = "1.0.2" 702 | source = "registry+https://github.com/rust-lang/crates.io-index" 703 | checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" 704 | 705 | [[package]] 706 | name = "tokio" 707 | version = "1.35.1" 708 | source = "registry+https://github.com/rust-lang/crates.io-index" 709 | checksum = "c89b4efa943be685f629b149f53829423f8f5531ea21249408e8e2f8671ec104" 710 | dependencies = [ 711 | "backtrace", 712 | "bytes", 713 | "libc", 714 | "mio", 715 | "num_cpus", 716 | "parking_lot", 717 | "pin-project-lite", 718 | "signal-hook-registry", 719 | "socket2", 720 | "tokio-macros", 721 | "windows-sys", 722 | ] 723 | 724 | [[package]] 725 | name = "tokio-macros" 726 | version = "2.2.0" 727 | source = "registry+https://github.com/rust-lang/crates.io-index" 728 | checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b" 729 | dependencies = [ 730 | "proc-macro2", 731 | "quote", 732 | "syn", 733 | ] 734 | 735 | [[package]] 736 | name = "tower" 737 | version = "0.5.2" 738 | source = "registry+https://github.com/rust-lang/crates.io-index" 739 | checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" 740 | dependencies = [ 741 | "futures-core", 742 | "futures-util", 743 | "pin-project-lite", 744 | "sync_wrapper", 745 | "tokio", 746 | "tower-layer", 747 | "tower-service", 748 | "tracing", 749 | ] 750 | 751 | [[package]] 752 | name = "tower-layer" 753 | version = "0.3.3" 754 | source = "registry+https://github.com/rust-lang/crates.io-index" 755 | checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" 756 | 757 | [[package]] 758 | name = "tower-service" 759 | version = "0.3.3" 760 | source = "registry+https://github.com/rust-lang/crates.io-index" 761 | checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" 762 | 763 | [[package]] 764 | name = "tracing" 765 | version = "0.1.40" 766 | source = "registry+https://github.com/rust-lang/crates.io-index" 767 | checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" 768 | dependencies = [ 769 | "log", 770 | "pin-project-lite", 771 | "tracing-attributes", 772 | "tracing-core", 773 | ] 774 | 775 | [[package]] 776 | name = "tracing-attributes" 777 | version = "0.1.27" 778 | source = "registry+https://github.com/rust-lang/crates.io-index" 779 | checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" 780 | dependencies = [ 781 | "proc-macro2", 782 | "quote", 783 | "syn", 784 | ] 785 | 786 | [[package]] 787 | name = "tracing-core" 788 | version = "0.1.32" 789 | source = "registry+https://github.com/rust-lang/crates.io-index" 790 | checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" 791 | dependencies = [ 792 | "once_cell", 793 | ] 794 | 795 | [[package]] 796 | name = "unicode-ident" 797 | version = "1.0.12" 798 | source = "registry+https://github.com/rust-lang/crates.io-index" 799 | checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" 800 | 801 | [[package]] 802 | name = "value-trait" 803 | version = "0.10.1" 804 | source = "registry+https://github.com/rust-lang/crates.io-index" 805 | checksum = "9170e001f458781e92711d2ad666110f153e4e50bfd5cbd02db6547625714187" 806 | dependencies = [ 807 | "float-cmp", 808 | "halfbrown", 809 | "itoa", 810 | "ryu", 811 | ] 812 | 813 | [[package]] 814 | name = "version_check" 815 | version = "0.9.4" 816 | source = "registry+https://github.com/rust-lang/crates.io-index" 817 | checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" 818 | 819 | [[package]] 820 | name = "wasi" 821 | version = "0.11.0+wasi-snapshot-preview1" 822 | source = "registry+https://github.com/rust-lang/crates.io-index" 823 | checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" 824 | 825 | [[package]] 826 | name = "wasm-bindgen" 827 | version = "0.2.90" 828 | source = "registry+https://github.com/rust-lang/crates.io-index" 829 | checksum = "b1223296a201415c7fad14792dbefaace9bd52b62d33453ade1c5b5f07555406" 830 | dependencies = [ 831 | "cfg-if", 832 | "wasm-bindgen-macro", 833 | ] 834 | 835 | [[package]] 836 | name = "wasm-bindgen-backend" 837 | version = "0.2.90" 838 | source = "registry+https://github.com/rust-lang/crates.io-index" 839 | checksum = "fcdc935b63408d58a32f8cc9738a0bffd8f05cc7c002086c6ef20b7312ad9dcd" 840 | dependencies = [ 841 | "bumpalo", 842 | "log", 843 | "once_cell", 844 | "proc-macro2", 845 | "quote", 846 | "syn", 847 | "wasm-bindgen-shared", 848 | ] 849 | 850 | [[package]] 851 | name = "wasm-bindgen-macro" 852 | version = "0.2.90" 853 | source = "registry+https://github.com/rust-lang/crates.io-index" 854 | checksum = "3e4c238561b2d428924c49815533a8b9121c664599558a5d9ec51f8a1740a999" 855 | dependencies = [ 856 | "quote", 857 | "wasm-bindgen-macro-support", 858 | ] 859 | 860 | [[package]] 861 | name = "wasm-bindgen-macro-support" 862 | version = "0.2.90" 863 | source = "registry+https://github.com/rust-lang/crates.io-index" 864 | checksum = "bae1abb6806dc1ad9e560ed242107c0f6c84335f1749dd4e8ddb012ebd5e25a7" 865 | dependencies = [ 866 | "proc-macro2", 867 | "quote", 868 | "syn", 869 | "wasm-bindgen-backend", 870 | "wasm-bindgen-shared", 871 | ] 872 | 873 | [[package]] 874 | name = "wasm-bindgen-shared" 875 | version = "0.2.90" 876 | source = "registry+https://github.com/rust-lang/crates.io-index" 877 | checksum = "4d91413b1c31d7539ba5ef2451af3f0b833a005eb27a631cec32bc0635a8602b" 878 | 879 | [[package]] 880 | name = "windows-sys" 881 | version = "0.48.0" 882 | source = "registry+https://github.com/rust-lang/crates.io-index" 883 | checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" 884 | dependencies = [ 885 | "windows-targets", 886 | ] 887 | 888 | [[package]] 889 | name = "windows-targets" 890 | version = "0.48.5" 891 | source = "registry+https://github.com/rust-lang/crates.io-index" 892 | checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" 893 | dependencies = [ 894 | "windows_aarch64_gnullvm", 895 | "windows_aarch64_msvc", 896 | "windows_i686_gnu", 897 | "windows_i686_msvc", 898 | "windows_x86_64_gnu", 899 | "windows_x86_64_gnullvm", 900 | "windows_x86_64_msvc", 901 | ] 902 | 903 | [[package]] 904 | name = "windows_aarch64_gnullvm" 905 | version = "0.48.5" 906 | source = "registry+https://github.com/rust-lang/crates.io-index" 907 | checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" 908 | 909 | [[package]] 910 | name = "windows_aarch64_msvc" 911 | version = "0.48.5" 912 | source = "registry+https://github.com/rust-lang/crates.io-index" 913 | checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" 914 | 915 | [[package]] 916 | name = "windows_i686_gnu" 917 | version = "0.48.5" 918 | source = "registry+https://github.com/rust-lang/crates.io-index" 919 | checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" 920 | 921 | [[package]] 922 | name = "windows_i686_msvc" 923 | version = "0.48.5" 924 | source = "registry+https://github.com/rust-lang/crates.io-index" 925 | checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" 926 | 927 | [[package]] 928 | name = "windows_x86_64_gnu" 929 | version = "0.48.5" 930 | source = "registry+https://github.com/rust-lang/crates.io-index" 931 | checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" 932 | 933 | [[package]] 934 | name = "windows_x86_64_gnullvm" 935 | version = "0.48.5" 936 | source = "registry+https://github.com/rust-lang/crates.io-index" 937 | checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" 938 | 939 | [[package]] 940 | name = "windows_x86_64_msvc" 941 | version = "0.48.5" 942 | source = "registry+https://github.com/rust-lang/crates.io-index" 943 | checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" 944 | 945 | [[package]] 946 | name = "zerocopy" 947 | version = "0.7.32" 948 | source = "registry+https://github.com/rust-lang/crates.io-index" 949 | checksum = "74d4d3961e53fa4c9a25a8637fc2bfaf2595b3d3ae34875568a5cf64787716be" 950 | dependencies = [ 951 | "zerocopy-derive", 952 | ] 953 | 954 | [[package]] 955 | name = "zerocopy-derive" 956 | version = "0.7.32" 957 | source = "registry+https://github.com/rust-lang/crates.io-index" 958 | checksum = "9ce1b18ccd8e73a9321186f97e46f9f04b778851177567b1975109d26a08d2a6" 959 | dependencies = [ 960 | "proc-macro2", 961 | "quote", 962 | "syn", 963 | ] 964 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "axum-content-negotiation" 3 | description = "Axum middleware to use Accept and Content-Type headers to serialize with different formats" 4 | keywords = ["axum-middleware", "middleware", "content-negotiation"] 5 | 6 | version = "0.1.3" 7 | edition = "2021" 8 | 9 | authors = ["Bruno Tavares "] 10 | license = "MIT OR Apache-2.0" 11 | homepage = "https://github.com/bltavares/axum-content-negotiation" 12 | repository = "https://github.com/bltavares/axum-content-negotiation" 13 | 14 | [dependencies] 15 | axum = "0.8.1" 16 | tower = "0.5.2" 17 | serde = "1.0.195" 18 | erased-serde = "0.4.2" 19 | 20 | serde_json = { version = "1.0.111", optional = true } 21 | simd-json = { version = "0.14.3", optional = true } 22 | 23 | cbor4ii = { version = "0.3.2", optional = true, features = ["serde1"] } 24 | 25 | tracing = "0.1.40" 26 | 27 | [features] 28 | default = ["cbor", "simd-json", "default-json"] 29 | json = ["serde_json"] 30 | simd-json = ["dep:simd-json", "serde_json"] 31 | cbor = ["cbor4ii"] 32 | default-json = [] 33 | default-cbor = [] 34 | 35 | [dev-dependencies] 36 | http-body-util = "0.1.0" 37 | tokio = { version = "1.35.1", features = ["full"] } 38 | axum = { version = "0.8.1", features = ["macros"] } 39 | serde = { version = "1.0.195", features = ["derive"] } 40 | 41 | # Used by `cargo-run-bin` 42 | [package.metadata.bin] 43 | cargo-binstall = { version = "1.6.1" } 44 | cargo-nextest = { version = "0.9.67", locked = true } 45 | bacon = { version = "2.14.1", locked = true } 46 | changelog = { version = "0.3.4" } 47 | cargo-semver-checks = { version = "0.38.0" } 48 | -------------------------------------------------------------------------------- /LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2018 Bruno Tavares 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2023 Bruno Tavares 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | 2 | # Default action: check 3 | default: check 4 | .PHONY: default 5 | 6 | # Runs cargo fmt and cargo clippy 7 | lint: 8 | cargo fmt --all --check 9 | cargo clippy --all -- -D warnings 10 | cargo clippy --all --no-default-features --features json,default-json -- -D warnings 11 | .PHONY: lint 12 | 13 | # Fix lint issues when possible 14 | lint-fix: 15 | cargo fmt --all 16 | cargo clippy --fix --all --allow-dirty --allow-staged -- -D warnings 17 | cargo clippy --fix --all --no-default-features --features json,default-json -- -D warnings 18 | cargo clippy --fix --all --no-default-features --features cbor,default-cbor -- -D warnings 19 | .PHONY: lint-fix 20 | 21 | # Be really annoying about lints 22 | lint-pedantic: 23 | cargo clippy -- -D clippy::pedantic 24 | cargo clippy --no-default-features --features json,default-json -- -D clippy::pedantic 25 | .PHONY: lint-pedantic 26 | 27 | # Run cargo check for quick compilation instead of full build 28 | check: 29 | cargo check --all 30 | cargo check --all --no-default-features --features json,default-json 31 | .PHONY: check 32 | 33 | # Run all tests 34 | test: 35 | cargo bin cargo-nextest run --all 36 | cargo bin cargo-nextest run --all --no-default-features --features json,default-json 37 | cargo bin cargo-nextest run --all --no-default-features --features cbor,default-cbor 38 | .PHONY: test 39 | 40 | # Build docs locally 41 | doc: 42 | cargo doc --no-deps --all --open 43 | .PHONY: doc 44 | 45 | # Generate new entry for CHANGELOG.md 46 | changelog: 47 | cargo bin changelog -o CHANGELOG.md 48 | .PHONY: changelog -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # axum-content-negotiation 2 | 3 | HTTP Content Negotiation middleware and extractor for Axum. 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 |
18 | 19 | A set of Axum Layers and Extractors that enable content negotiation using `Accept` and `Content-Type` headers. 20 | It implements schemaless serialization and deserialization content negotiation. Currently supported encodings are: 21 | - `application/json` 22 | - `application/cbor` 23 | 24 | ## Installation 25 | 26 | ```toml 27 | [dependencies] 28 | axum-content-negotiation = "0.1" 29 | ``` 30 | 31 | ### Features 32 | 33 | The following features can be enabled to include support for different encodings: 34 | - `simd-json` (default): Enables support for `application/json` encoding using `simd-json`. 35 | - `cbor` (default): Enables support for `application/cbor` encoding using `cbor4ii`. 36 | - `json`: Enables support for `application/json` encoding using `serde_json`. 37 | 38 | The following features enable the default content type when `Accept` header is missing or `Accept: */*` is present: 39 | - `default-json` (default): Assumes `application/json` as the default content type. 40 | - `default-cbor`: Assumes `application/cbor` as the default content type. 41 | 42 | In order to customize your dependencies, you can enable or disable the features as follows: 43 | 44 | ```toml 45 | [dependencies] 46 | axum-content-negotiation = { version = "0.1", default-features = false, features = ["json", "default-json"] } 47 | ``` 48 | 49 | ## Usage 50 | 51 | ### Request payloads 52 | 53 | The `axum_content_negotiation::Negotiate` is `Extractor` can be used in an Axum handlers to accept multiple `Content-Type` formats for the request body. 54 | This extractor will attempt to deserialize the request body into the desired type based on the `Content-Type` header and a list of supported schemaless encodings. 55 | 56 | ```rust,no_run 57 | use axum::{http::StatusCode, response::IntoResponse, routing::post, Router}; 58 | use axum_content_negotiation::Negotiate; 59 | 60 | #[derive(serde::Deserialize, Debug)] 61 | struct YourType { 62 | name: String, 63 | } 64 | 65 | async fn handler(Negotiate(request_body): Negotiate) -> impl IntoResponse { 66 | (StatusCode::OK, format!("Received ${:?}", request_body)) 67 | } 68 | 69 | let router: Router<()> = Router::new().route("/", post(handler)); 70 | ``` 71 | 72 | ### Response payloads 73 | 74 | In order to respond with the correct `Content-Type` header, the `axum_content_negotiation::Negotiate` also implements an `IntoResponse` trait, 75 | but it requires `axum_content_negotiation::NegotiateLayer` in order to actually perform the serialization on the desired format. 76 | 77 | ```rust,no_run 78 | use axum::{http::StatusCode, response::IntoResponse, routing::get, Router}; 79 | use axum_content_negotiation::{Negotiate, NegotiateLayer}; 80 | 81 | #[derive(serde::Serialize)] 82 | struct YourType { 83 | name: String, 84 | } 85 | 86 | async fn handler() -> impl IntoResponse { 87 | let response = YourType { 88 | name: "John".to_string(), 89 | }; 90 | (StatusCode::OK, Negotiate(response)) 91 | } 92 | 93 | let router: Router<()> = Router::new().route("/", get(handler)).layer(NegotiateLayer); 94 | ``` 95 | 96 | ## All together 97 | 98 | ```rust,no_run 99 | use axum::{http::StatusCode, response::IntoResponse, routing::*, Router}; 100 | use axum_content_negotiation::{Negotiate, NegotiateLayer}; 101 | 102 | #[derive(serde::Deserialize, Debug)] 103 | struct Input { 104 | name: String, 105 | } 106 | 107 | #[derive(serde::Serialize)] 108 | struct Output { 109 | name: String, 110 | } 111 | 112 | async fn handler(Negotiate(request_body): Negotiate) -> impl IntoResponse { 113 | let response = Output { 114 | name: format!("Hello there, {}!", request_body.name), 115 | }; 116 | (StatusCode::OK, Negotiate(response)) 117 | } 118 | 119 | let router: Router<()> = Router::new().route("/", put(handler)).layer(NegotiateLayer); 120 | ``` -------------------------------------------------------------------------------- /bacon.toml: -------------------------------------------------------------------------------- 1 | # This is a configuration file for the bacon tool 2 | # 3 | # Bacon repository: https://github.com/Canop/bacon 4 | # Complete help on configuration: https://dystroy.org/bacon/config/ 5 | # You can also check bacon's own bacon.toml file 6 | # as an example: https://github.com/Canop/bacon/blob/main/bacon.toml 7 | 8 | default_job = "check" 9 | 10 | [jobs.check] 11 | command = ["cargo", "check", "--color", "always"] 12 | need_stdout = false 13 | 14 | [jobs.check-all] 15 | command = ["cargo", "check", "--all-targets", "--color", "always"] 16 | need_stdout = false 17 | 18 | [jobs.clippy] 19 | command = [ 20 | "cargo", "clippy", 21 | "--all-targets", 22 | "--color", "always", 23 | "--", 24 | "-W", "clippy::pedantic", 25 | ] 26 | need_stdout = false 27 | 28 | # This job lets you run 29 | # - all tests: bacon test 30 | # - a specific test: bacon test -- config::test_default_files 31 | # - the tests of a package: bacon test -- -- -p config 32 | [jobs.test] 33 | command = [ 34 | "cargo", "test", "--color", "always", 35 | "--", "--color", "always", # see https://github.com/Canop/bacon/issues/124 36 | ] 37 | need_stdout = true 38 | 39 | [jobs.doc] 40 | command = ["cargo", "doc", "--color", "always", "--no-deps"] 41 | need_stdout = false 42 | 43 | # If the doc compiles, then it opens in your browser and bacon switches 44 | # to the previous job 45 | [jobs.doc-open] 46 | command = ["cargo", "doc", "--color", "always", "--no-deps", "--open"] 47 | need_stdout = false 48 | on_success = "back" # so that we don't open the browser at each change 49 | 50 | # You can run your application and have the result displayed in bacon, 51 | # *if* it makes sense for this crate. 52 | # Don't forget the `--color always` part or the errors won't be 53 | # properly parsed. 54 | # If your program never stops (eg a server), you may set `background` 55 | # to false to have the cargo run output immediately displayed instead 56 | # of waiting for program's end. 57 | [jobs.run] 58 | command = [ 59 | "cargo", "run", 60 | "--color", "always", 61 | # put launch parameters for your program behind a `--` separator 62 | ] 63 | need_stdout = true 64 | allow_warnings = true 65 | background = true 66 | 67 | # This parameterized job runs the example of your choice, as soon 68 | # as the code compiles. 69 | # Call it as 70 | # bacon ex -- my-example 71 | [jobs.ex] 72 | command = ["cargo", "run", "--color", "always", "--example"] 73 | need_stdout = true 74 | allow_warnings = true 75 | 76 | # You may define here keybindings that would be specific to 77 | # a project, for example a shortcut to launch a specific job. 78 | # Shortcuts to internal functions (scrolling, toggling, etc.) 79 | # should go in your personal global prefs.toml file instead. 80 | [keybindings] 81 | # alt-m = "job:my-job" 82 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | #![doc = include_str!("../README.md")] 2 | 3 | use std::{ 4 | future::Future, 5 | pin::Pin, 6 | sync::Arc, 7 | task::{Context, Poll}, 8 | }; 9 | 10 | use axum::{ 11 | body::Bytes, 12 | extract::{FromRequest, Request}, 13 | http::{ 14 | header::{HeaderValue, ACCEPT, CONTENT_LENGTH, CONTENT_TYPE}, 15 | StatusCode, 16 | }, 17 | response::{IntoResponse, Response}, 18 | Extension, 19 | }; 20 | use tower::Service; 21 | 22 | #[cfg(all(feature = "json", feature = "simd-json"))] 23 | compile_error!("json and simd-json features are mutually exclusive"); 24 | #[cfg(all(feature = "default-json", feature = "default-cbor"))] 25 | compile_error!("default-json and default-cbor features are mutually exclusive"); 26 | 27 | #[cfg(feature = "default-json")] 28 | /// Default to application/json if the request does not have any accept header or uses */* when json is enabled 29 | static DEFAULT_CONTENT_TYPE_VALUE: &str = "application/json"; 30 | 31 | #[cfg(feature = "default-cbor")] 32 | /// Default to application/cbor if the request does not have any accept header or uses */* when json is not enabled 33 | static DEFAULT_CONTENT_TYPE_VALUE: &str = "application/cbor"; 34 | 35 | #[cfg(not(any(feature = "default-json", feature = "default-cbor")))] 36 | compile_error!("A default-* feature must be enabled for fallback encoding"); 37 | 38 | static DEFAULT_CONTENT_TYPE: HeaderValue = HeaderValue::from_static(DEFAULT_CONTENT_TYPE_VALUE); 39 | 40 | static MALFORMED_RESPONSE: (StatusCode, &str) = (StatusCode::BAD_REQUEST, "Malformed request body"); 41 | 42 | /// Used either as an [Extract](axum::extract::FromRequest) or [Response](axum::response::IntoResponse) to negotiate the serialization format used. 43 | /// 44 | /// When used as an [Extract](axum::extract::FromRequest), it will attempt to deserialize the request body into the target type based on the `Content-Type` header. 45 | /// When used as a [Response](axum::response::IntoResponse), it will attempt to serialize the target type into the response body based on the `Accept` header. 46 | /// 47 | /// For the [Response](axum::response::IntoResponse) case, the [NegotiateLayer] must be used to wrap the service in order to acctually perform the serialization. 48 | /// If the [Layer](tower::Layer) is not used, the response will be an 415 Unsupported Media Type error. 49 | /// 50 | /// ## Example 51 | /// 52 | /// ```rust 53 | /// use axum_content_negotiation::Negotiate; 54 | /// 55 | /// #[derive(serde::Serialize, serde::Deserialize)] 56 | /// struct Example { 57 | /// message: String, 58 | /// } 59 | /// 60 | /// async fn handler( 61 | /// Negotiate(input): Negotiate 62 | /// ) -> impl axum::response::IntoResponse { 63 | /// Negotiate(Example { 64 | /// message: format!("Hello, {}!", input.message) 65 | /// }) 66 | /// } 67 | /// ``` 68 | #[derive(Debug, Clone)] 69 | pub struct Negotiate( 70 | /// The stored content to be serialized/deserialized 71 | pub T, 72 | ); 73 | 74 | /// [Negotiate] implements [FromRequest] if the target type is deserializable. 75 | /// 76 | /// It will attempt to deserialize the request body based on the `Content-Type` header. 77 | /// If the `Content-Type` header is not supported, it will return a 415 Unsupported Media Type response without running the handler. 78 | impl FromRequest for Negotiate 79 | where 80 | T: serde::de::DeserializeOwned, 81 | S: Send + Sync, 82 | { 83 | type Rejection = Response; 84 | 85 | async fn from_request(req: Request, state: &S) -> Result { 86 | let accept = req 87 | .headers() 88 | .get(CONTENT_TYPE) 89 | .unwrap_or(&DEFAULT_CONTENT_TYPE); 90 | 91 | match accept.as_bytes() { 92 | #[cfg(feature = "simd-json")] 93 | b"application/json" => { 94 | let mut body = Bytes::from_request(req, state) 95 | .await 96 | .map_err(|e| { 97 | tracing::error!(error = %e, "failed to ready request body as bytes"); 98 | e.into_response() 99 | })? 100 | .to_vec(); 101 | 102 | let body = simd_json::from_slice(&mut body).map_err(|e| { 103 | tracing::error!(error = %e, "failed to deserialize request body as json"); 104 | MALFORMED_RESPONSE.into_response() 105 | })?; 106 | 107 | Ok(Self(body)) 108 | } 109 | #[cfg(feature = "json")] 110 | b"application/json" => { 111 | let body = Bytes::from_request(req, state).await.map_err(|e| { 112 | tracing::error!(error = %e, "failed to ready request body as bytes"); 113 | e.into_response() 114 | })?; 115 | 116 | let body = serde_json::from_slice(&body).map_err(|e| { 117 | tracing::error!(error = %e, "failed to deserialize request body as json"); 118 | MALFORMED_RESPONSE.into_response() 119 | })?; 120 | 121 | Ok(Self(body)) 122 | } 123 | 124 | #[cfg(feature = "cbor")] 125 | b"application/cbor" => { 126 | let body = Bytes::from_request(req, state).await.map_err(|e| { 127 | tracing::error!(error = %e, "failed to ready request body as bytes"); 128 | e.into_response() 129 | })?; 130 | 131 | let body = cbor4ii::serde::from_slice(&body).map_err(|e| { 132 | tracing::error!(error = %e, "failed to deserialize request body as json"); 133 | MALFORMED_RESPONSE.into_response() 134 | })?; 135 | 136 | Ok(Self(body)) 137 | } 138 | 139 | _ => { 140 | tracing::error!("unsupported accept header: {:?}", accept); 141 | Err(( 142 | StatusCode::NOT_ACCEPTABLE, 143 | "Invalid content type on request", 144 | ) 145 | .into_response()) 146 | } 147 | } 148 | } 149 | } 150 | 151 | /// Internal Negotiate object without the type parameter explicitly, in order to be able retrieve it as an extension on the [Layer](tower::Layer) response processing. 152 | /// 153 | /// Considering [Extension]s are type safe, and we don't know ahead of time the type of the stored content, we must store it erased to dynamically dispatch for serialization latter. 154 | #[derive(Clone)] 155 | struct ErasedNegotiate(Arc>); 156 | 157 | impl From for ErasedNegotiate 158 | where 159 | T: serde::Serialize + Send + Sync + 'static, 160 | { 161 | fn from(value: T) -> Self { 162 | Self(Arc::new(Box::from(value))) 163 | } 164 | } 165 | 166 | /// [Negotiate] implements [IntoResponse] if the internal content is serialiazable. 167 | /// 168 | /// It will return convert it to a 415 Unsupported Media Type by default, which will be converted to the right response status on the [NegotiateLayer]. 169 | impl IntoResponse for Negotiate 170 | where 171 | T: serde::Serialize + Send + Sync + 'static, 172 | { 173 | fn into_response(self) -> Response { 174 | let data: ErasedNegotiate = self.0.into(); 175 | ( 176 | StatusCode::UNSUPPORTED_MEDIA_TYPE, 177 | Extension(data), 178 | "Misconfigured service layer", 179 | ) 180 | .into_response() 181 | } 182 | } 183 | 184 | /// Layer responsible to convert a [Negotiate] response into the right serialization format based on the `Accept` header. 185 | /// 186 | /// If the `Accept` header is not supported, it will return a 406 Not Acceptable response without running the handler. 187 | #[derive(Clone)] 188 | pub struct NegotiateLayer; 189 | 190 | impl tower::Layer for NegotiateLayer { 191 | type Service = NegotiateService; 192 | 193 | fn layer(&self, inner: S) -> Self::Service { 194 | NegotiateService(inner) 195 | } 196 | } 197 | 198 | trait SupportedEncodingExt { 199 | fn supported_encoding(&self) -> Option<&'static str>; 200 | } 201 | 202 | impl SupportedEncodingExt for &[u8] { 203 | fn supported_encoding(&self) -> Option<&'static str> { 204 | match *self { 205 | #[cfg(any(feature = "simd-json", feature = "json"))] 206 | b"application/json" => Some("application/json"), 207 | #[cfg(feature = "cbor")] 208 | b"application/cbor" => Some("application/cbor"), 209 | b"*/*" => Some(DEFAULT_CONTENT_TYPE_VALUE), 210 | _ => None, 211 | } 212 | } 213 | } 214 | 215 | trait AcceptExt { 216 | fn negotiate(&self) -> Option<&'static str>; 217 | } 218 | 219 | impl AcceptExt for axum::http::HeaderMap { 220 | fn negotiate(&self) -> Option<&'static str> { 221 | let accept = self.get(ACCEPT).unwrap_or(&DEFAULT_CONTENT_TYPE); 222 | let precise_mime = accept.as_bytes().supported_encoding(); 223 | 224 | // Avoid iterations and splits if it's an exact match 225 | if precise_mime.is_some() { 226 | return precise_mime; 227 | } 228 | 229 | accept 230 | .to_str() 231 | .ok()? 232 | .split(',') 233 | .map(str::trim) 234 | .filter_map(|s| { 235 | let mut segments = s.split(';').map(str::trim); 236 | let mime = segments.next().unwrap_or(s); 237 | 238 | // See if it's a type we support 239 | let mime_type = mime.as_bytes().supported_encoding()?; 240 | 241 | // If we support it, parse or default the q value 242 | let q = segments 243 | .find_map(|s| { 244 | let value = s.strip_prefix("q=")?; 245 | Some(value.parse::().unwrap_or(0.0)) 246 | }) 247 | .unwrap_or(1.0); 248 | Some((mime_type, q)) 249 | }) 250 | .min_by(|(_, a), (_, b)| b.total_cmp(a)) 251 | .map(|(mime, _)| mime) 252 | } 253 | } 254 | 255 | /// Serialize the stored [Extension] struct defined by a [Negotiate] into the right serialization format based on the `Accept` header. 256 | #[derive(Clone)] 257 | pub struct NegotiateService(S); 258 | 259 | impl Service for NegotiateService 260 | where 261 | T: Service, 262 | T::Response: IntoResponse, 263 | T::Future: Send + 'static, 264 | { 265 | type Response = axum::response::Response; 266 | type Error = T::Error; 267 | type Future = 268 | Pin> + Send + 'static>>; 269 | 270 | fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll> { 271 | self.0.poll_ready(cx) 272 | } 273 | 274 | fn call(&mut self, request: Request) -> Self::Future { 275 | let accept = request.headers().negotiate(); 276 | 277 | let Some(encoding) = accept else { 278 | return Box::pin(async move { 279 | let response: Response = ( 280 | StatusCode::NOT_ACCEPTABLE, 281 | "Invalid content type on request", 282 | ) 283 | .into_response(); 284 | Ok(response) 285 | }); 286 | }; 287 | 288 | let future = self.0.call(request); 289 | 290 | Box::pin(async move { 291 | let inner_service = future.await?; 292 | let response: Response = inner_service.into_response(); 293 | let data = response.extensions().get::(); 294 | 295 | let Some(ErasedNegotiate(payload)) = data else { 296 | return Ok(response); 297 | }; 298 | 299 | let body = match encoding { 300 | #[cfg(any(feature = "simd-json", feature = "json"))] 301 | "application/json" => { 302 | let mut body = Vec::new(); 303 | { 304 | let mut serializer = serde_json::Serializer::new(&mut body); 305 | let mut serializer = ::erase(&mut serializer); 306 | if let Err(e) = payload.erased_serialize(&mut serializer) { 307 | tracing::error!(error = %e, "failed to deserialize request body as json"); 308 | 309 | let response: Response = ( 310 | StatusCode::INTERNAL_SERVER_ERROR, 311 | "Failed to serialize response", 312 | ) 313 | .into_response(); 314 | return Ok(response); 315 | } 316 | } 317 | body 318 | } 319 | #[cfg(feature = "cbor")] 320 | "application/cbor" => { 321 | let mut body = cbor4ii::core::utils::BufWriter::new(Vec::new()); 322 | { 323 | let mut serializer = cbor4ii::serde::Serializer::new(&mut body); 324 | let mut serializer = ::erase(&mut serializer); 325 | if let Err(e) = payload.erased_serialize(&mut serializer) { 326 | tracing::error!(error = %e, "failed to deserialize request body as cbor"); 327 | 328 | let response: Response = ( 329 | StatusCode::INTERNAL_SERVER_ERROR, 330 | "Failed to serialize response", 331 | ) 332 | .into_response(); 333 | return Ok(response); 334 | } 335 | } 336 | body.into_inner() 337 | } 338 | _ => vec![], 339 | }; 340 | 341 | let (mut parts, _) = response.into_parts(); 342 | if parts.status == StatusCode::UNSUPPORTED_MEDIA_TYPE { 343 | parts.status = StatusCode::OK; 344 | } 345 | parts 346 | .headers 347 | .insert(CONTENT_TYPE, HeaderValue::from_static(encoding)); 348 | parts.headers.remove(CONTENT_LENGTH); 349 | 350 | Ok(Response::from_parts(parts, body.into())) 351 | }) 352 | } 353 | } 354 | 355 | #[cfg(test)] 356 | mod test { 357 | use crate::Negotiate; 358 | 359 | use axum::{ 360 | body::Body, 361 | http::{ 362 | header::{ACCEPT, CONTENT_LENGTH, CONTENT_TYPE}, 363 | Request, StatusCode, 364 | }, 365 | response::IntoResponse, 366 | routing::post, 367 | Router, 368 | }; 369 | use http_body_util::BodyExt; 370 | use tower::ServiceExt; 371 | 372 | use crate::NegotiateLayer; 373 | 374 | #[derive(Debug, serde::Serialize, serde::Deserialize)] 375 | struct Example { 376 | message: String, 377 | } 378 | 379 | fn content_length(headers: &axum::http::HeaderMap) -> usize { 380 | headers 381 | .get(CONTENT_LENGTH) 382 | .map(|v| v.to_str().unwrap().parse::().unwrap()) 383 | .unwrap() 384 | } 385 | 386 | mod general { 387 | use super::*; 388 | 389 | #[cfg(feature = "cbor")] 390 | pub fn expected_cbor_body() -> Vec { 391 | use cbor4ii::core::{enc::Encode, utils::BufWriter, Value}; 392 | 393 | let mut writer = BufWriter::new(Vec::new()); 394 | Value::Map(vec![( 395 | Value::Text("message".to_string()), 396 | Value::Text("Hello, test!".to_string()), 397 | )]) 398 | .encode(&mut writer) 399 | .unwrap(); 400 | writer.into_inner() 401 | } 402 | 403 | mod input { 404 | use super::*; 405 | 406 | #[tokio::test] 407 | async fn test_does_not_process_handler_if_content_type_is_not_supported() { 408 | #[axum::debug_handler] 409 | async fn handler(_: Negotiate) -> impl IntoResponse { 410 | unimplemented!("This should not be called"); 411 | #[allow(unreachable_code)] 412 | () 413 | } 414 | 415 | let app = Router::new() 416 | .route("/", post(handler)) 417 | .layer(NegotiateLayer); 418 | 419 | let response = app 420 | .oneshot( 421 | Request::builder() 422 | .uri("/") 423 | .header(CONTENT_TYPE, "non-supported") 424 | .method("POST") 425 | .body(Body::from("really-cool-format")) 426 | .unwrap(), 427 | ) 428 | .await 429 | .unwrap(); 430 | 431 | assert_eq!(response.status(), 406); 432 | assert_eq!( 433 | response.into_body().collect().await.unwrap().to_bytes(), 434 | "Invalid content type on request" 435 | ); 436 | } 437 | } 438 | 439 | mod output { 440 | use super::*; 441 | 442 | #[tokio::test] 443 | async fn test_inform_error_when_misconfigured() { 444 | #[axum::debug_handler] 445 | async fn handler() -> impl IntoResponse { 446 | Negotiate(Example { 447 | message: "Hello, test!".to_string(), 448 | }) 449 | } 450 | 451 | let app = Router::new().route("/", post(handler)); 452 | 453 | let response = app 454 | .oneshot( 455 | Request::builder() 456 | .uri("/") 457 | .method("POST") 458 | .body(Body::empty()) 459 | .unwrap(), 460 | ) 461 | .await 462 | .unwrap(); 463 | 464 | assert_eq!(response.status(), 415); 465 | assert_eq!( 466 | response.into_body().collect().await.unwrap().to_bytes(), 467 | "Misconfigured service layer" 468 | ); 469 | } 470 | 471 | #[tokio::test] 472 | async fn test_does_not_process_handler_if_accept_is_not_supported() { 473 | #[axum::debug_handler] 474 | async fn handler() -> impl IntoResponse { 475 | unimplemented!("This should not be called"); 476 | #[allow(unreachable_code)] 477 | () 478 | } 479 | 480 | let app = Router::new() 481 | .route("/", post(handler)) 482 | .layer(NegotiateLayer); 483 | 484 | let response = app 485 | .oneshot( 486 | Request::builder() 487 | .uri("/") 488 | .header(ACCEPT, "non-supported") 489 | .method("POST") 490 | .body(Body::empty()) 491 | .unwrap(), 492 | ) 493 | .await 494 | .unwrap(); 495 | 496 | assert_eq!(response.status(), 406); 497 | assert_eq!( 498 | response.into_body().collect().await.unwrap().to_bytes(), 499 | "Invalid content type on request" 500 | ); 501 | } 502 | } 503 | } 504 | 505 | #[cfg(any(feature = "simd-json", feature = "json"))] 506 | mod json { 507 | use serde_json::json; 508 | 509 | use super::*; 510 | 511 | mod input { 512 | use super::*; 513 | 514 | #[cfg(feature = "default-json")] 515 | #[tokio::test] 516 | async fn test_can_read_input_without_content_type_by_default() { 517 | #[axum::debug_handler] 518 | async fn handler(Negotiate(input): Negotiate) -> impl IntoResponse { 519 | format!("Hello, {}!", input.message) 520 | } 521 | 522 | let app = Router::new().route("/", post(handler)); 523 | 524 | let response = app 525 | .oneshot( 526 | Request::builder() 527 | .uri("/") 528 | .method("POST") 529 | .body(json!({ "message": "test" }).to_string()) 530 | .unwrap(), 531 | ) 532 | .await 533 | .unwrap(); 534 | 535 | assert_eq!(response.status(), 200); 536 | assert_eq!( 537 | response.into_body().collect().await.unwrap().to_bytes(), 538 | "Hello, test!" 539 | ); 540 | } 541 | 542 | #[tokio::test] 543 | async fn test_can_read_input_with_specified_header() { 544 | #[axum::debug_handler] 545 | async fn handler(Negotiate(input): Negotiate) -> impl IntoResponse { 546 | format!("Hello, {}!", input.message) 547 | } 548 | 549 | let app = Router::new().route("/", post(handler)); 550 | 551 | let response = app 552 | .oneshot( 553 | Request::builder() 554 | .uri("/") 555 | .header(CONTENT_TYPE, "application/json") 556 | .method("POST") 557 | .body(json!({ "message": "test" }).to_string()) 558 | .unwrap(), 559 | ) 560 | .await 561 | .unwrap(); 562 | 563 | assert_eq!(response.status(), 200); 564 | assert_eq!( 565 | response.into_body().collect().await.unwrap().to_bytes(), 566 | "Hello, test!" 567 | ); 568 | } 569 | 570 | #[tokio::test] 571 | async fn test_does_not_accept_invalid_inputs() { 572 | #[axum::debug_handler] 573 | async fn handler(_: Negotiate) -> impl IntoResponse { 574 | unimplemented!("This should not be called"); 575 | #[allow(unreachable_code)] 576 | () 577 | } 578 | 579 | let app = Router::new() 580 | .route("/", post(handler)) 581 | .layer(NegotiateLayer); 582 | 583 | let response = app 584 | .oneshot( 585 | Request::builder() 586 | .uri("/") 587 | .method("POST") 588 | .header(CONTENT_TYPE, "application/json") 589 | .body(json!({ "not": true }).to_string()) 590 | .unwrap(), 591 | ) 592 | .await 593 | .unwrap(); 594 | 595 | assert_eq!(response.status(), 400); 596 | assert_eq!( 597 | response.into_body().collect().await.unwrap().to_bytes(), 598 | "Malformed request body" 599 | ); 600 | } 601 | } 602 | 603 | mod output { 604 | use super::*; 605 | 606 | #[tokio::test] 607 | async fn test_encode_as_requested() { 608 | #[axum::debug_handler] 609 | async fn handler() -> impl IntoResponse { 610 | Negotiate(Example { 611 | message: "Hello, test!".to_string(), 612 | }) 613 | } 614 | 615 | let app = Router::new() 616 | .route("/", post(handler)) 617 | .layer(NegotiateLayer); 618 | 619 | let response = app 620 | .oneshot( 621 | Request::builder() 622 | .uri("/") 623 | .method("POST") 624 | .header(ACCEPT, "application/json") 625 | .body(Body::empty()) 626 | .unwrap(), 627 | ) 628 | .await 629 | .unwrap(); 630 | 631 | let expected_body = json!({ "message": "Hello, test!" }).to_string(); 632 | 633 | assert_eq!(response.status(), 200); 634 | assert_eq!( 635 | response.headers().get(CONTENT_TYPE).unwrap(), 636 | "application/json" 637 | ); 638 | assert_eq!(content_length(response.headers()), expected_body.len()); 639 | assert_eq!( 640 | response.into_body().collect().await.unwrap().to_bytes(), 641 | expected_body, 642 | ); 643 | } 644 | 645 | #[tokio::test] 646 | async fn test_encode_as_requested_multi() { 647 | #[axum::debug_handler] 648 | async fn handler() -> impl IntoResponse { 649 | Negotiate(Example { 650 | message: "Hello, test!".to_string(), 651 | }) 652 | } 653 | 654 | let app = Router::new() 655 | .route("/", post(handler)) 656 | .layer(NegotiateLayer); 657 | 658 | let response = app 659 | .oneshot( 660 | Request::builder() 661 | .uri("/") 662 | .method("POST") 663 | .header(ACCEPT, "not-supported, application/json;q=5,something-else") 664 | .body(Body::empty()) 665 | .unwrap(), 666 | ) 667 | .await 668 | .unwrap(); 669 | 670 | let expected_body = json!({ "message": "Hello, test!" }).to_string(); 671 | 672 | assert_eq!(response.status(), 200); 673 | assert_eq!( 674 | response.headers().get(CONTENT_TYPE).unwrap(), 675 | "application/json" 676 | ); 677 | assert_eq!(content_length(response.headers()), expected_body.len()); 678 | assert_eq!( 679 | response.into_body().collect().await.unwrap().to_bytes(), 680 | expected_body, 681 | ); 682 | } 683 | 684 | #[cfg(feature = "cbor")] 685 | #[tokio::test] 686 | async fn test_encode_as_requested_multi_w_q() { 687 | #[axum::debug_handler] 688 | async fn handler() -> impl IntoResponse { 689 | Negotiate(Example { 690 | message: "Hello, test!".to_string(), 691 | }) 692 | } 693 | 694 | let app = Router::new() 695 | .route("/", post(handler)) 696 | .layer(NegotiateLayer); 697 | 698 | let response = app 699 | .oneshot( 700 | Request::builder() 701 | .uri("/") 702 | .method("POST") 703 | .header( 704 | ACCEPT, 705 | "application/json;q=0.8;other;stuff,application/cbor;q=0.9", 706 | ) 707 | .body(Body::empty()) 708 | .unwrap(), 709 | ) 710 | .await 711 | .unwrap(); 712 | 713 | assert_eq!(response.status(), 200); 714 | assert_eq!( 715 | response.headers().get(CONTENT_TYPE).unwrap(), 716 | "application/cbor" 717 | ); 718 | } 719 | 720 | #[cfg(feature = "cbor")] 721 | #[tokio::test] 722 | async fn test_encode_as_requested_multi_w_q_same_weights() { 723 | #[axum::debug_handler] 724 | async fn handler() -> impl IntoResponse { 725 | Negotiate(Example { 726 | message: "Hello, test!".to_string(), 727 | }) 728 | } 729 | 730 | let app = Router::new() 731 | .route("/", post(handler)) 732 | .layer(NegotiateLayer); 733 | 734 | let response = app 735 | .oneshot( 736 | Request::builder() 737 | .uri("/") 738 | .method("POST") 739 | .header( 740 | ACCEPT, 741 | "application/cbor;q=0.9,application/json;q=0.9;other;stuff", 742 | ) 743 | .body(Body::empty()) 744 | .unwrap(), 745 | ) 746 | .await 747 | .unwrap(); 748 | 749 | assert_eq!(response.status(), 200); 750 | assert_eq!( 751 | response.headers().get(CONTENT_TYPE).unwrap(), 752 | "application/cbor" 753 | ); 754 | } 755 | 756 | #[cfg(feature = "default-json")] 757 | #[tokio::test] 758 | async fn test_use_default_encoding_without_headers() { 759 | #[axum::debug_handler] 760 | async fn handler() -> impl IntoResponse { 761 | Negotiate(Example { 762 | message: "Hello, test!".to_string(), 763 | }) 764 | } 765 | 766 | let app = Router::new() 767 | .route("/", post(handler)) 768 | .layer(NegotiateLayer); 769 | 770 | let response = app 771 | .oneshot( 772 | Request::builder() 773 | .uri("/") 774 | .method("POST") 775 | .body(Body::empty()) 776 | .unwrap(), 777 | ) 778 | .await 779 | .unwrap(); 780 | 781 | assert_eq!(response.status(), 200); 782 | assert_eq!( 783 | response.headers().get(CONTENT_TYPE).unwrap(), 784 | "application/json" 785 | ); 786 | assert_eq!( 787 | response.into_body().collect().await.unwrap().to_bytes(), 788 | json!({ "message": "Hello, test!" }).to_string() 789 | ); 790 | } 791 | 792 | #[tokio::test] 793 | async fn test_retain_handler_status_code() { 794 | #[axum::debug_handler] 795 | async fn handler() -> impl IntoResponse { 796 | ( 797 | StatusCode::CREATED, 798 | Negotiate(Example { 799 | message: "Hello, test!".to_string(), 800 | }), 801 | ) 802 | } 803 | 804 | let app = Router::new() 805 | .route("/", post(handler)) 806 | .layer(NegotiateLayer); 807 | 808 | let response = app 809 | .oneshot( 810 | Request::builder() 811 | .uri("/") 812 | .method("POST") 813 | .body(Body::empty()) 814 | .unwrap(), 815 | ) 816 | .await 817 | .unwrap(); 818 | 819 | assert_eq!(response.status(), StatusCode::CREATED); 820 | #[cfg(feature = "default-json")] 821 | assert_eq!( 822 | response.headers().get(CONTENT_TYPE).unwrap(), 823 | "application/json" 824 | ); 825 | #[cfg(feature = "default-json")] 826 | assert_eq!( 827 | response.into_body().collect().await.unwrap().to_bytes(), 828 | json!({ "message": "Hello, test!" }).to_string() 829 | ); 830 | #[cfg(feature = "default-cbor")] 831 | assert_eq!( 832 | response.headers().get(CONTENT_TYPE).unwrap(), 833 | "application/cbor" 834 | ); 835 | #[cfg(feature = "default-cbor")] 836 | assert_eq!( 837 | response.into_body().collect().await.unwrap().to_bytes(), 838 | general::expected_cbor_body() 839 | ); 840 | } 841 | } 842 | } 843 | 844 | #[cfg(feature = "cbor")] 845 | mod cbor { 846 | use cbor4ii::core::{enc::Encode, utils::BufWriter, Value}; 847 | 848 | use super::*; 849 | 850 | mod input { 851 | use super::*; 852 | 853 | #[cfg(feature = "default-cbor")] 854 | #[tokio::test] 855 | async fn test_can_read_input_without_content_type_by_default() { 856 | #[axum::debug_handler] 857 | async fn handler(Negotiate(input): Negotiate) -> impl IntoResponse { 858 | format!("Hello, {}!", input.message) 859 | } 860 | 861 | let app = Router::new().route("/", post(handler)); 862 | let body = { 863 | let mut writer = BufWriter::new(Vec::new()); 864 | Value::Map(vec![( 865 | Value::Text("message".to_string()), 866 | Value::Text("test".to_string()), 867 | )]) 868 | .encode(&mut writer) 869 | .unwrap(); 870 | writer.into_inner() 871 | }; 872 | 873 | let response = app 874 | .oneshot( 875 | Request::builder() 876 | .uri("/") 877 | .method("POST") 878 | .body(Body::from(body)) 879 | .unwrap(), 880 | ) 881 | .await 882 | .unwrap(); 883 | 884 | assert_eq!(response.status(), 200); 885 | assert_eq!( 886 | response.into_body().collect().await.unwrap().to_bytes(), 887 | "Hello, test!" 888 | ); 889 | } 890 | 891 | #[tokio::test] 892 | async fn test_can_read_input_with_specified_header() { 893 | #[axum::debug_handler] 894 | async fn handler(Negotiate(input): Negotiate) -> impl IntoResponse { 895 | format!("Hello, {}!", input.message) 896 | } 897 | 898 | let app = Router::new().route("/", post(handler)); 899 | let body = { 900 | let mut writer = BufWriter::new(Vec::new()); 901 | Value::Map(vec![( 902 | Value::Text("message".to_string()), 903 | Value::Text("test".to_string()), 904 | )]) 905 | .encode(&mut writer) 906 | .unwrap(); 907 | writer.into_inner() 908 | }; 909 | 910 | let response = app 911 | .oneshot( 912 | Request::builder() 913 | .uri("/") 914 | .header(CONTENT_TYPE, "application/cbor") 915 | .method("POST") 916 | .body(Body::from(body)) 917 | .unwrap(), 918 | ) 919 | .await 920 | .unwrap(); 921 | 922 | assert_eq!(response.status(), 200); 923 | assert_eq!( 924 | response.into_body().collect().await.unwrap().to_bytes(), 925 | "Hello, test!" 926 | ); 927 | } 928 | } 929 | 930 | mod output { 931 | use super::*; 932 | 933 | #[tokio::test] 934 | async fn test_encode_as_requested() { 935 | #[axum::debug_handler] 936 | async fn handler() -> impl IntoResponse { 937 | Negotiate(Example { 938 | message: "Hello, test!".to_string(), 939 | }) 940 | } 941 | 942 | let app = Router::new() 943 | .route("/", post(handler)) 944 | .layer(NegotiateLayer); 945 | 946 | let response = app 947 | .oneshot( 948 | Request::builder() 949 | .uri("/") 950 | .method("POST") 951 | .header(ACCEPT, "application/cbor") 952 | .body(Body::empty()) 953 | .unwrap(), 954 | ) 955 | .await 956 | .unwrap(); 957 | 958 | let expected_body = general::expected_cbor_body(); 959 | 960 | assert_eq!(response.status(), 200); 961 | assert_eq!( 962 | response.headers().get(CONTENT_TYPE).unwrap(), 963 | "application/cbor" 964 | ); 965 | assert_eq!(content_length(response.headers()), expected_body.len()); 966 | assert_eq!( 967 | response.into_body().collect().await.unwrap().to_bytes(), 968 | expected_body, 969 | ); 970 | } 971 | 972 | #[tokio::test] 973 | async fn test_encode_as_requested_multi() { 974 | #[axum::debug_handler] 975 | async fn handler() -> impl IntoResponse { 976 | Negotiate(Example { 977 | message: "Hello, test!".to_string(), 978 | }) 979 | } 980 | 981 | let app = Router::new() 982 | .route("/", post(handler)) 983 | .layer(NegotiateLayer); 984 | 985 | let response = app 986 | .oneshot( 987 | Request::builder() 988 | .uri("/") 989 | .method("POST") 990 | .header(ACCEPT, "something-else;q=0.5,application/cbor") 991 | .body(Body::empty()) 992 | .unwrap(), 993 | ) 994 | .await 995 | .unwrap(); 996 | 997 | let expected_body = general::expected_cbor_body(); 998 | 999 | assert_eq!(response.status(), 200); 1000 | assert_eq!( 1001 | response.headers().get(CONTENT_TYPE).unwrap(), 1002 | "application/cbor" 1003 | ); 1004 | assert_eq!(content_length(response.headers()), expected_body.len()); 1005 | assert_eq!( 1006 | response.into_body().collect().await.unwrap().to_bytes(), 1007 | expected_body, 1008 | ); 1009 | } 1010 | 1011 | #[cfg(feature = "json")] 1012 | #[tokio::test] 1013 | async fn test_encode_as_requested_multi_without_q_using_default_weight() { 1014 | #[axum::debug_handler] 1015 | async fn handler() -> impl IntoResponse { 1016 | Negotiate(Example { 1017 | message: "Hello, test!".to_string(), 1018 | }) 1019 | } 1020 | 1021 | let app = Router::new() 1022 | .route("/", post(handler)) 1023 | .layer(NegotiateLayer); 1024 | 1025 | let response = app 1026 | .oneshot( 1027 | Request::builder() 1028 | .uri("/") 1029 | .method("POST") 1030 | .header(ACCEPT, "application/cbor;q=0.2,application/json") 1031 | .body(Body::empty()) 1032 | .unwrap(), 1033 | ) 1034 | .await 1035 | .unwrap(); 1036 | 1037 | assert_eq!(response.status(), 200); 1038 | assert_eq!( 1039 | response.headers().get(CONTENT_TYPE).unwrap(), 1040 | "application/json" 1041 | ); 1042 | } 1043 | 1044 | // Given equal q values, the first mime type should be selected 1045 | #[cfg(feature = "json")] 1046 | #[tokio::test] 1047 | async fn test_encode_as_requested_equal_q() { 1048 | #[axum::debug_handler] 1049 | async fn handler() -> impl IntoResponse { 1050 | Negotiate(Example { 1051 | message: "Hello, test!".to_string(), 1052 | }) 1053 | } 1054 | 1055 | let app = Router::new() 1056 | .route("/", post(handler)) 1057 | .layer(NegotiateLayer); 1058 | 1059 | let response = app 1060 | .oneshot( 1061 | Request::builder() 1062 | .uri("/") 1063 | .method("POST") 1064 | .header(ACCEPT, "application/cbor,application/json") 1065 | .body(Body::empty()) 1066 | .unwrap(), 1067 | ) 1068 | .await 1069 | .unwrap(); 1070 | 1071 | assert_eq!(response.status(), 200); 1072 | assert_eq!( 1073 | response.headers().get(CONTENT_TYPE).unwrap(), 1074 | "application/cbor" 1075 | ); 1076 | } 1077 | // Given equal q values, the first mime type should be selected 1078 | #[cfg(feature = "json")] 1079 | #[tokio::test] 1080 | async fn test_encode_as_requested_equal_q2() { 1081 | #[axum::debug_handler] 1082 | async fn handler() -> impl IntoResponse { 1083 | Negotiate(Example { 1084 | message: "Hello, test!".to_string(), 1085 | }) 1086 | } 1087 | 1088 | let app = Router::new() 1089 | .route("/", post(handler)) 1090 | .layer(NegotiateLayer); 1091 | 1092 | let response = app 1093 | .oneshot( 1094 | Request::builder() 1095 | .uri("/") 1096 | .method("POST") 1097 | .header(ACCEPT, "application/json,application/cbor") 1098 | .body(Body::empty()) 1099 | .unwrap(), 1100 | ) 1101 | .await 1102 | .unwrap(); 1103 | 1104 | assert_eq!(response.status(), 200); 1105 | assert_eq!( 1106 | response.headers().get(CONTENT_TYPE).unwrap(), 1107 | "application/json" 1108 | ); 1109 | } 1110 | 1111 | #[tokio::test] 1112 | async fn test_retain_status_code() { 1113 | #[axum::debug_handler] 1114 | async fn handler() -> impl IntoResponse { 1115 | ( 1116 | StatusCode::CREATED, 1117 | Negotiate(Example { 1118 | message: "Hello, test!".to_string(), 1119 | }), 1120 | ) 1121 | } 1122 | 1123 | let app = Router::new() 1124 | .route("/", post(handler)) 1125 | .layer(NegotiateLayer); 1126 | 1127 | let response = app 1128 | .oneshot( 1129 | Request::builder() 1130 | .uri("/") 1131 | .method("POST") 1132 | .header(ACCEPT, "application/cbor") 1133 | .body(Body::empty()) 1134 | .unwrap(), 1135 | ) 1136 | .await 1137 | .unwrap(); 1138 | 1139 | assert_eq!(response.status(), StatusCode::CREATED); 1140 | assert_eq!( 1141 | response.headers().get(CONTENT_TYPE).unwrap(), 1142 | "application/cbor" 1143 | ); 1144 | assert_eq!( 1145 | response.into_body().collect().await.unwrap().to_bytes(), 1146 | general::expected_cbor_body() 1147 | ); 1148 | } 1149 | 1150 | #[cfg(feature = "default-cbor")] 1151 | #[tokio::test] 1152 | async fn test_default_encoding_without_header() { 1153 | #[axum::debug_handler] 1154 | async fn handler() -> impl IntoResponse { 1155 | ( 1156 | StatusCode::CREATED, 1157 | Negotiate(Example { 1158 | message: "Hello, test!".to_string(), 1159 | }), 1160 | ) 1161 | } 1162 | 1163 | let app = Router::new() 1164 | .route("/", post(handler)) 1165 | .layer(NegotiateLayer); 1166 | 1167 | let response = app 1168 | .oneshot( 1169 | Request::builder() 1170 | .uri("/") 1171 | .method("POST") 1172 | .body(Body::empty()) 1173 | .unwrap(), 1174 | ) 1175 | .await 1176 | .unwrap(); 1177 | 1178 | assert_eq!(response.status(), StatusCode::CREATED); 1179 | assert_eq!( 1180 | response.headers().get(CONTENT_TYPE).unwrap(), 1181 | "application/cbor" 1182 | ); 1183 | assert_eq!( 1184 | response.into_body().collect().await.unwrap().to_bytes(), 1185 | general::expected_cbor_body() 1186 | ); 1187 | } 1188 | 1189 | #[cfg(feature = "default-cbor")] 1190 | #[tokio::test] 1191 | async fn test_default_encoding_with_star() { 1192 | #[axum::debug_handler] 1193 | async fn handler() -> impl IntoResponse { 1194 | ( 1195 | StatusCode::CREATED, 1196 | Negotiate(Example { 1197 | message: "Hello, test!".to_string(), 1198 | }), 1199 | ) 1200 | } 1201 | 1202 | let app = Router::new() 1203 | .route("/", post(handler)) 1204 | .layer(NegotiateLayer); 1205 | 1206 | let response = app 1207 | .oneshot( 1208 | Request::builder() 1209 | .uri("/") 1210 | .method("POST") 1211 | .header(ACCEPT, "*/*") 1212 | .body(Body::empty()) 1213 | .unwrap(), 1214 | ) 1215 | .await 1216 | .unwrap(); 1217 | 1218 | assert_eq!(response.status(), StatusCode::CREATED); 1219 | assert_eq!( 1220 | response.headers().get(CONTENT_TYPE).unwrap(), 1221 | "application/cbor" 1222 | ); 1223 | assert_eq!( 1224 | response.into_body().collect().await.unwrap().to_bytes(), 1225 | general::expected_cbor_body() 1226 | ); 1227 | } 1228 | } 1229 | } 1230 | } 1231 | --------------------------------------------------------------------------------