├── .github ├── CODEOWNERS ├── dependabot.yml └── workflows │ ├── ci.yml │ ├── create-release-pr.yml │ └── publish-release.yml ├── .gitignore ├── CONTRIBUTING.md ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── Makefile ├── README.md ├── SUPPORT.md ├── crates ├── twirp-build │ ├── Cargo.toml │ ├── LICENSE │ ├── README.md │ └── src │ │ └── lib.rs └── twirp │ ├── Cargo.toml │ ├── LICENSE │ ├── README.md │ └── src │ ├── client.rs │ ├── context.rs │ ├── details.rs │ ├── error.rs │ ├── headers.rs │ ├── lib.rs │ ├── server.rs │ └── test.rs ├── example ├── Cargo.toml ├── build.rs ├── proto │ └── haberdash │ │ └── v1 │ │ └── haberdash_api.proto └── src │ └── bin │ ├── advanced-server.rs │ ├── client.rs │ └── simple-server.rs ├── release-plz.toml ├── rust-toolchain.toml └── script └── install-protoc /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @github/blackbird 2 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "cargo" 4 | directory: "/" 5 | schedule: 6 | interval: "weekly" 7 | 8 | - package-ecosystem: "github-actions" 9 | directory: "/" 10 | schedule: 11 | interval: "weekly" 12 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [ "main" ] 6 | pull_request: 7 | merge_group: 8 | 9 | permissions: 10 | contents: read 11 | packages: read 12 | 13 | env: 14 | CARGO_TERM_COLOR: always 15 | 16 | jobs: 17 | test: 18 | runs-on: ubuntu-latest 19 | steps: 20 | - uses: actions/checkout@v4 21 | - name: Install protoc 22 | run: script/install-protoc 23 | - name: Build 24 | run: make build 25 | - name: Run tests 26 | run: make test 27 | 28 | lint: 29 | runs-on: ubuntu-latest 30 | steps: 31 | - uses: actions/checkout@v4 32 | - name: Install protoc 33 | run: script/install-protoc 34 | - name: Lint 35 | run: make lint 36 | -------------------------------------------------------------------------------- /.github/workflows/create-release-pr.yml: -------------------------------------------------------------------------------- 1 | name: Create release PR 2 | 3 | permissions: 4 | pull-requests: write 5 | contents: write 6 | 7 | on: workflow_dispatch 8 | 9 | jobs: 10 | # Create a PR with the new versions and changelog, preparing the next release. When merged to main, 11 | # the publish-release.yml workflow will automatically publish any Rust package versions. 12 | create-release-pr: 13 | name: Create release PR 14 | runs-on: ubuntu-latest 15 | permissions: 16 | contents: write 17 | pull-requests: write 18 | concurrency: # Don't run overlapping instances of this workflow 19 | group: release-plz-${{ github.ref }} 20 | cancel-in-progress: false 21 | steps: 22 | - name: Checkout repository 23 | uses: actions/checkout@v4 24 | with: 25 | fetch-depth: 0 26 | - name: Install Rust toolchain 27 | uses: dtolnay/rust-toolchain@stable 28 | - name: Run release-plz 29 | uses: release-plz/action@v0.5 30 | with: 31 | command: release-pr 32 | env: 33 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 34 | CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} 35 | -------------------------------------------------------------------------------- /.github/workflows/publish-release.yml: -------------------------------------------------------------------------------- 1 | name: Release and unpublished twirp/twirp-build packages 2 | 3 | permissions: 4 | contents: write 5 | 6 | on: 7 | push: 8 | branches: 9 | - main 10 | 11 | jobs: 12 | # Release any unpublished packages 13 | release-plz-release: 14 | name: Release-plz release 15 | runs-on: ubuntu-latest 16 | permissions: 17 | contents: write 18 | steps: 19 | - name: Checkout repository 20 | uses: actions/checkout@v4 21 | with: 22 | fetch-depth: 0 23 | - name: Install Rust toolchain 24 | uses: dtolnay/rust-toolchain@stable 25 | - name: Run release-plz 26 | uses: release-plz/action@v0.5 27 | with: 28 | command: release 29 | env: 30 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 31 | CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} 32 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /crates/*/target 3 | /example/*/target 4 | heaptrack.* 5 | .DS_Store 6 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ## Contributing 2 | 3 | [fork]: https://github.com/github/twirp-rs/fork 4 | [pr]: https://github.com/github/twirp-rs/compare 5 | [code-of-conduct]: CODE_OF_CONDUCT.md 6 | 7 | Hi there! We're thrilled that you'd like to contribute to this project. Your help is essential for keeping it great. 8 | 9 | Contributions to this project are [released](https://help.github.com/articles/github-terms-of-service/#6-contributions-under-repository-license) to the public under the [project's open source license](LICENSE.md). 10 | 11 | Please note that this project is released with a [Contributor Code of Conduct](CODE_OF_CONDUCT.md). By participating in this project you agree to abide by its terms. 12 | 13 | ## Prerequisites for running and testing code 14 | 15 | We recommend that you install Rust with the `rustup` tool. `twirp-rs` targets stable Rust versions. 16 | 17 | ## Submitting a pull request 18 | 19 | 1. [Fork][fork] and clone the repository. 20 | 1. Install `protoc` with your package manager of choice. 21 | 1. Build the software: `cargo build`. 22 | 1. Create a new branch: `git checkout -b my-branch-name`. 23 | 1. Make your change, add tests, and make sure the tests and linter still pass. 24 | 1. Push to your fork and [submit a pull request][pr]. 25 | 1. Pat yourself on the back and wait for your pull request to be reviewed and merged. 26 | 27 | Here are a few things you can do that will increase the likelihood of your pull request being accepted: 28 | 29 | - Write tests. 30 | - Keep your change as focused as possible. If there are multiple changes you would like to make that are not dependent upon each other, consider submitting them as separate pull requests. 31 | - Write a [good commit message](http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html). 32 | 33 | ## Setting up a local build 34 | 35 | Make sure you have [rust toolchain installed](https://www.rust-lang.org/tools/install) on your system and then: 36 | 37 | ```sh 38 | cargo build && cargo test 39 | ``` 40 | 41 | Run clippy and fix any lints: 42 | 43 | ```sh 44 | make lint 45 | ``` 46 | 47 | ## Releasing 48 | 49 | 1. Go to the `Create Release PR` action and press the button to run the action. This will use `release-plz` to create a new release PR. 50 | 1. Adjust the generated changelog and version number(s) as necessary. 51 | 1. Get PR approval 52 | 1. Merge the PR. The `publish-release.yml` workflow will automatically publish a new release of any crate whose version has changed. 53 | 54 | ## Resources 55 | 56 | - [How to Contribute to Open Source](https://opensource.guide/how-to-contribute/) 57 | - [Using Pull Requests](https://help.github.com/articles/about-pull-requests/) 58 | - [GitHub Help](https://help.github.com) 59 | -------------------------------------------------------------------------------- /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 = "anyhow" 31 | version = "1.0.95" 32 | source = "registry+https://github.com/rust-lang/crates.io-index" 33 | checksum = "34ac096ce696dc2fcabef30516bb13c0a68a11d30131d3df6f04711467681b04" 34 | 35 | [[package]] 36 | name = "async-trait" 37 | version = "0.1.88" 38 | source = "registry+https://github.com/rust-lang/crates.io-index" 39 | checksum = "e539d3fca749fcee5236ab05e93a52867dd549cc157c8cb7f99595f3cedffdb5" 40 | dependencies = [ 41 | "proc-macro2", 42 | "quote", 43 | "syn", 44 | ] 45 | 46 | [[package]] 47 | name = "autocfg" 48 | version = "1.4.0" 49 | source = "registry+https://github.com/rust-lang/crates.io-index" 50 | checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" 51 | 52 | [[package]] 53 | name = "axum" 54 | version = "0.8.4" 55 | source = "registry+https://github.com/rust-lang/crates.io-index" 56 | checksum = "021e862c184ae977658b36c4500f7feac3221ca5da43e3f25bd04ab6c79a29b5" 57 | dependencies = [ 58 | "axum-core", 59 | "bytes", 60 | "form_urlencoded", 61 | "futures-util", 62 | "http", 63 | "http-body", 64 | "http-body-util", 65 | "hyper", 66 | "hyper-util", 67 | "itoa", 68 | "matchit", 69 | "memchr", 70 | "mime", 71 | "percent-encoding", 72 | "pin-project-lite", 73 | "rustversion", 74 | "serde", 75 | "serde_json", 76 | "serde_path_to_error", 77 | "serde_urlencoded", 78 | "sync_wrapper", 79 | "tokio", 80 | "tower", 81 | "tower-layer", 82 | "tower-service", 83 | "tracing", 84 | ] 85 | 86 | [[package]] 87 | name = "axum-core" 88 | version = "0.5.2" 89 | source = "registry+https://github.com/rust-lang/crates.io-index" 90 | checksum = "68464cd0412f486726fb3373129ef5d2993f90c34bc2bc1c1e9943b2f4fc7ca6" 91 | dependencies = [ 92 | "bytes", 93 | "futures-core", 94 | "http", 95 | "http-body", 96 | "http-body-util", 97 | "mime", 98 | "pin-project-lite", 99 | "rustversion", 100 | "sync_wrapper", 101 | "tower-layer", 102 | "tower-service", 103 | "tracing", 104 | ] 105 | 106 | [[package]] 107 | name = "backtrace" 108 | version = "0.3.74" 109 | source = "registry+https://github.com/rust-lang/crates.io-index" 110 | checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a" 111 | dependencies = [ 112 | "addr2line", 113 | "cfg-if", 114 | "libc", 115 | "miniz_oxide", 116 | "object", 117 | "rustc-demangle", 118 | "windows-targets", 119 | ] 120 | 121 | [[package]] 122 | name = "base64" 123 | version = "0.22.1" 124 | source = "registry+https://github.com/rust-lang/crates.io-index" 125 | checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" 126 | 127 | [[package]] 128 | name = "bitflags" 129 | version = "2.6.0" 130 | source = "registry+https://github.com/rust-lang/crates.io-index" 131 | checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" 132 | 133 | [[package]] 134 | name = "bumpalo" 135 | version = "3.16.0" 136 | source = "registry+https://github.com/rust-lang/crates.io-index" 137 | checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" 138 | 139 | [[package]] 140 | name = "bytes" 141 | version = "1.9.0" 142 | source = "registry+https://github.com/rust-lang/crates.io-index" 143 | checksum = "325918d6fe32f23b19878fe4b34794ae41fc19ddbe53b10571a4874d44ffd39b" 144 | 145 | [[package]] 146 | name = "cfg-if" 147 | version = "1.0.0" 148 | source = "registry+https://github.com/rust-lang/crates.io-index" 149 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 150 | 151 | [[package]] 152 | name = "chrono" 153 | version = "0.4.39" 154 | source = "registry+https://github.com/rust-lang/crates.io-index" 155 | checksum = "7e36cc9d416881d2e24f9a963be5fb1cd90966419ac844274161d10488b3e825" 156 | dependencies = [ 157 | "num-traits", 158 | "serde", 159 | ] 160 | 161 | [[package]] 162 | name = "displaydoc" 163 | version = "0.2.5" 164 | source = "registry+https://github.com/rust-lang/crates.io-index" 165 | checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" 166 | dependencies = [ 167 | "proc-macro2", 168 | "quote", 169 | "syn", 170 | ] 171 | 172 | [[package]] 173 | name = "either" 174 | version = "1.13.0" 175 | source = "registry+https://github.com/rust-lang/crates.io-index" 176 | checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" 177 | 178 | [[package]] 179 | name = "equivalent" 180 | version = "1.0.1" 181 | source = "registry+https://github.com/rust-lang/crates.io-index" 182 | checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" 183 | 184 | [[package]] 185 | name = "erased-serde" 186 | version = "0.4.5" 187 | source = "registry+https://github.com/rust-lang/crates.io-index" 188 | checksum = "24e2389d65ab4fab27dc2a5de7b191e1f6617d1f1c8855c0dc569c94a4cbb18d" 189 | dependencies = [ 190 | "serde", 191 | "typeid", 192 | ] 193 | 194 | [[package]] 195 | name = "errno" 196 | version = "0.3.10" 197 | source = "registry+https://github.com/rust-lang/crates.io-index" 198 | checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d" 199 | dependencies = [ 200 | "libc", 201 | "windows-sys 0.59.0", 202 | ] 203 | 204 | [[package]] 205 | name = "example" 206 | version = "0.1.0" 207 | dependencies = [ 208 | "fs-err", 209 | "glob", 210 | "prost", 211 | "prost-build", 212 | "prost-wkt", 213 | "prost-wkt-build", 214 | "prost-wkt-types", 215 | "serde", 216 | "tokio", 217 | "twirp", 218 | "twirp-build", 219 | ] 220 | 221 | [[package]] 222 | name = "fastrand" 223 | version = "2.3.0" 224 | source = "registry+https://github.com/rust-lang/crates.io-index" 225 | checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" 226 | 227 | [[package]] 228 | name = "fixedbitset" 229 | version = "0.4.2" 230 | source = "registry+https://github.com/rust-lang/crates.io-index" 231 | checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" 232 | 233 | [[package]] 234 | name = "fnv" 235 | version = "1.0.7" 236 | source = "registry+https://github.com/rust-lang/crates.io-index" 237 | checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" 238 | 239 | [[package]] 240 | name = "form_urlencoded" 241 | version = "1.2.1" 242 | source = "registry+https://github.com/rust-lang/crates.io-index" 243 | checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" 244 | dependencies = [ 245 | "percent-encoding", 246 | ] 247 | 248 | [[package]] 249 | name = "fs-err" 250 | version = "3.1.0" 251 | source = "registry+https://github.com/rust-lang/crates.io-index" 252 | checksum = "1f89bda4c2a21204059a977ed3bfe746677dfd137b83c339e702b0ac91d482aa" 253 | dependencies = [ 254 | "autocfg", 255 | ] 256 | 257 | [[package]] 258 | name = "futures" 259 | version = "0.3.31" 260 | source = "registry+https://github.com/rust-lang/crates.io-index" 261 | checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" 262 | dependencies = [ 263 | "futures-channel", 264 | "futures-core", 265 | "futures-executor", 266 | "futures-io", 267 | "futures-sink", 268 | "futures-task", 269 | "futures-util", 270 | ] 271 | 272 | [[package]] 273 | name = "futures-channel" 274 | version = "0.3.31" 275 | source = "registry+https://github.com/rust-lang/crates.io-index" 276 | checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" 277 | dependencies = [ 278 | "futures-core", 279 | "futures-sink", 280 | ] 281 | 282 | [[package]] 283 | name = "futures-core" 284 | version = "0.3.31" 285 | source = "registry+https://github.com/rust-lang/crates.io-index" 286 | checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" 287 | 288 | [[package]] 289 | name = "futures-executor" 290 | version = "0.3.31" 291 | source = "registry+https://github.com/rust-lang/crates.io-index" 292 | checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" 293 | dependencies = [ 294 | "futures-core", 295 | "futures-task", 296 | "futures-util", 297 | ] 298 | 299 | [[package]] 300 | name = "futures-io" 301 | version = "0.3.31" 302 | source = "registry+https://github.com/rust-lang/crates.io-index" 303 | checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" 304 | 305 | [[package]] 306 | name = "futures-macro" 307 | version = "0.3.31" 308 | source = "registry+https://github.com/rust-lang/crates.io-index" 309 | checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" 310 | dependencies = [ 311 | "proc-macro2", 312 | "quote", 313 | "syn", 314 | ] 315 | 316 | [[package]] 317 | name = "futures-sink" 318 | version = "0.3.31" 319 | source = "registry+https://github.com/rust-lang/crates.io-index" 320 | checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" 321 | 322 | [[package]] 323 | name = "futures-task" 324 | version = "0.3.31" 325 | source = "registry+https://github.com/rust-lang/crates.io-index" 326 | checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" 327 | 328 | [[package]] 329 | name = "futures-util" 330 | version = "0.3.31" 331 | source = "registry+https://github.com/rust-lang/crates.io-index" 332 | checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" 333 | dependencies = [ 334 | "futures-channel", 335 | "futures-core", 336 | "futures-io", 337 | "futures-macro", 338 | "futures-sink", 339 | "futures-task", 340 | "memchr", 341 | "pin-project-lite", 342 | "pin-utils", 343 | "slab", 344 | ] 345 | 346 | [[package]] 347 | name = "getrandom" 348 | version = "0.2.15" 349 | source = "registry+https://github.com/rust-lang/crates.io-index" 350 | checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" 351 | dependencies = [ 352 | "cfg-if", 353 | "libc", 354 | "wasi", 355 | ] 356 | 357 | [[package]] 358 | name = "gimli" 359 | version = "0.31.1" 360 | source = "registry+https://github.com/rust-lang/crates.io-index" 361 | checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" 362 | 363 | [[package]] 364 | name = "glob" 365 | version = "0.3.2" 366 | source = "registry+https://github.com/rust-lang/crates.io-index" 367 | checksum = "a8d1add55171497b4705a648c6b583acafb01d58050a51727785f0b2c8e0a2b2" 368 | 369 | [[package]] 370 | name = "hashbrown" 371 | version = "0.15.2" 372 | source = "registry+https://github.com/rust-lang/crates.io-index" 373 | checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" 374 | 375 | [[package]] 376 | name = "heck" 377 | version = "0.5.0" 378 | source = "registry+https://github.com/rust-lang/crates.io-index" 379 | checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" 380 | 381 | [[package]] 382 | name = "http" 383 | version = "1.3.1" 384 | source = "registry+https://github.com/rust-lang/crates.io-index" 385 | checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565" 386 | dependencies = [ 387 | "bytes", 388 | "fnv", 389 | "itoa", 390 | ] 391 | 392 | [[package]] 393 | name = "http-body" 394 | version = "1.0.1" 395 | source = "registry+https://github.com/rust-lang/crates.io-index" 396 | checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" 397 | dependencies = [ 398 | "bytes", 399 | "http", 400 | ] 401 | 402 | [[package]] 403 | name = "http-body-util" 404 | version = "0.1.3" 405 | source = "registry+https://github.com/rust-lang/crates.io-index" 406 | checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" 407 | dependencies = [ 408 | "bytes", 409 | "futures-core", 410 | "http", 411 | "http-body", 412 | "pin-project-lite", 413 | ] 414 | 415 | [[package]] 416 | name = "httparse" 417 | version = "1.9.5" 418 | source = "registry+https://github.com/rust-lang/crates.io-index" 419 | checksum = "7d71d3574edd2771538b901e6549113b4006ece66150fb69c0fb6d9a2adae946" 420 | 421 | [[package]] 422 | name = "httpdate" 423 | version = "1.0.3" 424 | source = "registry+https://github.com/rust-lang/crates.io-index" 425 | checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" 426 | 427 | [[package]] 428 | name = "hyper" 429 | version = "1.6.0" 430 | source = "registry+https://github.com/rust-lang/crates.io-index" 431 | checksum = "cc2b571658e38e0c01b1fdca3bbbe93c00d3d71693ff2770043f8c29bc7d6f80" 432 | dependencies = [ 433 | "bytes", 434 | "futures-channel", 435 | "futures-util", 436 | "http", 437 | "http-body", 438 | "httparse", 439 | "httpdate", 440 | "itoa", 441 | "pin-project-lite", 442 | "smallvec", 443 | "tokio", 444 | "want", 445 | ] 446 | 447 | [[package]] 448 | name = "hyper-util" 449 | version = "0.1.13" 450 | source = "registry+https://github.com/rust-lang/crates.io-index" 451 | checksum = "b1c293b6b3d21eca78250dc7dbebd6b9210ec5530e038cbfe0661b5c47ab06e8" 452 | dependencies = [ 453 | "base64", 454 | "bytes", 455 | "futures-channel", 456 | "futures-core", 457 | "futures-util", 458 | "http", 459 | "http-body", 460 | "hyper", 461 | "ipnet", 462 | "libc", 463 | "percent-encoding", 464 | "pin-project-lite", 465 | "socket2", 466 | "tokio", 467 | "tower-service", 468 | "tracing", 469 | ] 470 | 471 | [[package]] 472 | name = "icu_collections" 473 | version = "1.5.0" 474 | source = "registry+https://github.com/rust-lang/crates.io-index" 475 | checksum = "db2fa452206ebee18c4b5c2274dbf1de17008e874b4dc4f0aea9d01ca79e4526" 476 | dependencies = [ 477 | "displaydoc", 478 | "yoke", 479 | "zerofrom", 480 | "zerovec", 481 | ] 482 | 483 | [[package]] 484 | name = "icu_locid" 485 | version = "1.5.0" 486 | source = "registry+https://github.com/rust-lang/crates.io-index" 487 | checksum = "13acbb8371917fc971be86fc8057c41a64b521c184808a698c02acc242dbf637" 488 | dependencies = [ 489 | "displaydoc", 490 | "litemap", 491 | "tinystr", 492 | "writeable", 493 | "zerovec", 494 | ] 495 | 496 | [[package]] 497 | name = "icu_locid_transform" 498 | version = "1.5.0" 499 | source = "registry+https://github.com/rust-lang/crates.io-index" 500 | checksum = "01d11ac35de8e40fdeda00d9e1e9d92525f3f9d887cdd7aa81d727596788b54e" 501 | dependencies = [ 502 | "displaydoc", 503 | "icu_locid", 504 | "icu_locid_transform_data", 505 | "icu_provider", 506 | "tinystr", 507 | "zerovec", 508 | ] 509 | 510 | [[package]] 511 | name = "icu_locid_transform_data" 512 | version = "1.5.0" 513 | source = "registry+https://github.com/rust-lang/crates.io-index" 514 | checksum = "fdc8ff3388f852bede6b579ad4e978ab004f139284d7b28715f773507b946f6e" 515 | 516 | [[package]] 517 | name = "icu_normalizer" 518 | version = "1.5.0" 519 | source = "registry+https://github.com/rust-lang/crates.io-index" 520 | checksum = "19ce3e0da2ec68599d193c93d088142efd7f9c5d6fc9b803774855747dc6a84f" 521 | dependencies = [ 522 | "displaydoc", 523 | "icu_collections", 524 | "icu_normalizer_data", 525 | "icu_properties", 526 | "icu_provider", 527 | "smallvec", 528 | "utf16_iter", 529 | "utf8_iter", 530 | "write16", 531 | "zerovec", 532 | ] 533 | 534 | [[package]] 535 | name = "icu_normalizer_data" 536 | version = "1.5.0" 537 | source = "registry+https://github.com/rust-lang/crates.io-index" 538 | checksum = "f8cafbf7aa791e9b22bec55a167906f9e1215fd475cd22adfcf660e03e989516" 539 | 540 | [[package]] 541 | name = "icu_properties" 542 | version = "1.5.1" 543 | source = "registry+https://github.com/rust-lang/crates.io-index" 544 | checksum = "93d6020766cfc6302c15dbbc9c8778c37e62c14427cb7f6e601d849e092aeef5" 545 | dependencies = [ 546 | "displaydoc", 547 | "icu_collections", 548 | "icu_locid_transform", 549 | "icu_properties_data", 550 | "icu_provider", 551 | "tinystr", 552 | "zerovec", 553 | ] 554 | 555 | [[package]] 556 | name = "icu_properties_data" 557 | version = "1.5.0" 558 | source = "registry+https://github.com/rust-lang/crates.io-index" 559 | checksum = "67a8effbc3dd3e4ba1afa8ad918d5684b8868b3b26500753effea8d2eed19569" 560 | 561 | [[package]] 562 | name = "icu_provider" 563 | version = "1.5.0" 564 | source = "registry+https://github.com/rust-lang/crates.io-index" 565 | checksum = "6ed421c8a8ef78d3e2dbc98a973be2f3770cb42b606e3ab18d6237c4dfde68d9" 566 | dependencies = [ 567 | "displaydoc", 568 | "icu_locid", 569 | "icu_provider_macros", 570 | "stable_deref_trait", 571 | "tinystr", 572 | "writeable", 573 | "yoke", 574 | "zerofrom", 575 | "zerovec", 576 | ] 577 | 578 | [[package]] 579 | name = "icu_provider_macros" 580 | version = "1.5.0" 581 | source = "registry+https://github.com/rust-lang/crates.io-index" 582 | checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6" 583 | dependencies = [ 584 | "proc-macro2", 585 | "quote", 586 | "syn", 587 | ] 588 | 589 | [[package]] 590 | name = "idna" 591 | version = "1.0.3" 592 | source = "registry+https://github.com/rust-lang/crates.io-index" 593 | checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e" 594 | dependencies = [ 595 | "idna_adapter", 596 | "smallvec", 597 | "utf8_iter", 598 | ] 599 | 600 | [[package]] 601 | name = "idna_adapter" 602 | version = "1.2.0" 603 | source = "registry+https://github.com/rust-lang/crates.io-index" 604 | checksum = "daca1df1c957320b2cf139ac61e7bd64fed304c5040df000a745aa1de3b4ef71" 605 | dependencies = [ 606 | "icu_normalizer", 607 | "icu_properties", 608 | ] 609 | 610 | [[package]] 611 | name = "indexmap" 612 | version = "2.7.0" 613 | source = "registry+https://github.com/rust-lang/crates.io-index" 614 | checksum = "62f822373a4fe84d4bb149bf54e584a7f4abec90e072ed49cda0edea5b95471f" 615 | dependencies = [ 616 | "equivalent", 617 | "hashbrown", 618 | ] 619 | 620 | [[package]] 621 | name = "inventory" 622 | version = "0.3.16" 623 | source = "registry+https://github.com/rust-lang/crates.io-index" 624 | checksum = "e5d80fade88dd420ce0d9ab6f7c58ef2272dde38db874657950f827d4982c817" 625 | dependencies = [ 626 | "rustversion", 627 | ] 628 | 629 | [[package]] 630 | name = "ipnet" 631 | version = "2.10.1" 632 | source = "registry+https://github.com/rust-lang/crates.io-index" 633 | checksum = "ddc24109865250148c2e0f3d25d4f0f479571723792d3802153c60922a4fb708" 634 | 635 | [[package]] 636 | name = "iri-string" 637 | version = "0.7.8" 638 | source = "registry+https://github.com/rust-lang/crates.io-index" 639 | checksum = "dbc5ebe9c3a1a7a5127f920a418f7585e9e758e911d0466ed004f393b0e380b2" 640 | dependencies = [ 641 | "memchr", 642 | "serde", 643 | ] 644 | 645 | [[package]] 646 | name = "itertools" 647 | version = "0.13.0" 648 | source = "registry+https://github.com/rust-lang/crates.io-index" 649 | checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" 650 | dependencies = [ 651 | "either", 652 | ] 653 | 654 | [[package]] 655 | name = "itoa" 656 | version = "1.0.14" 657 | source = "registry+https://github.com/rust-lang/crates.io-index" 658 | checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674" 659 | 660 | [[package]] 661 | name = "js-sys" 662 | version = "0.3.77" 663 | source = "registry+https://github.com/rust-lang/crates.io-index" 664 | checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" 665 | dependencies = [ 666 | "once_cell", 667 | "wasm-bindgen", 668 | ] 669 | 670 | [[package]] 671 | name = "libc" 672 | version = "0.2.172" 673 | source = "registry+https://github.com/rust-lang/crates.io-index" 674 | checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa" 675 | 676 | [[package]] 677 | name = "linux-raw-sys" 678 | version = "0.4.14" 679 | source = "registry+https://github.com/rust-lang/crates.io-index" 680 | checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" 681 | 682 | [[package]] 683 | name = "litemap" 684 | version = "0.7.4" 685 | source = "registry+https://github.com/rust-lang/crates.io-index" 686 | checksum = "4ee93343901ab17bd981295f2cf0026d4ad018c7c31ba84549a4ddbb47a45104" 687 | 688 | [[package]] 689 | name = "log" 690 | version = "0.4.22" 691 | source = "registry+https://github.com/rust-lang/crates.io-index" 692 | checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" 693 | 694 | [[package]] 695 | name = "matchit" 696 | version = "0.8.4" 697 | source = "registry+https://github.com/rust-lang/crates.io-index" 698 | checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" 699 | 700 | [[package]] 701 | name = "memchr" 702 | version = "2.7.4" 703 | source = "registry+https://github.com/rust-lang/crates.io-index" 704 | checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" 705 | 706 | [[package]] 707 | name = "mime" 708 | version = "0.3.17" 709 | source = "registry+https://github.com/rust-lang/crates.io-index" 710 | checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" 711 | 712 | [[package]] 713 | name = "miniz_oxide" 714 | version = "0.8.2" 715 | source = "registry+https://github.com/rust-lang/crates.io-index" 716 | checksum = "4ffbe83022cedc1d264172192511ae958937694cd57ce297164951b8b3568394" 717 | dependencies = [ 718 | "adler2", 719 | ] 720 | 721 | [[package]] 722 | name = "mio" 723 | version = "1.0.3" 724 | source = "registry+https://github.com/rust-lang/crates.io-index" 725 | checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd" 726 | dependencies = [ 727 | "libc", 728 | "wasi", 729 | "windows-sys 0.52.0", 730 | ] 731 | 732 | [[package]] 733 | name = "multimap" 734 | version = "0.10.0" 735 | source = "registry+https://github.com/rust-lang/crates.io-index" 736 | checksum = "defc4c55412d89136f966bbb339008b474350e5e6e78d2714439c386b3137a03" 737 | 738 | [[package]] 739 | name = "num-traits" 740 | version = "0.2.19" 741 | source = "registry+https://github.com/rust-lang/crates.io-index" 742 | checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" 743 | dependencies = [ 744 | "autocfg", 745 | ] 746 | 747 | [[package]] 748 | name = "object" 749 | version = "0.36.7" 750 | source = "registry+https://github.com/rust-lang/crates.io-index" 751 | checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" 752 | dependencies = [ 753 | "memchr", 754 | ] 755 | 756 | [[package]] 757 | name = "once_cell" 758 | version = "1.20.2" 759 | source = "registry+https://github.com/rust-lang/crates.io-index" 760 | checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" 761 | 762 | [[package]] 763 | name = "percent-encoding" 764 | version = "2.3.1" 765 | source = "registry+https://github.com/rust-lang/crates.io-index" 766 | checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" 767 | 768 | [[package]] 769 | name = "petgraph" 770 | version = "0.6.5" 771 | source = "registry+https://github.com/rust-lang/crates.io-index" 772 | checksum = "b4c5cc86750666a3ed20bdaf5ca2a0344f9c67674cae0515bec2da16fbaa47db" 773 | dependencies = [ 774 | "fixedbitset", 775 | "indexmap", 776 | ] 777 | 778 | [[package]] 779 | name = "pin-project-lite" 780 | version = "0.2.16" 781 | source = "registry+https://github.com/rust-lang/crates.io-index" 782 | checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" 783 | 784 | [[package]] 785 | name = "pin-utils" 786 | version = "0.1.0" 787 | source = "registry+https://github.com/rust-lang/crates.io-index" 788 | checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" 789 | 790 | [[package]] 791 | name = "prettyplease" 792 | version = "0.2.33" 793 | source = "registry+https://github.com/rust-lang/crates.io-index" 794 | checksum = "9dee91521343f4c5c6a63edd65e54f31f5c92fe8978c40a4282f8372194c6a7d" 795 | dependencies = [ 796 | "proc-macro2", 797 | "syn", 798 | ] 799 | 800 | [[package]] 801 | name = "proc-macro2" 802 | version = "1.0.95" 803 | source = "registry+https://github.com/rust-lang/crates.io-index" 804 | checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" 805 | dependencies = [ 806 | "unicode-ident", 807 | ] 808 | 809 | [[package]] 810 | name = "prost" 811 | version = "0.13.5" 812 | source = "registry+https://github.com/rust-lang/crates.io-index" 813 | checksum = "2796faa41db3ec313a31f7624d9286acf277b52de526150b7e69f3debf891ee5" 814 | dependencies = [ 815 | "bytes", 816 | "prost-derive", 817 | ] 818 | 819 | [[package]] 820 | name = "prost-build" 821 | version = "0.13.5" 822 | source = "registry+https://github.com/rust-lang/crates.io-index" 823 | checksum = "be769465445e8c1474e9c5dac2018218498557af32d9ed057325ec9a41ae81bf" 824 | dependencies = [ 825 | "heck", 826 | "itertools", 827 | "log", 828 | "multimap", 829 | "once_cell", 830 | "petgraph", 831 | "prettyplease", 832 | "prost", 833 | "prost-types", 834 | "regex", 835 | "syn", 836 | "tempfile", 837 | ] 838 | 839 | [[package]] 840 | name = "prost-derive" 841 | version = "0.13.5" 842 | source = "registry+https://github.com/rust-lang/crates.io-index" 843 | checksum = "8a56d757972c98b346a9b766e3f02746cde6dd1cd1d1d563472929fdd74bec4d" 844 | dependencies = [ 845 | "anyhow", 846 | "itertools", 847 | "proc-macro2", 848 | "quote", 849 | "syn", 850 | ] 851 | 852 | [[package]] 853 | name = "prost-types" 854 | version = "0.13.5" 855 | source = "registry+https://github.com/rust-lang/crates.io-index" 856 | checksum = "52c2c1bf36ddb1a1c396b3601a3cec27c2462e45f07c386894ec3ccf5332bd16" 857 | dependencies = [ 858 | "prost", 859 | ] 860 | 861 | [[package]] 862 | name = "prost-wkt" 863 | version = "0.6.1" 864 | source = "registry+https://github.com/rust-lang/crates.io-index" 865 | checksum = "497e1e938f0c09ef9cabe1d49437b4016e03e8f82fbbe5d1c62a9b61b9decae1" 866 | dependencies = [ 867 | "chrono", 868 | "inventory", 869 | "prost", 870 | "serde", 871 | "serde_derive", 872 | "serde_json", 873 | "typetag", 874 | ] 875 | 876 | [[package]] 877 | name = "prost-wkt-build" 878 | version = "0.6.1" 879 | source = "registry+https://github.com/rust-lang/crates.io-index" 880 | checksum = "07b8bf115b70a7aa5af1fd5d6e9418492e9ccb6e4785e858c938e28d132a884b" 881 | dependencies = [ 882 | "heck", 883 | "prost", 884 | "prost-build", 885 | "prost-types", 886 | "quote", 887 | ] 888 | 889 | [[package]] 890 | name = "prost-wkt-types" 891 | version = "0.6.1" 892 | source = "registry+https://github.com/rust-lang/crates.io-index" 893 | checksum = "c8cdde6df0a98311c839392ca2f2f0bcecd545f86a62b4e3c6a49c336e970fe5" 894 | dependencies = [ 895 | "chrono", 896 | "prost", 897 | "prost-build", 898 | "prost-types", 899 | "prost-wkt", 900 | "prost-wkt-build", 901 | "regex", 902 | "serde", 903 | "serde_derive", 904 | "serde_json", 905 | ] 906 | 907 | [[package]] 908 | name = "quote" 909 | version = "1.0.40" 910 | source = "registry+https://github.com/rust-lang/crates.io-index" 911 | checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" 912 | dependencies = [ 913 | "proc-macro2", 914 | ] 915 | 916 | [[package]] 917 | name = "regex" 918 | version = "1.11.1" 919 | source = "registry+https://github.com/rust-lang/crates.io-index" 920 | checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" 921 | dependencies = [ 922 | "aho-corasick", 923 | "memchr", 924 | "regex-automata", 925 | "regex-syntax", 926 | ] 927 | 928 | [[package]] 929 | name = "regex-automata" 930 | version = "0.4.9" 931 | source = "registry+https://github.com/rust-lang/crates.io-index" 932 | checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" 933 | dependencies = [ 934 | "aho-corasick", 935 | "memchr", 936 | "regex-syntax", 937 | ] 938 | 939 | [[package]] 940 | name = "regex-syntax" 941 | version = "0.8.5" 942 | source = "registry+https://github.com/rust-lang/crates.io-index" 943 | checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" 944 | 945 | [[package]] 946 | name = "reqwest" 947 | version = "0.12.19" 948 | source = "registry+https://github.com/rust-lang/crates.io-index" 949 | checksum = "a2f8e5513d63f2e5b386eb5106dc67eaf3f84e95258e210489136b8b92ad6119" 950 | dependencies = [ 951 | "base64", 952 | "bytes", 953 | "futures-core", 954 | "http", 955 | "http-body", 956 | "http-body-util", 957 | "hyper", 958 | "hyper-util", 959 | "ipnet", 960 | "js-sys", 961 | "log", 962 | "mime", 963 | "once_cell", 964 | "percent-encoding", 965 | "pin-project-lite", 966 | "serde", 967 | "serde_json", 968 | "serde_urlencoded", 969 | "sync_wrapper", 970 | "tokio", 971 | "tower", 972 | "tower-http", 973 | "tower-service", 974 | "url", 975 | "wasm-bindgen", 976 | "wasm-bindgen-futures", 977 | "web-sys", 978 | ] 979 | 980 | [[package]] 981 | name = "rustc-demangle" 982 | version = "0.1.24" 983 | source = "registry+https://github.com/rust-lang/crates.io-index" 984 | checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" 985 | 986 | [[package]] 987 | name = "rustix" 988 | version = "0.38.42" 989 | source = "registry+https://github.com/rust-lang/crates.io-index" 990 | checksum = "f93dc38ecbab2eb790ff964bb77fa94faf256fd3e73285fd7ba0903b76bedb85" 991 | dependencies = [ 992 | "bitflags", 993 | "errno", 994 | "libc", 995 | "linux-raw-sys", 996 | "windows-sys 0.59.0", 997 | ] 998 | 999 | [[package]] 1000 | name = "rustversion" 1001 | version = "1.0.19" 1002 | source = "registry+https://github.com/rust-lang/crates.io-index" 1003 | checksum = "f7c45b9784283f1b2e7fb61b42047c2fd678ef0960d4f6f1eba131594cc369d4" 1004 | 1005 | [[package]] 1006 | name = "ryu" 1007 | version = "1.0.18" 1008 | source = "registry+https://github.com/rust-lang/crates.io-index" 1009 | checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" 1010 | 1011 | [[package]] 1012 | name = "serde" 1013 | version = "1.0.219" 1014 | source = "registry+https://github.com/rust-lang/crates.io-index" 1015 | checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" 1016 | dependencies = [ 1017 | "serde_derive", 1018 | ] 1019 | 1020 | [[package]] 1021 | name = "serde_derive" 1022 | version = "1.0.219" 1023 | source = "registry+https://github.com/rust-lang/crates.io-index" 1024 | checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" 1025 | dependencies = [ 1026 | "proc-macro2", 1027 | "quote", 1028 | "syn", 1029 | ] 1030 | 1031 | [[package]] 1032 | name = "serde_json" 1033 | version = "1.0.140" 1034 | source = "registry+https://github.com/rust-lang/crates.io-index" 1035 | checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373" 1036 | dependencies = [ 1037 | "itoa", 1038 | "memchr", 1039 | "ryu", 1040 | "serde", 1041 | ] 1042 | 1043 | [[package]] 1044 | name = "serde_path_to_error" 1045 | version = "0.1.16" 1046 | source = "registry+https://github.com/rust-lang/crates.io-index" 1047 | checksum = "af99884400da37c88f5e9146b7f1fd0fbcae8f6eec4e9da38b67d05486f814a6" 1048 | dependencies = [ 1049 | "itoa", 1050 | "serde", 1051 | ] 1052 | 1053 | [[package]] 1054 | name = "serde_urlencoded" 1055 | version = "0.7.1" 1056 | source = "registry+https://github.com/rust-lang/crates.io-index" 1057 | checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" 1058 | dependencies = [ 1059 | "form_urlencoded", 1060 | "itoa", 1061 | "ryu", 1062 | "serde", 1063 | ] 1064 | 1065 | [[package]] 1066 | name = "slab" 1067 | version = "0.4.9" 1068 | source = "registry+https://github.com/rust-lang/crates.io-index" 1069 | checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" 1070 | dependencies = [ 1071 | "autocfg", 1072 | ] 1073 | 1074 | [[package]] 1075 | name = "smallvec" 1076 | version = "1.13.2" 1077 | source = "registry+https://github.com/rust-lang/crates.io-index" 1078 | checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" 1079 | 1080 | [[package]] 1081 | name = "socket2" 1082 | version = "0.5.10" 1083 | source = "registry+https://github.com/rust-lang/crates.io-index" 1084 | checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678" 1085 | dependencies = [ 1086 | "libc", 1087 | "windows-sys 0.52.0", 1088 | ] 1089 | 1090 | [[package]] 1091 | name = "stable_deref_trait" 1092 | version = "1.2.0" 1093 | source = "registry+https://github.com/rust-lang/crates.io-index" 1094 | checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" 1095 | 1096 | [[package]] 1097 | name = "syn" 1098 | version = "2.0.101" 1099 | source = "registry+https://github.com/rust-lang/crates.io-index" 1100 | checksum = "8ce2b7fc941b3a24138a0a7cf8e858bfc6a992e7978a068a5c760deb0ed43caf" 1101 | dependencies = [ 1102 | "proc-macro2", 1103 | "quote", 1104 | "unicode-ident", 1105 | ] 1106 | 1107 | [[package]] 1108 | name = "sync_wrapper" 1109 | version = "1.0.2" 1110 | source = "registry+https://github.com/rust-lang/crates.io-index" 1111 | checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" 1112 | dependencies = [ 1113 | "futures-core", 1114 | ] 1115 | 1116 | [[package]] 1117 | name = "synstructure" 1118 | version = "0.13.1" 1119 | source = "registry+https://github.com/rust-lang/crates.io-index" 1120 | checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971" 1121 | dependencies = [ 1122 | "proc-macro2", 1123 | "quote", 1124 | "syn", 1125 | ] 1126 | 1127 | [[package]] 1128 | name = "tempfile" 1129 | version = "3.15.0" 1130 | source = "registry+https://github.com/rust-lang/crates.io-index" 1131 | checksum = "9a8a559c81686f576e8cd0290cd2a24a2a9ad80c98b3478856500fcbd7acd704" 1132 | dependencies = [ 1133 | "cfg-if", 1134 | "fastrand", 1135 | "getrandom", 1136 | "once_cell", 1137 | "rustix", 1138 | "windows-sys 0.59.0", 1139 | ] 1140 | 1141 | [[package]] 1142 | name = "thiserror" 1143 | version = "2.0.12" 1144 | source = "registry+https://github.com/rust-lang/crates.io-index" 1145 | checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708" 1146 | dependencies = [ 1147 | "thiserror-impl", 1148 | ] 1149 | 1150 | [[package]] 1151 | name = "thiserror-impl" 1152 | version = "2.0.12" 1153 | source = "registry+https://github.com/rust-lang/crates.io-index" 1154 | checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" 1155 | dependencies = [ 1156 | "proc-macro2", 1157 | "quote", 1158 | "syn", 1159 | ] 1160 | 1161 | [[package]] 1162 | name = "tinystr" 1163 | version = "0.7.6" 1164 | source = "registry+https://github.com/rust-lang/crates.io-index" 1165 | checksum = "9117f5d4db391c1cf6927e7bea3db74b9a1c1add8f7eda9ffd5364f40f57b82f" 1166 | dependencies = [ 1167 | "displaydoc", 1168 | "zerovec", 1169 | ] 1170 | 1171 | [[package]] 1172 | name = "tokio" 1173 | version = "1.45.1" 1174 | source = "registry+https://github.com/rust-lang/crates.io-index" 1175 | checksum = "75ef51a33ef1da925cea3e4eb122833cb377c61439ca401b770f54902b806779" 1176 | dependencies = [ 1177 | "backtrace", 1178 | "libc", 1179 | "mio", 1180 | "pin-project-lite", 1181 | "socket2", 1182 | "tokio-macros", 1183 | "windows-sys 0.52.0", 1184 | ] 1185 | 1186 | [[package]] 1187 | name = "tokio-macros" 1188 | version = "2.5.0" 1189 | source = "registry+https://github.com/rust-lang/crates.io-index" 1190 | checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" 1191 | dependencies = [ 1192 | "proc-macro2", 1193 | "quote", 1194 | "syn", 1195 | ] 1196 | 1197 | [[package]] 1198 | name = "tower" 1199 | version = "0.5.2" 1200 | source = "registry+https://github.com/rust-lang/crates.io-index" 1201 | checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" 1202 | dependencies = [ 1203 | "futures-core", 1204 | "futures-util", 1205 | "pin-project-lite", 1206 | "sync_wrapper", 1207 | "tokio", 1208 | "tower-layer", 1209 | "tower-service", 1210 | "tracing", 1211 | ] 1212 | 1213 | [[package]] 1214 | name = "tower-http" 1215 | version = "0.6.5" 1216 | source = "registry+https://github.com/rust-lang/crates.io-index" 1217 | checksum = "5cc2d9e086a412a451384326f521c8123a99a466b329941a9403696bff9b0da2" 1218 | dependencies = [ 1219 | "bitflags", 1220 | "bytes", 1221 | "futures-util", 1222 | "http", 1223 | "http-body", 1224 | "iri-string", 1225 | "pin-project-lite", 1226 | "tower", 1227 | "tower-layer", 1228 | "tower-service", 1229 | ] 1230 | 1231 | [[package]] 1232 | name = "tower-layer" 1233 | version = "0.3.3" 1234 | source = "registry+https://github.com/rust-lang/crates.io-index" 1235 | checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" 1236 | 1237 | [[package]] 1238 | name = "tower-service" 1239 | version = "0.3.3" 1240 | source = "registry+https://github.com/rust-lang/crates.io-index" 1241 | checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" 1242 | 1243 | [[package]] 1244 | name = "tracing" 1245 | version = "0.1.41" 1246 | source = "registry+https://github.com/rust-lang/crates.io-index" 1247 | checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" 1248 | dependencies = [ 1249 | "log", 1250 | "pin-project-lite", 1251 | "tracing-core", 1252 | ] 1253 | 1254 | [[package]] 1255 | name = "tracing-core" 1256 | version = "0.1.33" 1257 | source = "registry+https://github.com/rust-lang/crates.io-index" 1258 | checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c" 1259 | dependencies = [ 1260 | "once_cell", 1261 | ] 1262 | 1263 | [[package]] 1264 | name = "try-lock" 1265 | version = "0.2.5" 1266 | source = "registry+https://github.com/rust-lang/crates.io-index" 1267 | checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" 1268 | 1269 | [[package]] 1270 | name = "twirp" 1271 | version = "0.8.0" 1272 | dependencies = [ 1273 | "async-trait", 1274 | "axum", 1275 | "futures", 1276 | "http", 1277 | "http-body-util", 1278 | "hyper", 1279 | "prost", 1280 | "reqwest", 1281 | "serde", 1282 | "serde_json", 1283 | "thiserror", 1284 | "tokio", 1285 | "tower", 1286 | "url", 1287 | ] 1288 | 1289 | [[package]] 1290 | name = "twirp-build" 1291 | version = "0.8.0" 1292 | dependencies = [ 1293 | "prettyplease", 1294 | "proc-macro2", 1295 | "prost-build", 1296 | "quote", 1297 | "syn", 1298 | ] 1299 | 1300 | [[package]] 1301 | name = "typeid" 1302 | version = "1.0.2" 1303 | source = "registry+https://github.com/rust-lang/crates.io-index" 1304 | checksum = "0e13db2e0ccd5e14a544e8a246ba2312cd25223f616442d7f2cb0e3db614236e" 1305 | 1306 | [[package]] 1307 | name = "typetag" 1308 | version = "0.2.19" 1309 | source = "registry+https://github.com/rust-lang/crates.io-index" 1310 | checksum = "044fc3365ddd307c297fe0fe7b2e70588cdab4d0f62dc52055ca0d11b174cf0e" 1311 | dependencies = [ 1312 | "erased-serde", 1313 | "inventory", 1314 | "once_cell", 1315 | "serde", 1316 | "typetag-impl", 1317 | ] 1318 | 1319 | [[package]] 1320 | name = "typetag-impl" 1321 | version = "0.2.19" 1322 | source = "registry+https://github.com/rust-lang/crates.io-index" 1323 | checksum = "d9d30226ac9cbd2d1ff775f74e8febdab985dab14fb14aa2582c29a92d5555dc" 1324 | dependencies = [ 1325 | "proc-macro2", 1326 | "quote", 1327 | "syn", 1328 | ] 1329 | 1330 | [[package]] 1331 | name = "unicode-ident" 1332 | version = "1.0.14" 1333 | source = "registry+https://github.com/rust-lang/crates.io-index" 1334 | checksum = "adb9e6ca4f869e1180728b7950e35922a7fc6397f7b641499e8f3ef06e50dc83" 1335 | 1336 | [[package]] 1337 | name = "url" 1338 | version = "2.5.4" 1339 | source = "registry+https://github.com/rust-lang/crates.io-index" 1340 | checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60" 1341 | dependencies = [ 1342 | "form_urlencoded", 1343 | "idna", 1344 | "percent-encoding", 1345 | ] 1346 | 1347 | [[package]] 1348 | name = "utf16_iter" 1349 | version = "1.0.5" 1350 | source = "registry+https://github.com/rust-lang/crates.io-index" 1351 | checksum = "c8232dd3cdaed5356e0f716d285e4b40b932ac434100fe9b7e0e8e935b9e6246" 1352 | 1353 | [[package]] 1354 | name = "utf8_iter" 1355 | version = "1.0.4" 1356 | source = "registry+https://github.com/rust-lang/crates.io-index" 1357 | checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" 1358 | 1359 | [[package]] 1360 | name = "want" 1361 | version = "0.3.1" 1362 | source = "registry+https://github.com/rust-lang/crates.io-index" 1363 | checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" 1364 | dependencies = [ 1365 | "try-lock", 1366 | ] 1367 | 1368 | [[package]] 1369 | name = "wasi" 1370 | version = "0.11.0+wasi-snapshot-preview1" 1371 | source = "registry+https://github.com/rust-lang/crates.io-index" 1372 | checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" 1373 | 1374 | [[package]] 1375 | name = "wasm-bindgen" 1376 | version = "0.2.100" 1377 | source = "registry+https://github.com/rust-lang/crates.io-index" 1378 | checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" 1379 | dependencies = [ 1380 | "cfg-if", 1381 | "once_cell", 1382 | "rustversion", 1383 | "wasm-bindgen-macro", 1384 | ] 1385 | 1386 | [[package]] 1387 | name = "wasm-bindgen-backend" 1388 | version = "0.2.100" 1389 | source = "registry+https://github.com/rust-lang/crates.io-index" 1390 | checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" 1391 | dependencies = [ 1392 | "bumpalo", 1393 | "log", 1394 | "proc-macro2", 1395 | "quote", 1396 | "syn", 1397 | "wasm-bindgen-shared", 1398 | ] 1399 | 1400 | [[package]] 1401 | name = "wasm-bindgen-futures" 1402 | version = "0.4.50" 1403 | source = "registry+https://github.com/rust-lang/crates.io-index" 1404 | checksum = "555d470ec0bc3bb57890405e5d4322cc9ea83cebb085523ced7be4144dac1e61" 1405 | dependencies = [ 1406 | "cfg-if", 1407 | "js-sys", 1408 | "once_cell", 1409 | "wasm-bindgen", 1410 | "web-sys", 1411 | ] 1412 | 1413 | [[package]] 1414 | name = "wasm-bindgen-macro" 1415 | version = "0.2.100" 1416 | source = "registry+https://github.com/rust-lang/crates.io-index" 1417 | checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" 1418 | dependencies = [ 1419 | "quote", 1420 | "wasm-bindgen-macro-support", 1421 | ] 1422 | 1423 | [[package]] 1424 | name = "wasm-bindgen-macro-support" 1425 | version = "0.2.100" 1426 | source = "registry+https://github.com/rust-lang/crates.io-index" 1427 | checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" 1428 | dependencies = [ 1429 | "proc-macro2", 1430 | "quote", 1431 | "syn", 1432 | "wasm-bindgen-backend", 1433 | "wasm-bindgen-shared", 1434 | ] 1435 | 1436 | [[package]] 1437 | name = "wasm-bindgen-shared" 1438 | version = "0.2.100" 1439 | source = "registry+https://github.com/rust-lang/crates.io-index" 1440 | checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" 1441 | dependencies = [ 1442 | "unicode-ident", 1443 | ] 1444 | 1445 | [[package]] 1446 | name = "web-sys" 1447 | version = "0.3.77" 1448 | source = "registry+https://github.com/rust-lang/crates.io-index" 1449 | checksum = "33b6dd2ef9186f1f2072e409e99cd22a975331a6b3591b12c764e0e55c60d5d2" 1450 | dependencies = [ 1451 | "js-sys", 1452 | "wasm-bindgen", 1453 | ] 1454 | 1455 | [[package]] 1456 | name = "windows-sys" 1457 | version = "0.52.0" 1458 | source = "registry+https://github.com/rust-lang/crates.io-index" 1459 | checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" 1460 | dependencies = [ 1461 | "windows-targets", 1462 | ] 1463 | 1464 | [[package]] 1465 | name = "windows-sys" 1466 | version = "0.59.0" 1467 | source = "registry+https://github.com/rust-lang/crates.io-index" 1468 | checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" 1469 | dependencies = [ 1470 | "windows-targets", 1471 | ] 1472 | 1473 | [[package]] 1474 | name = "windows-targets" 1475 | version = "0.52.6" 1476 | source = "registry+https://github.com/rust-lang/crates.io-index" 1477 | checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" 1478 | dependencies = [ 1479 | "windows_aarch64_gnullvm", 1480 | "windows_aarch64_msvc", 1481 | "windows_i686_gnu", 1482 | "windows_i686_gnullvm", 1483 | "windows_i686_msvc", 1484 | "windows_x86_64_gnu", 1485 | "windows_x86_64_gnullvm", 1486 | "windows_x86_64_msvc", 1487 | ] 1488 | 1489 | [[package]] 1490 | name = "windows_aarch64_gnullvm" 1491 | version = "0.52.6" 1492 | source = "registry+https://github.com/rust-lang/crates.io-index" 1493 | checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" 1494 | 1495 | [[package]] 1496 | name = "windows_aarch64_msvc" 1497 | version = "0.52.6" 1498 | source = "registry+https://github.com/rust-lang/crates.io-index" 1499 | checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" 1500 | 1501 | [[package]] 1502 | name = "windows_i686_gnu" 1503 | version = "0.52.6" 1504 | source = "registry+https://github.com/rust-lang/crates.io-index" 1505 | checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" 1506 | 1507 | [[package]] 1508 | name = "windows_i686_gnullvm" 1509 | version = "0.52.6" 1510 | source = "registry+https://github.com/rust-lang/crates.io-index" 1511 | checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" 1512 | 1513 | [[package]] 1514 | name = "windows_i686_msvc" 1515 | version = "0.52.6" 1516 | source = "registry+https://github.com/rust-lang/crates.io-index" 1517 | checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" 1518 | 1519 | [[package]] 1520 | name = "windows_x86_64_gnu" 1521 | version = "0.52.6" 1522 | source = "registry+https://github.com/rust-lang/crates.io-index" 1523 | checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" 1524 | 1525 | [[package]] 1526 | name = "windows_x86_64_gnullvm" 1527 | version = "0.52.6" 1528 | source = "registry+https://github.com/rust-lang/crates.io-index" 1529 | checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" 1530 | 1531 | [[package]] 1532 | name = "windows_x86_64_msvc" 1533 | version = "0.52.6" 1534 | source = "registry+https://github.com/rust-lang/crates.io-index" 1535 | checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" 1536 | 1537 | [[package]] 1538 | name = "write16" 1539 | version = "1.0.0" 1540 | source = "registry+https://github.com/rust-lang/crates.io-index" 1541 | checksum = "d1890f4022759daae28ed4fe62859b1236caebfc61ede2f63ed4e695f3f6d936" 1542 | 1543 | [[package]] 1544 | name = "writeable" 1545 | version = "0.5.5" 1546 | source = "registry+https://github.com/rust-lang/crates.io-index" 1547 | checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" 1548 | 1549 | [[package]] 1550 | name = "yoke" 1551 | version = "0.7.5" 1552 | source = "registry+https://github.com/rust-lang/crates.io-index" 1553 | checksum = "120e6aef9aa629e3d4f52dc8cc43a015c7724194c97dfaf45180d2daf2b77f40" 1554 | dependencies = [ 1555 | "serde", 1556 | "stable_deref_trait", 1557 | "yoke-derive", 1558 | "zerofrom", 1559 | ] 1560 | 1561 | [[package]] 1562 | name = "yoke-derive" 1563 | version = "0.7.5" 1564 | source = "registry+https://github.com/rust-lang/crates.io-index" 1565 | checksum = "2380878cad4ac9aac1e2435f3eb4020e8374b5f13c296cb75b4620ff8e229154" 1566 | dependencies = [ 1567 | "proc-macro2", 1568 | "quote", 1569 | "syn", 1570 | "synstructure", 1571 | ] 1572 | 1573 | [[package]] 1574 | name = "zerofrom" 1575 | version = "0.1.5" 1576 | source = "registry+https://github.com/rust-lang/crates.io-index" 1577 | checksum = "cff3ee08c995dee1859d998dea82f7374f2826091dd9cd47def953cae446cd2e" 1578 | dependencies = [ 1579 | "zerofrom-derive", 1580 | ] 1581 | 1582 | [[package]] 1583 | name = "zerofrom-derive" 1584 | version = "0.1.5" 1585 | source = "registry+https://github.com/rust-lang/crates.io-index" 1586 | checksum = "595eed982f7d355beb85837f651fa22e90b3c044842dc7f2c2842c086f295808" 1587 | dependencies = [ 1588 | "proc-macro2", 1589 | "quote", 1590 | "syn", 1591 | "synstructure", 1592 | ] 1593 | 1594 | [[package]] 1595 | name = "zerovec" 1596 | version = "0.10.4" 1597 | source = "registry+https://github.com/rust-lang/crates.io-index" 1598 | checksum = "aa2b893d79df23bfb12d5461018d408ea19dfafe76c2c7ef6d4eba614f8ff079" 1599 | dependencies = [ 1600 | "yoke", 1601 | "zerofrom", 1602 | "zerovec-derive", 1603 | ] 1604 | 1605 | [[package]] 1606 | name = "zerovec-derive" 1607 | version = "0.10.3" 1608 | source = "registry+https://github.com/rust-lang/crates.io-index" 1609 | checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6" 1610 | dependencies = [ 1611 | "proc-macro2", 1612 | "quote", 1613 | "syn", 1614 | ] 1615 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | 3 | members = ["crates/*", "example"] 4 | resolver = "2" 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 GitHub, Inc. 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 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: all 2 | all: build lint test 3 | 4 | .PHONY: build 5 | build: 6 | cargo build --features test-support 7 | 8 | .PHONY: test 9 | test: 10 | cargo test --features test-support 11 | 12 | .PHONY: lint 13 | lint: 14 | cargo fmt --all -- --check 15 | cargo clippy --features test-support -- --no-deps --deny warnings -D clippy::unwrap_used 16 | cargo clippy --tests -- --no-deps --deny warnings -A clippy::unwrap_used 17 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # twirp-rs 2 | 3 | This repository contains the following crates published to crates.io. Please see their respective README files for more information. 4 | 5 | - [`twirp-build`](https://github.com/github/twirp-rs/tree/main/crates/twirp-build) - A crate for generating twirp client and server interfaces. This is probably what you are looking for. 6 | - [`twirp`](https://github.com/github/twirp-rs/tree/main/crates/twirp/) - A crate used by code that is generated by `twirp-build` 7 | -------------------------------------------------------------------------------- /SUPPORT.md: -------------------------------------------------------------------------------- 1 | # Support 2 | 3 | ## How to file issues and get help 4 | 5 | This project uses GitHub issues to track bugs and feature requests. Please search the existing issues before filing new issues to avoid duplicates. For new issues, file your bug or feature request as a new issue. 6 | 7 | For help or questions about using this project, please feel free to open an issue or start a discussion. 8 | 9 | `twirp-rs` is under active development and maintained by GitHub staff. We will do our best to respond to support, feature requests, and community questions in a timely manner. 10 | 11 | ## GitHub Support Policy 12 | 13 | Support for this project is limited to the resources listed above. 14 | -------------------------------------------------------------------------------- /crates/twirp-build/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "twirp-build" 3 | version = "0.8.0" 4 | edition = "2021" 5 | description = "Code generation for async-compatible Twirp RPC interfaces." 6 | readme = "README.md" 7 | keywords = ["twirp", "prost", "protocol-buffers"] 8 | categories = [ 9 | "development-tools::build-utils", 10 | "network-programming", 11 | "asynchronous", 12 | ] 13 | repository = "https://github.com/github/twirp-rs" 14 | license-file = "./LICENSE" 15 | 16 | [dependencies] 17 | prost-build = "0.13" 18 | prettyplease = { version = "0.2" } 19 | quote = "1.0" 20 | syn = "2.0" 21 | proc-macro2 = "1.0" 22 | -------------------------------------------------------------------------------- /crates/twirp-build/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 GitHub, Inc. 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 | -------------------------------------------------------------------------------- /crates/twirp-build/README.md: -------------------------------------------------------------------------------- 1 | # `twirp-build` 2 | 3 | [Twirp is an RPC protocol](https://twitchtv.github.io/twirp/docs/spec_v7.html) based on HTTP and Protocol Buffers (proto). The protocol uses HTTP URLs to specify the RPC endpoints, and sends/receives proto messages as HTTP request/response bodies. Services are defined in a [.proto file](https://developers.google.com/protocol-buffers/docs/proto3), allowing easy implementation of RPC services with auto-generated clients and servers in different languages. 4 | 5 | The [canonical implementation](https://github.com/twitchtv/twirp) is in Go, this is a Rust implementation of the protocol. Rust protocol buffer support is provided by the [`prost`](https://github.com/tokio-rs/prost) ecosystem. 6 | 7 | Unlike [`prost-twirp`](https://github.com/sourcefrog/prost-twirp), the generated traits for serving and accessing RPCs are implemented atop `async` functions. Because traits containing `async` functions [are not directly supported](https://smallcultfollowing.com/babysteps/blog/2019/10/26/async-fn-in-traits-are-hard/) in Rust versions prior to 1.75, this crate uses the [`async_trait`](https://github.com/dtolnay/async-trait) macro to encapsulate the scaffolding required to make them work. 8 | 9 | ## Usage 10 | 11 | See the [example](./example) for a complete example project. 12 | 13 | Define services and messages in a `.proto` file: 14 | 15 | ```proto 16 | // service.proto 17 | package service.haberdash.v1; 18 | 19 | service HaberdasherAPI { 20 | rpc MakeHat(MakeHatRequest) returns (MakeHatResponse); 21 | } 22 | message MakeHatRequest { } 23 | message MakeHatResponse { } 24 | ``` 25 | 26 | Add the `twirp-build` crate as a build dependency in your `Cargo.toml` (you'll need `prost-build` too): 27 | 28 | ```toml 29 | # Cargo.toml 30 | [build-dependencies] 31 | twirp-build = "0.7" 32 | prost-build = "0.13" 33 | ``` 34 | 35 | Add a `build.rs` file to your project to compile the protos and generate Rust code: 36 | 37 | ```rust 38 | fn main() { 39 | let proto_source_files = ["./service.proto"]; 40 | 41 | // Tell Cargo to rerun this build script if any of the proto files change 42 | for entry in &proto_source_files { 43 | println!("cargo:rerun-if-changed={}", entry); 44 | } 45 | 46 | prost_build::Config::new() 47 | .type_attribute(".", "#[derive(serde::Serialize, serde::Deserialize)]") // enable support for JSON encoding 48 | .service_generator(twirp_build::service_generator()) 49 | .compile_protos(&proto_source_files, &["./"]) 50 | .expect("error compiling protos"); 51 | } 52 | ``` 53 | 54 | This generates code that you can find in `target/build/your-project-*/out/example.service.rs`. In order to use this code, you'll need to implement the trait for the proto defined service and wire up the service handlers to a hyper web server. See [the example `main.rs`]( example/src/main.rs) for details. 55 | 56 | Include the generated code, create a router, register your service, and then serve those routes in the hyper server: 57 | 58 | ```rust 59 | mod haberdash { 60 | include!(concat!(env!("OUT_DIR"), "/service.haberdash.v1.rs")); 61 | } 62 | 63 | use axum::Router; 64 | use haberdash::{MakeHatRequest, MakeHatResponse}; 65 | 66 | #[tokio::main] 67 | pub async fn main() { 68 | let api_impl = Arc::new(HaberdasherApiServer {}); 69 | let twirp_routes = Router::new() 70 | .nest(haberdash::SERVICE_FQN, haberdash::router(api_impl)); 71 | let app = Router::new() 72 | .nest("/twirp", twirp_routes) 73 | .fallback(twirp::server::not_found_handler); 74 | 75 | let tcp_listener = tokio::net::TcpListener::bind("127.0.0.1:3000").await.unwrap(); 76 | if let Err(e) = axum::serve(tcp_listener, app).await { 77 | eprintln!("server error: {}", e); 78 | } 79 | } 80 | 81 | // Define the server and implement the trait. 82 | struct HaberdasherApiServer; 83 | 84 | #[async_trait] 85 | impl haberdash::HaberdasherApi for HaberdasherApiServer { 86 | type Error = TwirpErrorResponse; 87 | 88 | async fn make_hat(&self, ctx: twirp::Context, req: MakeHatRequest) -> Result { 89 | todo!() 90 | } 91 | } 92 | ``` 93 | 94 | This code creates an `axum::Router`, then hands it off to `axum::serve()` to handle networking. 95 | This use of `axum::serve` is optional. After building `app`, you can instead invoke it from any 96 | `hyper`-based server by importing `twirp::tower::Service` and doing `app.call(request).await`. 97 | 98 | ## Usage (client side) 99 | 100 | On the client side, you also get a generated twirp client (based on the rpc endpoints in your proto). Include the generated code, create a client, and start making rpc calls: 101 | 102 | ``` rust 103 | mod haberdash { 104 | include!(concat!(env!("OUT_DIR"), "/service.haberdash.v1.rs")); 105 | } 106 | 107 | use haberdash::{HaberdasherApiClient, MakeHatRequest, MakeHatResponse}; 108 | 109 | #[tokio::main] 110 | pub async fn main() { 111 | let client = Client::from_base_url(Url::parse("http://localhost:3000/twirp/")?)?; 112 | let resp = client.make_hat(MakeHatRequest { inches: 1 }).await; 113 | eprintln!("{:?}", resp); 114 | } 115 | ``` 116 | -------------------------------------------------------------------------------- /crates/twirp-build/src/lib.rs: -------------------------------------------------------------------------------- 1 | use quote::{format_ident, quote}; 2 | 3 | /// Generates twirp services for protobuf rpc service definitions. 4 | /// 5 | /// In your `build.rs`, using `prost_build`, you can wire in the twirp 6 | /// `ServiceGenerator` to produce a Rust server for your proto services. 7 | /// 8 | /// Add a call to `.service_generator(twirp_build::service_generator())` in 9 | /// main() of `build.rs`. 10 | pub fn service_generator() -> Box { 11 | Box::new(ServiceGenerator {}) 12 | } 13 | 14 | struct Service { 15 | /// The name of the server trait, as parsed into a Rust identifier. 16 | server_name: syn::Ident, 17 | 18 | /// The name of the client trait, as parsed into a Rust identifier. 19 | client_name: syn::Ident, 20 | 21 | /// The fully qualified protobuf name of this Service. 22 | fqn: String, 23 | 24 | /// The methods that make up this service. 25 | methods: Vec, 26 | } 27 | 28 | struct Method { 29 | /// The name of the method, as parsed into a Rust identifier. 30 | name: syn::Ident, 31 | 32 | /// The name of the method as it appears in the protobuf definition. 33 | proto_name: String, 34 | 35 | /// The input type of this method. 36 | input_type: syn::Type, 37 | 38 | /// The output type of this method. 39 | output_type: syn::Type, 40 | } 41 | 42 | impl Service { 43 | fn from_prost(s: prost_build::Service) -> Self { 44 | let fqn = format!("{}.{}", s.package, s.proto_name); 45 | let server_name = format_ident!("{}", &s.name); 46 | let client_name = format_ident!("{}Client", &s.name); 47 | let methods = s 48 | .methods 49 | .into_iter() 50 | .map(|m| Method::from_prost(&s.package, &s.proto_name, m)) 51 | .collect(); 52 | 53 | Self { 54 | server_name, 55 | client_name, 56 | fqn, 57 | methods, 58 | } 59 | } 60 | } 61 | 62 | impl Method { 63 | fn from_prost(pkg_name: &str, svc_name: &str, m: prost_build::Method) -> Self { 64 | let as_type = |s| -> syn::Type { 65 | let Ok(typ) = syn::parse_str::(s) else { 66 | panic!( 67 | "twirp-build failed generated invalid Rust while processing {pkg}.{svc}/{name}). this is a bug in twirp-build, please file a GitHub issue", 68 | pkg = pkg_name, 69 | svc = svc_name, 70 | name = m.proto_name, 71 | ); 72 | }; 73 | typ 74 | }; 75 | 76 | let input_type = as_type(&m.input_type); 77 | let output_type = as_type(&m.output_type); 78 | let name = format_ident!("{}", m.name); 79 | let message = m.proto_name; 80 | 81 | Self { 82 | name, 83 | proto_name: message, 84 | input_type, 85 | output_type, 86 | } 87 | } 88 | } 89 | 90 | pub struct ServiceGenerator; 91 | 92 | impl prost_build::ServiceGenerator for ServiceGenerator { 93 | fn generate(&mut self, service: prost_build::Service, buf: &mut String) { 94 | let service = Service::from_prost(service); 95 | 96 | // generate the twirp server 97 | let mut trait_methods = Vec::with_capacity(service.methods.len()); 98 | let mut proxy_methods = Vec::with_capacity(service.methods.len()); 99 | for m in &service.methods { 100 | let name = &m.name; 101 | let input_type = &m.input_type; 102 | let output_type = &m.output_type; 103 | 104 | trait_methods.push(quote! { 105 | async fn #name(&self, ctx: twirp::Context, req: #input_type) -> Result<#output_type, Self::Error>; 106 | }); 107 | 108 | proxy_methods.push(quote! { 109 | async fn #name(&self, ctx: twirp::Context, req: #input_type) -> Result<#output_type, Self::Error> { 110 | T::#name(&*self, ctx, req).await 111 | } 112 | }); 113 | } 114 | 115 | let server_name = &service.server_name; 116 | let server_trait = quote! { 117 | #[twirp::async_trait::async_trait] 118 | pub trait #server_name { 119 | type Error; 120 | 121 | #(#trait_methods)* 122 | } 123 | 124 | #[twirp::async_trait::async_trait] 125 | impl #server_name for std::sync::Arc 126 | where 127 | T: #server_name + Sync + Send 128 | { 129 | type Error = T::Error; 130 | 131 | #(#proxy_methods)* 132 | } 133 | }; 134 | 135 | // generate the router 136 | let mut route_calls = Vec::with_capacity(service.methods.len()); 137 | for m in &service.methods { 138 | let name = &m.name; 139 | let input_type = &m.input_type; 140 | let path = format!("/{uri}", uri = m.proto_name); 141 | 142 | route_calls.push(quote! { 143 | .route(#path, |api: T, ctx: twirp::Context, req: #input_type| async move { 144 | api.#name(ctx, req).await 145 | }) 146 | }); 147 | } 148 | let router = quote! { 149 | pub fn router(api: T) -> twirp::Router 150 | where 151 | T: #server_name + Clone + Send + Sync + 'static, 152 | ::Error: twirp::IntoTwirpResponse 153 | { 154 | twirp::details::TwirpRouterBuilder::new(api) 155 | #(#route_calls)* 156 | .build() 157 | } 158 | }; 159 | 160 | // 161 | // generate the twirp client 162 | // 163 | 164 | let client_name = service.client_name; 165 | let mut client_trait_methods = Vec::with_capacity(service.methods.len()); 166 | let mut client_methods = Vec::with_capacity(service.methods.len()); 167 | for m in &service.methods { 168 | let name = &m.name; 169 | let input_type = &m.input_type; 170 | let output_type = &m.output_type; 171 | let request_path = format!("{}/{}", service.fqn, m.proto_name); 172 | 173 | client_trait_methods.push(quote! { 174 | async fn #name(&self, req: #input_type) -> Result<#output_type, twirp::ClientError>; 175 | }); 176 | 177 | client_methods.push(quote! { 178 | async fn #name(&self, req: #input_type) -> Result<#output_type, twirp::ClientError> { 179 | self.request(#request_path, req).await 180 | } 181 | }) 182 | } 183 | let client_trait = quote! { 184 | #[twirp::async_trait::async_trait] 185 | pub trait #client_name: Send + Sync { 186 | #(#client_trait_methods)* 187 | } 188 | 189 | #[twirp::async_trait::async_trait] 190 | impl #client_name for twirp::client::Client { 191 | #(#client_methods)* 192 | } 193 | }; 194 | 195 | // generate the service and client as a single file. run it through 196 | // prettyplease before outputting it. 197 | let service_fqn_path = format!("/{}", service.fqn); 198 | let generated = quote! { 199 | pub use twirp; 200 | 201 | pub const SERVICE_FQN: &str = #service_fqn_path; 202 | 203 | #server_trait 204 | 205 | #router 206 | 207 | #client_trait 208 | }; 209 | 210 | let ast: syn::File = syn::parse2(generated) 211 | .expect("twirp-build generated invalid Rust. this is a bug in twirp-build, please file an issue"); 212 | let code = prettyplease::unparse(&ast); 213 | buf.push_str(&code); 214 | } 215 | } 216 | -------------------------------------------------------------------------------- /crates/twirp/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "twirp" 3 | version = "0.8.0" 4 | edition = "2021" 5 | description = "An async-compatible library for Twirp RPC in Rust." 6 | readme = "README.md" 7 | keywords = ["twirp", "prost", "protocol-buffers"] 8 | categories = [ 9 | "development-tools::build-utils", 10 | "network-programming", 11 | "asynchronous", 12 | ] 13 | repository = "https://github.com/github/twirp-rs" 14 | license-file = "./LICENSE" 15 | 16 | [features] 17 | test-support = [] 18 | 19 | [dependencies] 20 | async-trait = "0.1" 21 | axum = "0.8" 22 | futures = "0.3" 23 | http = "1.3" 24 | http-body-util = "0.1" 25 | hyper = { version = "1.6", default-features = false } 26 | prost = "0.13" 27 | reqwest = { version = "0.12", default-features = false } 28 | serde = { version = "1.0", features = ["derive"] } 29 | serde_json = "1.0" 30 | thiserror = "2.0" 31 | tokio = { version = "1.45", default-features = false } 32 | tower = { version = "0.5", default-features = false } 33 | url = { version = "2.5" } 34 | -------------------------------------------------------------------------------- /crates/twirp/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 GitHub, Inc. 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 | -------------------------------------------------------------------------------- /crates/twirp/README.md: -------------------------------------------------------------------------------- 1 | # `twirp` 2 | 3 | This crate is mainly used by the code generated by [`twirp-build`](https://github.com/github/twirp-rs/tree/main/crates/twirp-build/). Please see its readme for more details and usage information. 4 | -------------------------------------------------------------------------------- /crates/twirp/src/client.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | use std::vec; 3 | 4 | use async_trait::async_trait; 5 | use reqwest::header::{InvalidHeaderValue, CONTENT_TYPE}; 6 | use reqwest::StatusCode; 7 | use thiserror::Error; 8 | use url::Url; 9 | 10 | use crate::headers::{CONTENT_TYPE_JSON, CONTENT_TYPE_PROTOBUF}; 11 | use crate::{serialize_proto_message, GenericError, TwirpErrorResponse}; 12 | 13 | #[derive(Debug, Error)] 14 | #[non_exhaustive] 15 | pub enum ClientError { 16 | #[error(transparent)] 17 | InvalidHeader(#[from] InvalidHeaderValue), 18 | #[error("base_url must end in /, but got: {0}")] 19 | InvalidBaseUrl(Url), 20 | #[error(transparent)] 21 | InvalidUrl(#[from] url::ParseError), 22 | #[error( 23 | "http error, status code: {status}, msg:{msg} for path:{path} and content-type:{content_type}" 24 | )] 25 | HttpError { 26 | status: StatusCode, 27 | msg: String, 28 | path: String, 29 | content_type: String, 30 | }, 31 | #[error(transparent)] 32 | JsonDecodeError(#[from] serde_json::Error), 33 | #[error("malformed response: {0}")] 34 | MalformedResponse(String), 35 | #[error(transparent)] 36 | ProtoDecodeError(#[from] prost::DecodeError), 37 | #[error(transparent)] 38 | ReqwestError(#[from] reqwest::Error), 39 | #[error("twirp error: {0:?}")] 40 | TwirpError(TwirpErrorResponse), 41 | 42 | /// A generic error that can be used by custom middleware. 43 | #[error(transparent)] 44 | MiddlewareError(#[from] GenericError), 45 | } 46 | 47 | pub type Result = std::result::Result; 48 | 49 | pub struct ClientBuilder { 50 | base_url: Url, 51 | http_client: reqwest::Client, 52 | middleware: Vec>, 53 | } 54 | 55 | impl ClientBuilder { 56 | pub fn new(base_url: Url, http_client: reqwest::Client) -> Self { 57 | Self { 58 | base_url, 59 | middleware: vec![], 60 | http_client, 61 | } 62 | } 63 | 64 | /// Add middleware to the client that will be called on each request. 65 | /// Middlewares are invoked in the order they are added as part of the 66 | /// request cycle. 67 | pub fn with(self, middleware: M) -> Self 68 | where 69 | M: Middleware, 70 | { 71 | let mut mw = self.middleware; 72 | mw.push(Box::new(middleware)); 73 | Self { 74 | base_url: self.base_url, 75 | http_client: self.http_client, 76 | middleware: mw, 77 | } 78 | } 79 | 80 | pub fn build(self) -> Result { 81 | Client::new(self.base_url, self.http_client, self.middleware) 82 | } 83 | } 84 | 85 | /// `Client` is a Twirp HTTP client that uses `reqwest::Client` to make http 86 | /// requests. 87 | /// 88 | /// You do **not** have to wrap `Client` in an [`Rc`] or [`Arc`] to **reuse** it, 89 | /// because it already uses an [`Arc`] internally. 90 | #[derive(Clone)] 91 | pub struct Client { 92 | http_client: reqwest::Client, 93 | inner: Arc, 94 | host: Option, 95 | } 96 | 97 | struct ClientRef { 98 | base_url: Url, 99 | middlewares: Vec>, 100 | } 101 | 102 | impl std::fmt::Debug for Client { 103 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 104 | f.debug_struct("Client") 105 | .field("base_url", &self.inner.base_url) 106 | .field("client", &self.http_client) 107 | .field("middlewares", &self.inner.middlewares.len()) 108 | .finish() 109 | } 110 | } 111 | 112 | impl Client { 113 | /// Creates a `twirp::Client`. 114 | /// 115 | /// The underlying `reqwest::Client` holds a connection pool internally, so it is advised that 116 | /// you create one and **reuse** it. 117 | pub fn new( 118 | base_url: Url, 119 | http_client: reqwest::Client, 120 | middlewares: Vec>, 121 | ) -> Result { 122 | if base_url.path().ends_with('/') { 123 | Ok(Client { 124 | http_client, 125 | inner: Arc::new(ClientRef { 126 | base_url, 127 | middlewares, 128 | }), 129 | host: None, 130 | }) 131 | } else { 132 | Err(ClientError::InvalidBaseUrl(base_url)) 133 | } 134 | } 135 | 136 | /// Creates a `twirp::Client` with the default `reqwest::ClientBuilder`. 137 | /// 138 | /// The underlying `reqwest::Client` holds a connection pool internally, so it is advised that 139 | /// you create one and **reuse** it. 140 | pub fn from_base_url(base_url: Url) -> Result { 141 | Self::new(base_url, reqwest::Client::new(), vec![]) 142 | } 143 | 144 | pub fn base_url(&self) -> &Url { 145 | &self.inner.base_url 146 | } 147 | 148 | /// Creates a new `twirp::Client` with the same configuration as the current 149 | /// one, but with a different host in the base URL. 150 | pub fn with_host(&self, host: &str) -> Self { 151 | Self { 152 | http_client: self.http_client.clone(), 153 | inner: self.inner.clone(), 154 | host: Some(host.to_string()), 155 | } 156 | } 157 | 158 | /// Make an HTTP twirp request. 159 | pub async fn request(&self, path: &str, body: I) -> Result 160 | where 161 | I: prost::Message, 162 | O: prost::Message + Default, 163 | { 164 | let mut url = self.inner.base_url.join(path)?; 165 | if let Some(host) = &self.host { 166 | url.set_host(Some(host))? 167 | }; 168 | let path = url.path().to_string(); 169 | let req = self 170 | .http_client 171 | .post(url) 172 | .header(CONTENT_TYPE, CONTENT_TYPE_PROTOBUF) 173 | .body(serialize_proto_message(body)) 174 | .build()?; 175 | 176 | // Create and execute the middleware handlers 177 | let next = Next::new(&self.http_client, &self.inner.middlewares); 178 | let resp = next.run(req).await?; 179 | 180 | // These have to be extracted because reading the body consumes `Response`. 181 | let status = resp.status(); 182 | let content_type = resp.headers().get(CONTENT_TYPE).cloned(); 183 | 184 | // TODO: Include more info in the error cases: request path, content-type, etc. 185 | match (status, content_type) { 186 | (status, Some(ct)) if status.is_success() && ct.as_bytes() == CONTENT_TYPE_PROTOBUF => { 187 | O::decode(resp.bytes().await?).map_err(|e| e.into()) 188 | } 189 | (status, Some(ct)) 190 | if (status.is_client_error() || status.is_server_error()) 191 | && ct.as_bytes() == CONTENT_TYPE_JSON => 192 | { 193 | Err(ClientError::TwirpError(serde_json::from_slice( 194 | &resp.bytes().await?, 195 | )?)) 196 | } 197 | (status, ct) => Err(ClientError::HttpError { 198 | status, 199 | msg: "unknown error".to_string(), 200 | path, 201 | content_type: ct 202 | .map(|x| x.to_str().unwrap_or_default().to_string()) 203 | .unwrap_or_default(), 204 | }), 205 | } 206 | } 207 | } 208 | 209 | // This concept of reqwest middleware is taken pretty much directly from: 210 | // https://github.com/TrueLayer/reqwest-middleware, but simplified for the 211 | // specific needs of this twirp client. 212 | #[async_trait] 213 | pub trait Middleware: 'static + Send + Sync { 214 | async fn handle(&self, mut req: reqwest::Request, next: Next<'_>) -> Result; 215 | } 216 | 217 | #[async_trait] 218 | impl Middleware for F 219 | where 220 | F: Send 221 | + Sync 222 | + 'static 223 | + for<'a> Fn(reqwest::Request, Next<'a>) -> BoxFuture<'a, Result>, 224 | { 225 | async fn handle(&self, req: reqwest::Request, next: Next<'_>) -> Result { 226 | (self)(req, next).await 227 | } 228 | } 229 | 230 | #[derive(Clone)] 231 | pub struct Next<'a> { 232 | client: &'a reqwest::Client, 233 | middlewares: &'a [Box], 234 | } 235 | 236 | pub type BoxFuture<'a, T> = std::pin::Pin + Send + 'a>>; 237 | 238 | impl<'a> Next<'a> { 239 | pub(crate) fn new(client: &'a reqwest::Client, middlewares: &'a [Box]) -> Self { 240 | Next { 241 | client, 242 | middlewares, 243 | } 244 | } 245 | 246 | pub fn run(mut self, req: reqwest::Request) -> BoxFuture<'a, Result> { 247 | if let Some((current, rest)) = self.middlewares.split_first() { 248 | self.middlewares = rest; 249 | Box::pin(current.handle(req, self)) 250 | } else { 251 | Box::pin(async move { self.client.execute(req).await.map_err(ClientError::from) }) 252 | } 253 | } 254 | } 255 | 256 | #[cfg(test)] 257 | mod tests { 258 | use reqwest::{Request, Response}; 259 | 260 | use crate::test::*; 261 | 262 | use super::*; 263 | 264 | struct AssertRouting { 265 | expected_url: &'static str, 266 | } 267 | 268 | #[async_trait] 269 | impl Middleware for AssertRouting { 270 | async fn handle(&self, req: Request, next: Next<'_>) -> Result { 271 | assert_eq!(self.expected_url, &req.url().to_string()); 272 | next.run(req).await 273 | } 274 | } 275 | 276 | #[tokio::test] 277 | async fn test_base_url() { 278 | let url = Url::parse("http://localhost:3001/twirp/").unwrap(); 279 | assert!(Client::from_base_url(url).is_ok()); 280 | let url = Url::parse("http://localhost:3001/twirp").unwrap(); 281 | assert_eq!( 282 | Client::from_base_url(url).unwrap_err().to_string(), 283 | "base_url must end in /, but got: http://localhost:3001/twirp", 284 | ); 285 | } 286 | 287 | #[tokio::test] 288 | async fn test_routes() { 289 | let base_url = Url::parse("http://localhost:3001/twirp/").unwrap(); 290 | 291 | let client = ClientBuilder::new(base_url, reqwest::Client::new()) 292 | .with(AssertRouting { 293 | expected_url: "http://localhost:3001/twirp/test.TestAPI/Ping", 294 | }) 295 | .build() 296 | .unwrap(); 297 | assert!(client 298 | .ping(PingRequest { 299 | name: "hi".to_string(), 300 | }) 301 | .await 302 | .is_err()); // expected connection refused error. 303 | } 304 | 305 | #[tokio::test] 306 | async fn test_standard_client() { 307 | let h = run_test_server(3002).await; 308 | let base_url = Url::parse("http://localhost:3002/twirp/").unwrap(); 309 | let client = Client::from_base_url(base_url).unwrap(); 310 | let resp = client 311 | .ping(PingRequest { 312 | name: "hi".to_string(), 313 | }) 314 | .await 315 | .unwrap(); 316 | assert_eq!(&resp.name, "hi"); 317 | h.abort() 318 | } 319 | } 320 | -------------------------------------------------------------------------------- /crates/twirp/src/context.rs: -------------------------------------------------------------------------------- 1 | use std::sync::{Arc, Mutex}; 2 | 3 | use http::Extensions; 4 | 5 | /// Context allows passing information between twirp rpc handlers and http middleware by providing 6 | /// access to extensions on the `http::Request` and `http::Response`. 7 | /// 8 | /// An example use case is to extract a request id from an http header and use that id in subsequent 9 | /// handler code. 10 | #[derive(Default)] 11 | pub struct Context { 12 | extensions: Extensions, 13 | resp_extensions: Arc>, 14 | } 15 | 16 | impl Context { 17 | pub fn new(extensions: Extensions, resp_extensions: Arc>) -> Self { 18 | Self { 19 | extensions, 20 | resp_extensions, 21 | } 22 | } 23 | 24 | /// Get a request extension. 25 | pub fn get(&self) -> Option<&T> 26 | where 27 | T: Clone + Send + Sync + 'static, 28 | { 29 | self.extensions.get::() 30 | } 31 | 32 | /// Insert a response extension. 33 | pub fn insert(&self, val: T) -> Option 34 | where 35 | T: Clone + Send + Sync + 'static, 36 | { 37 | self.resp_extensions 38 | .lock() 39 | .expect("mutex poisoned") 40 | .insert(val) 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /crates/twirp/src/details.rs: -------------------------------------------------------------------------------- 1 | //! Undocumented features that are public for use in generated code (see `twirp-build`). 2 | 3 | use std::future::Future; 4 | 5 | use axum::extract::{Request, State}; 6 | use axum::Router; 7 | 8 | use crate::{server, Context, IntoTwirpResponse}; 9 | 10 | /// Builder object used by generated code to build a Twirp service. 11 | /// 12 | /// The type `S` is something like `Arc`, which can be cheaply cloned for each 13 | /// incoming request, providing access to the Rust value that actually implements the RPCs. 14 | pub struct TwirpRouterBuilder { 15 | service: S, 16 | router: Router, 17 | } 18 | 19 | impl TwirpRouterBuilder 20 | where 21 | S: Clone + Send + Sync + 'static, 22 | { 23 | pub fn new(service: S) -> Self { 24 | TwirpRouterBuilder { 25 | service, 26 | router: Router::new(), 27 | } 28 | } 29 | 30 | /// Add a handler for an `rpc` to the router. 31 | /// 32 | /// The generated code passes a closure that calls the method, like 33 | /// `|api: Arc, req: MakeHatRequest| async move { api.make_hat(req) }`. 34 | pub fn route(self, url: &str, f: F) -> Self 35 | where 36 | F: Fn(S, Context, Req) -> Fut + Clone + Sync + Send + 'static, 37 | Fut: Future> + Send, 38 | Req: prost::Message + Default + serde::de::DeserializeOwned, 39 | Res: prost::Message + serde::Serialize, 40 | Err: IntoTwirpResponse, 41 | { 42 | TwirpRouterBuilder { 43 | service: self.service, 44 | router: self.router.route( 45 | url, 46 | axum::routing::post(move |State(api): State, req: Request| async move { 47 | server::handle_request(api, req, f).await 48 | }), 49 | ), 50 | } 51 | } 52 | 53 | /// Finish building the axum router. 54 | pub fn build(self) -> axum::Router { 55 | self.router 56 | .fallback(crate::server::not_found_handler) 57 | .with_state(self.service) 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /crates/twirp/src/error.rs: -------------------------------------------------------------------------------- 1 | //! Implement [Twirp](https://twitchtv.github.io/twirp/) error responses 2 | 3 | use std::collections::HashMap; 4 | 5 | use axum::body::Body; 6 | use axum::response::IntoResponse; 7 | use http::header::{self, HeaderMap, HeaderValue}; 8 | use hyper::{Response, StatusCode}; 9 | use serde::{Deserialize, Serialize, Serializer}; 10 | 11 | /// Trait for user-defined error types that can be converted to Twirp responses. 12 | pub trait IntoTwirpResponse { 13 | /// Generate a Twirp response. The return type is the `http::Response` type, with a 14 | /// [`TwirpErrorResponse`] as the body. The simplest way to implement this is: 15 | /// 16 | /// ``` 17 | /// use axum::body::Body; 18 | /// use http::Response; 19 | /// use twirp::{TwirpErrorResponse, IntoTwirpResponse}; 20 | /// # struct MyError { message: String } 21 | /// 22 | /// impl IntoTwirpResponse for MyError { 23 | /// fn into_twirp_response(self) -> Response { 24 | /// // Use TwirpErrorResponse to generate a valid starting point 25 | /// let mut response = twirp::invalid_argument(&self.message) 26 | /// .into_twirp_response(); 27 | /// 28 | /// // Customize the response as desired. 29 | /// response.headers_mut().insert("X-Server-Pid", std::process::id().into()); 30 | /// response 31 | /// } 32 | /// } 33 | /// ``` 34 | /// 35 | /// The `Response` that `TwirpErrorResponse` generates can be used as a starting point, 36 | /// adding headers and extensions to it. 37 | fn into_twirp_response(self) -> Response; 38 | } 39 | 40 | /// Alias for a generic error 41 | pub type GenericError = Box; 42 | 43 | macro_rules! twirp_error_codes { 44 | ( 45 | $( 46 | $(#[$docs:meta])* 47 | ($konst:ident, $num:expr, $phrase:ident); 48 | )+ 49 | ) => { 50 | /// A Twirp error code as defined by . 51 | #[derive(Clone, Copy, Debug, PartialEq, Eq, Deserialize)] 52 | #[serde(field_identifier, rename_all = "snake_case")] 53 | #[non_exhaustive] 54 | pub enum TwirpErrorCode { 55 | $( 56 | $(#[$docs])* 57 | $konst, 58 | )+ 59 | } 60 | 61 | impl TwirpErrorCode { 62 | pub fn http_status_code(&self) -> StatusCode { 63 | match *self { 64 | $( 65 | TwirpErrorCode::$konst => $num, 66 | )+ 67 | } 68 | } 69 | 70 | pub fn twirp_code(&self) -> &'static str { 71 | match *self { 72 | $( 73 | TwirpErrorCode::$konst => stringify!($phrase), 74 | )+ 75 | } 76 | } 77 | } 78 | 79 | $( 80 | pub fn $phrase(msg: T) -> TwirpErrorResponse { 81 | TwirpErrorResponse { 82 | code: TwirpErrorCode::$konst, 83 | msg: msg.to_string(), 84 | meta: Default::default(), 85 | } 86 | } 87 | )+ 88 | } 89 | } 90 | 91 | // Define all twirp errors. 92 | twirp_error_codes! { 93 | /// The operation was cancelled. 94 | (Canceled, StatusCode::REQUEST_TIMEOUT, canceled); 95 | /// An unknown error occurred. For example, this can be used when handling 96 | /// errors raised by APIs that do not return any error information. 97 | (Unknown, StatusCode::INTERNAL_SERVER_ERROR, unknown); 98 | /// The client specified an invalid argument. This indicates arguments that 99 | /// are invalid regardless of the state of the system (i.e. a malformed file 100 | /// name, required argument, number out of range, etc.). 101 | (InvalidArgument, StatusCode::BAD_REQUEST, invalid_argument); 102 | /// The client sent a message which could not be decoded. This may mean that 103 | /// the message was encoded improperly or that the client and server have 104 | /// incompatible message definitions. 105 | (Malformed, StatusCode::BAD_REQUEST, malformed); 106 | /// Operation expired before completion. For operations that change the 107 | /// state of the system, this error may be returned even if the operation 108 | /// has completed successfully (timeout). 109 | (DeadlineExceeded, StatusCode::REQUEST_TIMEOUT, deadline_exceeded); 110 | /// Some requested entity was not found. 111 | (NotFound, StatusCode::NOT_FOUND, not_found); 112 | /// The requested URL path wasn't routable to a Twirp service and method. 113 | /// This is returned by generated server code and should not be returned by 114 | /// application code (use "not_found" or "unimplemented" instead). 115 | (BadRoute, StatusCode::NOT_FOUND, bad_route); 116 | /// An attempt to create an entity failed because one already exists. 117 | (AlreadyExists, StatusCode::CONFLICT, already_exists); 118 | // The caller does not have permission to execute the specified operation. 119 | // It must not be used if the caller cannot be identified (use 120 | // "unauthenticated" instead). 121 | (PermissionDenied, StatusCode::FORBIDDEN, permission_denied); 122 | // The request does not have valid authentication credentials for the 123 | // operation. 124 | (Unauthenticated, StatusCode::UNAUTHORIZED, unauthenticated); 125 | /// Some resource has been exhausted or rate-limited, perhaps a per-user 126 | /// quota, or perhaps the entire file system is out of space. 127 | (ResourceExhausted, StatusCode::TOO_MANY_REQUESTS, resource_exhausted); 128 | /// The operation was rejected because the system is not in a state required 129 | /// for the operation's execution. For example, doing an rmdir operation on 130 | /// a directory that is non-empty, or on a non-directory object, or when 131 | /// having conflicting read-modify-write on the same resource. 132 | (FailedPrecondition, StatusCode::PRECONDITION_FAILED, failed_precondition); 133 | /// The operation was aborted, typically due to a concurrency issue like 134 | /// sequencer check failures, transaction aborts, etc. 135 | (Aborted, StatusCode::CONFLICT, aborted); 136 | /// The operation was attempted past the valid range. For example, seeking 137 | /// or reading past end of a paginated collection. Unlike 138 | /// "invalid_argument", this error indicates a problem that may be fixed if 139 | /// the system state changes (i.e. adding more items to the collection). 140 | /// There is a fair bit of overlap between "failed_precondition" and 141 | /// "out_of_range". We recommend using "out_of_range" (the more specific 142 | /// error) when it applies so that callers who are iterating through a space 143 | /// can easily look for an "out_of_range" error to detect when they are 144 | /// done. 145 | (OutOfRange, StatusCode::BAD_REQUEST, out_of_range); 146 | /// The operation is not implemented or not supported/enabled in this 147 | /// service. 148 | (Unimplemented, StatusCode::NOT_IMPLEMENTED, unimplemented); 149 | /// When some invariants expected by the underlying system have been broken. 150 | /// In other words, something bad happened in the library or backend 151 | /// service. Twirp specific issues like wire and serialization problems are 152 | /// also reported as "internal" errors. 153 | (Internal, StatusCode::INTERNAL_SERVER_ERROR, internal); 154 | /// The service is currently unavailable. This is most likely a transient 155 | /// condition and may be corrected by retrying with a backoff. 156 | (Unavailable, StatusCode::SERVICE_UNAVAILABLE, unavailable); 157 | /// The operation resulted in unrecoverable data loss or corruption. 158 | (Dataloss, StatusCode::INTERNAL_SERVER_ERROR, dataloss); 159 | } 160 | 161 | impl Serialize for TwirpErrorCode { 162 | fn serialize(&self, serializer: S) -> Result 163 | where 164 | S: Serializer, 165 | { 166 | serializer.serialize_str(self.twirp_code()) 167 | } 168 | } 169 | 170 | // Twirp error responses are always JSON 171 | #[derive(Debug, Serialize, Deserialize, PartialEq, Eq)] 172 | pub struct TwirpErrorResponse { 173 | pub code: TwirpErrorCode, 174 | pub msg: String, 175 | #[serde(skip_serializing_if = "HashMap::is_empty")] 176 | #[serde(default)] 177 | pub meta: HashMap, 178 | } 179 | 180 | impl TwirpErrorResponse { 181 | pub fn insert_meta(&mut self, key: String, value: String) -> Option { 182 | self.meta.insert(key, value) 183 | } 184 | 185 | pub fn into_axum_body(self) -> Body { 186 | let json = 187 | serde_json::to_string(&self).expect("JSON serialization of an error should not fail"); 188 | Body::new(json) 189 | } 190 | } 191 | 192 | impl IntoTwirpResponse for TwirpErrorResponse { 193 | fn into_twirp_response(self) -> Response { 194 | let mut headers = HeaderMap::new(); 195 | headers.insert( 196 | header::CONTENT_TYPE, 197 | HeaderValue::from_static("application/json"), 198 | ); 199 | 200 | let code = self.code.http_status_code(); 201 | (code, headers).into_response().map(|_| self) 202 | } 203 | } 204 | 205 | impl IntoResponse for TwirpErrorResponse { 206 | fn into_response(self) -> Response { 207 | self.into_twirp_response().map(|err| err.into_axum_body()) 208 | } 209 | } 210 | 211 | #[cfg(test)] 212 | mod test { 213 | use crate::{TwirpErrorCode, TwirpErrorResponse}; 214 | 215 | #[test] 216 | fn twirp_status_mapping() { 217 | assert_code(TwirpErrorCode::Canceled, "canceled", 408); 218 | assert_code(TwirpErrorCode::Unknown, "unknown", 500); 219 | assert_code(TwirpErrorCode::InvalidArgument, "invalid_argument", 400); 220 | assert_code(TwirpErrorCode::Malformed, "malformed", 400); 221 | assert_code(TwirpErrorCode::Unauthenticated, "unauthenticated", 401); 222 | assert_code(TwirpErrorCode::PermissionDenied, "permission_denied", 403); 223 | assert_code(TwirpErrorCode::DeadlineExceeded, "deadline_exceeded", 408); 224 | assert_code(TwirpErrorCode::NotFound, "not_found", 404); 225 | assert_code(TwirpErrorCode::BadRoute, "bad_route", 404); 226 | assert_code(TwirpErrorCode::Unimplemented, "unimplemented", 501); 227 | assert_code(TwirpErrorCode::Internal, "internal", 500); 228 | assert_code(TwirpErrorCode::Unavailable, "unavailable", 503); 229 | } 230 | 231 | fn assert_code(code: TwirpErrorCode, msg: &str, http: u16) { 232 | assert_eq!( 233 | code.http_status_code(), 234 | http, 235 | "expected http status code {} but got {}", 236 | http, 237 | code.http_status_code() 238 | ); 239 | assert_eq!( 240 | code.twirp_code(), 241 | msg, 242 | "expected error message '{}' but got '{}'", 243 | msg, 244 | code.twirp_code() 245 | ); 246 | } 247 | 248 | #[test] 249 | fn twirp_error_response_serialization() { 250 | let response = TwirpErrorResponse { 251 | code: TwirpErrorCode::DeadlineExceeded, 252 | msg: "test".to_string(), 253 | meta: Default::default(), 254 | }; 255 | 256 | let result = serde_json::to_string(&response).unwrap(); 257 | assert!(result.contains(r#""code":"deadline_exceeded""#)); 258 | assert!(result.contains(r#""msg":"test""#)); 259 | 260 | let result = serde_json::from_str(&result).unwrap(); 261 | assert_eq!(response, result); 262 | } 263 | } 264 | -------------------------------------------------------------------------------- /crates/twirp/src/headers.rs: -------------------------------------------------------------------------------- 1 | pub(crate) const CONTENT_TYPE_PROTOBUF: &[u8] = b"application/protobuf"; 2 | pub(crate) const CONTENT_TYPE_JSON: &[u8] = b"application/json"; 3 | -------------------------------------------------------------------------------- /crates/twirp/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod client; 2 | pub mod context; 3 | pub mod error; 4 | pub mod headers; 5 | pub mod server; 6 | 7 | #[cfg(any(test, feature = "test-support"))] 8 | pub mod test; 9 | 10 | #[doc(hidden)] 11 | pub mod details; 12 | 13 | pub use client::{Client, ClientBuilder, ClientError, Middleware, Next, Result}; 14 | pub use context::Context; 15 | pub use error::*; // many constructors like `invalid_argument()` 16 | pub use http::Extensions; 17 | 18 | // Re-export this crate's dependencies that users are likely to code against. These can be used to 19 | // import the exact versions of these libraries `twirp` is built with -- useful if your project is 20 | // so sprawling that it builds multiple versions of some crates. 21 | pub use async_trait; 22 | pub use axum; 23 | pub use reqwest; 24 | pub use tower; 25 | pub use url; 26 | 27 | /// Re-export of `axum::Router`, the type that encapsulates a server-side implementation of a Twirp 28 | /// service. 29 | pub use axum::Router; 30 | 31 | pub(crate) fn serialize_proto_message(m: T) -> Vec 32 | where 33 | T: prost::Message, 34 | { 35 | let len = m.encoded_len(); 36 | let mut data = Vec::with_capacity(len); 37 | m.encode(&mut data) 38 | .expect("can only fail if buffer does not have capacity"); 39 | assert_eq!(data.len(), len); 40 | data 41 | } 42 | -------------------------------------------------------------------------------- /crates/twirp/src/server.rs: -------------------------------------------------------------------------------- 1 | //! Support for serving Twirp APIs. 2 | //! 3 | //! There is not much to see in the documentation here. This API is meant to be used with 4 | //! `twirp-build`. See for details and an example. 5 | 6 | use std::fmt::Debug; 7 | use std::sync::{Arc, Mutex}; 8 | 9 | use axum::body::Body; 10 | use axum::response::IntoResponse; 11 | use futures::Future; 12 | use http::Extensions; 13 | use http_body_util::BodyExt; 14 | use hyper::{header, Request, Response}; 15 | use serde::de::DeserializeOwned; 16 | use serde::Serialize; 17 | use tokio::time::{Duration, Instant}; 18 | 19 | use crate::headers::{CONTENT_TYPE_JSON, CONTENT_TYPE_PROTOBUF}; 20 | use crate::{error, serialize_proto_message, Context, GenericError, IntoTwirpResponse}; 21 | 22 | // TODO: Properly implement JsonPb (de)serialization as it is slightly different 23 | // than standard JSON. 24 | #[derive(Debug, Clone, Copy, Default)] 25 | enum BodyFormat { 26 | #[default] 27 | JsonPb, 28 | Pb, 29 | } 30 | 31 | impl BodyFormat { 32 | fn from_content_type(req: &Request) -> BodyFormat { 33 | match req 34 | .headers() 35 | .get(header::CONTENT_TYPE) 36 | .map(|x| x.as_bytes()) 37 | { 38 | Some(CONTENT_TYPE_PROTOBUF) => BodyFormat::Pb, 39 | _ => BodyFormat::JsonPb, 40 | } 41 | } 42 | } 43 | 44 | /// Entry point used in code generated by `twirp-build`. 45 | pub(crate) async fn handle_request( 46 | service: S, 47 | req: Request, 48 | f: F, 49 | ) -> Response 50 | where 51 | F: FnOnce(S, Context, Req) -> Fut + Clone + Sync + Send + 'static, 52 | Fut: Future> + Send, 53 | Req: prost::Message + Default + serde::de::DeserializeOwned, 54 | Resp: prost::Message + serde::Serialize, 55 | Err: IntoTwirpResponse, 56 | { 57 | let mut timings = req 58 | .extensions() 59 | .get::() 60 | .copied() 61 | .unwrap_or_else(|| Timings::new(Instant::now())); 62 | 63 | let (req, exts, resp_fmt) = match parse_request(req, &mut timings).await { 64 | Ok(pair) => pair, 65 | Err(err) => { 66 | // TODO: Capture original error in the response extensions. E.g.: 67 | // resp_exts 68 | // .lock() 69 | // .expect("mutex poisoned") 70 | // .insert(RequestError(err)); 71 | let mut twirp_err = error::malformed("bad request"); 72 | twirp_err.insert_meta("error".to_string(), err.to_string()); 73 | return twirp_err.into_response(); 74 | } 75 | }; 76 | 77 | let resp_exts = Arc::new(Mutex::new(Extensions::new())); 78 | let ctx = Context::new(exts, resp_exts.clone()); 79 | let res = f(service, ctx, req).await; 80 | timings.set_response_handled(); 81 | 82 | let mut resp = match write_response(res, resp_fmt) { 83 | Ok(resp) => resp, 84 | Err(err) => { 85 | // TODO: Capture original error in the response extensions. 86 | let mut twirp_err = error::unknown("error serializing response"); 87 | twirp_err.insert_meta("error".to_string(), err.to_string()); 88 | return twirp_err.into_response(); 89 | } 90 | }; 91 | timings.set_response_written(); 92 | 93 | resp.extensions_mut() 94 | .extend(resp_exts.lock().expect("mutex poisoned").clone()); 95 | resp.extensions_mut().insert(timings); 96 | resp 97 | } 98 | 99 | async fn parse_request( 100 | req: Request, 101 | timings: &mut Timings, 102 | ) -> Result<(T, Extensions, BodyFormat), GenericError> 103 | where 104 | T: prost::Message + Default + DeserializeOwned, 105 | { 106 | let format = BodyFormat::from_content_type(&req); 107 | let (parts, body) = req.into_parts(); 108 | let bytes = body.collect().await?.to_bytes(); 109 | timings.set_received(); 110 | let request = match format { 111 | BodyFormat::Pb => T::decode(&bytes[..])?, 112 | BodyFormat::JsonPb => serde_json::from_slice(&bytes)?, 113 | }; 114 | timings.set_parsed(); 115 | Ok((request, parts.extensions, format)) 116 | } 117 | 118 | fn write_response( 119 | response: Result, 120 | response_format: BodyFormat, 121 | ) -> Result, GenericError> 122 | where 123 | T: prost::Message + Serialize, 124 | Err: IntoTwirpResponse, 125 | { 126 | let res = match response { 127 | Ok(response) => match response_format { 128 | BodyFormat::Pb => Response::builder() 129 | .header(header::CONTENT_TYPE, CONTENT_TYPE_PROTOBUF) 130 | .body(Body::from(serialize_proto_message(response)))?, 131 | BodyFormat::JsonPb => { 132 | let data = serde_json::to_string(&response)?; 133 | Response::builder() 134 | .header(header::CONTENT_TYPE, CONTENT_TYPE_JSON) 135 | .body(Body::from(data))? 136 | } 137 | }, 138 | Err(err) => err.into_twirp_response().map(|err| err.into_axum_body()), 139 | }; 140 | Ok(res) 141 | } 142 | 143 | /// Axum handler function that returns 404 Not Found with a Twirp JSON payload. 144 | /// 145 | /// `axum::Router`'s default fallback handler returns a 404 Not Found with no body content. 146 | /// Use this fallback instead for full Twirp compliance. 147 | /// 148 | /// # Usage 149 | /// 150 | /// ``` 151 | /// use axum::Router; 152 | /// 153 | /// # fn build_app(twirp_routes: Router) -> Router { 154 | /// let app = Router::new() 155 | /// .nest("/twirp", twirp_routes) 156 | /// .fallback(twirp::server::not_found_handler); 157 | /// # app } 158 | /// ``` 159 | pub async fn not_found_handler() -> Response { 160 | error::bad_route("not found").into_response() 161 | } 162 | 163 | /// Contains timing information associated with a request. 164 | /// To access the timings in a given request, use the [extensions](Request::extensions) 165 | /// method and specialize to `Timings` appropriately. 166 | #[derive(Debug, Clone, Copy)] 167 | pub struct Timings { 168 | // When the request started. 169 | start: Instant, 170 | // When the request was received (headers and body). 171 | request_received: Option, 172 | // When the request body was parsed. 173 | request_parsed: Option, 174 | // When the response handler returned. 175 | response_handled: Option, 176 | // When the response was written. 177 | response_written: Option, 178 | } 179 | 180 | impl Timings { 181 | #[allow(clippy::new_without_default)] 182 | pub fn new(start: Instant) -> Self { 183 | Self { 184 | start, 185 | request_received: None, 186 | request_parsed: None, 187 | response_handled: None, 188 | response_written: None, 189 | } 190 | } 191 | 192 | fn set_received(&mut self) { 193 | self.request_received = Some(Instant::now()); 194 | } 195 | 196 | fn set_parsed(&mut self) { 197 | self.request_parsed = Some(Instant::now()); 198 | } 199 | 200 | fn set_response_handled(&mut self) { 201 | self.response_handled = Some(Instant::now()); 202 | } 203 | 204 | fn set_response_written(&mut self) { 205 | self.response_written = Some(Instant::now()); 206 | } 207 | 208 | pub fn received(&self) -> Option { 209 | self.request_received.map(|x| x - self.start) 210 | } 211 | 212 | pub fn parsed(&self) -> Option { 213 | match (self.request_parsed, self.request_received) { 214 | (Some(parsed), Some(received)) => Some(parsed - received), 215 | _ => None, 216 | } 217 | } 218 | 219 | pub fn response_handled(&self) -> Option { 220 | match (self.response_handled, self.request_parsed) { 221 | (Some(handled), Some(parsed)) => Some(handled - parsed), 222 | _ => None, 223 | } 224 | } 225 | 226 | pub fn response_written(&self) -> Option { 227 | match (self.response_written, self.response_handled) { 228 | (Some(written), Some(handled)) => Some(written - handled), 229 | (Some(written), None) => { 230 | if let Some(parsed) = self.request_parsed { 231 | Some(written - parsed) 232 | } else { 233 | self.request_received.map(|received| written - received) 234 | } 235 | } 236 | _ => None, 237 | } 238 | } 239 | 240 | /// The total duration since the request started. 241 | pub fn total_duration(&self) -> Duration { 242 | self.start.elapsed() 243 | } 244 | } 245 | 246 | #[cfg(test)] 247 | mod tests { 248 | 249 | use super::*; 250 | use crate::test::*; 251 | 252 | use axum::middleware::{self, Next}; 253 | use tower::Service; 254 | 255 | fn timings() -> Timings { 256 | Timings::new(Instant::now()) 257 | } 258 | 259 | #[tokio::test] 260 | async fn test_bad_route() { 261 | let mut router = test_api_router(); 262 | let req = Request::get("/nothing") 263 | .extension(timings()) 264 | .body(Body::empty()) 265 | .unwrap(); 266 | 267 | let resp = router.call(req).await.unwrap(); 268 | let data = read_err_body(resp.into_body()).await; 269 | assert_eq!(data, error::bad_route("not found")); 270 | } 271 | 272 | #[tokio::test] 273 | async fn test_ping_success() { 274 | let mut router = test_api_router(); 275 | let resp = router.call(gen_ping_request("hi")).await.unwrap(); 276 | assert!(resp.status().is_success(), "{:?}", resp); 277 | let data: PingResponse = read_json_body(resp.into_body()).await; 278 | assert_eq!(&data.name, "hi"); 279 | } 280 | 281 | #[tokio::test] 282 | async fn test_ping_invalid_request() { 283 | let mut router = test_api_router(); 284 | let req = Request::post("/twirp/test.TestAPI/Ping") 285 | .extension(timings()) 286 | .body(Body::empty()) // not a valid request 287 | .unwrap(); 288 | let resp = router.call(req).await.unwrap(); 289 | assert!(resp.status().is_client_error(), "{:?}", resp); 290 | let data = read_err_body(resp.into_body()).await; 291 | 292 | // TODO: I think malformed should return some info about what was wrong 293 | // with the request, but we don't want to leak server errors that have 294 | // other details. 295 | let mut expected = error::malformed("bad request"); 296 | expected.insert_meta( 297 | "error".to_string(), 298 | "EOF while parsing a value at line 1 column 0".to_string(), 299 | ); 300 | assert_eq!(data, expected); 301 | } 302 | 303 | #[tokio::test] 304 | async fn test_boom() { 305 | let mut router = test_api_router(); 306 | let req = serde_json::to_string(&PingRequest { 307 | name: "hi".to_string(), 308 | }) 309 | .unwrap(); 310 | let req = Request::post("/twirp/test.TestAPI/Boom") 311 | .extension(timings()) 312 | .body(Body::from(req)) 313 | .unwrap(); 314 | let resp = router.call(req).await.unwrap(); 315 | assert!(resp.status().is_server_error(), "{:?}", resp); 316 | let data = read_err_body(resp.into_body()).await; 317 | assert_eq!(data, error::internal("boom!")); 318 | } 319 | 320 | #[tokio::test] 321 | async fn test_middleware() { 322 | let mut router = test_api_router().layer(middleware::from_fn(request_id_middleware)); 323 | 324 | // no request-id header 325 | let resp = router.call(gen_ping_request("hi")).await.unwrap(); 326 | assert!(resp.status().is_success(), "{:?}", resp); 327 | let data: PingResponse = read_json_body(resp.into_body()).await; 328 | assert_eq!(&data.name, "hi"); 329 | 330 | // now pass a header with x-request-id 331 | let req = Request::post("/twirp/test.TestAPI/Ping") 332 | .header("x-request-id", "abcd") 333 | .body(Body::from( 334 | serde_json::to_string(&PingRequest { 335 | name: "hello".to_string(), 336 | }) 337 | .expect("will always be valid json"), 338 | )) 339 | .expect("always a valid twirp request"); 340 | let resp = router.call(req).await.unwrap(); 341 | assert!(resp.status().is_success(), "{:?}", resp); 342 | let data: PingResponse = read_json_body(resp.into_body()).await; 343 | assert_eq!(&data.name, "hello-abcd"); 344 | } 345 | 346 | async fn request_id_middleware( 347 | mut request: http::Request, 348 | next: Next, 349 | ) -> http::Response { 350 | let rid = request 351 | .headers() 352 | .get("x-request-id") 353 | .and_then(|v| v.to_str().ok()) 354 | .map(|x| RequestId(x.to_string())); 355 | if let Some(rid) = rid { 356 | request.extensions_mut().insert(rid); 357 | } 358 | 359 | next.run(request).await 360 | } 361 | } 362 | -------------------------------------------------------------------------------- /crates/twirp/src/test.rs: -------------------------------------------------------------------------------- 1 | //! Test helpers and mini twirp api server implementation. 2 | use std::sync::Arc; 3 | use std::time::Duration; 4 | 5 | use async_trait::async_trait; 6 | use axum::body::Body; 7 | use axum::Router; 8 | use http_body_util::BodyExt; 9 | use hyper::Request; 10 | use serde::de::DeserializeOwned; 11 | use tokio::task::JoinHandle; 12 | use tokio::time::Instant; 13 | 14 | use crate::details::TwirpRouterBuilder; 15 | use crate::server::Timings; 16 | use crate::{error, Client, Context, Result, TwirpErrorResponse}; 17 | 18 | pub async fn run_test_server(port: u16) -> JoinHandle> { 19 | let router = test_api_router(); 20 | let addr: std::net::SocketAddr = ([127, 0, 0, 1], port).into(); 21 | let tcp_listener = tokio::net::TcpListener::bind(addr) 22 | .await 23 | .expect("failed to bind to local port"); 24 | println!("Listening on {addr}"); 25 | let h = tokio::spawn(async move { axum::serve(tcp_listener, router).await }); 26 | tokio::time::sleep(Duration::from_millis(100)).await; 27 | h 28 | } 29 | 30 | pub fn test_api_router() -> Router { 31 | let api = Arc::new(TestApiServer {}); 32 | 33 | // NB: This part would be generated 34 | let test_router = TwirpRouterBuilder::new(api) 35 | .route( 36 | "/Ping", 37 | |api: Arc, ctx: Context, req: PingRequest| async move { 38 | api.ping(ctx, req).await 39 | }, 40 | ) 41 | .route( 42 | "/Boom", 43 | |api: Arc, ctx: Context, req: PingRequest| async move { 44 | api.boom(ctx, req).await 45 | }, 46 | ) 47 | .build(); 48 | 49 | axum::Router::new() 50 | .nest("/twirp/test.TestAPI", test_router) 51 | .fallback(crate::server::not_found_handler) 52 | } 53 | 54 | pub fn gen_ping_request(name: &str) -> Request { 55 | let req = serde_json::to_string(&PingRequest { 56 | name: name.to_string(), 57 | }) 58 | .expect("will always be valid json"); 59 | Request::post("/twirp/test.TestAPI/Ping") 60 | .extension(Timings::new(Instant::now())) 61 | .body(Body::from(req)) 62 | .expect("always a valid twirp request") 63 | } 64 | 65 | pub async fn read_string_body(body: Body) -> String { 66 | let data = Vec::::from(body.collect().await.expect("invalid body").to_bytes()); 67 | String::from_utf8(data).expect("non-utf8 body") 68 | } 69 | 70 | pub async fn read_json_body(body: Body) -> T 71 | where 72 | T: DeserializeOwned, 73 | { 74 | let data = Vec::::from(body.collect().await.expect("invalid body").to_bytes()); 75 | serde_json::from_slice(&data).expect("twirp response isn't valid JSON") 76 | } 77 | 78 | pub async fn read_err_body(body: Body) -> TwirpErrorResponse { 79 | read_json_body(body).await 80 | } 81 | 82 | // Hand written sample test server and client 83 | 84 | pub struct TestApiServer; 85 | 86 | #[async_trait] 87 | impl TestApi for TestApiServer { 88 | async fn ping( 89 | &self, 90 | ctx: Context, 91 | req: PingRequest, 92 | ) -> Result { 93 | if let Some(RequestId(rid)) = ctx.get::() { 94 | Ok(PingResponse { 95 | name: format!("{}-{}", req.name, rid), 96 | }) 97 | } else { 98 | Ok(PingResponse { name: req.name }) 99 | } 100 | } 101 | 102 | async fn boom( 103 | &self, 104 | _ctx: Context, 105 | _: PingRequest, 106 | ) -> Result { 107 | Err(error::internal("boom!")) 108 | } 109 | } 110 | 111 | #[derive(PartialEq, Eq, PartialOrd, Ord, Clone, Debug, Default)] 112 | pub struct RequestId(pub String); 113 | 114 | // Small test twirp services (this would usually be generated with twirp-build) 115 | #[async_trait] 116 | pub trait TestApiClient { 117 | async fn ping(&self, req: PingRequest) -> Result; 118 | async fn boom(&self, req: PingRequest) -> Result; 119 | } 120 | 121 | #[async_trait] 122 | impl TestApiClient for Client { 123 | async fn ping(&self, req: PingRequest) -> Result { 124 | self.request("test.TestAPI/Ping", req).await 125 | } 126 | 127 | async fn boom(&self, _req: PingRequest) -> Result { 128 | todo!() 129 | } 130 | } 131 | 132 | #[async_trait] 133 | pub trait TestApi { 134 | async fn ping( 135 | &self, 136 | ctx: Context, 137 | req: PingRequest, 138 | ) -> Result; 139 | async fn boom( 140 | &self, 141 | ctx: Context, 142 | req: PingRequest, 143 | ) -> Result; 144 | } 145 | 146 | #[derive(serde::Serialize, serde::Deserialize)] 147 | #[serde(default)] 148 | #[allow(clippy::derive_partial_eq_without_eq)] 149 | #[derive(Clone, PartialEq, ::prost::Message)] 150 | pub struct PingRequest { 151 | #[prost(string, tag = "2")] 152 | pub name: ::prost::alloc::string::String, 153 | } 154 | 155 | #[derive(serde::Serialize, serde::Deserialize)] 156 | #[serde(default)] 157 | #[allow(clippy::derive_partial_eq_without_eq)] 158 | #[derive(Clone, PartialEq, ::prost::Message)] 159 | pub struct PingResponse { 160 | #[prost(string, tag = "2")] 161 | pub name: ::prost::alloc::string::String, 162 | } 163 | -------------------------------------------------------------------------------- /example/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "example" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | twirp = { path = "../crates/twirp" } 8 | 9 | prost = "0.13" 10 | prost-wkt = "0.6" 11 | prost-wkt-types = "0.6" 12 | serde = { version = "1.0", features = ["derive"] } 13 | tokio = { version = "1.45", features = ["rt-multi-thread", "macros"] } 14 | 15 | [build-dependencies] 16 | twirp-build = { path = "../crates/twirp-build" } 17 | 18 | fs-err = "3.1" 19 | glob = "0.3.0" 20 | prost-build = "0.13" 21 | prost-wkt-build = "0.6" 22 | 23 | [[bin]] 24 | name = "client" 25 | path = "src/bin/client.rs" 26 | 27 | [[bin]] 28 | name = "simple-server" 29 | path = "src/bin/simple-server.rs" 30 | 31 | [[bin]] 32 | name = "advanced-server" 33 | path = "src/bin/advanced-server.rs" 34 | -------------------------------------------------------------------------------- /example/build.rs: -------------------------------------------------------------------------------- 1 | use std::env; 2 | use std::path::PathBuf; 3 | 4 | use prost_wkt_build::*; 5 | 6 | fn main() { 7 | let out = PathBuf::from(env::var("OUT_DIR").expect("failed to load OUT_DIR from environment")); 8 | let descriptor_file = out.join("descriptors.bin"); 9 | let mut prost_build = prost_build::Config::new(); 10 | 11 | let proto_source_files = protos(); 12 | for entry in &proto_source_files { 13 | println!("cargo:rerun-if-changed={}", entry.display()); 14 | } 15 | 16 | prost_build 17 | .service_generator(twirp_build::service_generator()) 18 | .type_attribute(".", "#[derive(serde::Serialize,serde::Deserialize)]") 19 | .extern_path(".google.protobuf.Timestamp", "::prost_wkt_types::Timestamp") 20 | .file_descriptor_set_path(&descriptor_file) 21 | .compile_protos(&proto_source_files, &["./proto"]) 22 | .expect("error compiling protos"); 23 | 24 | let descriptor_bytes = 25 | fs_err::read(descriptor_file).expect("failed to read proto file descriptor"); 26 | 27 | let descriptor = FileDescriptorSet::decode(&descriptor_bytes[..]) 28 | .expect("failed to decode proto file descriptor"); 29 | 30 | prost_wkt_build::add_serde(out, descriptor); 31 | } 32 | 33 | fn protos() -> Vec { 34 | glob::glob("./proto/**/*.proto") 35 | .expect("io error finding proto files") 36 | .flatten() 37 | .collect() 38 | } 39 | -------------------------------------------------------------------------------- /example/proto/haberdash/v1/haberdash_api.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | import "google/protobuf/timestamp.proto"; 4 | 5 | package service.haberdash.v1; 6 | option go_package = "haberdash.v1"; 7 | 8 | // A Haberdasher makes hats for clients. 9 | service HaberdasherAPI { 10 | // MakeHat produces a hat of mysterious, randomly-selected color! 11 | rpc MakeHat(MakeHatRequest) returns (MakeHatResponse); 12 | rpc GetStatus(GetStatusRequest) returns (GetStatusResponse); 13 | } 14 | 15 | // Size is passed when requesting a new hat to be made. It's always 16 | // measured in inches. 17 | message MakeHatRequest { 18 | int32 inches = 1; 19 | } 20 | 21 | // A Hat is a piece of headwear made by a Haberdasher. 22 | message MakeHatResponse { 23 | // The size of a hat should always be in inches. 24 | int32 size = 1; 25 | 26 | // The color of a hat will never be 'invisible', but other than 27 | // that, anything is fair game. 28 | string color = 2; 29 | 30 | // The name of a hat is it's type. Like, 'bowler', or something. 31 | string name = 3; 32 | 33 | // Demonstrate importing an external message. 34 | google.protobuf.Timestamp timestamp = 4; 35 | } 36 | 37 | message GetStatusRequest {} 38 | 39 | message GetStatusResponse { 40 | string status = 1; 41 | } 42 | -------------------------------------------------------------------------------- /example/src/bin/advanced-server.rs: -------------------------------------------------------------------------------- 1 | //! This example is like simple-server but uses middleware and a custom error type. 2 | 3 | use std::net::SocketAddr; 4 | use std::time::UNIX_EPOCH; 5 | 6 | use twirp::async_trait::async_trait; 7 | use twirp::axum::body::Body; 8 | use twirp::axum::http; 9 | use twirp::axum::middleware::{self, Next}; 10 | use twirp::axum::routing::get; 11 | use twirp::{invalid_argument, Context, IntoTwirpResponse, Router, TwirpErrorResponse}; 12 | 13 | pub mod service { 14 | pub mod haberdash { 15 | pub mod v1 { 16 | include!(concat!(env!("OUT_DIR"), "/service.haberdash.v1.rs")); 17 | } 18 | } 19 | } 20 | use service::haberdash::v1::{ 21 | self as haberdash, GetStatusRequest, GetStatusResponse, MakeHatRequest, MakeHatResponse, 22 | }; 23 | 24 | async fn ping() -> &'static str { 25 | "Pong\n" 26 | } 27 | 28 | #[tokio::main] 29 | pub async fn main() { 30 | let api_impl = HaberdasherApiServer {}; 31 | let middleware = twirp::tower::builder::ServiceBuilder::new() 32 | .layer(middleware::from_fn(request_id_middleware)); 33 | let twirp_routes = Router::new() 34 | .nest(haberdash::SERVICE_FQN, haberdash::router(api_impl)) 35 | .layer(middleware); 36 | let app = Router::new() 37 | .nest("/twirp", twirp_routes) 38 | .route("/_ping", get(ping)) 39 | .fallback(twirp::server::not_found_handler); 40 | 41 | let addr = SocketAddr::from(([127, 0, 0, 1], 3000)); 42 | let tcp_listener = tokio::net::TcpListener::bind(addr) 43 | .await 44 | .expect("failed to bind"); 45 | println!("Listening on {addr}"); 46 | if let Err(e) = twirp::axum::serve(tcp_listener, app).await { 47 | eprintln!("server error: {}", e); 48 | } 49 | } 50 | 51 | // Note: If your server type can't be Clone, consider wrapping it in `std::sync::Arc`. 52 | #[derive(Clone)] 53 | struct HaberdasherApiServer; 54 | 55 | #[derive(Debug, PartialEq)] 56 | enum HatError { 57 | InvalidSize, 58 | } 59 | 60 | impl IntoTwirpResponse for HatError { 61 | fn into_twirp_response(self) -> http::Response { 62 | match self { 63 | HatError::InvalidSize => invalid_argument("inches").into_twirp_response(), 64 | } 65 | } 66 | } 67 | 68 | #[async_trait] 69 | impl haberdash::HaberdasherApi for HaberdasherApiServer { 70 | type Error = HatError; 71 | 72 | async fn make_hat( 73 | &self, 74 | ctx: Context, 75 | req: MakeHatRequest, 76 | ) -> Result { 77 | if req.inches == 0 { 78 | return Err(HatError::InvalidSize); 79 | } 80 | 81 | if let Some(id) = ctx.get::() { 82 | println!("{id:?}"); 83 | }; 84 | 85 | println!("got {req:?}"); 86 | ctx.insert::(ResponseInfo(42)); 87 | let ts = std::time::SystemTime::now() 88 | .duration_since(UNIX_EPOCH) 89 | .unwrap_or_default(); 90 | Ok(MakeHatResponse { 91 | color: "black".to_string(), 92 | name: "top hat".to_string(), 93 | size: req.inches, 94 | timestamp: Some(prost_wkt_types::Timestamp { 95 | seconds: ts.as_secs() as i64, 96 | nanos: 0, 97 | }), 98 | }) 99 | } 100 | 101 | async fn get_status( 102 | &self, 103 | _ctx: Context, 104 | _req: GetStatusRequest, 105 | ) -> Result { 106 | Ok(GetStatusResponse { 107 | status: "making hats".to_string(), 108 | }) 109 | } 110 | } 111 | 112 | // Demonstrate sending back custom extensions from the handlers. 113 | #[derive(PartialEq, Eq, PartialOrd, Ord, Clone, Debug, Default)] 114 | struct ResponseInfo(u16); 115 | 116 | /// Demonstrate pulling the request id out of an http header and sharing it with the rpc handlers. 117 | #[derive(PartialEq, Eq, PartialOrd, Ord, Clone, Debug, Default)] 118 | struct RequestId(String); 119 | 120 | async fn request_id_middleware( 121 | mut request: http::Request, 122 | next: Next, 123 | ) -> http::Response { 124 | let rid = request 125 | .headers() 126 | .get("x-request-id") 127 | .and_then(|v| v.to_str().ok()) 128 | .map(|x| RequestId(x.to_string())); 129 | if let Some(rid) = rid { 130 | request.extensions_mut().insert(rid); 131 | } 132 | 133 | let mut res = next.run(request).await; 134 | 135 | let info = res 136 | .extensions() 137 | .get::() 138 | .expect("must include ResponseInfo") 139 | .0; 140 | res.headers_mut().insert("x-response-info", info.into()); 141 | 142 | res 143 | } 144 | 145 | #[cfg(test)] 146 | mod test { 147 | use service::haberdash::v1::HaberdasherApiClient; 148 | use twirp::client::Client; 149 | use twirp::url::Url; 150 | 151 | use crate::service::haberdash::v1::HaberdasherApi; 152 | 153 | use super::*; 154 | 155 | #[tokio::test] 156 | async fn success() { 157 | let api = HaberdasherApiServer {}; 158 | let ctx = twirp::Context::default(); 159 | let res = api.make_hat(ctx, MakeHatRequest { inches: 1 }).await; 160 | assert!(res.is_ok()); 161 | let res = res.unwrap(); 162 | assert_eq!(res.size, 1); 163 | } 164 | 165 | #[tokio::test] 166 | async fn invalid_request() { 167 | let api = HaberdasherApiServer {}; 168 | let ctx = twirp::Context::default(); 169 | let res = api.make_hat(ctx, MakeHatRequest { inches: 0 }).await; 170 | assert!(res.is_err()); 171 | let err = res.unwrap_err(); 172 | assert_eq!(err, HatError::InvalidSize); 173 | } 174 | 175 | /// A running network server task, bound to an arbitrary port on localhost, chosen by the OS 176 | struct NetServer { 177 | port: u16, 178 | server_task: tokio::task::JoinHandle<()>, 179 | shutdown_sender: tokio::sync::oneshot::Sender<()>, 180 | } 181 | 182 | impl NetServer { 183 | async fn start(api_impl: HaberdasherApiServer) -> Self { 184 | let twirp_routes = 185 | Router::new().nest(haberdash::SERVICE_FQN, haberdash::router(api_impl)); 186 | let app = Router::new() 187 | .nest("/twirp", twirp_routes) 188 | .route("/_ping", get(ping)) 189 | .fallback(twirp::server::not_found_handler); 190 | 191 | let tcp_listener = tokio::net::TcpListener::bind("localhost:0") 192 | .await 193 | .expect("failed to bind"); 194 | let addr = tcp_listener.local_addr().unwrap(); 195 | println!("Listening on {addr}"); 196 | let port = addr.port(); 197 | 198 | let (shutdown_sender, shutdown_receiver) = tokio::sync::oneshot::channel::<()>(); 199 | let server_task = tokio::spawn(async move { 200 | let shutdown_receiver = async move { 201 | shutdown_receiver.await.unwrap(); 202 | }; 203 | if let Err(e) = twirp::axum::serve(tcp_listener, app) 204 | .with_graceful_shutdown(shutdown_receiver) 205 | .await 206 | { 207 | eprintln!("server error: {}", e); 208 | } 209 | }); 210 | 211 | NetServer { 212 | port, 213 | server_task, 214 | shutdown_sender, 215 | } 216 | } 217 | 218 | async fn shutdown(self) { 219 | self.shutdown_sender.send(()).unwrap(); 220 | self.server_task.await.unwrap(); 221 | } 222 | } 223 | 224 | #[tokio::test] 225 | async fn test_net() { 226 | let api_impl = HaberdasherApiServer {}; 227 | let server = NetServer::start(api_impl).await; 228 | 229 | let url = Url::parse(&format!("http://localhost:{}/twirp/", server.port)).unwrap(); 230 | let client = Client::from_base_url(url).unwrap(); 231 | let resp = client.make_hat(MakeHatRequest { inches: 1 }).await; 232 | println!("{:?}", resp); 233 | assert_eq!(resp.unwrap().size, 1); 234 | 235 | server.shutdown().await; 236 | } 237 | } 238 | -------------------------------------------------------------------------------- /example/src/bin/client.rs: -------------------------------------------------------------------------------- 1 | use twirp::async_trait::async_trait; 2 | use twirp::client::{Client, ClientBuilder, Middleware, Next}; 3 | use twirp::reqwest::{Request, Response}; 4 | use twirp::url::Url; 5 | use twirp::GenericError; 6 | 7 | pub mod service { 8 | pub mod haberdash { 9 | pub mod v1 { 10 | include!(concat!(env!("OUT_DIR"), "/service.haberdash.v1.rs")); 11 | } 12 | } 13 | } 14 | 15 | use service::haberdash::v1::{ 16 | GetStatusRequest, GetStatusResponse, HaberdasherApiClient, MakeHatRequest, MakeHatResponse, 17 | }; 18 | 19 | #[tokio::main] 20 | pub async fn main() -> Result<(), GenericError> { 21 | // basic client 22 | use service::haberdash::v1::HaberdasherApiClient; 23 | let client = Client::from_base_url(Url::parse("http://localhost:3000/twirp/")?)?; 24 | let resp = client.make_hat(MakeHatRequest { inches: 1 }).await; 25 | eprintln!("{:?}", resp); 26 | 27 | // customize the client with middleware 28 | let client = ClientBuilder::new( 29 | Url::parse("http://xyz:3000/twirp/")?, 30 | twirp::reqwest::Client::default(), 31 | ) 32 | .with(RequestHeaders { hmac_key: None }) 33 | .with(PrintResponseHeaders {}) 34 | .build()?; 35 | let resp = client 36 | .with_host("localhost") 37 | .make_hat(MakeHatRequest { inches: 1 }) 38 | .await; 39 | eprintln!("{:?}", resp); 40 | 41 | Ok(()) 42 | } 43 | 44 | struct RequestHeaders { 45 | hmac_key: Option, 46 | } 47 | 48 | #[async_trait] 49 | impl Middleware for RequestHeaders { 50 | async fn handle(&self, mut req: Request, next: Next<'_>) -> twirp::client::Result { 51 | req.headers_mut().append("x-request-id", "XYZ".try_into()?); 52 | if let Some(_hmac_key) = &self.hmac_key { 53 | req.headers_mut() 54 | .append("Request-HMAC", "example:todo".try_into()?); 55 | } 56 | eprintln!("Set headers: {req:?}"); 57 | next.run(req).await 58 | } 59 | } 60 | 61 | struct PrintResponseHeaders; 62 | 63 | #[async_trait] 64 | impl Middleware for PrintResponseHeaders { 65 | async fn handle(&self, req: Request, next: Next<'_>) -> twirp::client::Result { 66 | let res = next.run(req).await?; 67 | eprintln!("Response headers: {res:?}"); 68 | Ok(res) 69 | } 70 | } 71 | 72 | #[allow(dead_code)] 73 | #[derive(Debug)] 74 | struct MockHaberdasherApiClient; 75 | 76 | #[async_trait] 77 | impl HaberdasherApiClient for MockHaberdasherApiClient { 78 | async fn make_hat( 79 | &self, 80 | _req: MakeHatRequest, 81 | ) -> Result { 82 | todo!() 83 | } 84 | 85 | async fn get_status( 86 | &self, 87 | _req: GetStatusRequest, 88 | ) -> Result { 89 | todo!() 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /example/src/bin/simple-server.rs: -------------------------------------------------------------------------------- 1 | use std::net::SocketAddr; 2 | use std::time::UNIX_EPOCH; 3 | 4 | use twirp::async_trait::async_trait; 5 | use twirp::axum::routing::get; 6 | use twirp::{invalid_argument, Context, Router, TwirpErrorResponse}; 7 | 8 | pub mod service { 9 | pub mod haberdash { 10 | pub mod v1 { 11 | include!(concat!(env!("OUT_DIR"), "/service.haberdash.v1.rs")); 12 | } 13 | } 14 | } 15 | use service::haberdash::v1::{ 16 | self as haberdash, GetStatusRequest, GetStatusResponse, MakeHatRequest, MakeHatResponse, 17 | }; 18 | 19 | async fn ping() -> &'static str { 20 | "Pong\n" 21 | } 22 | 23 | #[tokio::main] 24 | pub async fn main() { 25 | let api_impl = HaberdasherApiServer {}; 26 | let twirp_routes = Router::new().nest(haberdash::SERVICE_FQN, haberdash::router(api_impl)); 27 | let app = Router::new() 28 | .nest("/twirp", twirp_routes) 29 | .route("/_ping", get(ping)) 30 | .fallback(twirp::server::not_found_handler); 31 | 32 | let addr = SocketAddr::from(([127, 0, 0, 1], 3000)); 33 | let tcp_listener = tokio::net::TcpListener::bind(addr) 34 | .await 35 | .expect("failed to bind"); 36 | println!("Listening on {addr}"); 37 | if let Err(e) = twirp::axum::serve(tcp_listener, app).await { 38 | eprintln!("server error: {}", e); 39 | } 40 | } 41 | 42 | // Note: If your server type can't be Clone, consider wrapping it in `std::sync::Arc`. 43 | #[derive(Clone)] 44 | struct HaberdasherApiServer; 45 | 46 | #[async_trait] 47 | impl haberdash::HaberdasherApi for HaberdasherApiServer { 48 | type Error = TwirpErrorResponse; 49 | 50 | async fn make_hat( 51 | &self, 52 | ctx: Context, 53 | req: MakeHatRequest, 54 | ) -> Result { 55 | if req.inches == 0 { 56 | return Err(invalid_argument("inches")); 57 | } 58 | 59 | println!("got {req:?}"); 60 | ctx.insert::(ResponseInfo(42)); 61 | let ts = std::time::SystemTime::now() 62 | .duration_since(UNIX_EPOCH) 63 | .unwrap_or_default(); 64 | Ok(MakeHatResponse { 65 | color: "black".to_string(), 66 | name: "top hat".to_string(), 67 | size: req.inches, 68 | timestamp: Some(prost_wkt_types::Timestamp { 69 | seconds: ts.as_secs() as i64, 70 | nanos: 0, 71 | }), 72 | }) 73 | } 74 | 75 | async fn get_status( 76 | &self, 77 | _ctx: Context, 78 | _req: GetStatusRequest, 79 | ) -> Result { 80 | Ok(GetStatusResponse { 81 | status: "making hats".to_string(), 82 | }) 83 | } 84 | } 85 | 86 | // Demonstrate sending back custom extensions from the handlers. 87 | #[derive(PartialEq, Eq, PartialOrd, Ord, Clone, Debug, Default)] 88 | struct ResponseInfo(u16); 89 | 90 | #[cfg(test)] 91 | mod test { 92 | use service::haberdash::v1::HaberdasherApiClient; 93 | use twirp::client::Client; 94 | use twirp::url::Url; 95 | use twirp::TwirpErrorCode; 96 | 97 | use crate::service::haberdash::v1::HaberdasherApi; 98 | 99 | use super::*; 100 | 101 | #[tokio::test] 102 | async fn success() { 103 | let api = HaberdasherApiServer {}; 104 | let ctx = twirp::Context::default(); 105 | let res = api.make_hat(ctx, MakeHatRequest { inches: 1 }).await; 106 | assert!(res.is_ok()); 107 | let res = res.unwrap(); 108 | assert_eq!(res.size, 1); 109 | } 110 | 111 | #[tokio::test] 112 | async fn invalid_request() { 113 | let api = HaberdasherApiServer {}; 114 | let ctx = twirp::Context::default(); 115 | let res = api.make_hat(ctx, MakeHatRequest { inches: 0 }).await; 116 | assert!(res.is_err()); 117 | let err = res.unwrap_err(); 118 | assert_eq!(err.code, TwirpErrorCode::InvalidArgument); 119 | } 120 | 121 | /// A running network server task, bound to an arbitrary port on localhost, chosen by the OS 122 | struct NetServer { 123 | port: u16, 124 | server_task: tokio::task::JoinHandle<()>, 125 | shutdown_sender: tokio::sync::oneshot::Sender<()>, 126 | } 127 | 128 | impl NetServer { 129 | async fn start(api_impl: HaberdasherApiServer) -> Self { 130 | let twirp_routes = 131 | Router::new().nest(haberdash::SERVICE_FQN, haberdash::router(api_impl)); 132 | let app = Router::new() 133 | .nest("/twirp", twirp_routes) 134 | .route("/_ping", get(ping)) 135 | .fallback(twirp::server::not_found_handler); 136 | 137 | let tcp_listener = tokio::net::TcpListener::bind("localhost:0") 138 | .await 139 | .expect("failed to bind"); 140 | let addr = tcp_listener.local_addr().unwrap(); 141 | println!("Listening on {addr}"); 142 | let port = addr.port(); 143 | 144 | let (shutdown_sender, shutdown_receiver) = tokio::sync::oneshot::channel::<()>(); 145 | let server_task = tokio::spawn(async move { 146 | let shutdown_receiver = async move { 147 | shutdown_receiver.await.unwrap(); 148 | }; 149 | if let Err(e) = twirp::axum::serve(tcp_listener, app) 150 | .with_graceful_shutdown(shutdown_receiver) 151 | .await 152 | { 153 | eprintln!("server error: {}", e); 154 | } 155 | }); 156 | 157 | NetServer { 158 | port, 159 | server_task, 160 | shutdown_sender, 161 | } 162 | } 163 | 164 | async fn shutdown(self) { 165 | self.shutdown_sender.send(()).unwrap(); 166 | self.server_task.await.unwrap(); 167 | } 168 | } 169 | 170 | #[tokio::test] 171 | async fn test_net() { 172 | let api_impl = HaberdasherApiServer {}; 173 | let server = NetServer::start(api_impl).await; 174 | 175 | let url = Url::parse(&format!("http://localhost:{}/twirp/", server.port)).unwrap(); 176 | let client = Client::from_base_url(url).unwrap(); 177 | let resp = client.make_hat(MakeHatRequest { inches: 1 }).await; 178 | println!("{:?}", resp); 179 | assert_eq!(resp.unwrap().size, 1); 180 | 181 | server.shutdown().await; 182 | } 183 | } 184 | -------------------------------------------------------------------------------- /release-plz.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | release_always = false 3 | 4 | [[package]] 5 | name = "example" # Ignore the example crate 6 | release = false 7 | -------------------------------------------------------------------------------- /rust-toolchain.toml: -------------------------------------------------------------------------------- 1 | [toolchain] 2 | channel = "1.83.0" 3 | components = ["rustfmt"] 4 | profile = "default" 5 | 6 | # Details on supported fields: https://rust-lang.github.io/rustup/overrides.html#the-toolchain-file 7 | -------------------------------------------------------------------------------- /script/install-protoc: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env bash 2 | # Unconditionally install the exact version of protoc that we use, 3 | # overwriting whatever is installed in /usr/local/bin. 4 | # 5 | # We have a CI job that checks the generated code, looking for an exact match, 6 | # so it's important that everyone use the same version of protoc. 7 | 8 | # Unofficial bash strict mode 9 | set -euo pipefail 10 | IFS=$'\n\t' 11 | 12 | # Don't use sudo if we're root. Not every Docker image has sudo. 13 | SUDO=sudo 14 | if [[ $EUID == "0" ]]; then 15 | SUDO= 16 | fi 17 | 18 | if ! type -P unzip >/dev/null; then 19 | echo "Installing unzip..." 20 | # This should only happen on Linux. MacOS ships with unzip. 21 | sudo apt-get install -y unzip 22 | fi 23 | 24 | echo "Installing protoc..." 25 | 26 | # Download protoc 27 | protoc_version="3.17.0" 28 | protoc_os="osx-x86_64" 29 | if [[ $OSTYPE == linux* ]]; then 30 | protoc_os="linux-x86_64" 31 | fi 32 | mkdir _tools 33 | cd _tools 34 | protoc_zip="protoc-$protoc_version-$protoc_os.zip" 35 | curl -OL "https://github.com/protocolbuffers/protobuf/releases/download/v$protoc_version/$protoc_zip" 36 | 37 | # Install protoc to /usr/local 38 | prefix=/usr/local 39 | unzip -o $protoc_zip -d tmp 40 | $SUDO mkdir -p $prefix/bin 41 | $SUDO mv tmp/bin/protoc $prefix/bin/protoc 42 | $SUDO mkdir -p $prefix/include/google/protobuf 43 | $SUDO rm -rf $prefix/include/google/protobuf 44 | $SUDO mv tmp/include/google/protobuf $prefix/include/google/protobuf 45 | cd .. 46 | rm -rf _tools 47 | --------------------------------------------------------------------------------