├── .github └── workflows │ ├── ci.yml │ └── release.yml ├── .gitignore ├── .release.json ├── Cargo.lock ├── Cargo.toml ├── LICENSE-APACHE ├── LICENSE-MIT ├── README.md ├── examples ├── aide-validator │ ├── Cargo.toml │ └── src │ │ └── main.rs ├── basic │ ├── Cargo.toml │ └── src │ │ └── main.rs └── todo-api │ ├── Cargo.toml │ └── src │ ├── main.rs │ └── todo.rs ├── macros ├── Cargo.lock ├── Cargo.toml ├── README.md └── src │ ├── apply.rs │ ├── attr_parsing.rs │ ├── debug_handler.rs │ ├── lib.rs │ └── with_position.rs ├── rustfmt.toml └── src ├── content.rs ├── decode.rs ├── encode.rs ├── extract.rs ├── handler.rs ├── lib.rs ├── rejection.rs ├── response.rs └── routing.rs /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | paths: 4 | - '**.rs' 5 | - '**.toml' 6 | - '**.yml' 7 | - '.git*' 8 | - 'Cargo.lock' 9 | - 'README.md' # it's included in the crate root 10 | branches: 11 | - main 12 | pull_request: 13 | branches: 14 | - main 15 | 16 | name: ci 17 | 18 | jobs: 19 | build: 20 | runs-on: ubuntu-latest 21 | steps: 22 | - name: checkout 23 | uses: actions/checkout@v4 24 | - name: setup rust toolchain 25 | id: toolchain 26 | uses: dtolnay/rust-toolchain@stable 27 | with: 28 | toolchain: nightly 29 | components: rustfmt, clippy, miri 30 | - name: setup cache 31 | id: cache 32 | uses: actions/cache@v4 33 | with: 34 | path: | 35 | ~/.cargo 36 | target 37 | key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}-rustc-${{ steps.toolchain.outputs.cachekey }} 38 | 39 | - name: install cargo hack 40 | if: steps.cache.outputs.cache-hit != 'true' 41 | run: cargo install cargo-hack --locked 42 | 43 | - name: cargo hack 44 | # run: cargo hack check --feature-powerset --skip serde,macros,full-codecs,default --no-dev-deps --at-least-one-of bincode,bitcode,cbor,json,msgpack,toml,yaml,form --group-features cbor,json,msgpack,toml,yaml,form 45 | run: cargo hack check --feature-powerset --skip serde,macros,full-codecs,default --no-dev-deps --at-least-one-of bincode,bitcode,json,msgpack,toml,yaml,form --group-features json,msgpack,toml,yaml,form 46 | - name: cargo test 47 | run: cargo test 48 | - name: cargo miri test --tests extract 49 | run: cargo miri test 50 | - name: cargo fmt 51 | run: cargo fmt --all -- --check 52 | - name: cargo clippy 53 | run: cargo clippy --all-targets -- -D warnings 54 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: release 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v*.*.*' 7 | 8 | permissions: 9 | contents: write 10 | 11 | jobs: 12 | changelog: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v4 16 | - name: check semver 17 | uses: obi1kenobi/cargo-semver-checks-action@v2 18 | - name: get version 19 | id: version 20 | run: echo "version=${GITHUB_REF/refs\/tags\//}" >> $GITHUB_OUTPUT 21 | - name: Build changelog 22 | id: release 23 | uses: mikepenz/release-changelog-builder-action@v3 24 | env: 25 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 26 | with: 27 | configuration: .release.json 28 | commitMode: true 29 | - name: create release 30 | uses: softprops/action-gh-release@v1 31 | env: 32 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 33 | with: 34 | tag_name: ${{ github.ref }} 35 | name: ${{ steps.version.outputs.version }} 🎉 36 | body: ${{ steps.release.outputs.changelog }} 37 | 38 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | -------------------------------------------------------------------------------- /.release.json: -------------------------------------------------------------------------------- 1 | { 2 | "categories": [ 3 | { 4 | "title": "## 🚀 Features", 5 | "labels": [ 6 | "feat" 7 | ] 8 | }, 9 | { 10 | "title": "## 🐛 Fixes", 11 | "labels": [ 12 | "fix" 13 | ] 14 | }, 15 | { 16 | "title": "## 📄 Documentation", 17 | "labels": [ 18 | "docs" 19 | ] 20 | }, 21 | { 22 | "title": "## 🧹 Chores", 23 | "labels": [ 24 | "remove", 25 | "fmt", 26 | "chore" 27 | ] 28 | }, 29 | { 30 | "title": "## 🧪 Tests", 31 | "labels": [ 32 | "test", 33 | "ci" 34 | ] 35 | } 36 | ], 37 | "ignore_labels": [ 38 | "ignore" 39 | ], 40 | "sort": { 41 | "order": "ASC", 42 | "on_property": "mergedAt" 43 | }, 44 | "template": "${{CHANGELOG}}", 45 | "pr_template": "- [**${{TITLE}}**](https://github.com/matteopolak/axum-codec/commit/${{MERGE_SHA}}) by ${{AUTHOR}}", 46 | "empty_template": "- no changes", 47 | "max_tags_to_fetch": 200, 48 | "max_pull_requests": 200, 49 | "max_back_track_time_days": 365, 50 | "exclude_merge_branches": [], 51 | "tag_resolver": { 52 | "method": "semver", 53 | "filter": { 54 | "pattern": "v(.+)", 55 | "flags": "gu" 56 | } 57 | }, 58 | "label_extractor": [ 59 | { 60 | "pattern": "^(.+): (.+)", 61 | "target": "$1", 62 | "on_property": "title" 63 | } 64 | ], 65 | "base_branches": [ 66 | "dev" 67 | ] 68 | } 69 | -------------------------------------------------------------------------------- /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.24.2" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" 10 | dependencies = [ 11 | "gimli", 12 | ] 13 | 14 | [[package]] 15 | name = "adler2" 16 | version = "2.0.0" 17 | source = "registry+https://github.com/rust-lang/crates.io-index" 18 | checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" 19 | 20 | [[package]] 21 | name = "aho-corasick" 22 | version = "1.1.3" 23 | source = "registry+https://github.com/rust-lang/crates.io-index" 24 | checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" 25 | dependencies = [ 26 | "memchr", 27 | ] 28 | 29 | [[package]] 30 | name = "aide" 31 | version = "0.14.1" 32 | source = "registry+https://github.com/rust-lang/crates.io-index" 33 | checksum = "2ef7da148319b3f1ac7d338f7a144521ee399cd65e4381aa0c17994e74304aa8" 34 | dependencies = [ 35 | "axum", 36 | "bytes", 37 | "cfg-if", 38 | "http", 39 | "indexmap", 40 | "schemars", 41 | "serde", 42 | "serde_json", 43 | "thiserror 2.0.11", 44 | "tower-layer", 45 | "tower-service", 46 | "tracing", 47 | ] 48 | 49 | [[package]] 50 | name = "aide-validator" 51 | version = "0.1.0" 52 | dependencies = [ 53 | "aide", 54 | "axum", 55 | "axum-codec", 56 | "serde", 57 | "tokio", 58 | ] 59 | 60 | [[package]] 61 | name = "arrayvec" 62 | version = "0.7.6" 63 | source = "registry+https://github.com/rust-lang/crates.io-index" 64 | checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" 65 | 66 | [[package]] 67 | name = "autocfg" 68 | version = "1.4.0" 69 | source = "registry+https://github.com/rust-lang/crates.io-index" 70 | checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" 71 | 72 | [[package]] 73 | name = "axum" 74 | version = "0.8.1" 75 | source = "registry+https://github.com/rust-lang/crates.io-index" 76 | checksum = "6d6fd624c75e18b3b4c6b9caf42b1afe24437daaee904069137d8bab077be8b8" 77 | dependencies = [ 78 | "axum-core", 79 | "bytes", 80 | "form_urlencoded", 81 | "futures-util", 82 | "http", 83 | "http-body", 84 | "http-body-util", 85 | "hyper", 86 | "hyper-util", 87 | "itoa", 88 | "matchit", 89 | "memchr", 90 | "mime", 91 | "percent-encoding", 92 | "pin-project-lite", 93 | "rustversion", 94 | "serde", 95 | "serde_json", 96 | "serde_path_to_error", 97 | "serde_urlencoded", 98 | "sync_wrapper", 99 | "tokio", 100 | "tower", 101 | "tower-layer", 102 | "tower-service", 103 | "tracing", 104 | ] 105 | 106 | [[package]] 107 | name = "axum-codec" 108 | version = "0.0.19" 109 | dependencies = [ 110 | "aide", 111 | "axum", 112 | "axum-codec", 113 | "axum-codec-macros", 114 | "bincode", 115 | "bitcode", 116 | "bytes", 117 | "ciborium", 118 | "mime", 119 | "rmp-serde", 120 | "schemars", 121 | "serde", 122 | "serde_json", 123 | "serde_urlencoded", 124 | "serde_yaml", 125 | "thiserror 1.0.69", 126 | "tokio", 127 | "toml", 128 | "validator", 129 | ] 130 | 131 | [[package]] 132 | name = "axum-codec-macros" 133 | version = "0.0.12" 134 | dependencies = [ 135 | "proc-macro2", 136 | "quote", 137 | "syn", 138 | ] 139 | 140 | [[package]] 141 | name = "axum-core" 142 | version = "0.5.0" 143 | source = "registry+https://github.com/rust-lang/crates.io-index" 144 | checksum = "df1362f362fd16024ae199c1970ce98f9661bf5ef94b9808fee734bc3698b733" 145 | dependencies = [ 146 | "bytes", 147 | "futures-util", 148 | "http", 149 | "http-body", 150 | "http-body-util", 151 | "mime", 152 | "pin-project-lite", 153 | "rustversion", 154 | "sync_wrapper", 155 | "tower-layer", 156 | "tower-service", 157 | "tracing", 158 | ] 159 | 160 | [[package]] 161 | name = "backtrace" 162 | version = "0.3.74" 163 | source = "registry+https://github.com/rust-lang/crates.io-index" 164 | checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a" 165 | dependencies = [ 166 | "addr2line", 167 | "cfg-if", 168 | "libc", 169 | "miniz_oxide", 170 | "object", 171 | "rustc-demangle", 172 | "windows-targets", 173 | ] 174 | 175 | [[package]] 176 | name = "basic" 177 | version = "0.1.0" 178 | dependencies = [ 179 | "axum", 180 | "axum-codec", 181 | "serde", 182 | "tokio", 183 | ] 184 | 185 | [[package]] 186 | name = "bincode" 187 | version = "2.0.0-rc.3" 188 | source = "registry+https://github.com/rust-lang/crates.io-index" 189 | checksum = "f11ea1a0346b94ef188834a65c068a03aec181c94896d481d7a0a40d85b0ce95" 190 | dependencies = [ 191 | "bincode_derive", 192 | "serde", 193 | ] 194 | 195 | [[package]] 196 | name = "bincode_derive" 197 | version = "2.0.0-rc.3" 198 | source = "registry+https://github.com/rust-lang/crates.io-index" 199 | checksum = "7e30759b3b99a1b802a7a3aa21c85c3ded5c28e1c83170d82d70f08bbf7f3e4c" 200 | dependencies = [ 201 | "virtue", 202 | ] 203 | 204 | [[package]] 205 | name = "bitcode" 206 | version = "0.6.5" 207 | source = "registry+https://github.com/rust-lang/crates.io-index" 208 | checksum = "18c1406a27371b2f76232a2259df6ab607b91b5a0a7476a7729ff590df5a969a" 209 | dependencies = [ 210 | "arrayvec", 211 | "bitcode_derive", 212 | "bytemuck", 213 | "glam", 214 | "serde", 215 | ] 216 | 217 | [[package]] 218 | name = "bitcode_derive" 219 | version = "0.6.5" 220 | source = "registry+https://github.com/rust-lang/crates.io-index" 221 | checksum = "42b6b4cb608b8282dc3b53d0f4c9ab404655d562674c682db7e6c0458cc83c23" 222 | dependencies = [ 223 | "proc-macro2", 224 | "quote", 225 | "syn", 226 | ] 227 | 228 | [[package]] 229 | name = "bytemuck" 230 | version = "1.21.0" 231 | source = "registry+https://github.com/rust-lang/crates.io-index" 232 | checksum = "ef657dfab802224e671f5818e9a4935f9b1957ed18e58292690cc39e7a4092a3" 233 | 234 | [[package]] 235 | name = "byteorder" 236 | version = "1.5.0" 237 | source = "registry+https://github.com/rust-lang/crates.io-index" 238 | checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" 239 | 240 | [[package]] 241 | name = "bytes" 242 | version = "1.10.0" 243 | source = "registry+https://github.com/rust-lang/crates.io-index" 244 | checksum = "f61dac84819c6588b558454b194026eb1f09c293b9036ae9b159e74e73ab6cf9" 245 | 246 | [[package]] 247 | name = "cfg-if" 248 | version = "1.0.0" 249 | source = "registry+https://github.com/rust-lang/crates.io-index" 250 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 251 | 252 | [[package]] 253 | name = "ciborium" 254 | version = "0.2.2" 255 | source = "registry+https://github.com/rust-lang/crates.io-index" 256 | checksum = "42e69ffd6f0917f5c029256a24d0161db17cea3997d185db0d35926308770f0e" 257 | dependencies = [ 258 | "ciborium-io", 259 | "ciborium-ll", 260 | "serde", 261 | ] 262 | 263 | [[package]] 264 | name = "ciborium-io" 265 | version = "0.2.2" 266 | source = "registry+https://github.com/rust-lang/crates.io-index" 267 | checksum = "05afea1e0a06c9be33d539b876f1ce3692f4afea2cb41f740e7743225ed1c757" 268 | 269 | [[package]] 270 | name = "ciborium-ll" 271 | version = "0.2.2" 272 | source = "registry+https://github.com/rust-lang/crates.io-index" 273 | checksum = "57663b653d948a338bfb3eeba9bb2fd5fcfaecb9e199e87e1eda4d9e8b240fd9" 274 | dependencies = [ 275 | "ciborium-io", 276 | "half", 277 | ] 278 | 279 | [[package]] 280 | name = "crunchy" 281 | version = "0.2.3" 282 | source = "registry+https://github.com/rust-lang/crates.io-index" 283 | checksum = "43da5946c66ffcc7745f48db692ffbb10a83bfe0afd96235c5c2a4fb23994929" 284 | 285 | [[package]] 286 | name = "darling" 287 | version = "0.20.10" 288 | source = "registry+https://github.com/rust-lang/crates.io-index" 289 | checksum = "6f63b86c8a8826a49b8c21f08a2d07338eec8d900540f8630dc76284be802989" 290 | dependencies = [ 291 | "darling_core", 292 | "darling_macro", 293 | ] 294 | 295 | [[package]] 296 | name = "darling_core" 297 | version = "0.20.10" 298 | source = "registry+https://github.com/rust-lang/crates.io-index" 299 | checksum = "95133861a8032aaea082871032f5815eb9e98cef03fa916ab4500513994df9e5" 300 | dependencies = [ 301 | "fnv", 302 | "ident_case", 303 | "proc-macro2", 304 | "quote", 305 | "strsim", 306 | "syn", 307 | ] 308 | 309 | [[package]] 310 | name = "darling_macro" 311 | version = "0.20.10" 312 | source = "registry+https://github.com/rust-lang/crates.io-index" 313 | checksum = "d336a2a514f6ccccaa3e09b02d41d35330c07ddf03a62165fcec10bb561c7806" 314 | dependencies = [ 315 | "darling_core", 316 | "quote", 317 | "syn", 318 | ] 319 | 320 | [[package]] 321 | name = "displaydoc" 322 | version = "0.2.5" 323 | source = "registry+https://github.com/rust-lang/crates.io-index" 324 | checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" 325 | dependencies = [ 326 | "proc-macro2", 327 | "quote", 328 | "syn", 329 | ] 330 | 331 | [[package]] 332 | name = "dyn-clone" 333 | version = "1.0.18" 334 | source = "registry+https://github.com/rust-lang/crates.io-index" 335 | checksum = "feeef44e73baff3a26d371801df019877a9866a8c493d315ab00177843314f35" 336 | 337 | [[package]] 338 | name = "equivalent" 339 | version = "1.0.2" 340 | source = "registry+https://github.com/rust-lang/crates.io-index" 341 | checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" 342 | 343 | [[package]] 344 | name = "fnv" 345 | version = "1.0.7" 346 | source = "registry+https://github.com/rust-lang/crates.io-index" 347 | checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" 348 | 349 | [[package]] 350 | name = "form_urlencoded" 351 | version = "1.2.1" 352 | source = "registry+https://github.com/rust-lang/crates.io-index" 353 | checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" 354 | dependencies = [ 355 | "percent-encoding", 356 | ] 357 | 358 | [[package]] 359 | name = "futures-channel" 360 | version = "0.3.31" 361 | source = "registry+https://github.com/rust-lang/crates.io-index" 362 | checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" 363 | dependencies = [ 364 | "futures-core", 365 | ] 366 | 367 | [[package]] 368 | name = "futures-core" 369 | version = "0.3.31" 370 | source = "registry+https://github.com/rust-lang/crates.io-index" 371 | checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" 372 | 373 | [[package]] 374 | name = "futures-task" 375 | version = "0.3.31" 376 | source = "registry+https://github.com/rust-lang/crates.io-index" 377 | checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" 378 | 379 | [[package]] 380 | name = "futures-util" 381 | version = "0.3.31" 382 | source = "registry+https://github.com/rust-lang/crates.io-index" 383 | checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" 384 | dependencies = [ 385 | "futures-core", 386 | "futures-task", 387 | "pin-project-lite", 388 | "pin-utils", 389 | ] 390 | 391 | [[package]] 392 | name = "gimli" 393 | version = "0.31.1" 394 | source = "registry+https://github.com/rust-lang/crates.io-index" 395 | checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" 396 | 397 | [[package]] 398 | name = "glam" 399 | version = "0.30.0" 400 | source = "registry+https://github.com/rust-lang/crates.io-index" 401 | checksum = "17fcdf9683c406c2fc4d124afd29c0d595e22210d633cbdb8695ba9935ab1dc6" 402 | 403 | [[package]] 404 | name = "half" 405 | version = "2.4.1" 406 | source = "registry+https://github.com/rust-lang/crates.io-index" 407 | checksum = "6dd08c532ae367adf81c312a4580bc67f1d0fe8bc9c460520283f4c0ff277888" 408 | dependencies = [ 409 | "cfg-if", 410 | "crunchy", 411 | ] 412 | 413 | [[package]] 414 | name = "hashbrown" 415 | version = "0.15.2" 416 | source = "registry+https://github.com/rust-lang/crates.io-index" 417 | checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" 418 | 419 | [[package]] 420 | name = "http" 421 | version = "1.2.0" 422 | source = "registry+https://github.com/rust-lang/crates.io-index" 423 | checksum = "f16ca2af56261c99fba8bac40a10251ce8188205a4c448fbb745a2e4daa76fea" 424 | dependencies = [ 425 | "bytes", 426 | "fnv", 427 | "itoa", 428 | ] 429 | 430 | [[package]] 431 | name = "http-body" 432 | version = "1.0.1" 433 | source = "registry+https://github.com/rust-lang/crates.io-index" 434 | checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" 435 | dependencies = [ 436 | "bytes", 437 | "http", 438 | ] 439 | 440 | [[package]] 441 | name = "http-body-util" 442 | version = "0.1.2" 443 | source = "registry+https://github.com/rust-lang/crates.io-index" 444 | checksum = "793429d76616a256bcb62c2a2ec2bed781c8307e797e2598c50010f2bee2544f" 445 | dependencies = [ 446 | "bytes", 447 | "futures-util", 448 | "http", 449 | "http-body", 450 | "pin-project-lite", 451 | ] 452 | 453 | [[package]] 454 | name = "httparse" 455 | version = "1.10.0" 456 | source = "registry+https://github.com/rust-lang/crates.io-index" 457 | checksum = "f2d708df4e7140240a16cd6ab0ab65c972d7433ab77819ea693fde9c43811e2a" 458 | 459 | [[package]] 460 | name = "httpdate" 461 | version = "1.0.3" 462 | source = "registry+https://github.com/rust-lang/crates.io-index" 463 | checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" 464 | 465 | [[package]] 466 | name = "hyper" 467 | version = "1.6.0" 468 | source = "registry+https://github.com/rust-lang/crates.io-index" 469 | checksum = "cc2b571658e38e0c01b1fdca3bbbe93c00d3d71693ff2770043f8c29bc7d6f80" 470 | dependencies = [ 471 | "bytes", 472 | "futures-channel", 473 | "futures-util", 474 | "http", 475 | "http-body", 476 | "httparse", 477 | "httpdate", 478 | "itoa", 479 | "pin-project-lite", 480 | "smallvec", 481 | "tokio", 482 | ] 483 | 484 | [[package]] 485 | name = "hyper-util" 486 | version = "0.1.10" 487 | source = "registry+https://github.com/rust-lang/crates.io-index" 488 | checksum = "df2dcfbe0677734ab2f3ffa7fa7bfd4706bfdc1ef393f2ee30184aed67e631b4" 489 | dependencies = [ 490 | "bytes", 491 | "futures-util", 492 | "http", 493 | "http-body", 494 | "hyper", 495 | "pin-project-lite", 496 | "tokio", 497 | "tower-service", 498 | ] 499 | 500 | [[package]] 501 | name = "icu_collections" 502 | version = "1.5.0" 503 | source = "registry+https://github.com/rust-lang/crates.io-index" 504 | checksum = "db2fa452206ebee18c4b5c2274dbf1de17008e874b4dc4f0aea9d01ca79e4526" 505 | dependencies = [ 506 | "displaydoc", 507 | "yoke", 508 | "zerofrom", 509 | "zerovec", 510 | ] 511 | 512 | [[package]] 513 | name = "icu_locid" 514 | version = "1.5.0" 515 | source = "registry+https://github.com/rust-lang/crates.io-index" 516 | checksum = "13acbb8371917fc971be86fc8057c41a64b521c184808a698c02acc242dbf637" 517 | dependencies = [ 518 | "displaydoc", 519 | "litemap", 520 | "tinystr", 521 | "writeable", 522 | "zerovec", 523 | ] 524 | 525 | [[package]] 526 | name = "icu_locid_transform" 527 | version = "1.5.0" 528 | source = "registry+https://github.com/rust-lang/crates.io-index" 529 | checksum = "01d11ac35de8e40fdeda00d9e1e9d92525f3f9d887cdd7aa81d727596788b54e" 530 | dependencies = [ 531 | "displaydoc", 532 | "icu_locid", 533 | "icu_locid_transform_data", 534 | "icu_provider", 535 | "tinystr", 536 | "zerovec", 537 | ] 538 | 539 | [[package]] 540 | name = "icu_locid_transform_data" 541 | version = "1.5.0" 542 | source = "registry+https://github.com/rust-lang/crates.io-index" 543 | checksum = "fdc8ff3388f852bede6b579ad4e978ab004f139284d7b28715f773507b946f6e" 544 | 545 | [[package]] 546 | name = "icu_normalizer" 547 | version = "1.5.0" 548 | source = "registry+https://github.com/rust-lang/crates.io-index" 549 | checksum = "19ce3e0da2ec68599d193c93d088142efd7f9c5d6fc9b803774855747dc6a84f" 550 | dependencies = [ 551 | "displaydoc", 552 | "icu_collections", 553 | "icu_normalizer_data", 554 | "icu_properties", 555 | "icu_provider", 556 | "smallvec", 557 | "utf16_iter", 558 | "utf8_iter", 559 | "write16", 560 | "zerovec", 561 | ] 562 | 563 | [[package]] 564 | name = "icu_normalizer_data" 565 | version = "1.5.0" 566 | source = "registry+https://github.com/rust-lang/crates.io-index" 567 | checksum = "f8cafbf7aa791e9b22bec55a167906f9e1215fd475cd22adfcf660e03e989516" 568 | 569 | [[package]] 570 | name = "icu_properties" 571 | version = "1.5.1" 572 | source = "registry+https://github.com/rust-lang/crates.io-index" 573 | checksum = "93d6020766cfc6302c15dbbc9c8778c37e62c14427cb7f6e601d849e092aeef5" 574 | dependencies = [ 575 | "displaydoc", 576 | "icu_collections", 577 | "icu_locid_transform", 578 | "icu_properties_data", 579 | "icu_provider", 580 | "tinystr", 581 | "zerovec", 582 | ] 583 | 584 | [[package]] 585 | name = "icu_properties_data" 586 | version = "1.5.0" 587 | source = "registry+https://github.com/rust-lang/crates.io-index" 588 | checksum = "67a8effbc3dd3e4ba1afa8ad918d5684b8868b3b26500753effea8d2eed19569" 589 | 590 | [[package]] 591 | name = "icu_provider" 592 | version = "1.5.0" 593 | source = "registry+https://github.com/rust-lang/crates.io-index" 594 | checksum = "6ed421c8a8ef78d3e2dbc98a973be2f3770cb42b606e3ab18d6237c4dfde68d9" 595 | dependencies = [ 596 | "displaydoc", 597 | "icu_locid", 598 | "icu_provider_macros", 599 | "stable_deref_trait", 600 | "tinystr", 601 | "writeable", 602 | "yoke", 603 | "zerofrom", 604 | "zerovec", 605 | ] 606 | 607 | [[package]] 608 | name = "icu_provider_macros" 609 | version = "1.5.0" 610 | source = "registry+https://github.com/rust-lang/crates.io-index" 611 | checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6" 612 | dependencies = [ 613 | "proc-macro2", 614 | "quote", 615 | "syn", 616 | ] 617 | 618 | [[package]] 619 | name = "ident_case" 620 | version = "1.0.1" 621 | source = "registry+https://github.com/rust-lang/crates.io-index" 622 | checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" 623 | 624 | [[package]] 625 | name = "idna" 626 | version = "1.0.3" 627 | source = "registry+https://github.com/rust-lang/crates.io-index" 628 | checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e" 629 | dependencies = [ 630 | "idna_adapter", 631 | "smallvec", 632 | "utf8_iter", 633 | ] 634 | 635 | [[package]] 636 | name = "idna_adapter" 637 | version = "1.2.0" 638 | source = "registry+https://github.com/rust-lang/crates.io-index" 639 | checksum = "daca1df1c957320b2cf139ac61e7bd64fed304c5040df000a745aa1de3b4ef71" 640 | dependencies = [ 641 | "icu_normalizer", 642 | "icu_properties", 643 | ] 644 | 645 | [[package]] 646 | name = "indexmap" 647 | version = "2.7.1" 648 | source = "registry+https://github.com/rust-lang/crates.io-index" 649 | checksum = "8c9c992b02b5b4c94ea26e32fe5bccb7aa7d9f390ab5c1221ff895bc7ea8b652" 650 | dependencies = [ 651 | "equivalent", 652 | "hashbrown", 653 | "serde", 654 | ] 655 | 656 | [[package]] 657 | name = "itoa" 658 | version = "1.0.14" 659 | source = "registry+https://github.com/rust-lang/crates.io-index" 660 | checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674" 661 | 662 | [[package]] 663 | name = "libc" 664 | version = "0.2.169" 665 | source = "registry+https://github.com/rust-lang/crates.io-index" 666 | checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a" 667 | 668 | [[package]] 669 | name = "litemap" 670 | version = "0.7.4" 671 | source = "registry+https://github.com/rust-lang/crates.io-index" 672 | checksum = "4ee93343901ab17bd981295f2cf0026d4ad018c7c31ba84549a4ddbb47a45104" 673 | 674 | [[package]] 675 | name = "log" 676 | version = "0.4.26" 677 | source = "registry+https://github.com/rust-lang/crates.io-index" 678 | checksum = "30bde2b3dc3671ae49d8e2e9f044c7c005836e7a023ee57cffa25ab82764bb9e" 679 | 680 | [[package]] 681 | name = "matchit" 682 | version = "0.8.4" 683 | source = "registry+https://github.com/rust-lang/crates.io-index" 684 | checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" 685 | 686 | [[package]] 687 | name = "memchr" 688 | version = "2.7.4" 689 | source = "registry+https://github.com/rust-lang/crates.io-index" 690 | checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" 691 | 692 | [[package]] 693 | name = "mime" 694 | version = "0.3.17" 695 | source = "registry+https://github.com/rust-lang/crates.io-index" 696 | checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" 697 | 698 | [[package]] 699 | name = "miniz_oxide" 700 | version = "0.8.5" 701 | source = "registry+https://github.com/rust-lang/crates.io-index" 702 | checksum = "8e3e04debbb59698c15bacbb6d93584a8c0ca9cc3213cb423d31f760d8843ce5" 703 | dependencies = [ 704 | "adler2", 705 | ] 706 | 707 | [[package]] 708 | name = "mio" 709 | version = "1.0.3" 710 | source = "registry+https://github.com/rust-lang/crates.io-index" 711 | checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd" 712 | dependencies = [ 713 | "libc", 714 | "wasi", 715 | "windows-sys", 716 | ] 717 | 718 | [[package]] 719 | name = "num-traits" 720 | version = "0.2.19" 721 | source = "registry+https://github.com/rust-lang/crates.io-index" 722 | checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" 723 | dependencies = [ 724 | "autocfg", 725 | ] 726 | 727 | [[package]] 728 | name = "object" 729 | version = "0.36.7" 730 | source = "registry+https://github.com/rust-lang/crates.io-index" 731 | checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" 732 | dependencies = [ 733 | "memchr", 734 | ] 735 | 736 | [[package]] 737 | name = "once_cell" 738 | version = "1.20.3" 739 | source = "registry+https://github.com/rust-lang/crates.io-index" 740 | checksum = "945462a4b81e43c4e3ba96bd7b49d834c6f61198356aa858733bc4acf3cbe62e" 741 | 742 | [[package]] 743 | name = "paste" 744 | version = "1.0.15" 745 | source = "registry+https://github.com/rust-lang/crates.io-index" 746 | checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" 747 | 748 | [[package]] 749 | name = "percent-encoding" 750 | version = "2.3.1" 751 | source = "registry+https://github.com/rust-lang/crates.io-index" 752 | checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" 753 | 754 | [[package]] 755 | name = "pin-project-lite" 756 | version = "0.2.16" 757 | source = "registry+https://github.com/rust-lang/crates.io-index" 758 | checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" 759 | 760 | [[package]] 761 | name = "pin-utils" 762 | version = "0.1.0" 763 | source = "registry+https://github.com/rust-lang/crates.io-index" 764 | checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" 765 | 766 | [[package]] 767 | name = "proc-macro-error-attr2" 768 | version = "2.0.0" 769 | source = "registry+https://github.com/rust-lang/crates.io-index" 770 | checksum = "96de42df36bb9bba5542fe9f1a054b8cc87e172759a1868aa05c1f3acc89dfc5" 771 | dependencies = [ 772 | "proc-macro2", 773 | "quote", 774 | ] 775 | 776 | [[package]] 777 | name = "proc-macro-error2" 778 | version = "2.0.1" 779 | source = "registry+https://github.com/rust-lang/crates.io-index" 780 | checksum = "11ec05c52be0a07b08061f7dd003e7d7092e0472bc731b4af7bb1ef876109802" 781 | dependencies = [ 782 | "proc-macro-error-attr2", 783 | "proc-macro2", 784 | "quote", 785 | "syn", 786 | ] 787 | 788 | [[package]] 789 | name = "proc-macro2" 790 | version = "1.0.93" 791 | source = "registry+https://github.com/rust-lang/crates.io-index" 792 | checksum = "60946a68e5f9d28b0dc1c21bb8a97ee7d018a8b322fa57838ba31cc878e22d99" 793 | dependencies = [ 794 | "unicode-ident", 795 | ] 796 | 797 | [[package]] 798 | name = "quote" 799 | version = "1.0.38" 800 | source = "registry+https://github.com/rust-lang/crates.io-index" 801 | checksum = "0e4dccaaaf89514f546c693ddc140f729f958c247918a13380cccc6078391acc" 802 | dependencies = [ 803 | "proc-macro2", 804 | ] 805 | 806 | [[package]] 807 | name = "regex" 808 | version = "1.11.1" 809 | source = "registry+https://github.com/rust-lang/crates.io-index" 810 | checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" 811 | dependencies = [ 812 | "aho-corasick", 813 | "memchr", 814 | "regex-automata", 815 | "regex-syntax", 816 | ] 817 | 818 | [[package]] 819 | name = "regex-automata" 820 | version = "0.4.9" 821 | source = "registry+https://github.com/rust-lang/crates.io-index" 822 | checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" 823 | dependencies = [ 824 | "aho-corasick", 825 | "memchr", 826 | "regex-syntax", 827 | ] 828 | 829 | [[package]] 830 | name = "regex-syntax" 831 | version = "0.8.5" 832 | source = "registry+https://github.com/rust-lang/crates.io-index" 833 | checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" 834 | 835 | [[package]] 836 | name = "rmp" 837 | version = "0.8.14" 838 | source = "registry+https://github.com/rust-lang/crates.io-index" 839 | checksum = "228ed7c16fa39782c3b3468e974aec2795e9089153cd08ee2e9aefb3613334c4" 840 | dependencies = [ 841 | "byteorder", 842 | "num-traits", 843 | "paste", 844 | ] 845 | 846 | [[package]] 847 | name = "rmp-serde" 848 | version = "1.3.0" 849 | source = "registry+https://github.com/rust-lang/crates.io-index" 850 | checksum = "52e599a477cf9840e92f2cde9a7189e67b42c57532749bf90aea6ec10facd4db" 851 | dependencies = [ 852 | "byteorder", 853 | "rmp", 854 | "serde", 855 | ] 856 | 857 | [[package]] 858 | name = "rustc-demangle" 859 | version = "0.1.24" 860 | source = "registry+https://github.com/rust-lang/crates.io-index" 861 | checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" 862 | 863 | [[package]] 864 | name = "rustversion" 865 | version = "1.0.19" 866 | source = "registry+https://github.com/rust-lang/crates.io-index" 867 | checksum = "f7c45b9784283f1b2e7fb61b42047c2fd678ef0960d4f6f1eba131594cc369d4" 868 | 869 | [[package]] 870 | name = "ryu" 871 | version = "1.0.19" 872 | source = "registry+https://github.com/rust-lang/crates.io-index" 873 | checksum = "6ea1a2d0a644769cc99faa24c3ad26b379b786fe7c36fd3c546254801650e6dd" 874 | 875 | [[package]] 876 | name = "schemars" 877 | version = "0.8.21" 878 | source = "registry+https://github.com/rust-lang/crates.io-index" 879 | checksum = "09c024468a378b7e36765cd36702b7a90cc3cba11654f6685c8f233408e89e92" 880 | dependencies = [ 881 | "dyn-clone", 882 | "indexmap", 883 | "schemars_derive", 884 | "serde", 885 | "serde_json", 886 | ] 887 | 888 | [[package]] 889 | name = "schemars_derive" 890 | version = "0.8.21" 891 | source = "registry+https://github.com/rust-lang/crates.io-index" 892 | checksum = "b1eee588578aff73f856ab961cd2f79e36bc45d7ded33a7562adba4667aecc0e" 893 | dependencies = [ 894 | "proc-macro2", 895 | "quote", 896 | "serde_derive_internals", 897 | "syn", 898 | ] 899 | 900 | [[package]] 901 | name = "serde" 902 | version = "1.0.218" 903 | source = "registry+https://github.com/rust-lang/crates.io-index" 904 | checksum = "e8dfc9d19bdbf6d17e22319da49161d5d0108e4188e8b680aef6299eed22df60" 905 | dependencies = [ 906 | "serde_derive", 907 | ] 908 | 909 | [[package]] 910 | name = "serde_derive" 911 | version = "1.0.218" 912 | source = "registry+https://github.com/rust-lang/crates.io-index" 913 | checksum = "f09503e191f4e797cb8aac08e9a4a4695c5edf6a2e70e376d961ddd5c969f82b" 914 | dependencies = [ 915 | "proc-macro2", 916 | "quote", 917 | "syn", 918 | ] 919 | 920 | [[package]] 921 | name = "serde_derive_internals" 922 | version = "0.29.1" 923 | source = "registry+https://github.com/rust-lang/crates.io-index" 924 | checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" 925 | dependencies = [ 926 | "proc-macro2", 927 | "quote", 928 | "syn", 929 | ] 930 | 931 | [[package]] 932 | name = "serde_json" 933 | version = "1.0.139" 934 | source = "registry+https://github.com/rust-lang/crates.io-index" 935 | checksum = "44f86c3acccc9c65b153fe1b85a3be07fe5515274ec9f0653b4a0875731c72a6" 936 | dependencies = [ 937 | "itoa", 938 | "memchr", 939 | "ryu", 940 | "serde", 941 | ] 942 | 943 | [[package]] 944 | name = "serde_path_to_error" 945 | version = "0.1.16" 946 | source = "registry+https://github.com/rust-lang/crates.io-index" 947 | checksum = "af99884400da37c88f5e9146b7f1fd0fbcae8f6eec4e9da38b67d05486f814a6" 948 | dependencies = [ 949 | "itoa", 950 | "serde", 951 | ] 952 | 953 | [[package]] 954 | name = "serde_spanned" 955 | version = "0.6.8" 956 | source = "registry+https://github.com/rust-lang/crates.io-index" 957 | checksum = "87607cb1398ed59d48732e575a4c28a7a8ebf2454b964fe3f224f2afc07909e1" 958 | dependencies = [ 959 | "serde", 960 | ] 961 | 962 | [[package]] 963 | name = "serde_urlencoded" 964 | version = "0.7.1" 965 | source = "registry+https://github.com/rust-lang/crates.io-index" 966 | checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" 967 | dependencies = [ 968 | "form_urlencoded", 969 | "itoa", 970 | "ryu", 971 | "serde", 972 | ] 973 | 974 | [[package]] 975 | name = "serde_yaml" 976 | version = "0.9.34+deprecated" 977 | source = "registry+https://github.com/rust-lang/crates.io-index" 978 | checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" 979 | dependencies = [ 980 | "indexmap", 981 | "itoa", 982 | "ryu", 983 | "serde", 984 | "unsafe-libyaml", 985 | ] 986 | 987 | [[package]] 988 | name = "smallvec" 989 | version = "1.14.0" 990 | source = "registry+https://github.com/rust-lang/crates.io-index" 991 | checksum = "7fcf8323ef1faaee30a44a340193b1ac6814fd9b7b4e88e9d4519a3e4abe1cfd" 992 | 993 | [[package]] 994 | name = "socket2" 995 | version = "0.5.8" 996 | source = "registry+https://github.com/rust-lang/crates.io-index" 997 | checksum = "c970269d99b64e60ec3bd6ad27270092a5394c4e309314b18ae3fe575695fbe8" 998 | dependencies = [ 999 | "libc", 1000 | "windows-sys", 1001 | ] 1002 | 1003 | [[package]] 1004 | name = "stable_deref_trait" 1005 | version = "1.2.0" 1006 | source = "registry+https://github.com/rust-lang/crates.io-index" 1007 | checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" 1008 | 1009 | [[package]] 1010 | name = "strsim" 1011 | version = "0.11.1" 1012 | source = "registry+https://github.com/rust-lang/crates.io-index" 1013 | checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" 1014 | 1015 | [[package]] 1016 | name = "syn" 1017 | version = "2.0.98" 1018 | source = "registry+https://github.com/rust-lang/crates.io-index" 1019 | checksum = "36147f1a48ae0ec2b5b3bc5b537d267457555a10dc06f3dbc8cb11ba3006d3b1" 1020 | dependencies = [ 1021 | "proc-macro2", 1022 | "quote", 1023 | "unicode-ident", 1024 | ] 1025 | 1026 | [[package]] 1027 | name = "sync_wrapper" 1028 | version = "1.0.2" 1029 | source = "registry+https://github.com/rust-lang/crates.io-index" 1030 | checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" 1031 | 1032 | [[package]] 1033 | name = "synstructure" 1034 | version = "0.13.1" 1035 | source = "registry+https://github.com/rust-lang/crates.io-index" 1036 | checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971" 1037 | dependencies = [ 1038 | "proc-macro2", 1039 | "quote", 1040 | "syn", 1041 | ] 1042 | 1043 | [[package]] 1044 | name = "thiserror" 1045 | version = "1.0.69" 1046 | source = "registry+https://github.com/rust-lang/crates.io-index" 1047 | checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" 1048 | dependencies = [ 1049 | "thiserror-impl 1.0.69", 1050 | ] 1051 | 1052 | [[package]] 1053 | name = "thiserror" 1054 | version = "2.0.11" 1055 | source = "registry+https://github.com/rust-lang/crates.io-index" 1056 | checksum = "d452f284b73e6d76dd36758a0c8684b1d5be31f92b89d07fd5822175732206fc" 1057 | dependencies = [ 1058 | "thiserror-impl 2.0.11", 1059 | ] 1060 | 1061 | [[package]] 1062 | name = "thiserror-impl" 1063 | version = "1.0.69" 1064 | source = "registry+https://github.com/rust-lang/crates.io-index" 1065 | checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" 1066 | dependencies = [ 1067 | "proc-macro2", 1068 | "quote", 1069 | "syn", 1070 | ] 1071 | 1072 | [[package]] 1073 | name = "thiserror-impl" 1074 | version = "2.0.11" 1075 | source = "registry+https://github.com/rust-lang/crates.io-index" 1076 | checksum = "26afc1baea8a989337eeb52b6e72a039780ce45c3edfcc9c5b9d112feeb173c2" 1077 | dependencies = [ 1078 | "proc-macro2", 1079 | "quote", 1080 | "syn", 1081 | ] 1082 | 1083 | [[package]] 1084 | name = "tinystr" 1085 | version = "0.7.6" 1086 | source = "registry+https://github.com/rust-lang/crates.io-index" 1087 | checksum = "9117f5d4db391c1cf6927e7bea3db74b9a1c1add8f7eda9ffd5364f40f57b82f" 1088 | dependencies = [ 1089 | "displaydoc", 1090 | "zerovec", 1091 | ] 1092 | 1093 | [[package]] 1094 | name = "todo-api" 1095 | version = "0.1.0" 1096 | dependencies = [ 1097 | "aide", 1098 | "axum", 1099 | "axum-codec", 1100 | "serde", 1101 | "tokio", 1102 | ] 1103 | 1104 | [[package]] 1105 | name = "tokio" 1106 | version = "1.43.0" 1107 | source = "registry+https://github.com/rust-lang/crates.io-index" 1108 | checksum = "3d61fa4ffa3de412bfea335c6ecff681de2b609ba3c77ef3e00e521813a9ed9e" 1109 | dependencies = [ 1110 | "backtrace", 1111 | "libc", 1112 | "mio", 1113 | "pin-project-lite", 1114 | "socket2", 1115 | "tokio-macros", 1116 | "windows-sys", 1117 | ] 1118 | 1119 | [[package]] 1120 | name = "tokio-macros" 1121 | version = "2.5.0" 1122 | source = "registry+https://github.com/rust-lang/crates.io-index" 1123 | checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" 1124 | dependencies = [ 1125 | "proc-macro2", 1126 | "quote", 1127 | "syn", 1128 | ] 1129 | 1130 | [[package]] 1131 | name = "toml" 1132 | version = "0.8.20" 1133 | source = "registry+https://github.com/rust-lang/crates.io-index" 1134 | checksum = "cd87a5cdd6ffab733b2f74bc4fd7ee5fff6634124999ac278c35fc78c6120148" 1135 | dependencies = [ 1136 | "serde", 1137 | "serde_spanned", 1138 | "toml_datetime", 1139 | "toml_edit", 1140 | ] 1141 | 1142 | [[package]] 1143 | name = "toml_datetime" 1144 | version = "0.6.8" 1145 | source = "registry+https://github.com/rust-lang/crates.io-index" 1146 | checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41" 1147 | dependencies = [ 1148 | "serde", 1149 | ] 1150 | 1151 | [[package]] 1152 | name = "toml_edit" 1153 | version = "0.22.24" 1154 | source = "registry+https://github.com/rust-lang/crates.io-index" 1155 | checksum = "17b4795ff5edd201c7cd6dca065ae59972ce77d1b80fa0a84d94950ece7d1474" 1156 | dependencies = [ 1157 | "indexmap", 1158 | "serde", 1159 | "serde_spanned", 1160 | "toml_datetime", 1161 | "winnow", 1162 | ] 1163 | 1164 | [[package]] 1165 | name = "tower" 1166 | version = "0.5.2" 1167 | source = "registry+https://github.com/rust-lang/crates.io-index" 1168 | checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" 1169 | dependencies = [ 1170 | "futures-core", 1171 | "futures-util", 1172 | "pin-project-lite", 1173 | "sync_wrapper", 1174 | "tokio", 1175 | "tower-layer", 1176 | "tower-service", 1177 | "tracing", 1178 | ] 1179 | 1180 | [[package]] 1181 | name = "tower-layer" 1182 | version = "0.3.3" 1183 | source = "registry+https://github.com/rust-lang/crates.io-index" 1184 | checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" 1185 | 1186 | [[package]] 1187 | name = "tower-service" 1188 | version = "0.3.3" 1189 | source = "registry+https://github.com/rust-lang/crates.io-index" 1190 | checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" 1191 | 1192 | [[package]] 1193 | name = "tracing" 1194 | version = "0.1.41" 1195 | source = "registry+https://github.com/rust-lang/crates.io-index" 1196 | checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" 1197 | dependencies = [ 1198 | "log", 1199 | "pin-project-lite", 1200 | "tracing-attributes", 1201 | "tracing-core", 1202 | ] 1203 | 1204 | [[package]] 1205 | name = "tracing-attributes" 1206 | version = "0.1.28" 1207 | source = "registry+https://github.com/rust-lang/crates.io-index" 1208 | checksum = "395ae124c09f9e6918a2310af6038fba074bcf474ac352496d5910dd59a2226d" 1209 | dependencies = [ 1210 | "proc-macro2", 1211 | "quote", 1212 | "syn", 1213 | ] 1214 | 1215 | [[package]] 1216 | name = "tracing-core" 1217 | version = "0.1.33" 1218 | source = "registry+https://github.com/rust-lang/crates.io-index" 1219 | checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c" 1220 | dependencies = [ 1221 | "once_cell", 1222 | ] 1223 | 1224 | [[package]] 1225 | name = "unicode-ident" 1226 | version = "1.0.17" 1227 | source = "registry+https://github.com/rust-lang/crates.io-index" 1228 | checksum = "00e2473a93778eb0bad35909dff6a10d28e63f792f16ed15e404fca9d5eeedbe" 1229 | 1230 | [[package]] 1231 | name = "unsafe-libyaml" 1232 | version = "0.2.11" 1233 | source = "registry+https://github.com/rust-lang/crates.io-index" 1234 | checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861" 1235 | 1236 | [[package]] 1237 | name = "url" 1238 | version = "2.5.4" 1239 | source = "registry+https://github.com/rust-lang/crates.io-index" 1240 | checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60" 1241 | dependencies = [ 1242 | "form_urlencoded", 1243 | "idna", 1244 | "percent-encoding", 1245 | ] 1246 | 1247 | [[package]] 1248 | name = "utf16_iter" 1249 | version = "1.0.5" 1250 | source = "registry+https://github.com/rust-lang/crates.io-index" 1251 | checksum = "c8232dd3cdaed5356e0f716d285e4b40b932ac434100fe9b7e0e8e935b9e6246" 1252 | 1253 | [[package]] 1254 | name = "utf8_iter" 1255 | version = "1.0.4" 1256 | source = "registry+https://github.com/rust-lang/crates.io-index" 1257 | checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" 1258 | 1259 | [[package]] 1260 | name = "validator" 1261 | version = "0.20.0" 1262 | source = "registry+https://github.com/rust-lang/crates.io-index" 1263 | checksum = "43fb22e1a008ece370ce08a3e9e4447a910e92621bb49b85d6e48a45397e7cfa" 1264 | dependencies = [ 1265 | "idna", 1266 | "once_cell", 1267 | "regex", 1268 | "serde", 1269 | "serde_derive", 1270 | "serde_json", 1271 | "url", 1272 | "validator_derive", 1273 | ] 1274 | 1275 | [[package]] 1276 | name = "validator_derive" 1277 | version = "0.20.0" 1278 | source = "registry+https://github.com/rust-lang/crates.io-index" 1279 | checksum = "b7df16e474ef958526d1205f6dda359fdfab79d9aa6d54bafcb92dcd07673dca" 1280 | dependencies = [ 1281 | "darling", 1282 | "once_cell", 1283 | "proc-macro-error2", 1284 | "proc-macro2", 1285 | "quote", 1286 | "syn", 1287 | ] 1288 | 1289 | [[package]] 1290 | name = "virtue" 1291 | version = "0.0.13" 1292 | source = "registry+https://github.com/rust-lang/crates.io-index" 1293 | checksum = "9dcc60c0624df774c82a0ef104151231d37da4962957d691c011c852b2473314" 1294 | 1295 | [[package]] 1296 | name = "wasi" 1297 | version = "0.11.0+wasi-snapshot-preview1" 1298 | source = "registry+https://github.com/rust-lang/crates.io-index" 1299 | checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" 1300 | 1301 | [[package]] 1302 | name = "windows-sys" 1303 | version = "0.52.0" 1304 | source = "registry+https://github.com/rust-lang/crates.io-index" 1305 | checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" 1306 | dependencies = [ 1307 | "windows-targets", 1308 | ] 1309 | 1310 | [[package]] 1311 | name = "windows-targets" 1312 | version = "0.52.6" 1313 | source = "registry+https://github.com/rust-lang/crates.io-index" 1314 | checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" 1315 | dependencies = [ 1316 | "windows_aarch64_gnullvm", 1317 | "windows_aarch64_msvc", 1318 | "windows_i686_gnu", 1319 | "windows_i686_gnullvm", 1320 | "windows_i686_msvc", 1321 | "windows_x86_64_gnu", 1322 | "windows_x86_64_gnullvm", 1323 | "windows_x86_64_msvc", 1324 | ] 1325 | 1326 | [[package]] 1327 | name = "windows_aarch64_gnullvm" 1328 | version = "0.52.6" 1329 | source = "registry+https://github.com/rust-lang/crates.io-index" 1330 | checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" 1331 | 1332 | [[package]] 1333 | name = "windows_aarch64_msvc" 1334 | version = "0.52.6" 1335 | source = "registry+https://github.com/rust-lang/crates.io-index" 1336 | checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" 1337 | 1338 | [[package]] 1339 | name = "windows_i686_gnu" 1340 | version = "0.52.6" 1341 | source = "registry+https://github.com/rust-lang/crates.io-index" 1342 | checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" 1343 | 1344 | [[package]] 1345 | name = "windows_i686_gnullvm" 1346 | version = "0.52.6" 1347 | source = "registry+https://github.com/rust-lang/crates.io-index" 1348 | checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" 1349 | 1350 | [[package]] 1351 | name = "windows_i686_msvc" 1352 | version = "0.52.6" 1353 | source = "registry+https://github.com/rust-lang/crates.io-index" 1354 | checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" 1355 | 1356 | [[package]] 1357 | name = "windows_x86_64_gnu" 1358 | version = "0.52.6" 1359 | source = "registry+https://github.com/rust-lang/crates.io-index" 1360 | checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" 1361 | 1362 | [[package]] 1363 | name = "windows_x86_64_gnullvm" 1364 | version = "0.52.6" 1365 | source = "registry+https://github.com/rust-lang/crates.io-index" 1366 | checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" 1367 | 1368 | [[package]] 1369 | name = "windows_x86_64_msvc" 1370 | version = "0.52.6" 1371 | source = "registry+https://github.com/rust-lang/crates.io-index" 1372 | checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" 1373 | 1374 | [[package]] 1375 | name = "winnow" 1376 | version = "0.7.3" 1377 | source = "registry+https://github.com/rust-lang/crates.io-index" 1378 | checksum = "0e7f4ea97f6f78012141bcdb6a216b2609f0979ada50b20ca5b52dde2eac2bb1" 1379 | dependencies = [ 1380 | "memchr", 1381 | ] 1382 | 1383 | [[package]] 1384 | name = "write16" 1385 | version = "1.0.0" 1386 | source = "registry+https://github.com/rust-lang/crates.io-index" 1387 | checksum = "d1890f4022759daae28ed4fe62859b1236caebfc61ede2f63ed4e695f3f6d936" 1388 | 1389 | [[package]] 1390 | name = "writeable" 1391 | version = "0.5.5" 1392 | source = "registry+https://github.com/rust-lang/crates.io-index" 1393 | checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" 1394 | 1395 | [[package]] 1396 | name = "yoke" 1397 | version = "0.7.5" 1398 | source = "registry+https://github.com/rust-lang/crates.io-index" 1399 | checksum = "120e6aef9aa629e3d4f52dc8cc43a015c7724194c97dfaf45180d2daf2b77f40" 1400 | dependencies = [ 1401 | "serde", 1402 | "stable_deref_trait", 1403 | "yoke-derive", 1404 | "zerofrom", 1405 | ] 1406 | 1407 | [[package]] 1408 | name = "yoke-derive" 1409 | version = "0.7.5" 1410 | source = "registry+https://github.com/rust-lang/crates.io-index" 1411 | checksum = "2380878cad4ac9aac1e2435f3eb4020e8374b5f13c296cb75b4620ff8e229154" 1412 | dependencies = [ 1413 | "proc-macro2", 1414 | "quote", 1415 | "syn", 1416 | "synstructure", 1417 | ] 1418 | 1419 | [[package]] 1420 | name = "zerofrom" 1421 | version = "0.1.5" 1422 | source = "registry+https://github.com/rust-lang/crates.io-index" 1423 | checksum = "cff3ee08c995dee1859d998dea82f7374f2826091dd9cd47def953cae446cd2e" 1424 | dependencies = [ 1425 | "zerofrom-derive", 1426 | ] 1427 | 1428 | [[package]] 1429 | name = "zerofrom-derive" 1430 | version = "0.1.5" 1431 | source = "registry+https://github.com/rust-lang/crates.io-index" 1432 | checksum = "595eed982f7d355beb85837f651fa22e90b3c044842dc7f2c2842c086f295808" 1433 | dependencies = [ 1434 | "proc-macro2", 1435 | "quote", 1436 | "syn", 1437 | "synstructure", 1438 | ] 1439 | 1440 | [[package]] 1441 | name = "zerovec" 1442 | version = "0.10.4" 1443 | source = "registry+https://github.com/rust-lang/crates.io-index" 1444 | checksum = "aa2b893d79df23bfb12d5461018d408ea19dfafe76c2c7ef6d4eba614f8ff079" 1445 | dependencies = [ 1446 | "yoke", 1447 | "zerofrom", 1448 | "zerovec-derive", 1449 | ] 1450 | 1451 | [[package]] 1452 | name = "zerovec-derive" 1453 | version = "0.10.3" 1454 | source = "registry+https://github.com/rust-lang/crates.io-index" 1455 | checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6" 1456 | dependencies = [ 1457 | "proc-macro2", 1458 | "quote", 1459 | "syn", 1460 | ] 1461 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "axum-codec" 3 | version = "0.0.19" 4 | edition = "2021" 5 | description = "A multi-codec extractor and response writer for Axum" 6 | license = "MIT OR Apache-2.0" 7 | authors = ["Matthew Polak "] 8 | repository = "https://github.com/matteopolak/axum-codec" 9 | 10 | [package.metadata.docs.rs] 11 | all-features = true 12 | 13 | [workspace] 14 | members = ["macros", ".", "examples/*"] 15 | 16 | [dependencies] 17 | aide = { version = "0.14", optional = true, default-features = false, features = ["axum", "axum-json"] } 18 | axum = { version = "0.8", default-features = false } 19 | axum-codec-macros = { path = "macros", version = "0.0.12", default-features = false } 20 | bincode = { version = "2.0.0-rc.3", default-features = false, features = ["std"], optional = true } 21 | # 0.6.3 added the #[bitcode(crate = "...")] option 22 | bitcode = { version = "0.6.3", default-features = false, features = ["std"], optional = true } 23 | bytes = "1" 24 | ciborium = { version = "0.2", optional = true } 25 | mime = "0.3" 26 | rmp-serde = { version= "1", optional = true } 27 | schemars = { version = "0.8", optional = true, default-features = false } 28 | serde = { version = "1", optional = true, default-features = false } 29 | serde_json = { version = "1", optional = true } 30 | serde_urlencoded = { version = "0.7", optional = true } 31 | serde_yaml = { version = "0.9", optional = true } 32 | thiserror = "1" 33 | toml = { version = "0.8", optional = true } 34 | validator = { version = "0.20", optional = true } 35 | 36 | [dev-dependencies] 37 | axum = "0.8" 38 | serde = { version = "1", features = ["derive", "rc"] } 39 | tokio = { version = "1", features = ["macros", "rt-multi-thread"] } 40 | # cannot run `BorrowCodec` tests with `bitcode` since it doesn't support `Cow` (yet) 41 | axum-codec = { path = ".", features = ["json", "bincode", "msgpack", "toml", "yaml", "macros", "form"] } 42 | bitcode = "0.6.3" 43 | 44 | [features] 45 | default = ["json", "macros", "pretty-errors"] 46 | 47 | # Enables all codecs 48 | full-codecs = ["bincode", "bitcode", "json", "msgpack", "toml", "yaml", "form"] 49 | macros = ["schemars?/derive", "bincode?/derive", "bitcode?/derive", "serde?/derive", "validator?/derive", "axum-codec-macros/debug"] 50 | 51 | # Enables support for {get,put,..}_with and relevant chaning methods 52 | # to add documentation to routes 53 | aide = ["dep:aide", "dep:schemars", "axum-codec-macros/aide", "axum/json", "axum/form", "axum/original-uri", "axum/query", "axum/tokio", "axum/matched-path"] 54 | 55 | # Enables support for `validator`, adds an additional `validator::Validate` bound to `T` in `Codec` 56 | validator = ["dep:validator", "axum-codec-macros/validator"] 57 | 58 | # Enables more verbose (and expensive) error handling machinery, but significantly 59 | # improves the quality of error messages for consumers of the API. 60 | pretty-errors = ["macros"] 61 | 62 | form = ["dep:serde_urlencoded", "axum/form"] 63 | bincode = ["dep:bincode", "axum-codec-macros/bincode"] 64 | bitcode = ["dep:bitcode", "axum-codec-macros/bitcode"] 65 | # cbor = ["dep:ciborium", "serde"] 66 | json = ["dep:serde_json", "serde"] 67 | msgpack = ["dep:rmp-serde", "serde"] 68 | toml = ["dep:toml", "serde"] 69 | yaml = ["dep:serde_yaml", "serde"] 70 | 71 | # Should not be manually enabled, but will not cause any issues if it is. 72 | serde = ["dep:serde", "axum-codec-macros/serde"] 73 | 74 | [lints.rust] 75 | unexpected_cfgs = { level = "warn", check-cfg = ['cfg(feature, values("cbor"))'] } 76 | 77 | -------------------------------------------------------------------------------- /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 [yyyy] [name of copyright owner] 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. 202 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Matthew Polak 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. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Axum Codec 2 | 3 | [![](https://img.shields.io/crates/v/axum-codec)](https://crates.io/crates/axum-codec) 4 | [![](https://img.shields.io/docsrs/axum-codec)](https://docs.rs/axum-codec/latest/axum_codec/) 5 | [![ci status](https://github.com/matteopolak/axum-codec/workflows/ci/badge.svg)](https://github.com/matteopolak/axum-codec/actions) 6 | 7 | A body extractor for the [Axum](https://github.com/tokio-rs/axum) web framework. 8 | 9 | ## Features 10 | 11 | - Supports encoding and decoding of various formats with a single extractor. 12 | - Provides a wrapper for [`axum::routing::method_routing`](https://docs.rs/axum/latest/axum/routing/method_routing/index.html) to automatically encode responses in the correct format according to the specified `Accept` header (with a fallback to `Content-Type`, then one of the enabled formats). 13 | - Provides an attribute macro (under the `macros` feature) to add derives for all enabled formats to a struct/enum. 14 | - Zero-copy decoding with `BorrowCodec`. 15 | 16 | Here's a quick example that can do the following: 17 | 18 | - Decode a `User` from the request body in any of the supported formats. 19 | - Encode a `Greeting` to the response body in any of the supported formats. 20 | 21 | ```rust 22 | use axum::{Router, response::IntoResponse}; 23 | use axum_codec::{ 24 | response::IntoCodecResponse, 25 | routing::{get, post}, 26 | Codec, Accept, 27 | }; 28 | 29 | // Shorthand for the following (assuming all features are enabled): 30 | // 31 | // #[derive( 32 | // serde::Serialize, serde::Deserialize, 33 | // bincode::Encode, bincode::Decode, 34 | // bitcode::Encode, bitcode::Decode, 35 | // validator::Validate, 36 | // )] 37 | // #[serde(crate = "...")] 38 | // #[bincode(crate = "...")] 39 | // #[bitcode(crate = "...")] 40 | // #[validator(crate = "...")] 41 | #[axum_codec::apply(encode, decode)] 42 | struct User { 43 | name: String, 44 | age: u8, 45 | } 46 | 47 | async fn me() -> Codec { 48 | Codec(User { 49 | name: "Alice".into(), 50 | age: 42, 51 | }) 52 | } 53 | 54 | /// A manual implementation of the handler above. 55 | async fn manual_me(accept: Accept) -> impl IntoResponse { 56 | Codec(User { 57 | name: "Alice".into(), 58 | age: 42, 59 | }) 60 | .into_codec_response(accept.into()) 61 | } 62 | 63 | #[axum_codec::apply(encode)] 64 | struct Greeting { 65 | message: String, 66 | } 67 | 68 | /// Specify `impl IntoCodecResponse`, similar to `axum` 69 | async fn greet(Codec(user): Codec) -> impl IntoCodecResponse { 70 | Codec(Greeting { 71 | message: format!("Hello, {}! You are {} years old.", user.name, user.age), 72 | }) 73 | } 74 | 75 | #[tokio::main] 76 | async fn main() { 77 | let app: Router = Router::new() 78 | .route("/me", get(me).into()) 79 | .route("/manual", axum::routing::get(manual_me)) 80 | .route("/greet", post(greet).into()); 81 | 82 | let listener = tokio::net::TcpListener::bind(("127.0.0.1", 3000)) 83 | .await 84 | .unwrap(); 85 | 86 | // axum::serve(listener, app).await.unwrap(); 87 | } 88 | ``` 89 | 90 | # Feature flags 91 | 92 | - `macros`: Enables the `axum_codec::apply` attribute macro. 93 | - `json`\*: Enables [`JSON`](https://github.com/serde-rs/json) support. 94 | - `form`: Enables [`x-www-form-urlencoded`](https://github.com/nox/serde_urlencoded) support. 95 | - `msgpack`: Enables [`MessagePack`](https://github.com/3Hren/msgpack-rust) support. 96 | - `bincode`: Enables [`Bincode`](https://github.com/bincode-org/bincode) support. 97 | - `bitcode`: Enables [`Bitcode`](https://github.com/SoftbearStudios/bitcode) support. 98 | - `yaml`: Enables [`YAML`](https://github.com/dtolnay/serde-yaml/releases) support. 99 | - `toml`: Enables [`TOML`](https://github.com/toml-rs/toml) support. 100 | - `aide`: Enables support for the [`Aide`](https://github.com/tamasfe/aide) documentation library. 101 | - `validator`: Enables support for the [`Validator`](https://github.com/Keats/validator) validation library, validating all input when extracted with `Codec`. 102 | 103 | \* Enabled by default. 104 | 105 | ## A note about `#[axum::debug_handler]` 106 | 107 | Since `axum-codec` uses its own `IntoCodecResponse` trait for encoding responses, it is not compatible with `#[axum::debug_handler]`. However, a new `#[axum_codec::debug_handler]` (and `#[axum_codec::debug_middleware]`) macro 108 | is provided as a drop-in replacement. 109 | 110 | ## Roadmap 111 | 112 | - [ ] Add `codec!` macro for defining custom codecs that use a different subset of enabled formats. 113 | 114 | ## License 115 | 116 | Dual-licensed under MIT or Apache License v2.0. 117 | -------------------------------------------------------------------------------- /examples/aide-validator/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "aide-validator" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | aide = { version = "0.14", features = ["axum"] } 8 | axum = "0.8" 9 | serde = { version = "1", features = ["derive", "rc"] } 10 | tokio = { version = "1", features = ["macros", "rt-multi-thread"] } 11 | axum-codec = { path = "../..", features = ["full-codecs", "macros", "aide", "validator"] } 12 | -------------------------------------------------------------------------------- /examples/aide-validator/src/main.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | 3 | use aide::axum::ApiRouter; 4 | use axum::{extract::State, response::IntoResponse, Extension}; 5 | use axum_codec::{ 6 | routing::{get, post}, 7 | Codec, IntoCodecResponse, 8 | }; 9 | 10 | #[axum_codec::apply(encode, decode)] 11 | struct User { 12 | name: String, 13 | #[validate(range(min = 0, max = 120))] 14 | age: u8, 15 | } 16 | 17 | #[axum_codec::debug_handler] 18 | async fn me() -> impl IntoCodecResponse { 19 | Codec(User { 20 | name: "Alice".into(), 21 | age: 42, 22 | }) 23 | } 24 | 25 | #[axum_codec::apply(encode)] 26 | struct Greeting { 27 | message: String, 28 | } 29 | 30 | async fn greet(Codec(user): Codec) -> Codec { 31 | Codec(Greeting { 32 | message: format!("Hello, {}! You are {} years old.", user.name, user.age), 33 | }) 34 | } 35 | 36 | async fn state(State(state): State) -> Codec { 37 | Codec(Greeting { message: state }) 38 | } 39 | 40 | async fn openapi(Extension(api): Extension>) -> impl IntoCodecResponse { 41 | axum::Json(api).into_response() 42 | } 43 | 44 | #[tokio::main] 45 | async fn main() { 46 | let mut api = aide::openapi::OpenApi::default(); 47 | 48 | let app = ApiRouter::new() 49 | .api_route("/me", get(me).into()) 50 | .api_route("/greet", post(greet).into()) 51 | .api_route("/state", get(state).into()) 52 | .route("/openapi.json", get(openapi)) 53 | .finish_api(&mut api) 54 | .layer(Extension(Arc::new(api))) 55 | .with_state("Hello, world!".to_string()); 56 | 57 | let listener = tokio::net::TcpListener::bind(("127.0.0.1", 3000)) 58 | .await 59 | .unwrap(); 60 | 61 | axum::serve(listener, app).await.unwrap(); 62 | } 63 | -------------------------------------------------------------------------------- /examples/basic/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "basic" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | axum = "0.8" 8 | serde = { version = "1", features = ["derive", "rc"] } 9 | tokio = { version = "1", features = ["macros", "rt-multi-thread"] } 10 | axum-codec = { path = "../..", features = ["macros", "validator", "json", "bincode", "msgpack", "toml", "yaml", "form"] } 11 | 12 | -------------------------------------------------------------------------------- /examples/basic/src/main.rs: -------------------------------------------------------------------------------- 1 | use std::borrow::Cow; 2 | 3 | use axum::{ 4 | extract::{DefaultBodyLimit, State}, 5 | Router, 6 | }; 7 | use axum_codec::{ 8 | routing::{get, post}, 9 | BorrowCodec, Codec, IntoCodecResponse, 10 | }; 11 | 12 | #[axum_codec::apply(encode, decode)] 13 | struct User { 14 | #[validate(length(min = 1, max = 100))] 15 | name: String, 16 | #[validate(range(min = 0, max = 150))] 17 | age: u8, 18 | } 19 | 20 | async fn me() -> impl IntoCodecResponse { 21 | Codec(User { 22 | name: "Alice".into(), 23 | age: 42, 24 | }) 25 | } 26 | 27 | #[axum_codec::apply(encode)] 28 | struct Greeting { 29 | message: String, 30 | } 31 | 32 | async fn greet(Codec(user): Codec) -> Codec { 33 | Codec(Greeting { 34 | message: format!("Hello, {}! You are {} years old.", user.name, user.age), 35 | }) 36 | } 37 | 38 | async fn state(State(state): State) -> Codec { 39 | Codec(Greeting { message: state }) 40 | } 41 | 42 | #[axum_codec::apply(encode, decode)] 43 | struct BorrowGreeting<'d> { 44 | #[serde(borrow)] 45 | message: Cow<'d, str>, 46 | } 47 | 48 | async fn borrow_greet(greeting: BorrowCodec>) -> impl IntoCodecResponse { 49 | let is_borrowed = matches!(greeting.message, Cow::Borrowed(..)); 50 | 51 | Codec(Greeting { 52 | message: if is_borrowed { 53 | "Message is borrowed".into() 54 | } else { 55 | "Message is owned".into() 56 | }, 57 | }) 58 | } 59 | 60 | #[tokio::main] 61 | async fn main() { 62 | let app = Router::new() 63 | .route("/me", get(me).into()) 64 | .route("/greet", post(greet).into()) 65 | .route("/borrow-greet", post(borrow_greet).into()) 66 | .route("/state", get(state).into()) 67 | .layer(DefaultBodyLimit::max(1024)) 68 | .with_state("Hello, world!".to_string()); 69 | 70 | let listener = tokio::net::TcpListener::bind(("127.0.0.1", 3000)) 71 | .await 72 | .unwrap(); 73 | 74 | axum::serve(listener, app).await.unwrap(); 75 | } 76 | -------------------------------------------------------------------------------- /examples/todo-api/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "todo-api" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | aide = { version = "0.14", features = ["axum"] } 8 | axum = "0.8" 9 | serde = { version = "1", features = ["derive", "rc"] } 10 | tokio = { version = "1", features = ["macros", "rt-multi-thread"] } 11 | axum-codec = { path = "../..", features = ["full-codecs", "macros", "aide", "validator", "pretty-errors"] } 12 | -------------------------------------------------------------------------------- /examples/todo-api/src/main.rs: -------------------------------------------------------------------------------- 1 | mod todo; 2 | 3 | use std::sync::Arc; 4 | 5 | use aide::axum::ApiRouter; 6 | use axum::{response::IntoResponse, Extension}; 7 | use axum_codec::{routing::get, IntoCodecResponse}; 8 | 9 | async fn openapi(Extension(api): Extension>) -> impl IntoCodecResponse { 10 | axum::Json(api).into_response() 11 | } 12 | 13 | #[tokio::main] 14 | async fn main() { 15 | let mut api = aide::openapi::OpenApi::default(); 16 | 17 | let app = ApiRouter::new() 18 | .nest_api_service("/todos", todo::routes()) 19 | .route("/openapi.json", get(openapi)) 20 | .finish_api(&mut api) 21 | .layer(Extension(Arc::new(api))); 22 | 23 | let listener = tokio::net::TcpListener::bind(("127.0.0.1", 3000)) 24 | .await 25 | .unwrap(); 26 | 27 | axum::serve(listener, app).await.unwrap(); 28 | } 29 | -------------------------------------------------------------------------------- /examples/todo-api/src/todo.rs: -------------------------------------------------------------------------------- 1 | use std::sync::{Arc, Mutex}; 2 | 3 | use aide::axum::ApiRouter; 4 | use axum::{ 5 | extract::{Path, State}, 6 | http::StatusCode, 7 | }; 8 | use axum_codec::{ 9 | routing::{delete, get, patch, post}, 10 | Codec, IntoCodecResponse, 11 | }; 12 | 13 | pub fn routes() -> ApiRouter { 14 | ApiRouter::new() 15 | .api_route("/", get(get_all).into()) 16 | .api_route("/", post(create).into()) 17 | .api_route("/:id", get(get_one).into()) 18 | .api_route("/:id", patch(update_one).into()) 19 | .api_route("/:id", delete(delete_one).into()) 20 | .with_state(Arc::new(Tasks::default())) 21 | } 22 | 23 | #[derive(Default)] 24 | pub struct Tasks(Mutex>); 25 | 26 | pub struct TodoHandle { 27 | deleted: bool, 28 | inner: Todo, 29 | } 30 | 31 | #[axum_codec::apply(encode)] 32 | #[derive(Clone)] 33 | pub struct Todo { 34 | id: u64, 35 | /// A title describing the task to be done. 36 | title: String, 37 | /// Whether the task has been completed. 38 | completed: bool, 39 | } 40 | 41 | #[axum_codec::apply(decode)] 42 | pub struct CreateTodo { 43 | /// A title describing the task to be done. 44 | title: String, 45 | } 46 | 47 | #[axum_codec::apply(decode)] 48 | pub struct UpdateTodo { 49 | /// A title describing the task to be done. 50 | title: Option, 51 | /// Whether the task has been completed. 52 | completed: Option, 53 | } 54 | 55 | async fn get_all(State(tasks): State>) -> impl IntoCodecResponse { 56 | let tasks = tasks 57 | .0 58 | .lock() 59 | .unwrap() 60 | .iter() 61 | .filter(|handle| !handle.deleted) 62 | .map(|handle| handle.inner.clone()) 63 | .collect::>(); 64 | 65 | Codec(tasks) 66 | } 67 | 68 | async fn create( 69 | State(tasks): State>, 70 | Codec(todo): Codec, 71 | ) -> impl IntoCodecResponse { 72 | let mut tasks = tasks.0.lock().unwrap(); 73 | let id = tasks.len() as u64 + 1; 74 | 75 | let todo = Todo { 76 | id, 77 | completed: false, 78 | title: todo.title, 79 | }; 80 | 81 | tasks.push(TodoHandle { 82 | deleted: false, 83 | inner: todo.clone(), 84 | }); 85 | 86 | Codec(todo) 87 | } 88 | 89 | async fn get_one(State(tasks): State>, Path(id): Path) -> impl IntoCodecResponse { 90 | let tasks = tasks.0.lock().unwrap(); 91 | let handle = match tasks.get(id as usize - 1) { 92 | Some(handle) if !handle.deleted => handle, 93 | _ => { 94 | return Err(( 95 | StatusCode::NOT_FOUND, 96 | Codec(axum_codec::rejection::Message { 97 | code: "not_found", 98 | content: format!("Task with id {} not found", id), 99 | }), 100 | )) 101 | } 102 | }; 103 | 104 | Ok(Codec(handle.inner.clone())) 105 | } 106 | 107 | async fn update_one( 108 | State(tasks): State>, 109 | Path(id): Path, 110 | Codec(todo): Codec, 111 | ) -> impl IntoCodecResponse { 112 | let mut tasks = tasks.0.lock().unwrap(); 113 | let handle = match tasks.get_mut(id as usize - 1) { 114 | Some(handle) if !handle.deleted => handle, 115 | _ => { 116 | return Err(( 117 | StatusCode::NOT_FOUND, 118 | Codec(axum_codec::rejection::Message { 119 | code: "not_found", 120 | content: format!("Task with id {} not found", id), 121 | }), 122 | )) 123 | } 124 | }; 125 | 126 | if let Some(title) = todo.title { 127 | handle.inner.title = title; 128 | } 129 | 130 | if let Some(completed) = todo.completed { 131 | handle.inner.completed = completed; 132 | } 133 | 134 | Ok(Codec(handle.inner.clone())) 135 | } 136 | 137 | async fn delete_one( 138 | State(tasks): State>, 139 | Path(id): Path, 140 | ) -> impl IntoCodecResponse { 141 | let mut tasks = tasks.0.lock().unwrap(); 142 | let handle = match tasks.get_mut(id as usize - 1) { 143 | Some(handle) if !handle.deleted => handle, 144 | _ => { 145 | return Err(( 146 | StatusCode::NOT_FOUND, 147 | Codec(axum_codec::rejection::Message { 148 | code: "not_found", 149 | content: format!("Task with id {} not found", id), 150 | }), 151 | )) 152 | } 153 | }; 154 | 155 | handle.deleted = true; 156 | 157 | Ok(Codec(handle.inner.clone())) 158 | } 159 | -------------------------------------------------------------------------------- /macros/Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "axum-codec-macros" 7 | version = "0.1.0" 8 | -------------------------------------------------------------------------------- /macros/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "axum-codec-macros" 3 | version = "0.0.12" 4 | edition = "2021" 5 | description = "Procedural macros for axum-codec" 6 | license = "MIT OR Apache-2.0" 7 | authors = ["Matthew Polak "] 8 | repository = "https://github.com/matteopolak/axum-codec" 9 | 10 | [lib] 11 | proc-macro = true 12 | 13 | [dependencies] 14 | proc-macro2 = "1" 15 | quote = "1" 16 | syn = { version = "2", optional = true, features = ["full", "extra-traits"] } 17 | 18 | [features] 19 | default = ["debug"] 20 | 21 | debug = ["dep:syn"] 22 | bincode = ["dep:syn"] 23 | bitcode = ["dep:syn"] 24 | serde = ["dep:syn"] 25 | aide = ["dep:syn"] 26 | validator = ["dep:syn"] 27 | 28 | -------------------------------------------------------------------------------- /macros/README.md: -------------------------------------------------------------------------------- 1 | # Axum Codec Macros 2 | 3 | A set of procedural macros for the [axum-codec](https://github.com/matteopolak/axum-codec) crate. 4 | -------------------------------------------------------------------------------- /macros/src/apply.rs: -------------------------------------------------------------------------------- 1 | use proc_macro2::TokenStream; 2 | #[cfg(any( 3 | feature = "bincode", 4 | feature = "bitcode", 5 | feature = "serde", 6 | feature = "aide", 7 | feature = "validator" 8 | ))] 9 | use quote::{quote, ToTokens}; 10 | use syn::{ 11 | parse::{Parse, ParseStream}, 12 | punctuated::Punctuated, 13 | spanned::Spanned, 14 | Meta, Path, Token, 15 | }; 16 | 17 | struct Args { 18 | encode: bool, 19 | decode: bool, 20 | crate_name: Path, 21 | } 22 | 23 | impl Parse for Args { 24 | fn parse(input: ParseStream) -> syn::Result { 25 | let options = Punctuated::::parse_terminated(input)?; 26 | 27 | let mut encode = false; 28 | let mut decode = false; 29 | let mut crate_name = syn::parse_str("axum_codec").expect("failed to parse crate name"); 30 | 31 | for meta in options { 32 | match meta { 33 | Meta::List(list) => { 34 | return Err(syn::Error::new( 35 | list.span(), 36 | "expected `encode`, `decode`, or `crate`", 37 | )) 38 | } 39 | Meta::Path(path) => { 40 | let ident = path.get_ident().map(|ident| ident.to_string()); 41 | match ident.as_deref() { 42 | Some("encode") if encode => { 43 | return Err(syn::Error::new( 44 | path.span(), 45 | "option `encode` is already enabled", 46 | )) 47 | } 48 | Some("decode") if decode => { 49 | return Err(syn::Error::new( 50 | path.span(), 51 | "option `decode` is already enabled", 52 | )) 53 | } 54 | Some("encode") => encode = true, 55 | Some("decode") => decode = true, 56 | Some(other) => { 57 | return Err(syn::Error::new( 58 | path.span(), 59 | format!("unknown option `{other}`, expected `encode` or `decode`"), 60 | )) 61 | } 62 | None => { 63 | return Err(syn::Error::new( 64 | path.span(), 65 | "expected `encode` or `decode`", 66 | )) 67 | } 68 | } 69 | } 70 | Meta::NameValue(name_value) => { 71 | if !name_value.path.is_ident("crate") { 72 | return Err(syn::Error::new(name_value.path.span(), "expected `crate`")); 73 | } 74 | 75 | let path = match name_value.value { 76 | syn::Expr::Lit(ref lit) => match &lit.lit { 77 | syn::Lit::Str(path) => path, 78 | _ => return Err(syn::Error::new(lit.span(), "expected a string")), 79 | }, 80 | _ => { 81 | return Err(syn::Error::new( 82 | name_value.value.span(), 83 | "expected a literal string", 84 | )) 85 | } 86 | }; 87 | 88 | let mut path = syn::parse_str::(&path.value()).expect("failed to parse path"); 89 | 90 | path.leading_colon = if path.is_ident("crate") { 91 | None 92 | } else { 93 | Some(Token![::](name_value.value.span())) 94 | }; 95 | 96 | crate_name = path; 97 | } 98 | } 99 | } 100 | 101 | if !encode && !decode { 102 | return Err(syn::Error::new( 103 | input.span(), 104 | "at least one of `encode` or `decode` must be enabled", 105 | )); 106 | } 107 | 108 | Ok(Self { 109 | encode, 110 | decode, 111 | crate_name, 112 | }) 113 | } 114 | } 115 | 116 | pub fn apply( 117 | attr: proc_macro::TokenStream, 118 | input: proc_macro::TokenStream, 119 | ) -> proc_macro::TokenStream { 120 | let args = syn::parse_macro_input!(attr as Args); 121 | 122 | let crate_name = &args.crate_name; 123 | let mut tokens = TokenStream::default(); 124 | 125 | #[cfg(feature = "serde")] 126 | { 127 | if args.encode { 128 | tokens.extend(quote! { 129 | #[derive(#crate_name::__private::serde::Serialize)] 130 | }); 131 | } 132 | 133 | if args.decode { 134 | tokens.extend(quote! { 135 | #[derive(#crate_name::__private::serde::Deserialize)] 136 | }); 137 | } 138 | 139 | let crate_ = format!("{}::__private::serde", crate_name.to_token_stream()); 140 | 141 | tokens.extend(quote! { 142 | #[serde(crate = #crate_)] 143 | }); 144 | } 145 | 146 | #[cfg(feature = "bincode")] 147 | { 148 | if args.encode { 149 | tokens.extend(quote! { 150 | #[derive(#crate_name::__private::bincode::Encode)] 151 | }); 152 | } 153 | 154 | if args.decode { 155 | tokens.extend(quote! { 156 | #[derive(#crate_name::__private::bincode::BorrowDecode)] 157 | }); 158 | } 159 | 160 | let crate_ = format!("{}::__private::bincode", crate_name.to_token_stream()); 161 | 162 | tokens.extend(quote! { 163 | #[bincode(crate = #crate_)] 164 | }); 165 | } 166 | 167 | #[cfg(feature = "bitcode")] 168 | { 169 | if args.encode { 170 | tokens.extend(quote! { 171 | #[derive(#crate_name::__private::bitcode::Encode)] 172 | }); 173 | } 174 | 175 | if args.decode { 176 | tokens.extend(quote! { 177 | #[derive(#crate_name::__private::bitcode::Decode)] 178 | }); 179 | } 180 | 181 | let crate_ = format!("{}::__private::bitcode", crate_name.to_token_stream()); 182 | 183 | tokens.extend(quote! { 184 | #[bitcode(crate = #crate_)] 185 | }); 186 | } 187 | 188 | #[cfg(feature = "aide")] 189 | { 190 | let crate_ = format!("{}::__private::schemars", crate_name.to_token_stream()); 191 | 192 | tokens.extend(quote! { 193 | #[derive(#crate_name::__private::schemars::JsonSchema)] 194 | #[schemars(crate = #crate_)] 195 | }); 196 | } 197 | 198 | // TODO: Implement #[validate(crate = "...")] 199 | // For now, use the real crate name so the error is nicer. 200 | #[cfg(feature = "validator")] 201 | if args.decode { 202 | let crate_ = format!("{}::__private::validator", crate_name.to_token_stream()); 203 | 204 | tokens.extend(quote! { 205 | #[derive(#crate_name::__private::validator::Validate)] 206 | #[validate(crate = #crate_)] 207 | }); 208 | } 209 | 210 | tokens.extend(TokenStream::from(input)); 211 | tokens.into() 212 | } 213 | -------------------------------------------------------------------------------- /macros/src/attr_parsing.rs: -------------------------------------------------------------------------------- 1 | // This is copied from Axum under the following license: 2 | // 3 | // Copyright 2021 Axum Contributors 4 | // 5 | // Permission is hereby granted, free of charge, to any 6 | // person obtaining a copy of this software and associated 7 | // documentation files (the "Software"), to deal in the 8 | // Software without restriction, including without 9 | // limitation the rights to use, copy, modify, merge, 10 | // publish, distribute, sublicense, and/or sell copies of 11 | // the Software, and to permit persons to whom the Software 12 | // is furnished to do so, subject to the following 13 | // conditions: 14 | // 15 | // The above copyright notice and this permission notice 16 | // shall be included in all copies or substantial portions 17 | // of the Software. 18 | // 19 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF 20 | // ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 21 | // TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 22 | // PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT 23 | // SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 24 | // CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 25 | // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR 26 | // IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 27 | // DEALINGS IN THE SOFTWARE. 28 | 29 | use quote::ToTokens; 30 | use syn::{ 31 | parse::{Parse, ParseStream}, 32 | Token, 33 | }; 34 | 35 | pub(crate) fn parse_assignment_attribute( 36 | input: ParseStream, 37 | out: &mut Option<(K, T)>, 38 | ) -> syn::Result<()> 39 | where 40 | K: Parse + ToTokens, 41 | T: Parse, 42 | { 43 | let kw = input.parse()?; 44 | input.parse::()?; 45 | let inner = input.parse()?; 46 | 47 | if out.is_some() { 48 | let kw_name = std::any::type_name::().split("::").last().unwrap(); 49 | let msg = format!("`{kw_name}` specified more than once"); 50 | return Err(syn::Error::new_spanned(kw, msg)); 51 | } 52 | 53 | *out = Some((kw, inner)); 54 | 55 | Ok(()) 56 | } 57 | 58 | pub(crate) fn second(tuple: (T, K)) -> K { 59 | tuple.1 60 | } 61 | -------------------------------------------------------------------------------- /macros/src/debug_handler.rs: -------------------------------------------------------------------------------- 1 | // This is copied from Axum under the following license: 2 | // 3 | // Copyright 2021 Axum Contributors 4 | // 5 | // Permission is hereby granted, free of charge, to any 6 | // person obtaining a copy of this software and associated 7 | // documentation files (the "Software"), to deal in the 8 | // Software without restriction, including without 9 | // limitation the rights to use, copy, modify, merge, 10 | // publish, distribute, sublicense, and/or sell copies of 11 | // the Software, and to permit persons to whom the Software 12 | // is furnished to do so, subject to the following 13 | // conditions: 14 | // 15 | // The above copyright notice and this permission notice 16 | // shall be included in all copies or substantial portions 17 | // of the Software. 18 | // 19 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF 20 | // ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 21 | // TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 22 | // PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT 23 | // SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 24 | // CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 25 | // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR 26 | // IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 27 | // DEALINGS IN THE SOFTWARE. 28 | 29 | use std::{collections::HashSet, fmt}; 30 | 31 | use proc_macro2::{Ident, Span, TokenStream}; 32 | use quote::{format_ident, quote, quote_spanned}; 33 | use syn::{parse::Parse, spanned::Spanned, FnArg, ItemFn, ReturnType, Token, Type}; 34 | 35 | use crate::{ 36 | attr_parsing::{parse_assignment_attribute, second}, 37 | with_position::{Position, WithPosition}, 38 | }; 39 | 40 | pub(crate) fn expand(attr: Attrs, item_fn: ItemFn, kind: FunctionKind) -> TokenStream { 41 | let Attrs { state_ty } = attr; 42 | 43 | let mut state_ty = state_ty.map(second); 44 | 45 | let check_extractor_count = check_extractor_count(&item_fn, kind); 46 | let check_path_extractor = check_path_extractor(&item_fn, kind); 47 | let check_output_tuples = check_output_tuples(&item_fn); 48 | let check_output_impls_into_response = if check_output_tuples.is_empty() { 49 | check_output_impls_into_response(&item_fn) 50 | } else { 51 | check_output_tuples 52 | }; 53 | 54 | // If the function is generic, we can't reliably check its inputs or whether the 55 | // future it returns is `Send`. Skip those checks to avoid unhelpful additional 56 | // compiler errors. 57 | let check_inputs_and_future_send = if item_fn.sig.generics.params.is_empty() { 58 | let mut err = None; 59 | 60 | if state_ty.is_none() { 61 | let state_types_from_args = state_types_from_args(&item_fn); 62 | 63 | #[allow(clippy::comparison_chain)] 64 | if state_types_from_args.len() == 1 { 65 | state_ty = state_types_from_args.into_iter().next(); 66 | } else if state_types_from_args.len() > 1 { 67 | err = Some( 68 | syn::Error::new( 69 | Span::call_site(), 70 | format!( 71 | "can't infer state type, please add set it explicitly, as in \ 72 | `#[axum_codec::debug_{kind}(state = MyStateType)]`" 73 | ), 74 | ) 75 | .into_compile_error(), 76 | ); 77 | } 78 | } 79 | 80 | err.unwrap_or_else(|| { 81 | let state_ty = state_ty.unwrap_or_else(|| syn::parse_quote!(())); 82 | 83 | let check_future_send = check_future_send(&item_fn, kind); 84 | 85 | if let Some(check_input_order) = check_input_order(&item_fn, kind) { 86 | quote! { 87 | #check_input_order 88 | #check_future_send 89 | } 90 | } else { 91 | let check_inputs_impls_from_request = 92 | check_inputs_impls_from_request(&item_fn, state_ty, kind); 93 | 94 | quote! { 95 | #check_inputs_impls_from_request 96 | #check_future_send 97 | } 98 | } 99 | }) 100 | } else { 101 | syn::Error::new_spanned( 102 | &item_fn.sig.generics, 103 | format!("`#[axum_codec::debug_{kind}]` doesn't support generic functions"), 104 | ) 105 | .into_compile_error() 106 | }; 107 | 108 | let middleware_takes_next_as_last_arg = 109 | matches!(kind, FunctionKind::Middleware).then(|| next_is_last_input(&item_fn)); 110 | 111 | quote! { 112 | #item_fn 113 | #check_extractor_count 114 | #check_path_extractor 115 | #check_output_impls_into_response 116 | #check_inputs_and_future_send 117 | #middleware_takes_next_as_last_arg 118 | } 119 | } 120 | 121 | #[derive(Clone, Copy)] 122 | pub(crate) enum FunctionKind { 123 | Handler, 124 | Middleware, 125 | } 126 | 127 | impl fmt::Display for FunctionKind { 128 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 129 | match self { 130 | FunctionKind::Handler => f.write_str("handler"), 131 | FunctionKind::Middleware => f.write_str("middleware"), 132 | } 133 | } 134 | } 135 | 136 | impl FunctionKind { 137 | fn name_uppercase_plural(&self) -> &'static str { 138 | match self { 139 | FunctionKind::Handler => "Handlers", 140 | FunctionKind::Middleware => "Middleware", 141 | } 142 | } 143 | } 144 | 145 | mod kw { 146 | syn::custom_keyword!(body); 147 | syn::custom_keyword!(state); 148 | } 149 | 150 | pub(crate) struct Attrs { 151 | state_ty: Option<(kw::state, Type)>, 152 | } 153 | 154 | impl Parse for Attrs { 155 | fn parse(input: syn::parse::ParseStream) -> syn::Result { 156 | let mut state_ty = None; 157 | 158 | while !input.is_empty() { 159 | let lh = input.lookahead1(); 160 | if lh.peek(kw::state) { 161 | parse_assignment_attribute(input, &mut state_ty)?; 162 | } else { 163 | return Err(lh.error()); 164 | } 165 | 166 | let _ = input.parse::(); 167 | } 168 | 169 | Ok(Self { state_ty }) 170 | } 171 | } 172 | 173 | fn check_extractor_count(item_fn: &ItemFn, kind: FunctionKind) -> Option { 174 | let max_extractors = 16; 175 | let inputs = item_fn 176 | .sig 177 | .inputs 178 | .iter() 179 | .filter(|arg| skip_next_arg(arg, kind)) 180 | .count(); 181 | if inputs <= max_extractors { 182 | None 183 | } else { 184 | let error_message = format!( 185 | "{} cannot take more than {max_extractors} arguments. Use `(a, b): (ExtractorA, \ 186 | ExtractorA)` to further nest extractors", 187 | kind.name_uppercase_plural(), 188 | ); 189 | let error = syn::Error::new_spanned(&item_fn.sig.inputs, error_message).to_compile_error(); 190 | Some(error) 191 | } 192 | } 193 | 194 | fn extractor_idents( 195 | item_fn: &ItemFn, 196 | kind: FunctionKind, 197 | ) -> impl Iterator { 198 | item_fn 199 | .sig 200 | .inputs 201 | .iter() 202 | .filter(move |arg| skip_next_arg(arg, kind)) 203 | .enumerate() 204 | .filter_map(|(idx, fn_arg)| match fn_arg { 205 | FnArg::Receiver(_) => None, 206 | FnArg::Typed(pat_type) => { 207 | if let Type::Path(type_path) = &*pat_type.ty { 208 | type_path 209 | .path 210 | .segments 211 | .last() 212 | .map(|segment| (idx, fn_arg, &segment.ident)) 213 | } else { 214 | None 215 | } 216 | } 217 | }) 218 | } 219 | 220 | fn check_path_extractor(item_fn: &ItemFn, kind: FunctionKind) -> TokenStream { 221 | let path_extractors = extractor_idents(item_fn, kind) 222 | .filter(|(_, _, ident)| *ident == "Path") 223 | .collect::>(); 224 | 225 | if path_extractors.len() > 1 { 226 | path_extractors 227 | .into_iter() 228 | .map(|(_, arg, _)| { 229 | syn::Error::new_spanned( 230 | arg, 231 | "Multiple parameters must be extracted with a tuple `Path<(_, _)>` or a struct \ 232 | `Path`, not by applying multiple `Path<_>` extractors", 233 | ) 234 | .to_compile_error() 235 | }) 236 | .collect() 237 | } else { 238 | quote! {} 239 | } 240 | } 241 | 242 | fn is_self_pat_type(typed: &syn::PatType) -> bool { 243 | let ident = if let syn::Pat::Ident(ident) = &*typed.pat { 244 | &ident.ident 245 | } else { 246 | return false; 247 | }; 248 | 249 | ident == "self" 250 | } 251 | 252 | fn check_inputs_impls_from_request( 253 | item_fn: &ItemFn, 254 | state_ty: Type, 255 | kind: FunctionKind, 256 | ) -> TokenStream { 257 | let takes_self = item_fn.sig.inputs.first().is_some_and(|arg| match arg { 258 | FnArg::Receiver(_) => true, 259 | FnArg::Typed(typed) => is_self_pat_type(typed), 260 | }); 261 | 262 | WithPosition::new( 263 | item_fn 264 | .sig 265 | .inputs 266 | .iter() 267 | .filter(|arg| skip_next_arg(arg, kind)), 268 | ) 269 | .enumerate() 270 | .map(|(idx, arg)| { 271 | let must_impl_from_request_parts = match &arg { 272 | Position::First(_) | Position::Middle(_) => true, 273 | Position::Last(_) | Position::Only(_) => false, 274 | }; 275 | 276 | let arg = arg.into_inner(); 277 | 278 | let (span, ty) = match arg { 279 | FnArg::Receiver(receiver) => { 280 | if receiver.reference.is_some() { 281 | return syn::Error::new_spanned(receiver, "Handlers must only take owned values") 282 | .into_compile_error(); 283 | } 284 | 285 | let span = receiver.span(); 286 | (span, syn::parse_quote!(Self)) 287 | } 288 | FnArg::Typed(typed) => { 289 | let ty = &typed.ty; 290 | let span = ty.span(); 291 | 292 | if is_self_pat_type(typed) { 293 | (span, syn::parse_quote!(Self)) 294 | } else { 295 | (span, ty.clone()) 296 | } 297 | } 298 | }; 299 | 300 | let consumes_request = request_consuming_type_name(&ty).is_some(); 301 | 302 | let check_fn = format_ident!( 303 | "__axum_macros_check_{}_{}_from_request_check", 304 | item_fn.sig.ident, 305 | idx, 306 | span = span, 307 | ); 308 | 309 | let call_check_fn = format_ident!( 310 | "__axum_macros_check_{}_{}_from_request_call_check", 311 | item_fn.sig.ident, 312 | idx, 313 | span = span, 314 | ); 315 | 316 | let call_check_fn_body = if takes_self { 317 | quote_spanned! {span=> 318 | Self::#check_fn(); 319 | } 320 | } else { 321 | quote_spanned! {span=> 322 | #check_fn(); 323 | } 324 | }; 325 | 326 | let check_fn_generics = if must_impl_from_request_parts || consumes_request { 327 | quote! {} 328 | } else { 329 | quote! { } 330 | }; 331 | 332 | let from_request_bound = if must_impl_from_request_parts { 333 | quote_spanned! {span=> 334 | #ty: ::axum::extract::FromRequestParts<#state_ty> + Send 335 | } 336 | } else if consumes_request { 337 | quote_spanned! {span=> 338 | #ty: ::axum::extract::FromRequest<#state_ty> + Send 339 | } 340 | } else { 341 | quote_spanned! {span=> 342 | #ty: ::axum::extract::FromRequest<#state_ty, M> + Send 343 | } 344 | }; 345 | 346 | quote_spanned! {span=> 347 | #[allow(warnings)] 348 | #[doc(hidden)] 349 | fn #check_fn #check_fn_generics() 350 | where 351 | #from_request_bound, 352 | {} 353 | 354 | // we have to call the function to actually trigger a compile error 355 | // since the function is generic, just defining it is not enough 356 | #[allow(warnings)] 357 | #[doc(hidden)] 358 | fn #call_check_fn() 359 | { 360 | #call_check_fn_body 361 | } 362 | } 363 | }) 364 | .collect::() 365 | } 366 | 367 | fn check_output_tuples(item_fn: &ItemFn) -> TokenStream { 368 | let elems = match &item_fn.sig.output { 369 | ReturnType::Type(_, ty) => match &**ty { 370 | Type::Tuple(tuple) => &tuple.elems, 371 | _ => return quote! {}, 372 | }, 373 | ReturnType::Default => return quote! {}, 374 | }; 375 | 376 | let handler_ident = &item_fn.sig.ident; 377 | 378 | match elems.len() { 379 | 0 => quote! {}, 380 | n if n > 17 => syn::Error::new_spanned( 381 | &item_fn.sig.output, 382 | "Cannot return tuples with more than 17 elements", 383 | ) 384 | .to_compile_error(), 385 | _ => WithPosition::new(elems) 386 | .enumerate() 387 | .map(|(idx, arg)| match arg { 388 | Position::First(ty) => match extract_clean_typename(ty).as_deref() { 389 | Some("StatusCode" | "Response") => quote! {}, 390 | Some("Parts") => check_is_response_parts(ty, handler_ident, idx), 391 | Some(_) | None => { 392 | if let Some(tn) = well_known_last_response_type(ty) { 393 | syn::Error::new_spanned( 394 | ty, 395 | format!("`{tn}` must be the last element in a response tuple"), 396 | ) 397 | .to_compile_error() 398 | } else { 399 | check_into_response_parts(ty, handler_ident, idx) 400 | } 401 | } 402 | }, 403 | Position::Middle(ty) => { 404 | if let Some(tn) = well_known_last_response_type(ty) { 405 | syn::Error::new_spanned( 406 | ty, 407 | format!("`{tn}` must be the last element in a response tuple"), 408 | ) 409 | .to_compile_error() 410 | } else { 411 | check_into_response_parts(ty, handler_ident, idx) 412 | } 413 | } 414 | Position::Last(ty) | Position::Only(ty) => check_into_response(handler_ident, ty), 415 | }) 416 | .collect::(), 417 | } 418 | } 419 | 420 | fn check_into_response(handler: &Ident, ty: &Type) -> TokenStream { 421 | let (span, ty) = (ty.span(), ty.clone()); 422 | 423 | let check_fn = format_ident!( 424 | "__axum_macros_check_{handler}_into_response_check", 425 | span = span, 426 | ); 427 | 428 | let call_check_fn = format_ident!( 429 | "__axum_macros_check_{handler}_into_response_call_check", 430 | span = span, 431 | ); 432 | 433 | let call_check_fn_body = quote_spanned! {span=> 434 | #check_fn(); 435 | }; 436 | 437 | let from_request_bound = quote_spanned! {span=> 438 | #ty: ::axum_codec::response::IntoCodecResponse 439 | }; 440 | quote_spanned! {span=> 441 | #[allow(warnings)] 442 | #[allow(unreachable_code)] 443 | #[doc(hidden)] 444 | fn #check_fn() 445 | where 446 | #from_request_bound, 447 | {} 448 | 449 | // we have to call the function to actually trigger a compile error 450 | // since the function is generic, just defining it is not enough 451 | #[allow(warnings)] 452 | #[allow(unreachable_code)] 453 | #[doc(hidden)] 454 | fn #call_check_fn() { 455 | #call_check_fn_body 456 | } 457 | } 458 | } 459 | 460 | fn check_is_response_parts(ty: &Type, ident: &Ident, index: usize) -> TokenStream { 461 | let (span, ty) = (ty.span(), ty.clone()); 462 | 463 | let check_fn = format_ident!( 464 | "__axum_macros_check_{}_is_response_parts_{index}_check", 465 | ident, 466 | span = span, 467 | ); 468 | 469 | quote_spanned! {span=> 470 | #[allow(warnings)] 471 | #[allow(unreachable_code)] 472 | #[doc(hidden)] 473 | fn #check_fn(parts: #ty) -> ::axum::http::response::Parts { 474 | parts 475 | } 476 | } 477 | } 478 | 479 | fn check_into_response_parts(ty: &Type, ident: &Ident, index: usize) -> TokenStream { 480 | let (span, ty) = (ty.span(), ty.clone()); 481 | 482 | let check_fn = format_ident!( 483 | "__axum_codec_check_{}_into_response_parts_{index}_check", 484 | ident, 485 | span = span, 486 | ); 487 | 488 | let call_check_fn = format_ident!( 489 | "__axum_codec_check_{}_into_response_parts_{index}_call_check", 490 | ident, 491 | span = span, 492 | ); 493 | 494 | let call_check_fn_body = quote_spanned! {span=> 495 | #check_fn(); 496 | }; 497 | 498 | let from_request_bound = quote_spanned! {span=> 499 | #ty: ::axum::response::IntoResponseParts 500 | }; 501 | quote_spanned! {span=> 502 | #[allow(warnings)] 503 | #[allow(unreachable_code)] 504 | #[doc(hidden)] 505 | fn #check_fn() 506 | where 507 | #from_request_bound, 508 | {} 509 | 510 | // we have to call the function to actually trigger a compile error 511 | // since the function is generic, just defining it is not enough 512 | #[allow(warnings)] 513 | #[allow(unreachable_code)] 514 | #[doc(hidden)] 515 | fn #call_check_fn() { 516 | #call_check_fn_body 517 | } 518 | } 519 | } 520 | 521 | fn check_input_order(item_fn: &ItemFn, kind: FunctionKind) -> Option { 522 | let number_of_inputs = item_fn 523 | .sig 524 | .inputs 525 | .iter() 526 | .filter(|arg| skip_next_arg(arg, kind)) 527 | .count(); 528 | 529 | let types_that_consume_the_request = item_fn 530 | .sig 531 | .inputs 532 | .iter() 533 | .filter(|arg| skip_next_arg(arg, kind)) 534 | .enumerate() 535 | .filter_map(|(idx, arg)| { 536 | let ty = match arg { 537 | FnArg::Typed(pat_type) => &*pat_type.ty, 538 | FnArg::Receiver(_) => return None, 539 | }; 540 | let type_name = request_consuming_type_name(ty)?; 541 | 542 | Some((idx, type_name, ty.span())) 543 | }) 544 | .collect::>(); 545 | 546 | if types_that_consume_the_request.is_empty() { 547 | return None; 548 | }; 549 | 550 | // exactly one type that consumes the request 551 | if types_that_consume_the_request.len() == 1 { 552 | // and that is not the last 553 | if types_that_consume_the_request[0].0 != number_of_inputs - 1 { 554 | let (_idx, type_name, span) = &types_that_consume_the_request[0]; 555 | let error = format!( 556 | "`{type_name}` consumes the request body and thus must be the last argument to the \ 557 | handler function" 558 | ); 559 | return Some(quote_spanned! {*span=> 560 | compile_error!(#error); 561 | }); 562 | } else { 563 | return None; 564 | } 565 | } 566 | 567 | if types_that_consume_the_request.len() == 2 { 568 | let (_, first, _) = &types_that_consume_the_request[0]; 569 | let (_, second, _) = &types_that_consume_the_request[1]; 570 | let error = format!( 571 | "Can't have two extractors that consume the request body. `{first}` and `{second}` both do \ 572 | that.", 573 | ); 574 | let span = item_fn.sig.inputs.span(); 575 | Some(quote_spanned! {span=> 576 | compile_error!(#error); 577 | }) 578 | } else { 579 | let types = WithPosition::new(types_that_consume_the_request) 580 | .map(|pos| match pos { 581 | Position::First((_, type_name, _)) | Position::Middle((_, type_name, _)) => { 582 | format!("`{type_name}`, ") 583 | } 584 | Position::Last((_, type_name, _)) => format!("and `{type_name}`"), 585 | Position::Only(_) => unreachable!(), 586 | }) 587 | .collect::(); 588 | 589 | let error = format!( 590 | "Can't have more than one extractor that consume the request body. {types} all do that.", 591 | ); 592 | let span = item_fn.sig.inputs.span(); 593 | Some(quote_spanned! {span=> 594 | compile_error!(#error); 595 | }) 596 | } 597 | } 598 | 599 | fn extract_clean_typename(ty: &Type) -> Option { 600 | let path = match ty { 601 | Type::Path(type_path) => &type_path.path, 602 | _ => return None, 603 | }; 604 | path.segments.last().map(|p| p.ident.to_string()) 605 | } 606 | 607 | fn request_consuming_type_name(ty: &Type) -> Option<&'static str> { 608 | let typename = extract_clean_typename(ty)?; 609 | 610 | let type_name = match &*typename { 611 | "Json" => "Json<_>", 612 | "RawBody" => "RawBody<_>", 613 | "RawForm" => "RawForm", 614 | "Multipart" => "Multipart", 615 | "Protobuf" => "Protobuf", 616 | "JsonLines" => "JsonLines<_>", 617 | "Form" => "Form<_>", 618 | "Request" => "Request<_>", 619 | "Bytes" => "Bytes", 620 | "String" => "String", 621 | "Parts" => "Parts", 622 | _ => return None, 623 | }; 624 | 625 | Some(type_name) 626 | } 627 | 628 | fn well_known_last_response_type(ty: &Type) -> Option<&'static str> { 629 | let typename = extract_clean_typename(ty)?; 630 | 631 | let type_name = match &*typename { 632 | "Json" => "Json<_>", 633 | "Protobuf" => "Protobuf", 634 | "JsonLines" => "JsonLines<_>", 635 | "Form" => "Form<_>", 636 | "Bytes" => "Bytes", 637 | "String" => "String", 638 | _ => return None, 639 | }; 640 | 641 | Some(type_name) 642 | } 643 | 644 | fn check_output_impls_into_response(item_fn: &ItemFn) -> TokenStream { 645 | let ty = match &item_fn.sig.output { 646 | syn::ReturnType::Default => return quote! {}, 647 | syn::ReturnType::Type(_, ty) => ty, 648 | }; 649 | let span = ty.span(); 650 | 651 | let declare_inputs = item_fn 652 | .sig 653 | .inputs 654 | .iter() 655 | .filter_map(|arg| match arg { 656 | FnArg::Receiver(_) => None, 657 | FnArg::Typed(pat_ty) => { 658 | let pat = &pat_ty.pat; 659 | let ty = &pat_ty.ty; 660 | Some(quote! { 661 | let #pat: #ty = panic!(); 662 | }) 663 | } 664 | }) 665 | .collect::(); 666 | 667 | let block = &item_fn.block; 668 | 669 | let make_value_name = format_ident!( 670 | "__axum_macros_check_{}_into_response_make_value", 671 | item_fn.sig.ident 672 | ); 673 | 674 | let make = if item_fn.sig.asyncness.is_some() { 675 | quote_spanned! {span=> 676 | #[allow(warnings)] 677 | #[allow(unreachable_code)] 678 | #[doc(hidden)] 679 | async fn #make_value_name() -> #ty { 680 | #declare_inputs 681 | #block 682 | } 683 | } 684 | } else { 685 | quote_spanned! {span=> 686 | #[allow(warnings)] 687 | #[allow(unreachable_code)] 688 | #[doc(hidden)] 689 | fn #make_value_name() -> #ty { 690 | #declare_inputs 691 | #block 692 | } 693 | } 694 | }; 695 | 696 | let name = format_ident!("__axum_macros_check_{}_into_response", item_fn.sig.ident); 697 | 698 | if let Some(receiver) = self_receiver(item_fn) { 699 | quote_spanned! {span=> 700 | #make 701 | 702 | #[allow(warnings)] 703 | #[allow(unreachable_code)] 704 | #[doc(hidden)] 705 | async fn #name() { 706 | let value = #receiver #make_value_name().await; 707 | fn check(_: T) 708 | where T: ::axum_codec::response::IntoCodecResponse 709 | {} 710 | check(value); 711 | } 712 | } 713 | } else { 714 | quote_spanned! {span=> 715 | #[allow(warnings)] 716 | #[allow(unreachable_code)] 717 | #[doc(hidden)] 718 | async fn #name() { 719 | #make 720 | 721 | let value = #make_value_name().await; 722 | 723 | fn check(_: T) 724 | where T: ::axum_codec::response::IntoCodecResponse 725 | {} 726 | 727 | check(value); 728 | } 729 | } 730 | } 731 | } 732 | 733 | fn check_future_send(item_fn: &ItemFn, kind: FunctionKind) -> TokenStream { 734 | if item_fn.sig.asyncness.is_none() { 735 | match &item_fn.sig.output { 736 | syn::ReturnType::Default => { 737 | return syn::Error::new_spanned( 738 | item_fn.sig.fn_token, 739 | format!("{} must be `async fn`s", kind.name_uppercase_plural()), 740 | ) 741 | .into_compile_error(); 742 | } 743 | syn::ReturnType::Type(_, ty) => ty, 744 | }; 745 | } 746 | 747 | let span = item_fn.sig.ident.span(); 748 | 749 | let handler_name = &item_fn.sig.ident; 750 | 751 | let args = item_fn.sig.inputs.iter().map(|_| { 752 | quote_spanned! {span=> panic!() } 753 | }); 754 | 755 | let name = format_ident!("__axum_macros_check_{}_future", item_fn.sig.ident); 756 | 757 | let do_check = quote! { 758 | fn check(_: T) 759 | where T: ::std::future::Future + Send 760 | {} 761 | check(future); 762 | }; 763 | 764 | if let Some(receiver) = self_receiver(item_fn) { 765 | quote! { 766 | #[allow(warnings)] 767 | #[allow(unreachable_code)] 768 | #[doc(hidden)] 769 | fn #name() { 770 | let future = #receiver #handler_name(#(#args),*); 771 | #do_check 772 | } 773 | } 774 | } else { 775 | quote! { 776 | #[allow(warnings)] 777 | #[allow(unreachable_code)] 778 | #[doc(hidden)] 779 | fn #name() { 780 | #item_fn 781 | let future = #handler_name(#(#args),*); 782 | #do_check 783 | } 784 | } 785 | } 786 | } 787 | 788 | fn self_receiver(item_fn: &ItemFn) -> Option { 789 | let takes_self = item_fn.sig.inputs.iter().any(|arg| match arg { 790 | FnArg::Receiver(_) => true, 791 | FnArg::Typed(typed) => is_self_pat_type(typed), 792 | }); 793 | 794 | if takes_self { 795 | return Some(quote! { Self:: }); 796 | } 797 | 798 | if let syn::ReturnType::Type(_, ty) = &item_fn.sig.output { 799 | if let syn::Type::Path(path) = &**ty { 800 | let segments = &path.path.segments; 801 | if segments.len() == 1 { 802 | if let Some(last) = segments.last() { 803 | match &last.arguments { 804 | syn::PathArguments::None if last.ident == "Self" => { 805 | return Some(quote! { Self:: }); 806 | } 807 | _ => {} 808 | } 809 | } 810 | } 811 | } 812 | } 813 | 814 | None 815 | } 816 | 817 | /// Given a signature like 818 | /// 819 | /// ```skip 820 | /// #[debug_handler] 821 | /// async fn handler( 822 | /// _: axum::extract::State, 823 | /// _: State, 824 | /// ) {} 825 | /// ``` 826 | /// 827 | /// This will extract `AppState`. 828 | /// 829 | /// Returns `None` if there are no `State` args or multiple of different types. 830 | fn state_types_from_args(item_fn: &ItemFn) -> HashSet { 831 | let types = item_fn 832 | .sig 833 | .inputs 834 | .iter() 835 | .filter_map(|input| match input { 836 | FnArg::Receiver(_) => None, 837 | FnArg::Typed(pat_type) => Some(pat_type), 838 | }) 839 | .map(|pat_type| &*pat_type.ty); 840 | crate::infer_state_types(types).collect() 841 | } 842 | 843 | fn next_is_last_input(item_fn: &ItemFn) -> TokenStream { 844 | let next_args = item_fn 845 | .sig 846 | .inputs 847 | .iter() 848 | .enumerate() 849 | .filter(|(_, arg)| !skip_next_arg(arg, FunctionKind::Middleware)) 850 | .collect::>(); 851 | 852 | if next_args.is_empty() { 853 | return quote! { 854 | compile_error!( 855 | "Middleware functions must take `axum::middleware::Next` as the last argument", 856 | ); 857 | }; 858 | } 859 | 860 | if next_args.len() == 1 { 861 | let (idx, arg) = &next_args[0]; 862 | if *idx != item_fn.sig.inputs.len() - 1 { 863 | return quote_spanned! {arg.span()=> 864 | compile_error!("`axum::middleware::Next` must the last argument"); 865 | }; 866 | } 867 | } 868 | 869 | if next_args.len() >= 2 { 870 | return quote! { 871 | compile_error!( 872 | "Middleware functions can only take one argument of type `axum::middleware::Next`", 873 | ); 874 | }; 875 | } 876 | 877 | quote! {} 878 | } 879 | 880 | fn skip_next_arg(arg: &FnArg, kind: FunctionKind) -> bool { 881 | match kind { 882 | FunctionKind::Handler => true, 883 | FunctionKind::Middleware => match arg { 884 | FnArg::Receiver(_) => true, 885 | FnArg::Typed(pat_type) => { 886 | if let Type::Path(type_path) = &*pat_type.ty { 887 | type_path 888 | .path 889 | .segments 890 | .last() 891 | .is_none_or(|path_segment| path_segment.ident != "Next") 892 | } else { 893 | true 894 | } 895 | } 896 | }, 897 | } 898 | } 899 | -------------------------------------------------------------------------------- /macros/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![cfg_attr( 2 | not(any( 3 | feature = "bincode", 4 | feature = "bitcode", 5 | feature = "serde", 6 | feature = "aide", 7 | feature = "validator" 8 | )), 9 | allow(unused_variables, dead_code) 10 | )] 11 | 12 | use proc_macro::TokenStream; 13 | use syn::parse::Parse; 14 | 15 | mod apply; 16 | mod attr_parsing; 17 | mod debug_handler; 18 | mod with_position; 19 | 20 | /// A utility macro for automatically deriving the correct traits 21 | /// depending on the enabled features. 22 | #[proc_macro_attribute] 23 | pub fn apply( 24 | attr: proc_macro::TokenStream, 25 | input: proc_macro::TokenStream, 26 | ) -> proc_macro::TokenStream { 27 | apply::apply(attr, input) 28 | } 29 | 30 | /// Generates better error messages when applied to handler functions. 31 | /// 32 | /// For more information, see [`axum::debug_handler`](https://docs.rs/axum/latest/axum/attr.debug_handler.html). 33 | #[proc_macro_attribute] 34 | pub fn debug_handler(_attr: TokenStream, input: TokenStream) -> TokenStream { 35 | #[cfg(not(debug_assertions))] 36 | return input; 37 | 38 | #[cfg(debug_assertions)] 39 | return expand_attr_with(_attr, input, |attrs, item_fn| { 40 | debug_handler::expand(attrs, item_fn, debug_handler::FunctionKind::Handler) 41 | }); 42 | } 43 | 44 | /// Generates better error messages when applied to middleware functions. 45 | /// 46 | /// For more information, see [`axum::debug_middleware`](https://docs.rs/axum/latest/axum/attr.debug_middleware.html). 47 | #[proc_macro_attribute] 48 | pub fn debug_middleware(_attr: TokenStream, input: TokenStream) -> TokenStream { 49 | #[cfg(not(debug_assertions))] 50 | return input; 51 | 52 | #[cfg(debug_assertions)] 53 | return expand_attr_with(_attr, input, |attrs, item_fn| { 54 | debug_handler::expand(attrs, item_fn, debug_handler::FunctionKind::Middleware) 55 | }); 56 | } 57 | 58 | fn expand_attr_with(attr: TokenStream, input: TokenStream, f: F) -> TokenStream 59 | where 60 | F: FnOnce(A, I) -> K, 61 | A: Parse, 62 | I: Parse, 63 | K: quote::ToTokens, 64 | { 65 | let expand_result = (|| { 66 | let attr = syn::parse(attr)?; 67 | let input = syn::parse(input)?; 68 | Ok(f(attr, input)) 69 | })(); 70 | expand(expand_result) 71 | } 72 | 73 | fn expand(result: syn::Result) -> TokenStream 74 | where 75 | T: quote::ToTokens, 76 | { 77 | match result { 78 | Ok(tokens) => { 79 | let tokens = (quote::quote! { #tokens }).into(); 80 | if std::env::var_os("AXUM_MACROS_DEBUG").is_some() { 81 | eprintln!("{tokens}"); 82 | } 83 | tokens 84 | } 85 | Err(err) => err.into_compile_error().into(), 86 | } 87 | } 88 | 89 | fn infer_state_types<'a, I>(types: I) -> impl Iterator + 'a 90 | where 91 | I: Iterator + 'a, 92 | { 93 | types 94 | .filter_map(|ty| { 95 | if let syn::Type::Path(path) = ty { 96 | Some(&path.path) 97 | } else { 98 | None 99 | } 100 | }) 101 | .filter_map(|path| { 102 | if let Some(last_segment) = path.segments.last() { 103 | if last_segment.ident != "State" { 104 | return None; 105 | } 106 | 107 | match &last_segment.arguments { 108 | syn::PathArguments::AngleBracketed(args) if args.args.len() == 1 => { 109 | Some(args.args.first().unwrap()) 110 | } 111 | _ => None, 112 | } 113 | } else { 114 | None 115 | } 116 | }) 117 | .filter_map(|generic_arg| { 118 | if let syn::GenericArgument::Type(ty) = generic_arg { 119 | Some(ty) 120 | } else { 121 | None 122 | } 123 | }) 124 | .cloned() 125 | } 126 | 127 | #[doc(hidden)] 128 | #[proc_macro] 129 | pub fn __private_decode_trait(input: TokenStream) -> TokenStream { 130 | __private::decode_trait(input.into()).into() 131 | } 132 | 133 | #[doc(hidden)] 134 | #[proc_macro] 135 | pub fn __private_encode_trait(input: TokenStream) -> TokenStream { 136 | __private::encode_trait(input.into()).into() 137 | } 138 | 139 | #[allow(unused_imports, unused_mut)] 140 | mod __private { 141 | use proc_macro2::TokenStream; 142 | use quote::quote; 143 | 144 | pub fn decode_trait(input: TokenStream) -> TokenStream { 145 | let mut codec_trait = TokenStream::default(); 146 | let mut codec_impl = TokenStream::default(); 147 | 148 | codec_trait.extend(quote! { 149 | #input 150 | #[diagnostic::on_unimplemented( 151 | note = "If you're looking for a zero-copy extractor, use `BorrowCodec`" 152 | )] 153 | pub trait CodecDecode<'de> 154 | }); 155 | 156 | codec_impl.extend(quote! { 157 | impl<'de, T> CodecDecode<'de> for T 158 | }); 159 | 160 | #[cfg(any( 161 | feature = "bincode", 162 | feature = "bitcode", 163 | feature = "serde", 164 | feature = "aide", 165 | feature = "validator" 166 | ))] 167 | { 168 | codec_trait.extend(quote! { 169 | : 170 | }); 171 | 172 | codec_impl.extend(quote! { 173 | where T: 174 | }); 175 | } 176 | 177 | let mut constraints = TokenStream::default(); 178 | 179 | #[cfg(feature = "serde")] 180 | { 181 | if !constraints.is_empty() { 182 | constraints.extend(quote! { + }); 183 | } 184 | 185 | constraints.extend(quote! { 186 | serde::de::Deserialize<'de> 187 | }); 188 | } 189 | 190 | #[cfg(feature = "bincode")] 191 | { 192 | if !constraints.is_empty() { 193 | constraints.extend(quote! { + }); 194 | } 195 | 196 | constraints.extend(quote! { 197 | bincode::BorrowDecode<'de> 198 | }); 199 | } 200 | 201 | #[cfg(feature = "bitcode")] 202 | { 203 | if !constraints.is_empty() { 204 | constraints.extend(quote! { + }); 205 | } 206 | 207 | constraints.extend(quote! { 208 | bitcode::Decode<'de> 209 | }); 210 | } 211 | 212 | #[cfg(feature = "validator")] 213 | { 214 | if !constraints.is_empty() { 215 | constraints.extend(quote! { + }); 216 | } 217 | 218 | constraints.extend(quote! { 219 | validator::Validate 220 | }); 221 | } 222 | 223 | codec_trait.extend(constraints.clone()); 224 | codec_impl.extend(constraints); 225 | 226 | codec_trait.extend(quote!({})); 227 | codec_impl.extend(quote!({})); 228 | 229 | codec_trait.extend(codec_impl); 230 | codec_trait 231 | } 232 | 233 | pub fn encode_trait(input: TokenStream) -> TokenStream { 234 | let mut codec_trait = TokenStream::default(); 235 | let mut codec_impl = TokenStream::default(); 236 | 237 | codec_trait.extend(quote! { 238 | #input 239 | pub trait CodecEncode 240 | }); 241 | 242 | codec_impl.extend(quote! { 243 | impl CodecEncode for T 244 | }); 245 | 246 | #[cfg(any( 247 | feature = "bincode", 248 | feature = "bitcode", 249 | feature = "serde", 250 | feature = "aide", 251 | feature = "validator" 252 | ))] 253 | { 254 | codec_trait.extend(quote! { 255 | : 256 | }); 257 | 258 | codec_impl.extend(quote! { 259 | where T: 260 | }); 261 | } 262 | 263 | let mut constraints = TokenStream::default(); 264 | 265 | #[cfg(feature = "serde")] 266 | { 267 | if !constraints.is_empty() { 268 | constraints.extend(quote! { + }); 269 | } 270 | 271 | constraints.extend(quote! { 272 | serde::Serialize 273 | }); 274 | } 275 | 276 | #[cfg(feature = "bincode")] 277 | { 278 | if !constraints.is_empty() { 279 | constraints.extend(quote! { + }); 280 | } 281 | 282 | constraints.extend(quote! { 283 | bincode::Encode 284 | }); 285 | } 286 | 287 | #[cfg(feature = "bitcode")] 288 | { 289 | if !constraints.is_empty() { 290 | constraints.extend(quote! { + }); 291 | } 292 | 293 | constraints.extend(quote! { 294 | bitcode::Encode 295 | }); 296 | } 297 | 298 | codec_trait.extend(constraints.clone()); 299 | codec_impl.extend(constraints); 300 | 301 | codec_trait.extend(quote!({})); 302 | codec_impl.extend(quote!({})); 303 | 304 | codec_trait.extend(codec_impl); 305 | codec_trait 306 | } 307 | } 308 | -------------------------------------------------------------------------------- /macros/src/with_position.rs: -------------------------------------------------------------------------------- 1 | // this is copied from itertools under the following license 2 | // 3 | // Copyright (c) 2015 4 | // 5 | // Permission is hereby granted, free of charge, to any 6 | // person obtaining a copy of this software and associated 7 | // documentation files (the "Software"), to deal in the 8 | // Software without restriction, including without 9 | // limitation the rights to use, copy, modify, merge, 10 | // publish, distribute, sublicense, and/or sell copies of 11 | // the Software, and to permit persons to whom the Software 12 | // is furnished to do so, subject to the following 13 | // conditions: 14 | // 15 | // The above copyright notice and this permission notice 16 | // shall be included in all copies or substantial portions 17 | // of the Software. 18 | // 19 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF 20 | // ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 21 | // TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 22 | // PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT 23 | // SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 24 | // CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 25 | // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR 26 | // IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 27 | // DEALINGS IN THE SOFTWARE. 28 | 29 | use std::iter::{Fuse, FusedIterator, Peekable}; 30 | 31 | pub(crate) struct WithPosition 32 | where 33 | I: Iterator, 34 | { 35 | handled_first: bool, 36 | peekable: Peekable>, 37 | } 38 | 39 | impl WithPosition 40 | where 41 | I: Iterator, 42 | { 43 | pub(crate) fn new(iter: impl IntoIterator) -> WithPosition { 44 | WithPosition { 45 | handled_first: false, 46 | peekable: iter.into_iter().fuse().peekable(), 47 | } 48 | } 49 | } 50 | 51 | impl Clone for WithPosition 52 | where 53 | I: Clone + Iterator, 54 | I::Item: Clone, 55 | { 56 | fn clone(&self) -> Self { 57 | Self { 58 | handled_first: self.handled_first, 59 | peekable: self.peekable.clone(), 60 | } 61 | } 62 | } 63 | 64 | #[derive(Copy, Clone, Debug, PartialEq)] 65 | pub(crate) enum Position { 66 | First(T), 67 | Middle(T), 68 | Last(T), 69 | Only(T), 70 | } 71 | 72 | impl Position { 73 | pub(crate) fn into_inner(self) -> T { 74 | match self { 75 | Position::First(x) | Position::Middle(x) | Position::Last(x) | Position::Only(x) => x, 76 | } 77 | } 78 | } 79 | 80 | impl Iterator for WithPosition { 81 | type Item = Position; 82 | 83 | fn next(&mut self) -> Option { 84 | match self.peekable.next() { 85 | Some(item) => { 86 | if !self.handled_first { 87 | // Haven't seen the first item yet, and there is one to give. 88 | self.handled_first = true; 89 | // Peek to see if this is also the last item, 90 | // in which case tag it as `Only`. 91 | match self.peekable.peek() { 92 | Some(_) => Some(Position::First(item)), 93 | None => Some(Position::Only(item)), 94 | } 95 | } else { 96 | // Have seen the first item, and there's something left. 97 | // Peek to see if this is the last item. 98 | match self.peekable.peek() { 99 | Some(_) => Some(Position::Middle(item)), 100 | None => Some(Position::Last(item)), 101 | } 102 | } 103 | } 104 | // Iterator is finished. 105 | None => None, 106 | } 107 | } 108 | 109 | fn size_hint(&self) -> (usize, Option) { 110 | self.peekable.size_hint() 111 | } 112 | } 113 | 114 | impl ExactSizeIterator for WithPosition where I: ExactSizeIterator {} 115 | 116 | impl FusedIterator for WithPosition {} 117 | -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | hard_tabs = true 2 | tab_spaces = 2 3 | newline_style = "Unix" 4 | 5 | group_imports = "StdExternalCrate" 6 | imports_granularity = "Crate" 7 | 8 | format_macro_matchers = true 9 | format_strings = true 10 | hex_literal_case = "Lower" 11 | use_field_init_shorthand = true 12 | condense_wildcard_suffixes = true 13 | 14 | normalize_comments = true 15 | normalize_doc_attributes = true 16 | overflow_delimited_expr = true 17 | wrap_comments = true 18 | 19 | reorder_impl_items = true 20 | reorder_imports = true 21 | 22 | -------------------------------------------------------------------------------- /src/content.rs: -------------------------------------------------------------------------------- 1 | use core::fmt; 2 | use std::{convert::Infallible, str::FromStr}; 3 | 4 | use axum::{ 5 | extract::FromRequestParts, 6 | http::{header, request::Parts, HeaderValue}, 7 | }; 8 | 9 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] 10 | #[non_exhaustive] 11 | pub enum ContentType { 12 | #[cfg(feature = "json")] 13 | Json, 14 | #[cfg(feature = "form")] 15 | Form, 16 | #[cfg(feature = "msgpack")] 17 | MsgPack, 18 | #[cfg(feature = "bincode")] 19 | Bincode, 20 | #[cfg(feature = "bitcode")] 21 | Bitcode, 22 | #[cfg(feature = "cbor")] 23 | Cbor, 24 | #[cfg(feature = "yaml")] 25 | Yaml, 26 | #[cfg(feature = "toml")] 27 | Toml, 28 | } 29 | 30 | #[cfg(not(any( 31 | feature = "json", 32 | feature = "form", 33 | feature = "msgpack", 34 | feature = "bincode", 35 | feature = "bitcode", 36 | feature = "cbor", 37 | feature = "yaml", 38 | feature = "toml" 39 | )))] 40 | const _: () = { 41 | compile_error!( 42 | "At least one of the following features must be enabled: `json`, `form`, `msgpack`, \ 43 | `bincode`, `bitcode`, `cbor`, `yaml`, `toml`." 44 | ); 45 | 46 | impl Default for ContentType { 47 | fn default() -> Self { 48 | unimplemented!() 49 | } 50 | } 51 | }; 52 | 53 | #[cfg(any( 54 | feature = "json", 55 | feature = "form", 56 | feature = "msgpack", 57 | feature = "bincode", 58 | feature = "bitcode", 59 | feature = "cbor", 60 | feature = "yaml", 61 | feature = "toml" 62 | ))] 63 | impl Default for ContentType { 64 | #[allow(unreachable_code)] 65 | fn default() -> Self { 66 | #[cfg(feature = "json")] 67 | return Self::Json; 68 | #[cfg(feature = "form")] 69 | return Self::Form; 70 | #[cfg(feature = "msgpack")] 71 | return Self::MsgPack; 72 | #[cfg(feature = "bincode")] 73 | return Self::Bincode; 74 | #[cfg(feature = "bitcode")] 75 | return Self::Bitcode; 76 | #[cfg(feature = "cbor")] 77 | return Self::Cbor; 78 | #[cfg(feature = "yaml")] 79 | return Self::Yaml; 80 | #[cfg(feature = "toml")] 81 | return Self::Toml; 82 | } 83 | } 84 | 85 | impl fmt::Display for ContentType { 86 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 87 | f.write_str(self.as_str()) 88 | } 89 | } 90 | 91 | #[derive(Debug, thiserror::Error)] 92 | pub enum FromStrError { 93 | #[error("invalid content type")] 94 | InvalidContentType, 95 | #[error(transparent)] 96 | Mime(#[from] mime::FromStrError), 97 | } 98 | 99 | impl FromStr for ContentType { 100 | type Err = FromStrError; 101 | 102 | fn from_str(s: &str) -> Result { 103 | let mime = s.parse::()?; 104 | let subtype = mime.suffix().unwrap_or_else(|| mime.subtype()); 105 | 106 | Ok(match (mime.type_().as_str(), subtype.as_str()) { 107 | #[cfg(feature = "json")] 108 | ("application", "json") => Self::Json, 109 | #[cfg(feature = "form")] 110 | ("application", "x-www-form-urlencoded") => Self::Form, 111 | #[cfg(feature = "msgpack")] 112 | ("application", "msgpack" | "vnd.msgpack" | "x-msgpack" | "x.msgpack") => Self::MsgPack, 113 | #[cfg(feature = "bincode")] 114 | ("application", "bincode" | "vnd.bincode" | "x-bincode" | "x.bincode") => Self::Bincode, 115 | #[cfg(feature = "bitcode")] 116 | ("application", "bitcode" | "vnd.bitcode" | "x-bitcode" | "x.bitcode") => Self::Bitcode, 117 | #[cfg(feature = "cbor")] 118 | ("application", "cbor") => Self::Cbor, 119 | #[cfg(feature = "yaml")] 120 | ("application" | "text", "yaml" | "yml" | "x-yaml") => Self::Yaml, 121 | #[cfg(feature = "toml")] 122 | ("application" | "text", "toml" | "x-toml" | "vnd.toml") => Self::Toml, 123 | _ => return Err(FromStrError::InvalidContentType), 124 | }) 125 | } 126 | } 127 | 128 | impl ContentType { 129 | /// Attempts to parse the given [`HeaderValue`] into a [`ContentType`] 130 | /// by treating it as a MIME type. 131 | /// 132 | /// Note that, along with official MIME types, this method also recognizes 133 | /// some unofficial MIME types that are commonly used in practice. 134 | /// 135 | /// ```edition2021 136 | /// # use axum_codec::ContentType; 137 | /// # use axum::http::HeaderValue; 138 | /// # 139 | /// # fn main() { 140 | /// let header = HeaderValue::from_static("application/json"); 141 | /// let content_type = ContentType::from_header(&header).unwrap(); 142 | /// 143 | /// assert_eq!(content_type, ContentType::Json); 144 | /// 145 | /// let header = HeaderValue::from_static("application/vnd.msgpack"); 146 | /// let content_type = ContentType::from_header(&header).unwrap(); 147 | /// 148 | /// assert_eq!(content_type, ContentType::MsgPack); 149 | /// 150 | /// let header = HeaderValue::from_static("application/x-msgpack"); 151 | /// let content_type = ContentType::from_header(&header).unwrap(); 152 | /// 153 | /// assert_eq!(content_type, ContentType::MsgPack); 154 | /// # } 155 | pub fn from_header(header: &HeaderValue) -> Option { 156 | header.to_str().ok()?.parse().ok() 157 | } 158 | 159 | /// Returns the MIME type as a string slice. 160 | /// 161 | /// ```edition2021 162 | /// # use axum_codec::ContentType; 163 | /// # 164 | /// let content_type = ContentType::Json; 165 | /// 166 | /// assert_eq!(content_type.as_str(), "application/json"); 167 | /// ``` 168 | #[must_use] 169 | pub fn as_str(&self) -> &'static str { 170 | match *self { 171 | #[cfg(feature = "json")] 172 | Self::Json => "application/json", 173 | #[cfg(feature = "form")] 174 | Self::Form => "application/x-www-form-urlencoded", 175 | #[cfg(feature = "msgpack")] 176 | Self::MsgPack => "application/vnd.msgpack", 177 | #[cfg(feature = "bincode")] 178 | Self::Bincode => "application/vnd.bincode", 179 | #[cfg(feature = "bitcode")] 180 | Self::Bitcode => "application/vnd.bitcode", 181 | #[cfg(feature = "cbor")] 182 | Self::Cbor => "application/cbor", 183 | #[cfg(feature = "yaml")] 184 | Self::Yaml => "application/x-yaml", 185 | #[cfg(feature = "toml")] 186 | Self::Toml => "text/toml", 187 | } 188 | } 189 | 190 | /// Converts the [`ContentType`] into a [`HeaderValue`]. 191 | /// 192 | /// ```edition2021 193 | /// # use axum_codec::ContentType; 194 | /// # use axum::http::HeaderValue; 195 | /// # 196 | /// # fn main() { 197 | /// let content_type = ContentType::Json; 198 | /// let header = content_type.into_header(); 199 | /// 200 | /// assert_eq!(header, HeaderValue::from_static("application/json")); 201 | /// 202 | /// let content_type = ContentType::MsgPack; 203 | /// let header = content_type.into_header(); 204 | /// 205 | /// assert_eq!(header, HeaderValue::from_static("application/vnd.msgpack")); 206 | /// 207 | /// let content_type = ContentType::Yaml; 208 | /// let header = content_type.into_header(); 209 | /// 210 | /// assert_eq!(header, HeaderValue::from_static("application/x-yaml")); 211 | /// 212 | /// let content_type = ContentType::Toml; 213 | /// let header = content_type.into_header(); 214 | /// 215 | /// assert_eq!(header, HeaderValue::from_static("text/toml")); 216 | /// # } 217 | #[must_use] 218 | pub fn into_header(self) -> HeaderValue { 219 | HeaderValue::from_static(self.as_str()) 220 | } 221 | } 222 | 223 | impl FromRequestParts for ContentType 224 | where 225 | S: Sync, 226 | { 227 | type Rejection = Infallible; 228 | 229 | async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result { 230 | let header = parts 231 | .headers 232 | .get(header::CONTENT_TYPE) 233 | .and_then(Self::from_header); 234 | 235 | Ok(header.unwrap_or_default()) 236 | } 237 | } 238 | 239 | /// Extractor for the request's desired response [`ContentType`]. 240 | /// 241 | /// # Examples 242 | /// 243 | /// ```edition2021 244 | /// # use axum_codec::{Accept, Codec}; 245 | /// # use axum::{http::HeaderValue, response::IntoResponse}; 246 | /// # use serde::Serialize; 247 | /// # 248 | /// #[axum_codec::apply(encode)] 249 | /// struct User { 250 | /// name: String, 251 | /// age: u8, 252 | /// } 253 | /// 254 | /// fn get_user(accept: Accept) -> impl IntoResponse { 255 | /// Codec(User { 256 | /// name: "Alice".into(), 257 | /// age: 42, 258 | /// }) 259 | /// .to_response(accept) 260 | /// } 261 | /// # 262 | /// # fn main() {} 263 | /// ``` 264 | #[derive(Debug, Clone, Copy)] 265 | pub struct Accept(ContentType); 266 | 267 | impl Accept { 268 | /// Returns the request's desired response [`ContentType`]. 269 | #[inline] 270 | #[must_use] 271 | pub fn content_type(self) -> ContentType { 272 | self.0 273 | } 274 | } 275 | 276 | impl From for ContentType { 277 | #[inline] 278 | fn from(accept: Accept) -> Self { 279 | accept.0 280 | } 281 | } 282 | 283 | impl FromRequestParts for Accept 284 | where 285 | S: Send + Sync + 'static, 286 | { 287 | type Rejection = Infallible; 288 | 289 | async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result { 290 | let header = None 291 | .or_else(|| { 292 | parts 293 | .headers 294 | .get(header::ACCEPT) 295 | .and_then(ContentType::from_header) 296 | }) 297 | .or_else(|| { 298 | parts 299 | .headers 300 | .get(header::CONTENT_TYPE) 301 | .and_then(ContentType::from_header) 302 | }) 303 | .unwrap_or_default(); 304 | 305 | Ok(Self(header)) 306 | } 307 | } 308 | -------------------------------------------------------------------------------- /src/decode.rs: -------------------------------------------------------------------------------- 1 | use crate::{Codec, CodecRejection, ContentType}; 2 | 3 | crate::macros::__private_decode_trait! { 4 | /// Decoder trait for deserializing bytes into all supported formats. 5 | /// 6 | /// Note that feature flags affect this trait differently than normal. In this case, 7 | /// feature flags further restrict the trait instead of being additive. This may change 8 | /// in the future. 9 | } 10 | 11 | #[cfg(feature = "serde")] 12 | impl<'b, T> Codec 13 | where 14 | T: serde::de::Deserialize<'b>, 15 | { 16 | /// Attempts to deserialize the given bytes as [JSON](https://www.json.org). 17 | /// 18 | /// # Errors 19 | /// 20 | /// See [`serde_json::from_slice`]. 21 | #[cfg(feature = "json")] 22 | #[inline] 23 | pub fn from_json(bytes: &'b [u8]) -> Result { 24 | serde_json::from_slice(bytes).map(Self) 25 | } 26 | 27 | /// Attempts to deserialize the given bytes as [URL-encoded form data](https://url.spec.whatwg.org/#urlencoded-parsing). 28 | /// 29 | /// # Errors 30 | /// 31 | /// See [`serde_urlencoded::from_bytes`]. 32 | #[cfg(feature = "form")] 33 | #[inline] 34 | pub fn from_form(bytes: &'b [u8]) -> Result { 35 | serde_urlencoded::from_bytes(bytes).map(Self) 36 | } 37 | 38 | /// Attempts to deserialize the given bytes as [MessagePack](https://msgpack.org). 39 | /// Does not perform any validation if the `validator` feature is enabled. For 40 | /// validation, use [`Self::from_bytes`]. 41 | /// 42 | /// # Errors 43 | /// 44 | /// See [`rmp_serde::from_slice`]. 45 | #[cfg(feature = "msgpack")] 46 | #[inline] 47 | pub fn from_msgpack(bytes: &'b [u8]) -> Result { 48 | let mut deserializer = rmp_serde::Deserializer::new(bytes).with_human_readable(); 49 | 50 | serde::Deserialize::deserialize(&mut deserializer).map(Self) 51 | } 52 | 53 | /// Attemps to deserialize the given bytes as [CBOR](https://cbor.io). 54 | /// Does not perform any validation if the `validator` feature is enabled. For 55 | /// validation, use [`Self::from_bytes`]. 56 | /// 57 | /// # Errors 58 | /// 59 | /// See [`ciborium::from_slice`]. 60 | #[cfg(feature = "cbor")] 61 | #[inline] 62 | pub fn from_cbor(bytes: &'b [u8]) -> Result> { 63 | ciborium::from_reader(bytes).map(Self) 64 | } 65 | 66 | /// Attempts to deserialize the given text as [YAML](https://yaml.org). 67 | /// Does not perform any validation if the `validator` feature is enabled. For 68 | /// validation, use [`Self::from_bytes`]. 69 | /// 70 | /// # Errors 71 | /// 72 | /// See [`serde_yaml::from_slice`]. 73 | #[cfg(feature = "yaml")] 74 | #[inline] 75 | pub fn from_yaml(text: &'b str) -> Result { 76 | serde_yaml::from_str(text).map(Self) 77 | } 78 | 79 | /// Attempts to deserialize the given text as [TOML](https://toml.io). 80 | /// Does not perform any validation if the `validator` feature is enabled. For 81 | /// validation, use [`Self::from_bytes`]. 82 | /// 83 | /// # Errors 84 | /// 85 | /// See [`toml::from_str`]. 86 | #[cfg(feature = "toml")] 87 | #[inline] 88 | pub fn from_toml(text: &'b str) -> Result { 89 | T::deserialize(toml::Deserializer::new(text)).map(Self) 90 | } 91 | } 92 | 93 | impl<'b, T> Codec { 94 | /// Attempts to deserialize the given bytes as [Bincode](https://github.com/bincode-org/bincode). 95 | /// Does not perform any validation if the `validator` feature is enabled. For 96 | /// validation, use [`Self::from_bytes`]. 97 | /// 98 | /// # Errors 99 | /// 100 | /// See [`bincode::decode_from_slice`]. 101 | #[cfg(feature = "bincode")] 102 | #[inline] 103 | pub fn from_bincode(bytes: &'b [u8]) -> Result 104 | where 105 | T: bincode::BorrowDecode<'b>, 106 | { 107 | bincode::borrow_decode_from_slice(bytes, bincode::config::standard()).map(|t| Self(t.0)) 108 | } 109 | 110 | /// Attempts to deserialize the given bytes as [Bitcode](https://github.com/SoftbearStudios/bitcode). 111 | /// Does not perform any validation if the `validator` feature is enabled. For 112 | /// validation, use [`Self::from_bytes`]. 113 | /// 114 | /// # Errors 115 | /// 116 | /// See [`bitcode::decode`]. 117 | #[cfg(feature = "bitcode")] 118 | #[inline] 119 | pub fn from_bitcode(bytes: &'b [u8]) -> Result 120 | where 121 | T: bitcode::Decode<'b>, 122 | { 123 | bitcode::decode(bytes).map(Self) 124 | } 125 | 126 | /// Attempts to deserialize the given bytes as the specified [`ContentType`]. 127 | /// 128 | /// # Errors 129 | /// 130 | /// See [`CodecRejection`]. 131 | pub fn from_bytes(bytes: &'b [u8], content_type: ContentType) -> Result 132 | where 133 | T: CodecDecode<'b>, 134 | { 135 | let codec = match content_type { 136 | #[cfg(feature = "json")] 137 | ContentType::Json => Self::from_json(bytes)?, 138 | #[cfg(feature = "form")] 139 | ContentType::Form => Self::from_form(bytes)?, 140 | #[cfg(feature = "msgpack")] 141 | ContentType::MsgPack => Self::from_msgpack(bytes)?, 142 | #[cfg(feature = "bincode")] 143 | ContentType::Bincode => Self::from_bincode(bytes)?, 144 | #[cfg(feature = "bitcode")] 145 | ContentType::Bitcode => Self::from_bitcode(bytes)?, 146 | #[cfg(feature = "cbor")] 147 | ContentType::Cbor => Self::from_cbor(bytes)?, 148 | #[cfg(feature = "yaml")] 149 | ContentType::Yaml => Self::from_yaml(core::str::from_utf8(bytes)?)?, 150 | #[cfg(feature = "toml")] 151 | ContentType::Toml => Self::from_toml(core::str::from_utf8(bytes)?)?, 152 | }; 153 | 154 | #[cfg(feature = "validator")] 155 | validator::Validate::validate(&codec)?; 156 | 157 | Ok(codec) 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /src/encode.rs: -------------------------------------------------------------------------------- 1 | use axum::response::{IntoResponse, Response}; 2 | 3 | use crate::{Codec, ContentType}; 4 | 5 | crate::macros::__private_encode_trait! { 6 | /// Encoder trait for encoding a value into any supported format. 7 | /// 8 | /// Note that feature flags affect this trait differently than normal. In this case, 9 | /// feature flags further restrict the trait instead of being additive. This may change 10 | /// in the future. 11 | } 12 | 13 | /// Errors that can occur during encoding. 14 | /// 15 | /// In debug mode this will include the error message. In release mode it will 16 | /// only include a status code of `500 Internal Server Error`. 17 | #[derive(Debug, thiserror::Error)] 18 | #[non_exhaustive] 19 | pub enum Error { 20 | #[cfg(feature = "json")] 21 | #[error(transparent)] 22 | Json(#[from] serde_json::Error), 23 | #[cfg(feature = "form")] 24 | #[error(transparent)] 25 | Form(#[from] serde_urlencoded::ser::Error), 26 | #[cfg(feature = "msgpack")] 27 | #[error(transparent)] 28 | MsgPack(#[from] rmp_serde::encode::Error), 29 | #[cfg(feature = "cbor")] 30 | #[error(transparent)] 31 | Cbor(#[from] ciborium::ser::Error), 32 | #[cfg(feature = "bincode")] 33 | #[error(transparent)] 34 | Bincode(#[from] bincode::error::EncodeError), 35 | #[cfg(feature = "yaml")] 36 | #[error(transparent)] 37 | Yaml(#[from] serde_yaml::Error), 38 | #[cfg(feature = "toml")] 39 | #[error(transparent)] 40 | Toml(#[from] toml::ser::Error), 41 | } 42 | 43 | impl IntoResponse for Error { 44 | fn into_response(self) -> Response { 45 | use axum::http::StatusCode; 46 | 47 | #[cfg(debug_assertions)] 48 | return (StatusCode::INTERNAL_SERVER_ERROR, self.to_string()).into_response(); 49 | #[cfg(not(debug_assertions))] 50 | StatusCode::INTERNAL_SERVER_ERROR.into_response() 51 | } 52 | } 53 | 54 | #[cfg(feature = "serde")] 55 | impl Codec 56 | where 57 | T: serde::Serialize, 58 | { 59 | /// Attempts to serialize the given value as [JSON](https://www.json.org). 60 | /// 61 | /// # Errors 62 | /// 63 | /// See [`serde_json::to_vec`]. 64 | #[cfg(feature = "json")] 65 | #[inline] 66 | pub fn to_json(&self) -> Result, serde_json::Error> { 67 | serde_json::to_vec(&self.0) 68 | } 69 | 70 | /// Attempts to serialize the given value as [URL-encoded form data](https://url.spec.whatwg.org/#urlencoded-parsing). 71 | /// 72 | /// # Errors 73 | /// 74 | /// See [`serde_urlencoded::to_string`]. 75 | #[cfg(feature = "form")] 76 | #[inline] 77 | pub fn to_form(&self) -> Result, serde_urlencoded::ser::Error> { 78 | serde_urlencoded::to_string(&self.0).map(String::into_bytes) 79 | } 80 | 81 | /// Attempts to serialize the given value as [MessagePack](https://msgpack.org). 82 | /// 83 | /// # Errors 84 | /// 85 | /// See [`rmp_serde::to_vec_named`]. 86 | #[cfg(feature = "msgpack")] 87 | #[inline] 88 | pub fn to_msgpack(&self) -> Result, rmp_serde::encode::Error> { 89 | rmp_serde::to_vec_named(&self.0) 90 | } 91 | 92 | /// Attempts to serialize the given value as [CBOR](https://cbor.io). 93 | /// 94 | /// # Errors 95 | /// 96 | /// See [`ciborium::into_writer`]. 97 | #[cfg(feature = "cbor")] 98 | #[inline] 99 | pub fn to_cbor(&self) -> Result, ciborium::ser::Error> { 100 | let mut buf = Vec::new(); 101 | ciborium::into_writer(&self.0, &mut buf)?; 102 | Ok(buf) 103 | } 104 | 105 | /// Attempts to serialize the given value as [YAML](https://yaml.org). 106 | /// 107 | /// # Errors 108 | /// 109 | /// See [`serde_yaml::to_vec`]. 110 | #[cfg(feature = "yaml")] 111 | #[inline] 112 | pub fn to_yaml(&self) -> Result { 113 | serde_yaml::to_string(&self.0) 114 | } 115 | 116 | /// Attempts to serialize the given value as [TOML](https://toml.io). 117 | /// 118 | /// # Errors 119 | /// 120 | /// See [`toml::to_string`]. 121 | #[cfg(feature = "toml")] 122 | #[inline] 123 | pub fn to_toml(&self) -> Result { 124 | toml::to_string(&self.0) 125 | } 126 | } 127 | 128 | impl Codec { 129 | /// Attempts to serialize the given value as [Bincode]() 130 | /// 131 | /// # Errors 132 | /// 133 | /// See [`bincode::serialize`]. 134 | #[cfg(feature = "bincode")] 135 | #[inline] 136 | pub fn to_bincode(&self) -> Result, bincode::error::EncodeError> 137 | where 138 | T: bincode::Encode, 139 | { 140 | bincode::encode_to_vec(&self.0, bincode::config::standard()) 141 | } 142 | 143 | /// Attempts to serialize the given value as [Bitcode]() 144 | /// 145 | /// # Errors 146 | /// 147 | /// See [`bitcode::encode`]. 148 | #[cfg(feature = "bitcode")] 149 | #[inline] 150 | pub fn to_bitcode(&self) -> Vec 151 | where 152 | T: bitcode::Encode, 153 | { 154 | bitcode::encode(&self.0) 155 | } 156 | 157 | /// Attempts to serialize the given value as the specified [`ContentType`]. 158 | /// 159 | /// # Errors 160 | /// 161 | /// See [`Error`]. 162 | pub fn to_bytes(&self, content_type: ContentType) -> Result, Error> 163 | where 164 | T: CodecEncode, 165 | { 166 | Ok(match content_type { 167 | #[cfg(feature = "json")] 168 | ContentType::Json => self.to_json()?, 169 | #[cfg(feature = "form")] 170 | ContentType::Form => self.to_form()?, 171 | #[cfg(feature = "msgpack")] 172 | ContentType::MsgPack => self.to_msgpack()?, 173 | #[cfg(feature = "bincode")] 174 | ContentType::Bincode => self.to_bincode()?, 175 | #[cfg(feature = "bitcode")] 176 | ContentType::Bitcode => self.to_bitcode(), 177 | #[cfg(feature = "cbor")] 178 | ContentType::Cbor => self.to_cbor()?, 179 | #[cfg(feature = "yaml")] 180 | ContentType::Yaml => self.to_yaml()?.into_bytes(), 181 | #[cfg(feature = "toml")] 182 | ContentType::Toml => self.to_toml()?.into_bytes(), 183 | }) 184 | } 185 | } 186 | -------------------------------------------------------------------------------- /src/extract.rs: -------------------------------------------------------------------------------- 1 | use core::{ 2 | fmt, 3 | ops::{Deref, DerefMut}, 4 | }; 5 | 6 | use axum::{ 7 | extract::{FromRequest, FromRequestParts, Request}, 8 | http::header, 9 | response::{IntoResponse, Response}, 10 | }; 11 | use bytes::{Bytes, BytesMut}; 12 | 13 | use crate::{Accept, CodecDecode, CodecEncode, CodecRejection, ContentType, IntoCodecResponse}; 14 | 15 | /// Codec extractor / response. 16 | /// 17 | /// The serialized data is not specified. Upon deserialization, the request's 18 | /// `Content-Type` header is used to determine the format of the data. 19 | /// 20 | /// By default, only JSON is supported. To enable other formats, use the 21 | /// corresponding feature flags. 22 | /// 23 | /// Note that [`IntoResponse`] is not implemented for this type, as the headers 24 | /// are not available when serializing the data. Instead, use 25 | /// [`Codec::to_response`] to create a response with the appropriate 26 | /// `Content-Type` header extracted from the request with [`Accept`]. 27 | /// 28 | /// # Examples 29 | /// 30 | /// ```edition2021 31 | /// # use axum_codec::{Codec, ContentType}; 32 | /// # use axum::http::HeaderValue; 33 | /// # use serde_json::json; 34 | /// # 35 | /// # fn main() { 36 | /// #[axum_codec::apply(decode)] 37 | /// struct Greeting { 38 | /// hello: String, 39 | /// } 40 | /// 41 | /// let bytes = b"{\"hello\": \"world\"}"; 42 | /// let content_type = ContentType::Json; 43 | /// 44 | /// let Codec(data) = Codec::::from_bytes(bytes, content_type).unwrap(); 45 | /// 46 | /// assert_eq!(data.hello, "world"); 47 | /// # } 48 | /// ``` 49 | #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Default)] 50 | pub struct Codec(pub T); 51 | 52 | impl Codec { 53 | /// Consumes the [`Codec`] and returns the inner value. 54 | pub fn into_inner(self) -> T { 55 | self.0 56 | } 57 | } 58 | 59 | impl Codec 60 | where 61 | T: CodecEncode, 62 | { 63 | /// Converts the inner value into a response with the given content type. 64 | /// 65 | /// If serialization fails, the rejection is converted into a response. See 66 | /// [`encode::Error`](crate::encode::Error) for possible errors. 67 | pub fn to_response>(&self, content_type: C) -> Response { 68 | let content_type = content_type.into(); 69 | let bytes = match self.to_bytes(content_type) { 70 | Ok(bytes) => bytes, 71 | Err(rejection) => return rejection.into_response(), 72 | }; 73 | 74 | ([(header::CONTENT_TYPE, content_type.into_header())], bytes).into_response() 75 | } 76 | } 77 | 78 | impl Deref for Codec { 79 | type Target = T; 80 | 81 | fn deref(&self) -> &Self::Target { 82 | &self.0 83 | } 84 | } 85 | 86 | impl DerefMut for Codec { 87 | fn deref_mut(&mut self) -> &mut Self::Target { 88 | &mut self.0 89 | } 90 | } 91 | 92 | impl fmt::Display for Codec { 93 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 94 | self.0.fmt(f) 95 | } 96 | } 97 | 98 | impl FromRequest for Codec 99 | where 100 | T: for<'de> CodecDecode<'de>, 101 | S: Send + Sync + 'static, 102 | { 103 | type Rejection = Response; 104 | 105 | async fn from_request(req: Request, state: &S) -> Result { 106 | let (mut parts, body) = req.into_parts(); 107 | let accept = Accept::from_request_parts(&mut parts, state).await.unwrap(); 108 | 109 | let req = Request::from_parts(parts, body); 110 | 111 | let content_type = req 112 | .headers() 113 | .get(header::CONTENT_TYPE) 114 | .and_then(ContentType::from_header) 115 | .unwrap_or_default(); 116 | 117 | let data = match () { 118 | #[cfg(feature = "form")] 119 | () if content_type == ContentType::Form && req.method() == axum::http::Method::GET => { 120 | let query = req.uri().query().unwrap_or(""); 121 | 122 | Codec::from_form(query.as_bytes()).map_err(CodecRejection::from) 123 | } 124 | () => { 125 | let bytes = Bytes::from_request(req, state) 126 | .await 127 | .map_err(|e| CodecRejection::from(e).into_codec_response(accept.into()))?; 128 | 129 | Codec::from_bytes(&bytes, content_type) 130 | } 131 | } 132 | .map_err(|e| e.into_codec_response(accept.into()))?; 133 | 134 | Ok(data) 135 | } 136 | } 137 | 138 | #[cfg(feature = "aide")] 139 | impl aide::operation::OperationInput for Codec 140 | where 141 | T: schemars::JsonSchema, 142 | { 143 | fn operation_input( 144 | ctx: &mut aide::generate::GenContext, 145 | operation: &mut aide::openapi::Operation, 146 | ) { 147 | axum::Json::::operation_input(ctx, operation); 148 | } 149 | 150 | fn inferred_early_responses( 151 | ctx: &mut aide::generate::GenContext, 152 | operation: &mut aide::openapi::Operation, 153 | ) -> Vec<(Option, aide::openapi::Response)> { 154 | axum::Json::::inferred_early_responses(ctx, operation) 155 | } 156 | } 157 | 158 | #[cfg(feature = "aide")] 159 | impl aide::operation::OperationOutput for Codec 160 | where 161 | T: schemars::JsonSchema, 162 | { 163 | type Inner = T; 164 | 165 | fn operation_response( 166 | ctx: &mut aide::generate::GenContext, 167 | operation: &mut aide::openapi::Operation, 168 | ) -> Option { 169 | axum::Json::::operation_response(ctx, operation) 170 | } 171 | 172 | fn inferred_responses( 173 | ctx: &mut aide::generate::GenContext, 174 | operation: &mut aide::openapi::Operation, 175 | ) -> Vec<(Option, aide::openapi::Response)> { 176 | axum::Json::::inferred_responses(ctx, operation) 177 | } 178 | } 179 | 180 | #[cfg(feature = "validator")] 181 | impl validator::Validate for Codec 182 | where 183 | T: validator::Validate, 184 | { 185 | fn validate(&self) -> Result<(), validator::ValidationErrors> { 186 | self.0.validate() 187 | } 188 | } 189 | 190 | /// Zero-copy codec extractor. 191 | /// 192 | /// Similar to [`Codec`] in that it can decode from various formats, 193 | /// but different in that the backing bytes are kept alive after decoding 194 | /// and it cannot be used as a response encoder. 195 | /// 196 | /// Note that the decoded data cannot be modified, as it is borrowed. 197 | /// If you need to modify the data, use [`Codec`] instead. 198 | /// 199 | /// # Examples 200 | /// 201 | /// ```edition2021 202 | /// # use axum_codec::{BorrowCodec, ContentType}; 203 | /// # use axum::response::Response; 204 | /// # use std::borrow::Cow; 205 | /// # 206 | /// # fn main() { 207 | /// #[axum_codec::apply(decode)] 208 | /// struct Greeting<'d> { 209 | /// hello: Cow<'d, [u8]>, 210 | /// } 211 | /// 212 | /// async fn my_route(body: BorrowCodec>) -> Result<(), Response> { 213 | /// // do something with `body.hello`... 214 | /// println!("{:?}", body.hello); 215 | /// 216 | /// Ok(()) 217 | /// } 218 | /// # } 219 | /// ``` 220 | /// 221 | /// # Errors 222 | /// 223 | /// See [`CodecRejection`] for more information. 224 | pub struct BorrowCodec { 225 | data: T, 226 | #[allow(dead_code)] 227 | #[doc(hidden)] 228 | bytes: BytesMut, 229 | } 230 | 231 | impl BorrowCodec { 232 | /// Returns a mutable reference to the inner value. 233 | /// 234 | /// # Safety 235 | /// 236 | /// Callers must ensure that the inner value is not kept alive longer 237 | /// than the original [`BorrowCodec`] that it came from. For example, 238 | /// via [`std::mem::swap`] or [`std::mem::replace`]. 239 | pub unsafe fn as_mut_unchecked(&mut self) -> &mut T { 240 | &mut self.data 241 | } 242 | } 243 | 244 | impl AsRef for BorrowCodec { 245 | fn as_ref(&self) -> &T { 246 | self 247 | } 248 | } 249 | 250 | impl Deref for BorrowCodec { 251 | type Target = T; 252 | 253 | fn deref(&self) -> &Self::Target { 254 | &self.data 255 | } 256 | } 257 | 258 | impl fmt::Debug for BorrowCodec 259 | where 260 | T: fmt::Debug, 261 | { 262 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 263 | f.debug_struct("BorrowCodec") 264 | .field("data", &self.data) 265 | .finish_non_exhaustive() 266 | } 267 | } 268 | 269 | impl PartialEq for BorrowCodec 270 | where 271 | T: PartialEq, 272 | { 273 | fn eq(&self, other: &Self) -> bool { 274 | self.data == other.data 275 | } 276 | } 277 | 278 | impl Eq for BorrowCodec where T: Eq {} 279 | 280 | impl PartialOrd for BorrowCodec 281 | where 282 | T: PartialOrd, 283 | { 284 | fn partial_cmp(&self, other: &Self) -> Option { 285 | self.data.partial_cmp(&other.data) 286 | } 287 | } 288 | 289 | impl Ord for BorrowCodec 290 | where 291 | T: Ord, 292 | { 293 | fn cmp(&self, other: &Self) -> std::cmp::Ordering { 294 | self.data.cmp(&other.data) 295 | } 296 | } 297 | 298 | impl std::hash::Hash for BorrowCodec 299 | where 300 | T: std::hash::Hash, 301 | { 302 | fn hash(&self, state: &mut H) { 303 | self.data.hash(state); 304 | } 305 | } 306 | 307 | impl<'de, T> BorrowCodec 308 | where 309 | T: CodecDecode<'de>, 310 | { 311 | /// Creates a new [`BorrowCodec`] from the given bytes and content type. 312 | /// 313 | /// # Errors 314 | /// 315 | /// See [`CodecRejection`] for more information. 316 | pub fn from_bytes(bytes: BytesMut, content_type: ContentType) -> Result { 317 | let data = Codec::::from_bytes( 318 | // SAFETY: The bytes that are being referenced by the slice are behind a pointer 319 | // so they will not move. The bytes are also kept alive by the struct that contains 320 | // this struct that references the slice, so the bytes will not be deallocated 321 | // while this struct is alive. 322 | unsafe { std::slice::from_raw_parts(bytes.as_ptr(), bytes.len()) }, 323 | content_type, 324 | )? 325 | .into_inner(); 326 | 327 | Ok(Self { data, bytes }) 328 | } 329 | } 330 | 331 | impl FromRequest for BorrowCodec 332 | where 333 | T: CodecDecode<'static>, 334 | S: Send + Sync + 'static, 335 | { 336 | type Rejection = Response; 337 | 338 | async fn from_request(req: Request, state: &S) -> Result { 339 | let (mut parts, body) = req.into_parts(); 340 | let accept = Accept::from_request_parts(&mut parts, state).await.unwrap(); 341 | 342 | let req = Request::from_parts(parts, body); 343 | 344 | let content_type = req 345 | .headers() 346 | .get(header::CONTENT_TYPE) 347 | .and_then(ContentType::from_header) 348 | .unwrap_or_default(); 349 | 350 | let bytes = match () { 351 | #[cfg(feature = "form")] 352 | () if content_type == ContentType::Form && req.method() == axum::http::Method::GET => { 353 | req.uri().query().map_or_else(BytesMut::new, BytesMut::from) 354 | } 355 | () => BytesMut::from_request(req, state) 356 | .await 357 | .map_err(|e| CodecRejection::from(e).into_codec_response(accept.into()))?, 358 | }; 359 | 360 | let data = 361 | Self::from_bytes(bytes, content_type).map_err(|e| e.into_codec_response(accept.into()))?; 362 | 363 | #[cfg(feature = "validator")] 364 | data 365 | .as_ref() 366 | .validate() 367 | .map_err(|e| CodecRejection::from(e).into_codec_response(accept.into()))?; 368 | 369 | Ok(data) 370 | } 371 | } 372 | 373 | #[cfg(feature = "aide")] 374 | impl aide::operation::OperationInput for BorrowCodec 375 | where 376 | T: schemars::JsonSchema, 377 | { 378 | fn operation_input( 379 | ctx: &mut aide::generate::GenContext, 380 | operation: &mut aide::openapi::Operation, 381 | ) { 382 | axum::Json::::operation_input(ctx, operation); 383 | } 384 | 385 | fn inferred_early_responses( 386 | ctx: &mut aide::generate::GenContext, 387 | operation: &mut aide::openapi::Operation, 388 | ) -> Vec<(Option, aide::openapi::Response)> { 389 | axum::Json::::inferred_early_responses(ctx, operation) 390 | } 391 | } 392 | 393 | #[cfg(test)] 394 | mod test { 395 | use super::{Codec, ContentType}; 396 | 397 | #[crate::apply(decode)] 398 | #[derive(Debug, PartialEq, Eq)] 399 | struct Data { 400 | hello: String, 401 | } 402 | 403 | #[test] 404 | fn test_json_codec() { 405 | let bytes = b"{\"hello\": \"world\"}"; 406 | 407 | let Codec(data) = Codec::::from_bytes(bytes, ContentType::Json).unwrap(); 408 | 409 | assert_eq!(data, Data { 410 | hello: "world".into() 411 | }); 412 | } 413 | 414 | #[test] 415 | fn test_msgpack_codec() { 416 | let bytes = b"\x81\xa5hello\xa5world"; 417 | 418 | let Codec(data) = Codec::::from_bytes(bytes, ContentType::MsgPack).unwrap(); 419 | 420 | assert_eq!(data, Data { 421 | hello: "world".into() 422 | }); 423 | } 424 | } 425 | 426 | #[cfg(any(test, miri))] 427 | mod miri { 428 | use std::borrow::Cow; 429 | 430 | use bytes::Bytes; 431 | 432 | use super::*; 433 | 434 | #[crate::apply(decode, crate = "crate")] 435 | #[derive(Debug, PartialEq, Eq)] 436 | struct BorrowData<'a> { 437 | #[serde(borrow)] 438 | hello: Cow<'a, str>, 439 | } 440 | 441 | #[test] 442 | fn test_zero_copy() { 443 | let bytes = b"{\"hello\": \"world\"}".to_vec(); 444 | let data = 445 | BorrowCodec::::from_bytes(BytesMut::from(Bytes::from(bytes)), ContentType::Json) 446 | .unwrap(); 447 | 448 | assert_eq!(data.hello, Cow::Borrowed("world")); 449 | } 450 | } 451 | -------------------------------------------------------------------------------- /src/handler.rs: -------------------------------------------------------------------------------- 1 | use std::{future::Future, pin::Pin}; 2 | 3 | use axum::{ 4 | extract::{FromRequest, FromRequestParts, Request}, 5 | handler::Handler, 6 | response::{IntoResponse, Response}, 7 | }; 8 | 9 | use crate::{Accept, IntoCodecResponse}; 10 | 11 | #[cfg(not(feature = "aide"))] 12 | pub trait Input {} 13 | #[cfg(not(feature = "aide"))] 14 | impl Input for T {} 15 | 16 | #[cfg(feature = "aide")] 17 | pub trait Input: aide::OperationInput {} 18 | #[cfg(feature = "aide")] 19 | impl Input for T where T: aide::OperationInput {} 20 | 21 | #[diagnostic::on_unimplemented( 22 | note = "Consider wrapping the return value in `Codec` if appropriate", 23 | note = "Consider using `#[axum_codec::debug_handler]` to improve the error message" 24 | )] 25 | pub trait CodecHandler: Clone + Send + 'static { 26 | type Future: Future + Send; 27 | 28 | fn call(self, req: Request, state: S) -> Self::Future; 29 | } 30 | 31 | /// Transforms a function (that returns [`IntoCodecResponse`]) into a regular 32 | /// handler. 33 | pub struct CodecHandlerFn { 34 | pub(crate) f: H, 35 | pub(crate) _marker: std::marker::PhantomData<(I, fn() -> D)>, 36 | } 37 | 38 | impl CodecHandlerFn { 39 | pub(crate) fn new(f: H) -> Self { 40 | Self { 41 | f, 42 | _marker: std::marker::PhantomData, 43 | } 44 | } 45 | } 46 | 47 | impl Clone for CodecHandlerFn 48 | where 49 | H: Clone, 50 | { 51 | fn clone(&self) -> Self { 52 | Self { 53 | f: self.f.clone(), 54 | _marker: std::marker::PhantomData, 55 | } 56 | } 57 | } 58 | 59 | #[cfg(feature = "aide")] 60 | impl aide::OperationInput for CodecHandlerFn 61 | where 62 | I: aide::OperationInput, 63 | { 64 | fn operation_input( 65 | ctx: &mut aide::generate::GenContext, 66 | operation: &mut aide::openapi::Operation, 67 | ) { 68 | I::operation_input(ctx, operation); 69 | } 70 | 71 | fn inferred_early_responses( 72 | ctx: &mut aide::generate::GenContext, 73 | operation: &mut aide::openapi::Operation, 74 | ) -> Vec<(Option, aide::openapi::Response)> { 75 | I::inferred_early_responses(ctx, operation) 76 | } 77 | } 78 | 79 | #[cfg(feature = "aide")] 80 | impl aide::OperationOutput for CodecHandlerFn 81 | where 82 | D: aide::OperationOutput, 83 | { 84 | type Inner = D; 85 | 86 | fn operation_response( 87 | ctx: &mut aide::generate::GenContext, 88 | operation: &mut aide::openapi::Operation, 89 | ) -> Option { 90 | D::operation_response(ctx, operation) 91 | } 92 | 93 | fn inferred_responses( 94 | ctx: &mut aide::generate::GenContext, 95 | operation: &mut aide::openapi::Operation, 96 | ) -> Vec<(Option, aide::openapi::Response)> { 97 | D::inferred_responses(ctx, operation) 98 | } 99 | } 100 | 101 | #[cfg(feature = "aide")] 102 | impl aide::operation::OperationHandler> for CodecHandlerFn 103 | where 104 | I: aide::OperationInput, 105 | D: schemars::JsonSchema, 106 | { 107 | } 108 | 109 | #[cfg(feature = "aide")] 110 | impl aide::operation::OperationHandler for CodecHandlerFn 111 | where 112 | I: aide::OperationInput, 113 | D: aide::OperationOutput, 114 | { 115 | } 116 | 117 | impl Handler for CodecHandlerFn 118 | where 119 | H: CodecHandler + Sync, 120 | S: Send + 'static, 121 | I: Input + Send + Sync + 'static, 122 | D: IntoCodecResponse + Send + 'static, 123 | { 124 | type Future = Pin + Send>>; 125 | 126 | #[inline] 127 | fn call(self, req: Request, state: S) -> Self::Future { 128 | Box::pin(async move { CodecHandler::::call(self.f, req, state).await }) 129 | } 130 | } 131 | 132 | impl CodecHandler<((),), (), Res, S> for F 133 | where 134 | F: FnOnce() -> Fut + Clone + Send + 'static, 135 | Fut: Future + Send, 136 | Res: IntoCodecResponse, 137 | S: Send + Sync + 'static, 138 | { 139 | type Future = Pin + Send>>; 140 | 141 | fn call(self, req: Request, state: S) -> Self::Future { 142 | Box::pin(async move { 143 | let (mut parts, ..) = req.into_parts(); 144 | 145 | let content_type = Accept::from_request_parts(&mut parts, &state) 146 | .await 147 | .unwrap(); 148 | 149 | self().await.into_codec_response(content_type.into()) 150 | }) 151 | } 152 | } 153 | 154 | macro_rules! impl_handler { 155 | ( 156 | [$($ty:ident),*], $last:ident 157 | ) => { 158 | #[allow(non_snake_case, unused_mut)] 159 | impl CodecHandler<(M, $($ty,)* $last,), ($($ty,)* $last,), Res, S> for F 160 | where 161 | F: FnOnce($($ty,)* $last,) -> Fut + Clone + Send + 'static, 162 | Fut: Future + Send, 163 | S: Send + Sync + 'static, 164 | Res: IntoCodecResponse, 165 | $( $ty: Input + FromRequestParts + Send, )* 166 | $last: Input + FromRequest + Send, 167 | { 168 | type Future = Pin + Send>>; 169 | 170 | fn call(self, req: Request, state: S) -> Self::Future { 171 | Box::pin(async move { 172 | let (mut parts, body) = req.into_parts(); 173 | 174 | let content_type = Accept::from_request_parts(&mut parts, &state).await.unwrap(); 175 | 176 | $( 177 | let $ty = match $ty::from_request_parts(&mut parts, &state).await { 178 | Ok(value) => value, 179 | Err(rejection) => return rejection.into_response(), 180 | }; 181 | )* 182 | 183 | let req = Request::from_parts(parts, body); 184 | 185 | let $last = match $last::from_request(req, &state).await { 186 | Ok(value) => value, 187 | Err(rejection) => return rejection.into_response(), 188 | }; 189 | 190 | self($($ty,)* $last,).await 191 | .into_codec_response(content_type.into()) 192 | }) 193 | } 194 | } 195 | }; 196 | } 197 | 198 | macro_rules! all_the_tuples { 199 | ($name:ident) => { 200 | $name!([], T1); 201 | $name!([T1], T2); 202 | $name!([T1, T2], T3); 203 | $name!([T1, T2, T3], T4); 204 | $name!([T1, T2, T3, T4], T5); 205 | $name!([T1, T2, T3, T4, T5], T6); 206 | $name!([T1, T2, T3, T4, T5, T6], T7); 207 | $name!([T1, T2, T3, T4, T5, T6, T7], T8); 208 | $name!([T1, T2, T3, T4, T5, T6, T7, T8], T9); 209 | $name!([T1, T2, T3, T4, T5, T6, T7, T8, T9], T10); 210 | $name!([T1, T2, T3, T4, T5, T6, T7, T8, T9, T10], T11); 211 | $name!([T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11], T12); 212 | $name!([T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12], T13); 213 | $name!( 214 | [T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13], 215 | T14 216 | ); 217 | $name!( 218 | [T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14], 219 | T15 220 | ); 221 | $name!( 222 | [T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15], 223 | T16 224 | ); 225 | }; 226 | } 227 | 228 | all_the_tuples!(impl_handler); 229 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | #![warn(clippy::pedantic)] 2 | #![allow(clippy::module_name_repetitions)] 3 | #![cfg_attr( 4 | not(any( 5 | feature = "json", 6 | feature = "msgpack", 7 | feature = "bincode", 8 | feature = "bitcode", 9 | feature = "cbor", 10 | feature = "yaml", 11 | feature = "toml" 12 | )), 13 | allow(unreachable_code, unused_variables) 14 | )] 15 | #![cfg_attr(not(miri), doc = include_str!("../README.md"))] 16 | 17 | mod content; 18 | mod decode; 19 | mod encode; 20 | pub mod extract; 21 | pub mod handler; 22 | pub mod rejection; 23 | pub mod response; 24 | pub mod routing; 25 | 26 | pub use content::{Accept, ContentType}; 27 | pub use decode::CodecDecode; 28 | pub use encode::CodecEncode; 29 | pub use extract::{BorrowCodec, Codec}; 30 | pub use handler::CodecHandler; 31 | pub use rejection::CodecRejection; 32 | pub use response::IntoCodecResponse; 33 | 34 | #[doc(hidden)] 35 | pub mod __private { 36 | #[cfg(feature = "bincode")] 37 | pub use bincode; 38 | #[cfg(feature = "bitcode")] 39 | pub use bitcode; 40 | #[cfg(feature = "aide")] 41 | pub use schemars; 42 | #[cfg(feature = "serde")] 43 | pub use serde; 44 | #[cfg(feature = "validator")] 45 | pub use validator; 46 | } 47 | 48 | pub use axum_codec_macros as macros; 49 | #[cfg(feature = "macros")] 50 | pub use macros::apply; 51 | #[cfg(feature = "macros")] 52 | pub use macros::debug_handler; 53 | #[cfg(feature = "macros")] 54 | pub use macros::debug_middleware; 55 | 56 | #[cfg(test)] 57 | mod test { 58 | use std::borrow::Cow; 59 | 60 | use super::*; 61 | 62 | #[apply(decode, encode)] 63 | #[derive(Debug, PartialEq)] 64 | struct Data { 65 | string: String, 66 | integer: i32, 67 | boolean: bool, 68 | } 69 | 70 | #[apply(decode, encode)] 71 | #[derive(Debug, PartialEq)] 72 | struct BorrowedData<'a> { 73 | string: Cow<'a, str>, 74 | integer: i32, 75 | boolean: bool, 76 | } 77 | 78 | fn data() -> Data { 79 | Data { 80 | string: "hello".into(), 81 | integer: 42, 82 | boolean: true, 83 | } 84 | } 85 | 86 | fn borrowed_data<'a>() -> BorrowedData<'a> { 87 | BorrowedData { 88 | string: Cow::Borrowed("hello"), 89 | integer: 42, 90 | boolean: true, 91 | } 92 | } 93 | 94 | #[test] 95 | fn test_msgpack_roundtrip() { 96 | let data = data(); 97 | let encoded = Codec(&data).to_msgpack().unwrap(); 98 | 99 | let Codec(decoded) = Codec::::from_msgpack(&encoded).unwrap(); 100 | 101 | assert_eq!(decoded, data); 102 | } 103 | 104 | #[test] 105 | fn test_borrowed_msgpack_roundtrip() { 106 | let data = borrowed_data(); 107 | let encoded = Codec(&data).to_msgpack().unwrap(); 108 | 109 | let Codec(decoded) = Codec::::from_msgpack(&encoded).unwrap(); 110 | 111 | assert_eq!(decoded, data); 112 | } 113 | 114 | #[test] 115 | fn test_json_roundtrip() { 116 | let data = data(); 117 | let encoded = Codec(&data).to_json().unwrap(); 118 | 119 | let Codec(decoded) = Codec::::from_json(&encoded).unwrap(); 120 | 121 | assert_eq!(decoded, data); 122 | } 123 | 124 | #[test] 125 | fn test_borrowed_json_roundtrip() { 126 | let data = borrowed_data(); 127 | let encoded = Codec(&data).to_json().unwrap(); 128 | 129 | let Codec(decoded) = Codec::::from_json(&encoded).unwrap(); 130 | 131 | assert_eq!(decoded, data); 132 | } 133 | 134 | #[test] 135 | fn test_form_roundtrip() { 136 | let data = data(); 137 | let encoded = Codec(&data).to_form().unwrap(); 138 | 139 | let Codec(decoded) = Codec::::from_form(&encoded).unwrap(); 140 | 141 | assert_eq!(decoded, data); 142 | } 143 | 144 | #[test] 145 | fn test_borrowed_form_roundtrip() { 146 | let data = borrowed_data(); 147 | let encoded = Codec(&data).to_form().unwrap(); 148 | 149 | let Codec(decoded) = Codec::::from_form(&encoded).unwrap(); 150 | 151 | assert_eq!(decoded, data); 152 | } 153 | 154 | #[test] 155 | #[cfg(feature = "cbor")] 156 | fn test_cbor_roundtrip() { 157 | let data = data(); 158 | let encoded = Codec(&data).to_cbor().unwrap(); 159 | 160 | let Codec(decoded) = Codec::::from_cbor(&encoded).unwrap(); 161 | 162 | assert_eq!(decoded, data); 163 | } 164 | 165 | #[test] 166 | #[cfg(feature = "cbor")] 167 | fn test_borrowed_cbor_roundtrip() { 168 | let data = borrowed_data(); 169 | let encoded = Codec(&data).to_cbor().unwrap(); 170 | 171 | let Codec(decoded) = Codec::::from_cbor(&encoded).unwrap(); 172 | 173 | assert_eq!(decoded, data); 174 | } 175 | 176 | #[test] 177 | fn test_yaml_roundtrip() { 178 | let data = data(); 179 | let encoded = Codec(&data).to_yaml().unwrap(); 180 | 181 | let Codec(decoded) = Codec::::from_yaml(&encoded).unwrap(); 182 | 183 | assert_eq!(decoded, data); 184 | } 185 | 186 | #[test] 187 | fn test_borrowed_yaml_roundtrip() { 188 | let data = borrowed_data(); 189 | let encoded = Codec(&data).to_yaml().unwrap(); 190 | 191 | let Codec(decoded) = Codec::::from_yaml(&encoded).unwrap(); 192 | 193 | assert_eq!(decoded, data); 194 | } 195 | 196 | #[test] 197 | fn test_toml_roundtrip() { 198 | let data = data(); 199 | let encoded = Codec(&data).to_toml().unwrap(); 200 | 201 | let Codec(decoded) = Codec::::from_toml(&encoded).unwrap(); 202 | 203 | assert_eq!(decoded, data); 204 | } 205 | 206 | #[test] 207 | fn test_borrowed_toml_roundtrip() { 208 | let data = borrowed_data(); 209 | let encoded = Codec(&data).to_toml().unwrap(); 210 | 211 | let Codec(decoded) = Codec::::from_toml(&encoded).unwrap(); 212 | 213 | assert_eq!(decoded, data); 214 | } 215 | 216 | #[test] 217 | fn test_bincode_roundtrip() { 218 | let data = data(); 219 | let encoded = Codec(&data).to_bincode().unwrap(); 220 | 221 | let Codec(decoded) = Codec::::from_bincode(&encoded).unwrap(); 222 | 223 | assert_eq!(decoded, data); 224 | } 225 | 226 | #[test] 227 | fn test_borrowed_bincode_roundtrip() { 228 | let data = borrowed_data(); 229 | let encoded = Codec(&data).to_bincode().unwrap(); 230 | 231 | let Codec(decoded) = Codec::::from_bincode(&encoded).unwrap(); 232 | 233 | assert_eq!(decoded, data); 234 | } 235 | 236 | #[test] 237 | #[cfg(feature = "bitcode")] 238 | fn test_bitcode_roundtrip() { 239 | let encoded = Codec(data()).to_bitcode(); 240 | 241 | let Codec(decoded) = Codec::::from_bitcode(&encoded).unwrap(); 242 | 243 | assert_eq!(decoded, data()); 244 | } 245 | 246 | #[test] 247 | #[cfg(feature = "bitcode")] 248 | fn test_borrowed_bitcode_roundtrip() { 249 | let encoded = Codec(borrowed_data()).to_bitcode(); 250 | 251 | let Codec(decoded) = Codec::::from_bitcode(&encoded).unwrap(); 252 | 253 | assert_eq!(decoded, borrowed_data()); 254 | } 255 | } 256 | -------------------------------------------------------------------------------- /src/rejection.rs: -------------------------------------------------------------------------------- 1 | use axum::{extract::rejection::BytesRejection, http::StatusCode, response::Response}; 2 | 3 | use crate::{ContentType, IntoCodecResponse}; 4 | 5 | /// Rejection used for [`Codec`](crate::Codec). 6 | /// 7 | /// Contains one variant for each way the [`Codec`](crate::Codec) extractor 8 | /// can fail. 9 | #[derive(Debug, thiserror::Error)] 10 | #[non_exhaustive] 11 | pub enum CodecRejection { 12 | #[error(transparent)] 13 | Bytes(#[from] BytesRejection), 14 | #[cfg(feature = "json")] 15 | #[error(transparent)] 16 | Json(#[from] serde_json::Error), 17 | #[cfg(feature = "form")] 18 | #[error(transparent)] 19 | Form(#[from] serde_urlencoded::de::Error), 20 | #[cfg(feature = "msgpack")] 21 | #[error(transparent)] 22 | MsgPack(#[from] rmp_serde::decode::Error), 23 | #[cfg(feature = "cbor")] 24 | #[error(transparent)] 25 | Cbor(#[from] ciborium::de::Error), 26 | #[cfg(feature = "bincode")] 27 | #[error(transparent)] 28 | Bincode(#[from] bincode::error::DecodeError), 29 | #[cfg(feature = "bitcode")] 30 | #[error(transparent)] 31 | Bitcode(#[from] bitcode::Error), 32 | #[cfg(feature = "yaml")] 33 | #[error(transparent)] 34 | Yaml(#[from] serde_yaml::Error), 35 | #[cfg(feature = "toml")] 36 | #[error(transparent)] 37 | Toml(#[from] toml::de::Error), 38 | #[cfg(any(feature = "toml", feature = "yaml"))] 39 | #[error(transparent)] 40 | Utf8Error(#[from] core::str::Utf8Error), 41 | #[cfg(feature = "validator")] 42 | #[error("validator error")] 43 | Validator(#[from] validator::ValidationErrors), 44 | } 45 | 46 | #[cfg(not(feature = "pretty-errors"))] 47 | impl IntoCodecResponse for CodecRejection { 48 | fn into_codec_response(self, _content_type: ContentType) -> Response { 49 | use axum::response::IntoResponse; 50 | 51 | let mut response = self.to_string().into_response(); 52 | 53 | *response.status_mut() = self.status_code(); 54 | response 55 | } 56 | } 57 | 58 | #[cfg(all(feature = "aide", feature = "pretty-errors"))] 59 | impl aide::OperationOutput for CodecRejection { 60 | type Inner = Message; 61 | 62 | fn operation_response( 63 | ctx: &mut aide::generate::GenContext, 64 | operation: &mut aide::openapi::Operation, 65 | ) -> Option { 66 | axum::Json::::operation_response(ctx, operation) 67 | } 68 | 69 | fn inferred_responses( 70 | ctx: &mut aide::generate::GenContext, 71 | operation: &mut aide::openapi::Operation, 72 | ) -> Vec<(Option, aide::openapi::Response)> { 73 | axum::Json::::inferred_responses(ctx, operation) 74 | } 75 | } 76 | 77 | #[cfg(all(feature = "aide", not(feature = "pretty-errors")))] 78 | impl aide::OperationOutput for CodecRejection { 79 | type Inner = String; 80 | 81 | fn operation_response( 82 | ctx: &mut aide::generate::GenContext, 83 | operation: &mut aide::openapi::Operation, 84 | ) -> Option { 85 | axum::Json::::operation_response(ctx, operation) 86 | } 87 | 88 | fn inferred_responses( 89 | ctx: &mut aide::generate::GenContext, 90 | operation: &mut aide::openapi::Operation, 91 | ) -> Vec<(Option, aide::openapi::Response)> { 92 | axum::Json::::inferred_responses(ctx, operation) 93 | } 94 | } 95 | 96 | #[cfg(feature = "pretty-errors")] 97 | impl IntoCodecResponse for CodecRejection { 98 | fn into_codec_response(self, content_type: ContentType) -> Response { 99 | let mut response = crate::Codec(self.message()).into_codec_response(content_type); 100 | 101 | *response.status_mut() = self.status_code(); 102 | response 103 | } 104 | } 105 | 106 | #[cfg(feature = "pretty-errors")] 107 | #[crate::apply(encode, crate = "crate")] 108 | pub struct Message { 109 | /// A unique error code, useful for localization. 110 | pub code: &'static str, 111 | /// A human-readable error message in English. 112 | // TODO: use Cow<'static, str> when bitcode supports it 113 | pub content: String, 114 | } 115 | 116 | #[cfg(all(feature = "aide", feature = "pretty-errors"))] 117 | impl aide::OperationOutput for Message { 118 | type Inner = Self; 119 | 120 | fn operation_response( 121 | ctx: &mut aide::generate::GenContext, 122 | operation: &mut aide::openapi::Operation, 123 | ) -> Option { 124 | axum::Json::::operation_response(ctx, operation) 125 | } 126 | 127 | fn inferred_responses( 128 | ctx: &mut aide::generate::GenContext, 129 | operation: &mut aide::openapi::Operation, 130 | ) -> Vec<(Option, aide::openapi::Response)> { 131 | axum::Json::::inferred_responses(ctx, operation) 132 | } 133 | } 134 | 135 | impl CodecRejection { 136 | /// Returns the HTTP status code for the rejection. 137 | #[must_use] 138 | pub fn status_code(&self) -> StatusCode { 139 | if matches!(self, Self::Bytes(..)) { 140 | StatusCode::PAYLOAD_TOO_LARGE 141 | } else { 142 | StatusCode::BAD_REQUEST 143 | } 144 | } 145 | 146 | /// Consumes the rejection and returns a pretty [`Message`] representing the 147 | /// error. 148 | /// 149 | /// Useful for sending a detailed error message to the client, but not so much 150 | /// for local debugging. 151 | #[cfg(feature = "pretty-errors")] 152 | #[must_use] 153 | pub fn message(&self) -> Message { 154 | let code = match self { 155 | Self::Bytes(..) => { 156 | return Message { 157 | code: "payload_too_large", 158 | content: "The request payload is too large.".into(), 159 | } 160 | } 161 | #[cfg(feature = "json")] 162 | Self::Json(..) => "decode", 163 | #[cfg(feature = "form")] 164 | Self::Form(..) => "decode", 165 | #[cfg(feature = "msgpack")] 166 | Self::MsgPack(..) => "decode", 167 | #[cfg(feature = "cbor")] 168 | Self::Cbor(..) => "decode", 169 | #[cfg(feature = "bincode")] 170 | Self::Bincode(..) => "decode", 171 | #[cfg(feature = "bitcode")] 172 | Self::Bitcode(..) => "decode", 173 | #[cfg(feature = "yaml")] 174 | Self::Yaml(..) => "decode", 175 | #[cfg(feature = "toml")] 176 | Self::Toml(..) => "decode", 177 | #[cfg(any(feature = "toml", feature = "yaml"))] 178 | Self::Utf8Error(..) => { 179 | return Message { 180 | code: "malformed_utf8", 181 | content: "The request payload is not valid UTF-8 when it should be.".into(), 182 | } 183 | } 184 | #[cfg(feature = "validator")] 185 | Self::Validator(err) => { 186 | return Message { 187 | code: "invalid_input", 188 | content: format_validator(err), 189 | } 190 | } 191 | }; 192 | 193 | Message { 194 | code, 195 | content: self.to_string(), 196 | } 197 | } 198 | } 199 | 200 | #[cfg(all(feature = "pretty-errors", feature = "validator"))] 201 | fn format_validator(err: &validator::ValidationErrors) -> String { 202 | let mut buf = String::new(); 203 | 204 | for (field, error) in err.errors() { 205 | append_validator_errors(field, error, &mut buf); 206 | } 207 | 208 | buf 209 | } 210 | 211 | #[cfg(all(feature = "pretty-errors", feature = "validator"))] 212 | fn append_validator_errors(field: &str, err: &validator::ValidationErrorsKind, buf: &mut String) { 213 | match err { 214 | validator::ValidationErrorsKind::Field(errors) => { 215 | for error in errors { 216 | if !buf.is_empty() { 217 | buf.push_str(", "); 218 | } 219 | 220 | buf.push_str(field); 221 | buf.push_str(": "); 222 | 223 | if let Some(message) = &error.message { 224 | buf.push_str(message); 225 | } else { 226 | buf.push_str(&error.code); 227 | } 228 | 229 | if !error.params.is_empty() { 230 | buf.push_str(" ("); 231 | 232 | let mut params = error.params.iter(); 233 | 234 | if let Some((key, value)) = params.next() { 235 | buf.push_str(key); 236 | buf.push_str(": "); 237 | buf.push_str(&value.to_string()); 238 | } 239 | 240 | for (key, value) in params { 241 | buf.push_str(", "); 242 | buf.push_str(key); 243 | buf.push_str(": "); 244 | buf.push_str(&value.to_string()); 245 | } 246 | 247 | buf.push(')'); 248 | } 249 | } 250 | } 251 | validator::ValidationErrorsKind::List(message) => { 252 | for error in message.values() { 253 | for (field, errors) in error.errors() { 254 | append_validator_errors(field, errors, buf); 255 | } 256 | } 257 | } 258 | validator::ValidationErrorsKind::Struct(struct_) => { 259 | for (field, errors) in struct_.errors() { 260 | append_validator_errors(field, errors, buf); 261 | } 262 | } 263 | } 264 | } 265 | -------------------------------------------------------------------------------- /src/response.rs: -------------------------------------------------------------------------------- 1 | use axum::response::Response; 2 | 3 | use crate::{Codec, CodecEncode, ContentType}; 4 | 5 | #[cfg(not(feature = "aide"))] 6 | pub trait IntoCodecResponse { 7 | fn into_codec_response(self, content_type: ContentType) -> Response; 8 | } 9 | 10 | #[cfg(feature = "aide")] 11 | pub trait IntoCodecResponse: aide::OperationOutput { 12 | fn into_codec_response(self, content_type: ContentType) -> Response; 13 | } 14 | 15 | #[cfg(not(feature = "aide"))] 16 | impl IntoCodecResponse for Codec 17 | where 18 | D: CodecEncode, 19 | { 20 | fn into_codec_response(self, content_type: ContentType) -> Response { 21 | self.to_response(content_type) 22 | } 23 | } 24 | 25 | #[cfg(feature = "aide")] 26 | impl IntoCodecResponse for Codec 27 | where 28 | D: CodecEncode, 29 | Self: aide::OperationOutput, 30 | { 31 | fn into_codec_response(self, content_type: ContentType) -> Response { 32 | self.to_response(content_type) 33 | } 34 | } 35 | 36 | mod axum_impls { 37 | use std::borrow::Cow; 38 | 39 | use axum::{ 40 | body::Bytes, 41 | http::StatusCode, 42 | response::{IntoResponse, Response}, 43 | BoxError, 44 | }; 45 | 46 | use super::{ContentType, IntoCodecResponse}; 47 | 48 | impl IntoCodecResponse for Result 49 | where 50 | T: IntoCodecResponse, 51 | E: IntoCodecResponse, 52 | { 53 | fn into_codec_response(self, content_type: ContentType) -> Response { 54 | match self { 55 | Ok(value) => value.into_codec_response(content_type), 56 | Err(err) => err.into_codec_response(content_type), 57 | } 58 | } 59 | } 60 | 61 | impl IntoCodecResponse for Response 62 | where 63 | B: axum::body::HttpBody + Send + 'static, 64 | B::Error: Into, 65 | { 66 | fn into_codec_response(self, _ct: ContentType) -> Response { 67 | self.into_response() 68 | } 69 | } 70 | 71 | macro_rules! forward_to_into_response { 72 | ( $($ty:ty),* ) => { 73 | $( 74 | impl IntoCodecResponse for $ty { 75 | fn into_codec_response(self, _ct: ContentType) -> Response { 76 | self.into_response() 77 | } 78 | } 79 | )* 80 | } 81 | } 82 | 83 | forward_to_into_response! { 84 | StatusCode, (), &'static str, String, Bytes, Cow<'static, str>, &'static [u8], Vec, Cow<'static, [u8]> 85 | } 86 | 87 | impl IntoCodecResponse for (StatusCode, R) 88 | where 89 | R: IntoCodecResponse, 90 | { 91 | fn into_codec_response(self, content_type: ContentType) -> Response { 92 | let mut res = self.1.into_codec_response(content_type); 93 | *res.status_mut() = self.0; 94 | res 95 | } 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/routing.rs: -------------------------------------------------------------------------------- 1 | use std::convert::Infallible; 2 | 3 | use axum::routing; 4 | 5 | use crate::{ 6 | handler::{CodecHandlerFn, Input}, 7 | CodecHandler, IntoCodecResponse, 8 | }; 9 | 10 | /// A light wrapper around axum's [`MethodRouter`](axum::routing::MethodRouter) 11 | /// (or [`ApiMethodRouter`](aide::axum::routing::ApiMethodRouter) if the `aide` 12 | /// feature is enabled). 13 | /// 14 | /// However, responses are expected to be [`IntoCodecResponse`] (instead of 15 | /// [`IntoResponse`](axum::response::IntoResponse)), as they are automatically 16 | /// converted to the appropriate response type when appropriate. 17 | pub struct MethodRouter { 18 | #[cfg(not(feature = "aide"))] 19 | inner: routing::MethodRouter, 20 | #[cfg(feature = "aide")] 21 | inner: aide::axum::routing::ApiMethodRouter, 22 | } 23 | 24 | impl Clone for MethodRouter { 25 | fn clone(&self) -> Self { 26 | Self { 27 | inner: self.inner.clone(), 28 | } 29 | } 30 | } 31 | 32 | #[cfg(not(feature = "aide"))] 33 | impl From> for routing::MethodRouter { 34 | fn from(router: MethodRouter) -> Self { 35 | router.inner 36 | } 37 | } 38 | 39 | #[cfg(not(feature = "aide"))] 40 | impl From> for MethodRouter { 41 | fn from(router: routing::MethodRouter) -> Self { 42 | Self { inner: router } 43 | } 44 | } 45 | 46 | #[cfg(feature = "aide")] 47 | impl From> for MethodRouter { 48 | fn from(router: routing::MethodRouter) -> Self { 49 | Self { 50 | inner: router.into(), 51 | } 52 | } 53 | } 54 | 55 | #[cfg(feature = "aide")] 56 | impl From> for routing::MethodRouter { 57 | fn from(router: MethodRouter) -> Self { 58 | router.inner.into() 59 | } 60 | } 61 | 62 | #[cfg(feature = "aide")] 63 | impl From> for MethodRouter { 64 | fn from(router: aide::axum::routing::ApiMethodRouter) -> Self { 65 | Self { inner: router } 66 | } 67 | } 68 | 69 | #[cfg(feature = "aide")] 70 | impl From> for aide::axum::routing::ApiMethodRouter { 71 | fn from(router: MethodRouter) -> Self { 72 | router.inner 73 | } 74 | } 75 | 76 | #[cfg(not(feature = "aide"))] 77 | macro_rules! method_router_chain_method { 78 | ($name:ident, $with:ident) => { 79 | #[doc = concat!("Route `", stringify!($name) ,"` requests to the given handler. See [`axum::routing::MethodRouter::", stringify!($name) , "`] for more details.")] 80 | #[must_use] 81 | pub fn $name(mut self, handler: H) -> Self 82 | where 83 | H: CodecHandler + Clone + Send + Sync + 'static, 84 | I: Input + Send + Sync + 'static, 85 | D: IntoCodecResponse + Send + 'static, 86 | S: Clone + Send + Sync + 'static, 87 | T: Sync + 'static 88 | { 89 | self.inner = self.inner.$name(CodecHandlerFn::new(handler)); 90 | self 91 | } 92 | }; 93 | } 94 | 95 | #[cfg(feature = "aide")] 96 | macro_rules! method_router_chain_method { 97 | ($name:ident, $with:ident) => { 98 | #[doc = concat!("Route `", stringify!($name) ,"` requests to the given handler. See [`axum::routing::MethodRouter::", stringify!($name) , "`] for more details.")] 99 | #[must_use] 100 | pub fn $name(mut self, handler: H) -> Self 101 | where 102 | H: CodecHandler + Clone + Send + Sync + 'static, 103 | I: Input + Send + Sync + 'static, 104 | D: IntoCodecResponse + Send + 'static, 105 | S: Clone + Send + Sync + 'static, 106 | T: Sync + 'static, 107 | { 108 | self.inner = self.inner.$name(CodecHandlerFn::::new(handler)); 109 | self 110 | } 111 | 112 | #[doc = concat!("Route `", stringify!($name) ,"` requests to the given handler. See [`axum::routing::MethodRouter::", stringify!($name) , "`] for more details.")] 113 | #[must_use] 114 | pub fn $with(mut self, handler: H, transform: F) -> Self 115 | where 116 | H: CodecHandler + Clone + Send + Sync + 'static, 117 | I: Input + Send + Sync + 'static, 118 | D: IntoCodecResponse + Send + 'static, 119 | S: Clone + Send + Sync + 'static, 120 | T: Sync + 'static, 121 | F: FnOnce(aide::transform::TransformOperation) -> aide::transform::TransformOperation, 122 | { 123 | self.inner = self.inner.$with(CodecHandlerFn::::new(handler), transform); 124 | self 125 | } 126 | }; 127 | } 128 | 129 | impl MethodRouter 130 | where 131 | S: Clone + Send + Sync + 'static, 132 | { 133 | method_router_chain_method!(delete, delete_with); 134 | 135 | method_router_chain_method!(get, get_with); 136 | 137 | method_router_chain_method!(head, head_with); 138 | 139 | method_router_chain_method!(options, options_with); 140 | 141 | method_router_chain_method!(patch, patch_with); 142 | 143 | method_router_chain_method!(post, post_with); 144 | 145 | method_router_chain_method!(put, put_with); 146 | 147 | method_router_chain_method!(trace, trace_with); 148 | } 149 | 150 | #[cfg(not(feature = "aide"))] 151 | macro_rules! method_router_top_level { 152 | ($name:ident, $with:ident) => { 153 | #[doc = concat!("Route `", stringify!($name) ,"` requests to the given handler. See [`axum::routing::", stringify!($name) , "`] for more details.")] 154 | pub fn $name(handler: H) -> MethodRouter 155 | where 156 | H: CodecHandler + Clone + Send + Sync + 'static, 157 | I: Input + Send + Sync + 'static, 158 | D: IntoCodecResponse + Send + 'static, 159 | S: Clone + Send + Sync + 'static, 160 | T: Sync + 'static 161 | { 162 | MethodRouter::from(routing::$name(CodecHandlerFn::new(handler))) 163 | } 164 | }; 165 | } 166 | 167 | #[cfg(feature = "aide")] 168 | macro_rules! method_router_top_level { 169 | ($name:ident, $with:ident) => { 170 | #[doc = concat!("Route `", stringify!($name) ,"` requests to the given handler. See [`axum::routing::", stringify!($name) , "`] for more details.")] 171 | pub fn $name(handler: H) -> MethodRouter 172 | where 173 | H: CodecHandler + Clone + Send + Sync + 'static, 174 | I: Input + Send + Sync + 'static, 175 | D: IntoCodecResponse + Send + 'static, 176 | S: Clone + Send + Sync + 'static, 177 | T: Sync + 'static, 178 | { 179 | MethodRouter::from(aide::axum::routing::$name( 180 | CodecHandlerFn::::new(handler), 181 | )) 182 | } 183 | 184 | #[doc = concat!("Route `", stringify!($name) ,"` requests to the given handler. See [`axum::routing::", stringify!($name) , "`] for more details.")] 185 | #[must_use] 186 | pub fn $with(handler: H, transform: F) -> MethodRouter 187 | where 188 | H: CodecHandler + Clone + Send + Sync + 'static, 189 | I: Input + Send + Sync + 'static, 190 | D: IntoCodecResponse + Send + 'static, 191 | S: Clone + Send + Sync + 'static, 192 | T: Sync + 'static, 193 | F: FnOnce(aide::transform::TransformOperation) -> aide::transform::TransformOperation, 194 | { 195 | MethodRouter::from(aide::axum::routing::$with(CodecHandlerFn::::new(handler), transform)) 196 | } 197 | }; 198 | } 199 | 200 | method_router_top_level!(delete, delete_with); 201 | method_router_top_level!(get, get_with); 202 | method_router_top_level!(head, head_with); 203 | method_router_top_level!(options, options_with); 204 | method_router_top_level!(patch, patch_with); 205 | method_router_top_level!(post, post_with); 206 | method_router_top_level!(put, put_with); 207 | method_router_top_level!(trace, trace_with); 208 | --------------------------------------------------------------------------------