├── README.md ├── .github ├── CODEOWNERS ├── dependabot.yml └── workflows │ ├── ci.yml │ ├── publish-release.yml │ └── create-release-pr.yml ├── Cargo.toml ├── .gitignore ├── release-plz.toml ├── crates ├── twirp │ ├── src │ │ ├── headers.rs │ │ ├── lib.rs │ │ ├── details.rs │ │ ├── test.rs │ │ ├── server.rs │ │ ├── client.rs │ │ └── error.rs │ ├── Cargo.toml │ ├── LICENSE │ ├── CHANGELOG.md │ └── README.md └── twirp-build │ ├── README.md │ ├── Cargo.toml │ ├── CHANGELOG.md │ ├── LICENSE │ └── src │ └── lib.rs ├── rust-toolchain.toml ├── example ├── proto │ ├── status │ │ └── v1 │ │ │ └── status_api.proto │ └── haberdash │ │ └── v1 │ │ └── haberdash_api.proto ├── Cargo.toml ├── build.rs └── src │ └── bin │ ├── client.rs │ ├── simple-server.rs │ └── advanced-server.rs ├── Makefile ├── SUPPORT.md ├── LICENSE ├── script └── install-protoc ├── CONTRIBUTING.md └── Cargo.lock /README.md: -------------------------------------------------------------------------------- 1 | crates/twirp/README.md -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @github/blackbird 2 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | 3 | members = ["crates/*", "example"] 4 | resolver = "2" 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /crates/*/target 3 | /example/*/target 4 | heaptrack.* 5 | .DS_Store 6 | -------------------------------------------------------------------------------- /release-plz.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | release_always = false 3 | 4 | [[package]] 5 | name = "example" # Ignore the example crate 6 | release = false 7 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /crates/twirp-build/README.md: -------------------------------------------------------------------------------- 1 | # `twirp-build` 2 | 3 | `twirp-build` does code generation of structs and traits that match your protobuf definitions that you can then use with the `twirp` crate. 4 | 5 | More information about this crate can be found in the [`twirp` crate documentation](https://github.com/github/twirp-rs/). 6 | -------------------------------------------------------------------------------- /example/proto/status/v1/status_api.proto: -------------------------------------------------------------------------------- 1 | 2 | syntax = "proto3"; 3 | 4 | import "google/protobuf/timestamp.proto"; 5 | 6 | package service.status.v1; 7 | option go_package = "status.v1"; 8 | 9 | service StatusAPI { 10 | rpc GetSystemStatus(GetSystemStatusRequest) returns (GetSystemStatusResponse); 11 | } 12 | 13 | message GetSystemStatusRequest {} 14 | 15 | message GetSystemStatusResponse { 16 | string status = 1; 17 | } 18 | -------------------------------------------------------------------------------- /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 -- --no-deps --deny warnings -D clippy::unwrap_used 16 | cargo clippy --features test-support -- --no-deps --deny warnings -D clippy::unwrap_used 17 | cargo clippy --tests -- --no-deps --deny warnings -A clippy::unwrap_used 18 | -------------------------------------------------------------------------------- /crates/twirp-build/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "twirp-build" 3 | version = "0.10.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.14" 18 | prettyplease = { version = "0.2" } 19 | quote = "1.0" 20 | syn = "2.0" 21 | proc-macro2 = "1.0" 22 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /.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@v6 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@v6 32 | - name: Install protoc 33 | run: script/install-protoc 34 | - name: Lint 35 | run: make lint 36 | -------------------------------------------------------------------------------- /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.14" 10 | prost-wkt = "0.7" 11 | prost-wkt-types = "0.7" 12 | serde = { version = "1.0", features = ["derive"] } 13 | thiserror = "2.0" 14 | tokio = { version = "1.48", features = ["rt-multi-thread", "macros"] } 15 | http = "1.4" 16 | http-body-util = "0.1" 17 | 18 | [build-dependencies] 19 | twirp-build = { path = "../crates/twirp-build" } 20 | 21 | fs-err = "3.2" 22 | glob = "0.3.3" 23 | prost-build = "0.14" 24 | prost-wkt-build = "0.7" 25 | 26 | [[bin]] 27 | name = "client" 28 | path = "src/bin/client.rs" 29 | 30 | [[bin]] 31 | name = "simple-server" 32 | path = "src/bin/simple-server.rs" 33 | 34 | [[bin]] 35 | name = "advanced-server" 36 | path = "src/bin/advanced-server.rs" 37 | -------------------------------------------------------------------------------- /crates/twirp-build/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 7 | 8 | ## [Unreleased] 9 | 10 | ## [0.10.0](https://github.com/github/twirp-rs/compare/twirp-build-v0.9.0...twirp-build-v0.9.1) - 2025-10-13 11 | 12 | ### Changed 13 | 14 | - Use `parse_quote!` to make build time development easier ([#249](https://github.com/github/twirp-rs/pull/249)) 15 | - update prost; prost-wkt[-build] doesn't have a release, yet ([#223](https://github.com/github/twirp-rs/pull/223)) 16 | 17 | ## [0.9.0](https://github.com/github/twirp-rs/compare/twirp-build-v0.8.0...twirp-build-v0.9.0) - 2025-07-31 18 | 19 | - See [the changelog for twirp](https://github.com/github/twirp-rs/blob/main/crates/twirp/CHANGELOG.md) for this release. 20 | -------------------------------------------------------------------------------- /crates/twirp/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "twirp" 3 | version = "0.10.1" 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 | anyhow = "1" 21 | async-trait = "0.1" 22 | axum = "0.8" 23 | futures = "0.3" 24 | http = "1.4" 25 | http-body-util = "0.1" 26 | hyper = { version = "1.8", default-features = false } 27 | prost = "0.14" 28 | reqwest = { version = "0.12", default-features = false } 29 | serde = { version = "1.0", features = ["derive"] } 30 | serde_json = "1.0" 31 | thiserror = "2.0" 32 | tokio = { version = "1.48", default-features = false } 33 | tower = { version = "0.5", default-features = false } 34 | url = { version = "2.5" } 35 | -------------------------------------------------------------------------------- /.github/workflows/publish-release.yml: -------------------------------------------------------------------------------- 1 | # This workflow only publishes releases for PR's created by create-release-pr.yml 2 | # 3 | # See https://github.com/github/twirp-rs/blob/main/CONTRIBUTING.md#releasing for more details. 4 | name: Release any unpublished twirp/twirp-build packages 5 | 6 | permissions: 7 | contents: write 8 | 9 | on: 10 | push: 11 | branches: 12 | - main 13 | 14 | jobs: 15 | # Release any unpublished packages 16 | release-plz-release: 17 | name: Release-plz release 18 | runs-on: ubuntu-latest 19 | permissions: 20 | contents: write 21 | steps: 22 | - name: Checkout repository 23 | uses: actions/checkout@v6 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 32 | env: 33 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 34 | CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} 35 | -------------------------------------------------------------------------------- /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/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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /.github/workflows/create-release-pr.yml: -------------------------------------------------------------------------------- 1 | # Launch this workflow with the "Run workflow" button in the Actions tab of the repository. 2 | # 3 | # See https://github.com/github/twirp-rs/blob/main/CONTRIBUTING.md#releasing for more details. 4 | name: Create release PR 5 | 6 | permissions: 7 | pull-requests: write 8 | contents: write 9 | 10 | on: workflow_dispatch 11 | 12 | jobs: 13 | # Create a PR with the new versions and changelog, preparing the next release. When merged to main, 14 | # the publish-release.yml workflow will automatically publish any Rust package versions. 15 | create-release-pr: 16 | name: Create release PR 17 | runs-on: ubuntu-latest 18 | permissions: 19 | contents: write 20 | pull-requests: write 21 | concurrency: # Don't run overlapping instances of this workflow 22 | group: release-plz-${{ github.ref }} 23 | cancel-in-progress: false 24 | steps: 25 | - name: Checkout repository 26 | uses: actions/checkout@v6 27 | with: 28 | fetch-depth: 0 29 | - name: Install Rust toolchain 30 | uses: dtolnay/rust-toolchain@stable 31 | - name: Run release-plz 32 | uses: release-plz/action@v0.5 33 | with: 34 | command: release-pr 35 | env: 36 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 37 | CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} 38 | -------------------------------------------------------------------------------- /crates/twirp/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![doc = include_str!("../README.md")] 2 | 3 | pub mod client; 4 | pub mod error; 5 | pub mod headers; 6 | pub mod server; 7 | 8 | #[cfg(any(test, feature = "test-support"))] 9 | pub mod test; 10 | 11 | #[doc(hidden)] 12 | pub mod details; 13 | 14 | pub use client::{Client, ClientBuilder, Middleware, Next}; 15 | pub use error::*; // many constructors like `invalid_argument()` 16 | pub use http::{Extensions, Request, Response}; 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 type Result = std::result::Result; 32 | 33 | pub(crate) fn serialize_proto_message(m: T) -> Vec 34 | where 35 | T: prost::Message, 36 | { 37 | let len = m.encoded_len(); 38 | let mut data = Vec::with_capacity(len); 39 | m.encode(&mut data) 40 | .expect("can only fail if buffer does not have capacity"); 41 | assert_eq!(data.len(), len); 42 | data 43 | } 44 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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="31.1" 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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | use http_body_util::BodyExt; 8 | 9 | use crate::{malformed, serialize_proto_message, server, Result, TwirpErrorResponse}; 10 | 11 | /// Builder object used by generated code to build a Twirp service. 12 | /// 13 | /// The type `S` is something like `Arc`, which can be cheaply cloned for each 14 | /// incoming request, providing access to the Rust value that actually implements the RPCs. 15 | pub struct TwirpRouterBuilder { 16 | service: S, 17 | fqn: &'static str, 18 | router: Router, 19 | } 20 | 21 | impl TwirpRouterBuilder 22 | where 23 | S: Clone + Send + Sync + 'static, 24 | { 25 | pub fn new(fqn: &'static str, service: S) -> Self { 26 | TwirpRouterBuilder { 27 | service, 28 | fqn, 29 | router: Router::new(), 30 | } 31 | } 32 | 33 | /// Add a handler for an `rpc` to the router. 34 | /// 35 | /// The generated code passes a closure that calls the method, like 36 | /// `|api: Arc, req: http::Request| async move { api.make_hat(req) }`. 37 | pub fn route(self, url: &str, f: F) -> Self 38 | where 39 | F: Fn(S, http::Request) -> Fut + Clone + Sync + Send + 'static, 40 | Fut: Future, TwirpErrorResponse>> + Send, 41 | Req: prost::Message + Default + serde::de::DeserializeOwned, 42 | Res: prost::Message + Default + serde::Serialize, 43 | { 44 | TwirpRouterBuilder { 45 | service: self.service, 46 | fqn: self.fqn, 47 | router: self.router.route( 48 | url, 49 | axum::routing::post(move |State(api): State, req: Request| async move { 50 | server::handle_request(api, req, f).await 51 | }), 52 | ), 53 | } 54 | } 55 | 56 | /// Finish building the axum router. 57 | pub fn build(self) -> axum::Router { 58 | Router::new().nest( 59 | self.fqn, 60 | self.router 61 | .fallback(crate::server::not_found_handler) 62 | .with_state(self.service), 63 | ) 64 | } 65 | } 66 | 67 | /// Decode a `reqwest::Request` into a `http::Request`. 68 | pub async fn decode_request(mut req: reqwest::Request) -> Result> 69 | where 70 | I: prost::Message + Default, 71 | { 72 | let url = req.url().clone(); 73 | let headers = req.headers().clone(); 74 | let body = std::mem::take(req.body_mut()) 75 | .ok_or_else(|| malformed("failed to read the request body"))? 76 | .collect() 77 | .await? 78 | .to_bytes(); 79 | let data = I::decode(body).map_err(|e| malformed(format!("failed to decode request: {e}")))?; 80 | let mut req = Request::builder().method("POST").uri(url.to_string()); 81 | req.headers_mut() 82 | .expect("failed to get headers") 83 | .extend(headers); 84 | let req = req 85 | .body(data) 86 | .map_err(|e| malformed(format!("failed to build the request: {e}")))?; 87 | Ok(req) 88 | } 89 | 90 | /// Encode a `http::Response` into a `reqwest::Response`. 91 | pub fn encode_response(resp: http::Response) -> Result 92 | where 93 | O: prost::Message + Default, 94 | { 95 | let mut resp = resp.map(serialize_proto_message); 96 | resp.headers_mut() 97 | .insert("Content-Type", "application/protobuf".try_into()?); 98 | Ok(reqwest::Response::from(resp)) 99 | } 100 | -------------------------------------------------------------------------------- /crates/twirp/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 7 | 8 | ## [Unreleased] 9 | 10 | ## [0.10.1](https://github.com/github/twirp-rs/compare/twirp-v0.10.0...twirp-v0.10.1) - 2025-10-14 11 | 12 | ### Added 13 | 14 | - Default headers work around ([#263](https://github.com/github/twirp-rs/pull/263)) 15 | 16 | ## [0.10.0](https://github.com/github/twirp-rs/compare/twirp-v0.9.1...twirp-v0.9.2) - 2025-10-13 17 | 18 | ### Changed 19 | 20 | - Move http client on twirp client build to it's own method. ([#261](https://github.com/github/twirp-rs/pull/261)) 21 | - update prost; prost-wkt[-build] doesn't have a release, yet ([#223](https://github.com/github/twirp-rs/pull/223)) 22 | 23 | ## [0.9.1](https://github.com/github/twirp-rs/compare/twirp-v0.9.0...twirp-v0.9.1) - 2025-08-14 24 | 25 | ### Fixed 26 | 27 | - Preserve HTTP version in twirp client response ([#235](https://github.com/github/twirp-rs/pull/235)) 28 | 29 | ### Other 30 | 31 | - Bump tokio from 1.46.1 to 1.47.1 ([#231](https://github.com/github/twirp-rs/pull/231)) 32 | 33 | ## [0.9.0](https://github.com/github/twirp-rs/compare/twirp-build-v0.8.0...twirp-build-v0.9.0) - 2025-07-31 34 | 35 | ### Breaking 36 | 37 | - Remove `SERVICE_FQN` to avoid upgrade confusion ([#222](https://github.com/github/twirp-rs/pull/222)) 38 | 39 | #### Breaking: Allow custom headers and extensions for twirp clients and servers; unify traits; unify error type ([#212](https://github.com/github/twirp-rs/pull/212)) 40 | 41 | - No more `Context`. The same capabilities now exist via http request and response [Extensions](https://docs.rs/http/latest/http/struct.Extensions.html) and [Headers](https://docs.rs/http/latest/http/header/struct.HeaderMap.html). 42 | - Clients and servers now share a single trait (the rpc interface). 43 | - It is possible to set custom headers on requests (client side) and it's possible for server handlers to read request headers and set custom response headers. 44 | - The same ☝🏻 is true for extensions to allow interactivity with middleware. 45 | - All the above is accomplished by using `http::request::Request` and `http::response::Response` where `In` and `Out` are the individual rpc message types. 46 | - We have unifyied and simplified the error types. There is now just `TwirpErrorResponse` which models the [twirp error response spec](https://twitchtv.github.io/twirp/docs/spec_v7.html#error-codes). 47 | 48 | 49 | #### Breaking: Generate service fqn ([#221](https://github.com/github/twirp-rs/pull/221)) 50 | 51 | Applications will need to remove any manual service nesting they are doing today. 52 | 53 | In 0.8.0, server consumers of this library have to know how to properly construct the fully qualified service path by using `nest` on an `axum` `Router` like so: 54 | 55 | ```rust 56 | let twirp_routes = Router::new() 57 | .nest(haberdash::SERVICE_FQN, haberdash::router(api_impl)); 58 | ``` 59 | 60 | This is unnecessary in 0.9.0 (the generated `router` function for each service does that for you). Instead, you would write: 61 | 62 | ``` rust 63 | let twirp_routes = haberdash::router(api_impl); 64 | ``` 65 | 66 | It is still canonical (but not required) to then nest with a `/twirp` prefix (the examples show this). 67 | 68 | ### Other 69 | 70 | - Allow mocking out requests ([#220](https://github.com/github/twirp-rs/pull/220)) 71 | - Swap twirp and twirp-build readmes. Replace the repo readme with a symlink to twirp's readme. ([#215](https://github.com/github/twirp-rs/pull/215)) 72 | - Update the content of the readme ([#216](https://github.com/github/twirp-rs/pull/216)) 73 | - Include the readme in rustdoc ([#225](https://github.com/github/twirp-rs/pull/225)) 74 | -------------------------------------------------------------------------------- /example/src/bin/client.rs: -------------------------------------------------------------------------------- 1 | use twirp::async_trait::async_trait; 2 | use twirp::client::{Client, ClientBuilder, Middleware, Next}; 3 | use twirp::url::Url; 4 | use twirp::{GenericError, Request}; 5 | 6 | pub mod service { 7 | pub mod haberdash { 8 | pub mod v1 { 9 | include!(concat!(env!("OUT_DIR"), "/service.haberdash.v1.rs")); 10 | } 11 | } 12 | pub mod status { 13 | pub mod v1 { 14 | include!(concat!(env!("OUT_DIR"), "/service.status.v1.rs")); 15 | } 16 | } 17 | } 18 | 19 | use service::haberdash::v1::{HaberdasherApi, MakeHatRequest}; 20 | 21 | /// You can run this end-to-end example by running both a server and a client and observing the requests/responses. 22 | /// 23 | /// 1. Run the server: 24 | /// ```sh 25 | /// cargo run --bin advanced-server # OR cargo run --bin simple-server 26 | /// ``` 27 | /// 28 | /// 2. In another shell, run the client: 29 | /// ```sh 30 | /// cargo run --bin client 31 | /// ``` 32 | #[tokio::main] 33 | pub async fn main() -> Result<(), GenericError> { 34 | // basic client 35 | let client = Client::from_base_url(Url::parse("http://localhost:3000/twirp/")?); 36 | let resp = client 37 | .make_hat(Request::new(MakeHatRequest { inches: 1 })) 38 | .await; 39 | eprintln!("{:?}", resp); 40 | 41 | // customize the client with middleware 42 | let client = ClientBuilder::new(Url::parse("http://xyz:3000/twirp/")?) 43 | .with_middleware(RequestHeaders { hmac_key: None }) 44 | .with_middleware(PrintResponseHeaders {}) 45 | .build(); 46 | let resp = client 47 | .with_host("localhost") 48 | .make_hat(Request::new(MakeHatRequest { inches: 1 })) 49 | .await; 50 | eprintln!("{:?}", resp); 51 | 52 | Ok(()) 53 | } 54 | 55 | struct RequestHeaders { 56 | hmac_key: Option, 57 | } 58 | 59 | #[async_trait] 60 | impl Middleware for RequestHeaders { 61 | async fn handle( 62 | &self, 63 | mut req: twirp::reqwest::Request, 64 | next: Next<'_>, 65 | ) -> twirp::Result { 66 | req.headers_mut().append("x-request-id", "XYZ".try_into()?); 67 | if let Some(_hmac_key) = &self.hmac_key { 68 | req.headers_mut() 69 | .append("Request-HMAC", "example:todo".try_into()?); 70 | } 71 | eprintln!("Set headers: {req:?}"); 72 | next.run(req).await 73 | } 74 | } 75 | 76 | struct PrintResponseHeaders; 77 | 78 | #[async_trait] 79 | impl Middleware for PrintResponseHeaders { 80 | async fn handle( 81 | &self, 82 | req: twirp::reqwest::Request, 83 | next: Next<'_>, 84 | ) -> twirp::Result { 85 | let res = next.run(req).await?; 86 | eprintln!("Response headers: {res:?}"); 87 | Ok(res) 88 | } 89 | } 90 | 91 | #[cfg(test)] 92 | mod tests { 93 | use crate::service::haberdash::v1::handler::HaberdasherApiHandler; 94 | use crate::service::haberdash::v1::{GetStatusRequest, GetStatusResponse, MakeHatResponse}; 95 | use crate::service::status::v1::handler::StatusApiHandler; 96 | use crate::service::status::v1::{GetSystemStatusRequest, GetSystemStatusResponse, StatusApi}; 97 | 98 | use super::*; 99 | 100 | #[tokio::test] 101 | async fn test_client_with_mocks() { 102 | let client = ClientBuilder::direct() 103 | .with_handler(HaberdasherApiHandler::new(Mock)) 104 | .with_handler(StatusApiHandler::new(Mock)) 105 | .build(); 106 | let resp = client 107 | .make_hat(Request::new(MakeHatRequest { inches: 1 })) 108 | .await; 109 | eprintln!("{:?}", resp); 110 | assert!(resp.is_ok()); 111 | assert_eq!(42, resp.unwrap().into_body().size); 112 | 113 | let resp = client 114 | .get_system_status(Request::new(GetSystemStatusRequest {})) 115 | .await; 116 | eprintln!("{:?}", resp); 117 | assert!(resp.is_ok()); 118 | assert_eq!("ok", resp.unwrap().into_body().status); 119 | } 120 | 121 | struct Mock; 122 | 123 | #[async_trait] 124 | impl HaberdasherApi for Mock { 125 | async fn make_hat( 126 | &self, 127 | req: Request, 128 | ) -> twirp::Result> { 129 | eprintln!("Mock make_hat called with: {:?}", req); 130 | Ok(twirp::Response::new(MakeHatResponse { 131 | size: 42, 132 | ..Default::default() 133 | })) 134 | } 135 | 136 | async fn get_status( 137 | &self, 138 | _req: Request, 139 | ) -> twirp::Result> { 140 | todo!() 141 | } 142 | } 143 | 144 | #[async_trait] 145 | impl StatusApi for Mock { 146 | async fn get_system_status( 147 | &self, 148 | req: Request, 149 | ) -> twirp::Result> { 150 | eprintln!("Mock get_system_status called with: {:?}", req); 151 | Ok(twirp::Response::new(GetSystemStatusResponse { 152 | status: "ok".into(), 153 | })) 154 | } 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /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, 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("/test.TestAPI", api) 35 | .route( 36 | "/Ping", 37 | |api: Arc, req: http::Request| async move { 38 | api.ping(req).await 39 | }, 40 | ) 41 | .route( 42 | "/Boom", 43 | |api: Arc, req: http::Request| async move { 44 | api.boom(req).await 45 | }, 46 | ) 47 | .build(); 48 | 49 | axum::Router::new() 50 | .nest("/twirp", 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(&self, req: http::Request) -> Result> { 89 | let request_id = req.extensions().get::().cloned(); 90 | let data = req.into_body(); 91 | if let Some(RequestId(rid)) = request_id { 92 | Ok(http::Response::new(PingResponse { 93 | name: format!("{}-{}", data.name, rid), 94 | })) 95 | } else { 96 | Ok(http::Response::new(PingResponse { name: data.name })) 97 | } 98 | } 99 | 100 | async fn boom(&self, _: http::Request) -> Result> { 101 | Err(error::internal("boom!")) 102 | } 103 | } 104 | 105 | #[derive(PartialEq, Eq, PartialOrd, Ord, Clone, Debug, Default)] 106 | pub struct RequestId(pub String); 107 | 108 | // Small test twirp services (this would usually be generated with twirp-build) 109 | #[async_trait] 110 | pub trait TestApiClient { 111 | async fn ping(&self, req: http::Request) -> Result>; 112 | async fn boom(&self, req: http::Request) -> Result>; 113 | } 114 | 115 | #[async_trait] 116 | impl TestApiClient for Client { 117 | async fn ping(&self, req: http::Request) -> Result> { 118 | self.request("test.TestAPI/Ping", req).await 119 | } 120 | 121 | async fn boom(&self, _req: http::Request) -> Result> { 122 | todo!() 123 | } 124 | } 125 | 126 | #[async_trait] 127 | pub trait TestApi { 128 | async fn ping(&self, req: http::Request) -> Result>; 129 | async fn boom(&self, req: http::Request) -> Result>; 130 | } 131 | 132 | #[derive(serde::Serialize, serde::Deserialize)] 133 | #[serde(default)] 134 | #[allow(clippy::derive_partial_eq_without_eq)] 135 | #[derive(Clone, PartialEq, ::prost::Message)] 136 | pub struct PingRequest { 137 | #[prost(string, tag = "2")] 138 | pub name: ::prost::alloc::string::String, 139 | } 140 | 141 | #[derive(serde::Serialize, serde::Deserialize)] 142 | #[serde(default)] 143 | #[allow(clippy::derive_partial_eq_without_eq)] 144 | #[derive(Clone, PartialEq, ::prost::Message)] 145 | pub struct PingResponse { 146 | #[prost(string, tag = "2")] 147 | pub name: ::prost::alloc::string::String, 148 | } 149 | -------------------------------------------------------------------------------- /crates/twirp/README.md: -------------------------------------------------------------------------------- 1 | # `twirp` 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, and 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](https://github.com/github/twirp-rs/tree/main/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 | 41 | ```rust ,ignore 42 | fn main() { 43 | let proto_source_files = ["./service.proto"]; 44 | 45 | // Tell Cargo to rerun this build script if any of the proto files change 46 | for entry in &proto_source_files { 47 | println!("cargo:rerun-if-changed={}", entry); 48 | } 49 | 50 | prost_build::Config::new() 51 | .type_attribute(".", "#[derive(serde::Serialize, serde::Deserialize)]") // enable support for JSON encoding 52 | .service_generator(twirp_build::service_generator()) 53 | .compile_protos(&proto_source_files, &["./"]) 54 | .expect("error compiling protos"); 55 | } 56 | ``` 57 | 58 | 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](https://github.com/github/twirp-rs/tree/main/example) for details. 59 | 60 | Include the generated code, create a router, register your service, and then serve those routes in the hyper server: 61 | 62 | 66 | ```rust ,ignore 67 | mod haberdash { 68 | include!(concat!(env!("OUT_DIR"), "/service.haberdash.v1.rs")); 69 | } 70 | 71 | use axum::Router; 72 | use haberdash::{MakeHatRequest, MakeHatResponse}; 73 | 74 | #[tokio::main] 75 | pub async fn main() { 76 | let api_impl = Arc::new(HaberdasherApiServer {}); 77 | let app = Router::new() 78 | .nest("/twirp", haberdash::router(api_impl)) 79 | .fallback(twirp::server::not_found_handler); 80 | 81 | let tcp_listener = tokio::net::TcpListener::bind("127.0.0.1:3000").await.unwrap(); 82 | if let Err(e) = axum::serve(tcp_listener, app).await { 83 | eprintln!("server error: {}", e); 84 | } 85 | } 86 | 87 | // Define the server and implement the trait. 88 | struct HaberdasherApiServer; 89 | 90 | #[async_trait] 91 | impl haberdash::HaberdasherApi for HaberdasherApiServer { 92 | async fn make_hat(&self, req: twirp::Request) -> twirp::Result> { 93 | todo!() 94 | } 95 | } 96 | ``` 97 | 98 | This code creates an `axum::Router`, then hands it off to `axum::serve()` to handle networking. This use of `axum::serve` is optional. After building `app`, you can instead invoke it from any `hyper`-based server by importing `twirp::tower::Service` and doing `app.call(request).await`. 99 | 100 | ## Usage (client side) 101 | 102 | 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: 103 | 104 | 108 | ```rust ,ignore 109 | mod haberdash { 110 | include!(concat!(env!("OUT_DIR"), "/service.haberdash.v1.rs")); 111 | } 112 | 113 | use haberdash::{HaberdasherApiClient, MakeHatRequest, MakeHatResponse}; 114 | 115 | #[tokio::main] 116 | pub async fn main() { 117 | let client = Client::from_base_url(Url::parse("http://localhost:3000/twirp/")?)?; 118 | let resp = client.make_hat(MakeHatRequest { inches: 1 }).await; 119 | eprintln!("{:?}", resp); 120 | } 121 | ``` 122 | ## Minimum supported Rust version 123 | 124 | The MSRV for this crate is the version defined in [`rust-toolchain.toml`](https://github.com/github/twirp-rs/blob/main/rust-toolchain.toml) 125 | 126 | ## Getting Help 127 | 128 | You are welcome to open an [issue](https://github.com/github/twirp-rs/issues/new) with your question. 129 | 130 | ## Contributing 131 | 132 | 🎈 Thanks for your help improving the project! We are so happy to have you! We have a [contributing guide](https://github.com/github/twirp-rs/blob/main/CONTRIBUTING.md) to help you get involved in the project. 133 | 134 | ## License 135 | 136 | This project is licensed under the [MIT license](https://github.com/github/twirp-rs/blob/main/LICENSE). 137 | -------------------------------------------------------------------------------- /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, Router}; 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 | /// You can run this end-to-end example by running both a server and a client and observing the requests/responses. 24 | /// 25 | /// 1. Run the server: 26 | /// ```sh 27 | /// cargo run --bin simple-server 28 | /// ``` 29 | /// 30 | /// 2. In another shell, run the client: 31 | /// ```sh 32 | /// cargo run --bin client 33 | /// ``` 34 | #[tokio::main] 35 | pub async fn main() { 36 | let api_impl = HaberdasherApiServer {}; 37 | let app = Router::new() 38 | .nest("/twirp", haberdash::router(api_impl)) 39 | .route("/_ping", get(ping)) 40 | .fallback(twirp::server::not_found_handler); 41 | 42 | let addr = SocketAddr::from(([127, 0, 0, 1], 3000)); 43 | let tcp_listener = tokio::net::TcpListener::bind(addr) 44 | .await 45 | .expect("failed to bind"); 46 | println!("Listening on {addr}"); 47 | if let Err(e) = twirp::axum::serve(tcp_listener, app).await { 48 | eprintln!("server error: {}", e); 49 | } 50 | } 51 | 52 | // Note: If your server type can't be Clone, consider wrapping it in `std::sync::Arc`. 53 | #[derive(Clone)] 54 | struct HaberdasherApiServer; 55 | 56 | #[async_trait] 57 | impl haberdash::HaberdasherApi for HaberdasherApiServer { 58 | async fn make_hat( 59 | &self, 60 | req: twirp::Request, 61 | ) -> twirp::Result> { 62 | let data = req.into_body(); 63 | if data.inches == 0 { 64 | return Err(invalid_argument("inches")); 65 | } 66 | 67 | println!("got {data:?}"); 68 | let ts = std::time::SystemTime::now() 69 | .duration_since(UNIX_EPOCH) 70 | .unwrap_or_default(); 71 | let mut resp = twirp::Response::new(MakeHatResponse { 72 | color: "black".to_string(), 73 | name: "top hat".to_string(), 74 | size: data.inches, 75 | timestamp: Some(prost_wkt_types::Timestamp { 76 | seconds: ts.as_secs() as i64, 77 | nanos: 0, 78 | }), 79 | }); 80 | // Demonstrate adding custom extensions to the response (this could be handled by middleware). 81 | resp.extensions_mut().insert(ResponseInfo(42)); 82 | Ok(resp) 83 | } 84 | 85 | async fn get_status( 86 | &self, 87 | _req: twirp::Request, 88 | ) -> twirp::Result> { 89 | Ok(twirp::Response::new(GetStatusResponse { 90 | status: "making hats".to_string(), 91 | })) 92 | } 93 | } 94 | 95 | // Demonstrate sending back custom extensions from the handlers. 96 | #[derive(PartialEq, Eq, PartialOrd, Ord, Clone, Debug, Default)] 97 | struct ResponseInfo(u16); 98 | 99 | #[cfg(test)] 100 | mod test { 101 | use twirp::client::Client; 102 | use twirp::url::Url; 103 | use twirp::TwirpErrorCode; 104 | 105 | use crate::service::haberdash::v1::HaberdasherApi; 106 | 107 | use super::*; 108 | 109 | #[tokio::test] 110 | async fn success() { 111 | let api = HaberdasherApiServer {}; 112 | let res = api 113 | .make_hat(twirp::Request::new(MakeHatRequest { inches: 1 })) 114 | .await; 115 | assert!(res.is_ok()); 116 | let res = res.unwrap().into_body(); 117 | assert_eq!(res.size, 1); 118 | } 119 | 120 | #[tokio::test] 121 | async fn invalid_request() { 122 | let api = HaberdasherApiServer {}; 123 | let res = api 124 | .make_hat(twirp::Request::new(MakeHatRequest { inches: 0 })) 125 | .await; 126 | assert!(res.is_err()); 127 | let err = res.unwrap_err(); 128 | assert_eq!(err.code, TwirpErrorCode::InvalidArgument); 129 | } 130 | 131 | /// A running network server task, bound to an arbitrary port on localhost, chosen by the OS 132 | struct NetServer { 133 | port: u16, 134 | server_task: tokio::task::JoinHandle<()>, 135 | shutdown_sender: tokio::sync::oneshot::Sender<()>, 136 | } 137 | 138 | impl NetServer { 139 | async fn start(api_impl: HaberdasherApiServer) -> Self { 140 | let app = Router::new() 141 | .nest("/twirp", haberdash::router(api_impl)) 142 | .route("/_ping", get(ping)) 143 | .fallback(twirp::server::not_found_handler); 144 | 145 | let tcp_listener = tokio::net::TcpListener::bind("localhost:0") 146 | .await 147 | .expect("failed to bind"); 148 | let addr = tcp_listener.local_addr().unwrap(); 149 | println!("Listening on {addr}"); 150 | let port = addr.port(); 151 | 152 | let (shutdown_sender, shutdown_receiver) = tokio::sync::oneshot::channel::<()>(); 153 | let server_task = tokio::spawn(async move { 154 | let shutdown_receiver = async move { 155 | shutdown_receiver.await.unwrap(); 156 | }; 157 | if let Err(e) = twirp::axum::serve(tcp_listener, app) 158 | .with_graceful_shutdown(shutdown_receiver) 159 | .await 160 | { 161 | eprintln!("server error: {}", e); 162 | } 163 | }); 164 | 165 | NetServer { 166 | port, 167 | server_task, 168 | shutdown_sender, 169 | } 170 | } 171 | 172 | async fn shutdown(self) { 173 | self.shutdown_sender.send(()).unwrap(); 174 | self.server_task.await.unwrap(); 175 | } 176 | } 177 | 178 | #[tokio::test] 179 | async fn test_net() { 180 | let api_impl = HaberdasherApiServer {}; 181 | let server = NetServer::start(api_impl).await; 182 | 183 | let url = Url::parse(&format!("http://localhost:{}/twirp/", server.port)).unwrap(); 184 | let client = Client::from_base_url(url); 185 | let resp = client 186 | .make_hat(twirp::Request::new(MakeHatRequest { inches: 1 })) 187 | .await; 188 | println!("{:?}", resp); 189 | let data = resp.unwrap().into_body(); 190 | assert_eq!(data.size, 1); 191 | 192 | server.shutdown().await; 193 | } 194 | } 195 | -------------------------------------------------------------------------------- /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, Router}; 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 | /// You can run this end-to-end example by running both a server and a client and observing the requests/responses. 29 | /// 30 | /// 1. Run the server: 31 | /// ```sh 32 | /// cargo run --bin advanced-server 33 | /// ``` 34 | /// 35 | /// 2. In another shell, run the client: 36 | /// ```sh 37 | /// cargo run --bin client 38 | /// ``` 39 | #[tokio::main] 40 | pub async fn main() { 41 | let api_impl = HaberdasherApiServer {}; 42 | let middleware = twirp::tower::builder::ServiceBuilder::new() 43 | .layer(middleware::from_fn(request_id_middleware)); 44 | let twirp_routes = haberdash::router(api_impl).layer(middleware); 45 | let app = Router::new() 46 | .nest("/twirp", twirp_routes) 47 | .route("/_ping", get(ping)) 48 | .fallback(twirp::server::not_found_handler); 49 | 50 | let addr = SocketAddr::from(([127, 0, 0, 1], 3000)); 51 | let tcp_listener = tokio::net::TcpListener::bind(addr) 52 | .await 53 | .expect("failed to bind"); 54 | println!("Listening on {addr}"); 55 | if let Err(e) = twirp::axum::serve(tcp_listener, app).await { 56 | eprintln!("server error: {}", e); 57 | } 58 | } 59 | 60 | // Note: If your server type can't be Clone, consider wrapping it in `std::sync::Arc`. 61 | #[derive(Clone)] 62 | struct HaberdasherApiServer; 63 | 64 | #[async_trait] 65 | impl haberdash::HaberdasherApi for HaberdasherApiServer { 66 | async fn make_hat( 67 | &self, 68 | req: http::Request, 69 | ) -> twirp::Result> { 70 | if let Some(rid) = req.extensions().get::() { 71 | println!("got request_id: {rid:?}"); 72 | } 73 | 74 | let data = req.into_body(); 75 | if data.inches == 0 { 76 | return Err(invalid_argument("inches must be greater than 0")); 77 | } 78 | 79 | println!("got {data:?}"); 80 | let ts = std::time::SystemTime::now() 81 | .duration_since(UNIX_EPOCH) 82 | .unwrap_or_default(); 83 | let mut resp = http::Response::new(MakeHatResponse { 84 | color: "black".to_string(), 85 | name: "top hat".to_string(), 86 | size: data.inches, 87 | timestamp: Some(prost_wkt_types::Timestamp { 88 | seconds: ts.as_secs() as i64, 89 | nanos: 0, 90 | }), 91 | }); 92 | // Demonstrate adding custom extensions to the response (this could be handled by middleware). 93 | resp.extensions_mut().insert(ResponseInfo(42)); 94 | Ok(resp) 95 | } 96 | 97 | async fn get_status( 98 | &self, 99 | _req: http::Request, 100 | ) -> twirp::Result> { 101 | Ok(http::Response::new(GetStatusResponse { 102 | status: "making hats".to_string(), 103 | })) 104 | } 105 | } 106 | 107 | // Demonstrate sending back custom extensions from the handlers. 108 | #[derive(PartialEq, Eq, PartialOrd, Ord, Clone, Debug, Default)] 109 | struct ResponseInfo(u16); 110 | 111 | /// Demonstrate pulling the request id out of an http header and sharing it with the rpc handlers. 112 | #[derive(PartialEq, Eq, PartialOrd, Ord, Clone, Debug, Default)] 113 | struct RequestId(String); 114 | 115 | async fn request_id_middleware( 116 | mut request: http::Request, 117 | next: Next, 118 | ) -> http::Response { 119 | let rid = request 120 | .headers() 121 | .get("x-request-id") 122 | .and_then(|v| v.to_str().ok()) 123 | .map(|x| RequestId(x.to_string())); 124 | if let Some(rid) = rid { 125 | request.extensions_mut().insert(rid); 126 | } 127 | 128 | let mut res = next.run(request).await; 129 | 130 | let info = res 131 | .extensions() 132 | .get::() 133 | .expect("must include ResponseInfo") 134 | .0; 135 | res.headers_mut().insert("x-response-info", info.into()); 136 | 137 | res 138 | } 139 | 140 | #[cfg(test)] 141 | mod test { 142 | use service::haberdash::v1::HaberdasherApi; 143 | use twirp::client::Client; 144 | use twirp::url::Url; 145 | 146 | use super::*; 147 | 148 | #[tokio::test] 149 | async fn success() { 150 | let api = HaberdasherApiServer {}; 151 | let res = api 152 | .make_hat(http::Request::new(MakeHatRequest { inches: 1 })) 153 | .await; 154 | assert!(res.is_ok()); 155 | let data = res.unwrap().into_body(); 156 | assert_eq!(data.size, 1); 157 | } 158 | 159 | #[tokio::test] 160 | async fn invalid_request() { 161 | let api = HaberdasherApiServer {}; 162 | let res = api 163 | .make_hat(http::Request::new(MakeHatRequest { inches: 0 })) 164 | .await; 165 | assert!(res.is_err()); 166 | let err = res.unwrap_err(); 167 | assert_eq!(err.msg, "inches must be greater than 0"); 168 | } 169 | 170 | /// A running network server task, bound to an arbitrary port on localhost, chosen by the OS 171 | struct NetServer { 172 | port: u16, 173 | server_task: tokio::task::JoinHandle<()>, 174 | shutdown_sender: tokio::sync::oneshot::Sender<()>, 175 | } 176 | 177 | impl NetServer { 178 | async fn start(api_impl: HaberdasherApiServer) -> Self { 179 | let app = Router::new() 180 | .nest("/twirp", haberdash::router(api_impl)) 181 | .route("/_ping", get(ping)) 182 | .fallback(twirp::server::not_found_handler); 183 | 184 | let tcp_listener = tokio::net::TcpListener::bind("localhost:0") 185 | .await 186 | .expect("failed to bind"); 187 | let addr = tcp_listener.local_addr().unwrap(); 188 | println!("Listening on {addr}"); 189 | let port = addr.port(); 190 | 191 | let (shutdown_sender, shutdown_receiver) = tokio::sync::oneshot::channel::<()>(); 192 | let server_task = tokio::spawn(async move { 193 | let shutdown_receiver = async move { 194 | shutdown_receiver.await.unwrap(); 195 | }; 196 | if let Err(e) = twirp::axum::serve(tcp_listener, app) 197 | .with_graceful_shutdown(shutdown_receiver) 198 | .await 199 | { 200 | eprintln!("server error: {}", e); 201 | } 202 | }); 203 | 204 | NetServer { 205 | port, 206 | server_task, 207 | shutdown_sender, 208 | } 209 | } 210 | 211 | async fn shutdown(self) { 212 | self.shutdown_sender.send(()).unwrap(); 213 | self.server_task.await.unwrap(); 214 | } 215 | } 216 | 217 | #[tokio::test] 218 | async fn test_net() { 219 | let api_impl = HaberdasherApiServer {}; 220 | let server = NetServer::start(api_impl).await; 221 | 222 | let url = Url::parse(&format!("http://localhost:{}/twirp/", server.port)).unwrap(); 223 | let client = Client::from_base_url(url); 224 | let resp = client 225 | .make_hat(http::Request::new(MakeHatRequest { inches: 1 })) 226 | .await; 227 | println!("{:?}", resp); 228 | let data = resp.unwrap().into_body(); 229 | assert_eq!(data.size, 1); 230 | 231 | server.shutdown().await; 232 | } 233 | } 234 | -------------------------------------------------------------------------------- /crates/twirp-build/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![doc = include_str!("../README.md")] 2 | 3 | use quote::format_ident; 4 | use syn::parse_quote; 5 | 6 | /// Generates twirp services for protobuf rpc service definitions. 7 | /// 8 | /// In your `build.rs`, using `prost_build`, you can wire in the twirp 9 | /// `ServiceGenerator` to produce a Rust server for your proto services. 10 | /// 11 | /// Add a call to `.service_generator(twirp_build::service_generator())` in 12 | /// main() of `build.rs`. 13 | pub fn service_generator() -> Box { 14 | Box::new(ServiceGenerator {}) 15 | } 16 | 17 | struct Service { 18 | /// The name of the server trait, as parsed into a Rust identifier. 19 | rpc_trait_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 rpc_trait_name = format_ident!("{}", &s.name); 46 | let methods = s 47 | .methods 48 | .into_iter() 49 | .map(|m| Method::from_prost(&s.package, &s.proto_name, m)) 50 | .collect(); 51 | 52 | Self { 53 | rpc_trait_name, 54 | fqn, 55 | methods, 56 | } 57 | } 58 | } 59 | 60 | impl Method { 61 | fn from_prost(pkg_name: &str, svc_name: &str, m: prost_build::Method) -> Self { 62 | let as_type = |s| -> syn::Type { 63 | let Ok(typ) = syn::parse_str::(s) else { 64 | panic!( 65 | "twirp-build failed generated invalid Rust while processing {pkg}.{svc}/{name}). this is a bug in twirp-build, please file a GitHub issue", 66 | pkg = pkg_name, 67 | svc = svc_name, 68 | name = m.proto_name, 69 | ); 70 | }; 71 | typ 72 | }; 73 | 74 | let input_type = as_type(&m.input_type); 75 | let output_type = as_type(&m.output_type); 76 | let name = format_ident!("{}", m.name); 77 | let message = m.proto_name; 78 | 79 | Self { 80 | name, 81 | proto_name: message, 82 | input_type, 83 | output_type, 84 | } 85 | } 86 | } 87 | 88 | pub struct ServiceGenerator; 89 | 90 | impl prost_build::ServiceGenerator for ServiceGenerator { 91 | fn generate(&mut self, service: prost_build::Service, buf: &mut String) { 92 | let service = Service::from_prost(service); 93 | 94 | // generate the twirp server 95 | let service_fqn_path = format!("/{}", service.fqn); 96 | let mut trait_methods: Vec = Vec::with_capacity(service.methods.len()); 97 | let mut proxy_methods: Vec = Vec::with_capacity(service.methods.len()); 98 | for m in &service.methods { 99 | let name = &m.name; 100 | let input_type = &m.input_type; 101 | let output_type = &m.output_type; 102 | 103 | trait_methods.push(parse_quote! { 104 | async fn #name(&self, req: twirp::Request<#input_type>) -> twirp::Result>; 105 | }); 106 | 107 | proxy_methods.push(parse_quote! { 108 | async fn #name(&self, req: twirp::Request<#input_type>) -> twirp::Result> { 109 | T::#name(&*self, req).await 110 | } 111 | }); 112 | } 113 | 114 | let rpc_trait_name = &service.rpc_trait_name; 115 | let server_trait: syn::ItemTrait = parse_quote! { 116 | #[twirp::async_trait::async_trait] 117 | pub trait #rpc_trait_name: Send + Sync { 118 | #(#trait_methods)* 119 | } 120 | }; 121 | let server_trait_impl: syn::ItemImpl = parse_quote! { 122 | #[twirp::async_trait::async_trait] 123 | impl #rpc_trait_name for std::sync::Arc 124 | where 125 | T: #rpc_trait_name + Sync + Send 126 | { 127 | #(#proxy_methods)* 128 | } 129 | }; 130 | 131 | // generate the router 132 | let mut expr: syn::Expr = parse_quote! { 133 | twirp::details::TwirpRouterBuilder::new(#service_fqn_path, api) 134 | }; 135 | for m in &service.methods { 136 | let name = &m.name; 137 | let input_type = &m.input_type; 138 | let path = format!("/{}", m.proto_name); 139 | 140 | expr = parse_quote! { 141 | #expr.route(#path, |api: T, req: twirp::Request<#input_type>| async move { 142 | api.#name(req).await 143 | }) 144 | }; 145 | } 146 | let router: syn::ItemFn = parse_quote! { 147 | pub fn router(api: T) -> twirp::Router 148 | where 149 | T: #rpc_trait_name + Clone + Send + Sync + 'static 150 | { 151 | #expr.build() 152 | } 153 | }; 154 | 155 | // 156 | // generate the twirp client 157 | // 158 | let mut client_methods: Vec = Vec::with_capacity(service.methods.len()); 159 | for m in &service.methods { 160 | let name = &m.name; 161 | let input_type = &m.input_type; 162 | let output_type = &m.output_type; 163 | let request_path = format!("{}/{}", service.fqn, m.proto_name); 164 | 165 | client_methods.push(parse_quote! { 166 | async fn #name(&self, req: twirp::Request<#input_type>) -> twirp::Result> { 167 | self.request(#request_path, req).await 168 | } 169 | }) 170 | } 171 | let client_trait: syn::ItemImpl = parse_quote! { 172 | #[twirp::async_trait::async_trait] 173 | impl #rpc_trait_name for twirp::client::Client { 174 | #(#client_methods)* 175 | } 176 | }; 177 | 178 | // 179 | // generate the client mock helpers 180 | // 181 | // TODO: Gate this code on a feature flag e.g. `std::env::var("CARGO_CFG_FEATURE_").is_ok()` 182 | // 183 | let service_fqn = &service.fqn; 184 | let handler_name = format_ident!("{rpc_trait_name}Handler"); 185 | let handler_struct: syn::ItemStruct = parse_quote! { 186 | pub struct #handler_name { 187 | inner: std::sync::Arc, 188 | } 189 | }; 190 | let mut method_matches: Vec = Vec::with_capacity(service.methods.len()); 191 | for m in &service.methods { 192 | let name = &m.name; 193 | let method = &m.proto_name; 194 | method_matches.push(parse_quote! { 195 | #method => { 196 | twirp::details::encode_response(self.inner.#name(twirp::details::decode_request(req).await?).await?) 197 | } 198 | }); 199 | } 200 | let handler_impl: syn::ItemImpl = parse_quote! { 201 | impl #handler_name { 202 | #[allow(clippy::new_ret_no_self)] 203 | pub fn new(inner: M) -> Self { 204 | Self { inner: std::sync::Arc::new(inner) } 205 | } 206 | } 207 | 208 | }; 209 | let handler_direct_impl: syn::ItemImpl = parse_quote! { 210 | #[twirp::async_trait::async_trait] 211 | impl twirp::client::DirectHandler for #handler_name { 212 | fn service(&self) -> &str { 213 | #service_fqn 214 | } 215 | async fn handle(&self, method: &str, req: twirp::reqwest::Request) -> twirp::Result { 216 | match method { 217 | #(#method_matches)* 218 | _ => Err(twirp::bad_route(format!("unknown rpc `{method}` for service `{}`, url: {:?}", #service_fqn, req.url()))), 219 | } 220 | } 221 | } 222 | }; 223 | let direct_api_handler: syn::ItemMod = parse_quote! { 224 | #[allow(dead_code)] 225 | pub mod handler { 226 | use super::*; 227 | 228 | #handler_struct 229 | #handler_impl 230 | #handler_direct_impl 231 | } 232 | }; 233 | 234 | // generate the service and client as a single file. run it through 235 | // prettyplease before outputting it. 236 | let ast: syn::File = parse_quote! { 237 | pub use twirp; 238 | 239 | #server_trait 240 | #server_trait_impl 241 | 242 | #router 243 | 244 | #client_trait 245 | 246 | #direct_api_handler 247 | }; 248 | 249 | let code = prettyplease::unparse(&ast); 250 | buf.push_str(&code); 251 | } 252 | } 253 | -------------------------------------------------------------------------------- /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 | 8 | use axum::body::Body; 9 | use axum::response::IntoResponse; 10 | use futures::Future; 11 | use http::request::Parts; 12 | use http::HeaderValue; 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, GenericError, TwirpErrorResponse}; 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, http::Request) -> Fut + Clone + Sync + Send + 'static, 52 | Fut: Future, TwirpErrorResponse>> + Send, 53 | In: prost::Message + Default + serde::de::DeserializeOwned, 54 | Out: prost::Message + Default + serde::Serialize, 55 | { 56 | let mut timings = req 57 | .extensions() 58 | .get::() 59 | .copied() 60 | .unwrap_or_else(|| Timings::new(Instant::now())); 61 | 62 | let (parts, req, resp_fmt) = match parse_request::(req, &mut timings).await { 63 | Ok(tuple) => tuple, 64 | Err(err) => { 65 | return error::malformed("bad request") 66 | .with_meta("error", &err.to_string()) 67 | .with_generic_error(err) 68 | .into_response(); 69 | } 70 | }; 71 | 72 | let r = Request::from_parts(parts, req); 73 | let res = f(service, r).await; 74 | timings.set_response_handled(); 75 | 76 | let mut resp = match write_response(res, resp_fmt) { 77 | Ok(resp) => resp, 78 | Err(err) => { 79 | return error::internal("error serializing response") 80 | .with_meta("error", &err.to_string()) 81 | .with_generic_error(err) 82 | .into_response(); 83 | } 84 | }; 85 | timings.set_response_written(); 86 | resp.extensions_mut().insert(timings); 87 | resp 88 | } 89 | 90 | async fn parse_request( 91 | req: Request, 92 | timings: &mut Timings, 93 | ) -> Result<(Parts, T, BodyFormat), GenericError> 94 | where 95 | T: prost::Message + Default + DeserializeOwned, 96 | { 97 | let format = BodyFormat::from_content_type(&req); 98 | let (parts, body) = req.into_parts(); 99 | let bytes = body.collect().await?.to_bytes(); 100 | timings.set_received(); 101 | let request = match format { 102 | BodyFormat::Pb => T::decode(&bytes[..])?, 103 | BodyFormat::JsonPb => serde_json::from_slice(&bytes)?, 104 | }; 105 | timings.set_parsed(); 106 | Ok((parts, request, format)) 107 | } 108 | 109 | fn write_response( 110 | out: Result, TwirpErrorResponse>, 111 | out_format: BodyFormat, 112 | ) -> Result, GenericError> 113 | where 114 | T: prost::Message + Default + Serialize, 115 | { 116 | let res = match out { 117 | Ok(out) => { 118 | let (parts, body) = out.into_parts(); 119 | let (body, content_type) = match out_format { 120 | BodyFormat::Pb => ( 121 | Body::from(serialize_proto_message(body)), 122 | CONTENT_TYPE_PROTOBUF, 123 | ), 124 | BodyFormat::JsonPb => { 125 | (Body::from(serde_json::to_string(&body)?), CONTENT_TYPE_JSON) 126 | } 127 | }; 128 | let mut resp = Response::new(body); 129 | resp.extensions_mut().extend(parts.extensions); 130 | resp.headers_mut().extend(parts.headers); 131 | resp.headers_mut() 132 | .insert(header::CONTENT_TYPE, HeaderValue::from_bytes(content_type)?); 133 | resp 134 | } 135 | Err(err) => err.into_response(), 136 | }; 137 | Ok(res) 138 | } 139 | 140 | /// Axum handler function that returns 404 Not Found with a Twirp JSON payload. 141 | /// 142 | /// `axum::Router`'s default fallback handler returns a 404 Not Found with no body content. 143 | /// Use this fallback instead for full Twirp compliance. 144 | /// 145 | /// # Usage 146 | /// 147 | /// ``` 148 | /// use axum::Router; 149 | /// 150 | /// # fn build_app(twirp_routes: Router) -> Router { 151 | /// let app = Router::new() 152 | /// .nest("/twirp", twirp_routes) 153 | /// .fallback(twirp::server::not_found_handler); 154 | /// # app } 155 | /// ``` 156 | pub async fn not_found_handler() -> Response { 157 | error::bad_route("not found").into_response() 158 | } 159 | 160 | /// Contains timing information associated with a request. 161 | /// To access the timings in a given request, use the [extensions](Request::extensions) 162 | /// method and specialize to `Timings` appropriately. 163 | #[derive(Debug, Clone, Copy)] 164 | pub struct Timings { 165 | // When the request started. 166 | start: Instant, 167 | // When the request was received (headers and body). 168 | request_received: Option, 169 | // When the request body was parsed. 170 | request_parsed: Option, 171 | // When the response handler returned. 172 | response_handled: Option, 173 | // When the response was written. 174 | response_written: Option, 175 | } 176 | 177 | impl Timings { 178 | #[allow(clippy::new_without_default)] 179 | pub fn new(start: Instant) -> Self { 180 | Self { 181 | start, 182 | request_received: None, 183 | request_parsed: None, 184 | response_handled: None, 185 | response_written: None, 186 | } 187 | } 188 | 189 | fn set_received(&mut self) { 190 | self.request_received = Some(Instant::now()); 191 | } 192 | 193 | fn set_parsed(&mut self) { 194 | self.request_parsed = Some(Instant::now()); 195 | } 196 | 197 | fn set_response_handled(&mut self) { 198 | self.response_handled = Some(Instant::now()); 199 | } 200 | 201 | fn set_response_written(&mut self) { 202 | self.response_written = Some(Instant::now()); 203 | } 204 | 205 | pub fn received(&self) -> Option { 206 | self.request_received.map(|x| x - self.start) 207 | } 208 | 209 | pub fn parsed(&self) -> Option { 210 | match (self.request_parsed, self.request_received) { 211 | (Some(parsed), Some(received)) => Some(parsed - received), 212 | _ => None, 213 | } 214 | } 215 | 216 | pub fn response_handled(&self) -> Option { 217 | match (self.response_handled, self.request_parsed) { 218 | (Some(handled), Some(parsed)) => Some(handled - parsed), 219 | _ => None, 220 | } 221 | } 222 | 223 | pub fn response_written(&self) -> Option { 224 | match (self.response_written, self.response_handled) { 225 | (Some(written), Some(handled)) => Some(written - handled), 226 | (Some(written), None) => { 227 | if let Some(parsed) = self.request_parsed { 228 | Some(written - parsed) 229 | } else { 230 | self.request_received.map(|received| written - received) 231 | } 232 | } 233 | _ => None, 234 | } 235 | } 236 | 237 | /// The total duration since the request started. 238 | pub fn total_duration(&self) -> Duration { 239 | self.start.elapsed() 240 | } 241 | } 242 | 243 | #[cfg(test)] 244 | mod tests { 245 | 246 | use super::*; 247 | use crate::test::*; 248 | 249 | use axum::middleware::{self, Next}; 250 | use tower::Service; 251 | 252 | fn timings() -> Timings { 253 | Timings::new(Instant::now()) 254 | } 255 | 256 | #[tokio::test] 257 | async fn test_bad_route() { 258 | let mut router = test_api_router(); 259 | let req = Request::get("/nothing") 260 | .extension(timings()) 261 | .body(Body::empty()) 262 | .unwrap(); 263 | 264 | let resp = router.call(req).await.unwrap(); 265 | let data = read_err_body(resp.into_body()).await; 266 | assert_eq!(data, error::bad_route("not found")); 267 | } 268 | 269 | #[tokio::test] 270 | async fn test_ping_success() { 271 | let mut router = test_api_router(); 272 | let resp = router.call(gen_ping_request("hi")).await.unwrap(); 273 | assert!(resp.status().is_success(), "{:?}", resp); 274 | let data: PingResponse = read_json_body(resp.into_body()).await; 275 | assert_eq!(&data.name, "hi"); 276 | } 277 | 278 | #[tokio::test] 279 | async fn test_ping_invalid_request() { 280 | let mut router = test_api_router(); 281 | let req = Request::post("/twirp/test.TestAPI/Ping") 282 | .extension(timings()) 283 | .body(Body::empty()) // not a valid request 284 | .unwrap(); 285 | let resp = router.call(req).await.unwrap(); 286 | assert!(resp.status().is_client_error(), "{:?}", resp); 287 | let data = read_err_body(resp.into_body()).await; 288 | 289 | let expected = error::malformed("bad request") 290 | .with_meta("error", "EOF while parsing a value at line 1 column 0"); 291 | assert_eq!(data, expected); 292 | } 293 | 294 | #[tokio::test] 295 | async fn test_boom() { 296 | let mut router = test_api_router(); 297 | let req = serde_json::to_string(&PingRequest { 298 | name: "hi".to_string(), 299 | }) 300 | .unwrap(); 301 | let req = Request::post("/twirp/test.TestAPI/Boom") 302 | .extension(timings()) 303 | .body(Body::from(req)) 304 | .unwrap(); 305 | let resp = router.call(req).await.unwrap(); 306 | assert!(resp.status().is_server_error(), "{:?}", resp); 307 | let data = read_err_body(resp.into_body()).await; 308 | assert_eq!(data, error::internal("boom!")); 309 | } 310 | 311 | #[tokio::test] 312 | async fn test_middleware() { 313 | let mut router = test_api_router().layer(middleware::from_fn(request_id_middleware)); 314 | 315 | // no request-id header 316 | let resp = router.call(gen_ping_request("hi")).await.unwrap(); 317 | assert!(resp.status().is_success(), "{:?}", resp); 318 | let data: PingResponse = read_json_body(resp.into_body()).await; 319 | assert_eq!(&data.name, "hi"); 320 | 321 | // now pass a header with x-request-id 322 | let req = Request::post("/twirp/test.TestAPI/Ping") 323 | .header("x-request-id", "abcd") 324 | .body(Body::from( 325 | serde_json::to_string(&PingRequest { 326 | name: "hello".to_string(), 327 | }) 328 | .expect("will always be valid json"), 329 | )) 330 | .expect("always a valid twirp request"); 331 | let resp = router.call(req).await.unwrap(); 332 | assert!(resp.status().is_success(), "{:?}", resp); 333 | let data: PingResponse = read_json_body(resp.into_body()).await; 334 | assert_eq!(&data.name, "hello-abcd"); 335 | } 336 | 337 | async fn request_id_middleware( 338 | mut request: http::Request, 339 | next: Next, 340 | ) -> http::Response { 341 | let rid = request 342 | .headers() 343 | .get("x-request-id") 344 | .and_then(|v| v.to_str().ok()) 345 | .map(|x| RequestId(x.to_string())); 346 | if let Some(rid) = rid { 347 | request.extensions_mut().insert(rid); 348 | } 349 | 350 | next.run(request).await 351 | } 352 | } 353 | -------------------------------------------------------------------------------- /crates/twirp/src/client.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | use std::sync::Arc; 3 | use std::vec; 4 | 5 | use async_trait::async_trait; 6 | use http::header::Entry; 7 | use http::header::IntoHeaderName; 8 | use http::HeaderMap; 9 | use http::HeaderValue; 10 | use reqwest::header::CONTENT_TYPE; 11 | use url::Host; 12 | use url::Url; 13 | 14 | use crate::headers::{CONTENT_TYPE_JSON, CONTENT_TYPE_PROTOBUF}; 15 | use crate::{serialize_proto_message, Result, TwirpErrorResponse}; 16 | 17 | /// Builder to easily create twirp clients. 18 | pub struct ClientBuilder { 19 | base_url: Url, 20 | http_client: Option, 21 | handlers: Option, 22 | middleware: Vec>, 23 | } 24 | 25 | impl ClientBuilder { 26 | /// Creates a `twirp::ClientBuilder` with a base URL. 27 | pub fn new(base_url: Url) -> Self { 28 | Self { 29 | base_url, 30 | http_client: None, 31 | middleware: vec![], 32 | handlers: None, 33 | } 34 | } 35 | 36 | const DEFAULT_HOST: &'static str = "localhost"; 37 | 38 | /// Creates a `twirp::ClientBuilder` suitable for registering request handlers instead of making http requests. 39 | /// NOTE: uses a default base URL and HTTP client. 40 | pub fn direct() -> Self { 41 | Self { 42 | base_url: Url::parse(&format!("http://{}/", Self::DEFAULT_HOST)) 43 | .expect("must be a valid URL"), 44 | http_client: None, 45 | middleware: vec![], 46 | handlers: Some(RequestHandlers::new()), 47 | } 48 | } 49 | 50 | /// Set the HTTP client. Without this a default HTTP client is used. 51 | pub fn with_http_client(mut self, http_client: reqwest::Client) -> Self { 52 | self.http_client = Some(http_client); 53 | self 54 | } 55 | 56 | /// Add middleware to the client that will be called on each request. 57 | /// Middlewares are invoked in the order they are added as part of the 58 | /// request cycle. 59 | pub fn with_middleware(mut self, middleware: M) -> Self 60 | where 61 | M: Middleware, 62 | { 63 | self.middleware.push(Box::new(middleware)); 64 | self 65 | } 66 | 67 | /// Add a handler for a service using the default host. 68 | /// 69 | /// Warning: If you register `DirectHandler`s like this, they will be called instead of making HTTP requests. 70 | pub fn with_handler(self, handler: M) -> Self { 71 | self.with_handler_for_host(Self::DEFAULT_HOST, handler) 72 | } 73 | 74 | /// Add a handler for a service for a specific host. 75 | /// 76 | /// Warning: If you register `DirectHandler`s like this, they will be called instead of making HTTP requests. 77 | pub fn with_handler_for_host( 78 | mut self, 79 | host: &str, 80 | handler: M, 81 | ) -> Self { 82 | if let Some(handlers) = &mut self.handlers { 83 | handlers.add(host, handler); 84 | } else { 85 | panic!("you must use `ClientBuilder::direct()` to register handlers"); 86 | } 87 | self 88 | } 89 | 90 | /// Set a default header for use in direct mode. 91 | pub fn with_default_header(mut self, key: K, value: HeaderValue) -> Self 92 | where 93 | K: IntoHeaderName, 94 | { 95 | if let Some(handlers) = &mut self.handlers { 96 | handlers.default_headers.insert(key, value); 97 | } else { 98 | panic!("you must use `ClientBuilder::direct()` to register handler default headers"); 99 | } 100 | self 101 | } 102 | 103 | /// Creates a `twirp::Client`. 104 | /// 105 | /// The underlying `reqwest::Client` holds a connection pool internally, so it is advised that 106 | /// you create one and **reuse** it. 107 | pub fn build(self) -> Client { 108 | let http_client = self.http_client.unwrap_or_default(); 109 | Client::new(self.base_url, http_client, self.middleware, self.handlers) 110 | } 111 | } 112 | 113 | /// `Client` is a Twirp HTTP client that uses `reqwest::Client` to make http 114 | /// requests. 115 | /// 116 | /// You do **not** have to wrap `Client` in an [`Rc`] or [`Arc`] to **reuse** it, 117 | /// because it already uses an [`Arc`] internally. 118 | #[derive(Clone)] 119 | pub struct Client { 120 | http_client: reqwest::Client, 121 | inner: Arc, 122 | host: Option, 123 | } 124 | 125 | struct ClientRef { 126 | base_url: Url, 127 | middlewares: Vec>, 128 | handlers: Option, 129 | } 130 | 131 | impl std::fmt::Debug for Client { 132 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 133 | f.debug_struct("Client") 134 | .field("base_url", &self.inner.base_url) 135 | .field("client", &self.http_client) 136 | .field("middlewares", &self.inner.middlewares.len()) 137 | .field( 138 | "handlers", 139 | &self 140 | .inner 141 | .handlers 142 | .as_ref() 143 | .map(|x| x.len()) 144 | .unwrap_or_default(), 145 | ) 146 | .finish() 147 | } 148 | } 149 | 150 | impl Client { 151 | /// Creates a `twirp::Client`. 152 | /// 153 | /// The underlying `reqwest::Client` holds a connection pool internally, so it is advised that 154 | /// you create one and **reuse** it. 155 | pub fn new( 156 | base_url: Url, 157 | http_client: reqwest::Client, 158 | middlewares: Vec>, 159 | handlers: Option, 160 | ) -> Self { 161 | let base_url = if base_url.path().ends_with('/') { 162 | base_url 163 | } else { 164 | let mut base_url = base_url; 165 | let mut path = base_url.path().to_string(); 166 | path.push('/'); 167 | base_url.set_path(&path); 168 | base_url 169 | }; 170 | Client { 171 | http_client, 172 | inner: Arc::new(ClientRef { 173 | base_url, 174 | middlewares, 175 | handlers, 176 | }), 177 | host: None, 178 | } 179 | } 180 | 181 | /// Creates a `twirp::Client` with the default `reqwest::ClientBuilder`. 182 | /// 183 | /// The underlying `reqwest::Client` holds a connection pool internally, so it is advised that 184 | /// you create one and **reuse** it. 185 | pub fn from_base_url(base_url: Url) -> Self { 186 | Self::new(base_url, reqwest::Client::new(), vec![], None) 187 | } 188 | 189 | /// The base URL of the service the client will call. 190 | pub fn base_url(&self) -> &Url { 191 | &self.inner.base_url 192 | } 193 | 194 | /// Creates a new `twirp::Client` with the same configuration as the current 195 | /// one, but with a different host in the base URL. 196 | pub fn with_host(&self, host: &str) -> Self { 197 | Self { 198 | http_client: self.http_client.clone(), 199 | inner: self.inner.clone(), 200 | host: Some(host.to_string()), 201 | } 202 | } 203 | 204 | /// Make an HTTP twirp request. 205 | pub async fn request( 206 | &self, 207 | path: &str, 208 | req: http::Request, 209 | ) -> Result> 210 | where 211 | I: prost::Message, 212 | O: prost::Message + Default, 213 | { 214 | let mut url = self.inner.base_url.join(path)?; 215 | if let Some(host) = &self.host { 216 | url.set_host(Some(host))? 217 | }; 218 | let (parts, body) = req.into_parts(); 219 | let request = self 220 | .http_client 221 | .post(url) 222 | .headers(parts.headers) 223 | .header(CONTENT_TYPE, CONTENT_TYPE_PROTOBUF) 224 | .body(serialize_proto_message(body)) 225 | .build()?; 226 | 227 | // Create and execute the middleware handlers 228 | let next = Next::new( 229 | &self.http_client, 230 | &self.inner.middlewares, 231 | self.inner.handlers.as_ref(), 232 | ); 233 | let response = next.run(request).await?; 234 | 235 | // These have to be extracted because reading the body consumes `Response`. 236 | let version = response.version(); 237 | let status = response.status(); 238 | let headers = response.headers().clone(); 239 | let extensions = response.extensions().clone(); 240 | let content_type = headers.get(CONTENT_TYPE).cloned(); 241 | 242 | // TODO: Include more info in the error cases: request path, content-type, etc. 243 | match (status, content_type) { 244 | (status, Some(ct)) if status.is_success() && ct.as_bytes() == CONTENT_TYPE_PROTOBUF => { 245 | O::decode(response.bytes().await?) 246 | .map(|x| { 247 | let mut resp = http::Response::new(x); 248 | *resp.version_mut() = version; 249 | resp.headers_mut().extend(headers); 250 | resp.extensions_mut().extend(extensions); 251 | resp 252 | }) 253 | .map_err(|e| e.into()) 254 | } 255 | (status, Some(ct)) 256 | if (status.is_client_error() || status.is_server_error()) 257 | && ct.as_bytes() == CONTENT_TYPE_JSON => 258 | { 259 | // TODO: Should middleware response extensions and headers be included in the error case? 260 | Err(serde_json::from_slice(&response.bytes().await?)?) 261 | } 262 | (status, ct) => Err(TwirpErrorResponse::new( 263 | status.into(), 264 | format!("unexpected content type: {:?}", ct), 265 | )), 266 | } 267 | } 268 | } 269 | 270 | // This concept of reqwest middleware is taken pretty much directly from: 271 | // https://github.com/TrueLayer/reqwest-middleware, but simplified for the 272 | // specific needs of this twirp client. 273 | #[async_trait] 274 | pub trait Middleware: 'static + Send + Sync { 275 | async fn handle(&self, mut req: reqwest::Request, next: Next<'_>) -> Result; 276 | } 277 | 278 | #[async_trait] 279 | impl Middleware for F 280 | where 281 | F: Send 282 | + Sync 283 | + 'static 284 | + for<'a> Fn(reqwest::Request, Next<'a>) -> BoxFuture<'a, Result>, 285 | { 286 | async fn handle(&self, req: reqwest::Request, next: Next<'_>) -> Result { 287 | (self)(req, next).await 288 | } 289 | } 290 | 291 | #[derive(Clone)] 292 | pub struct Next<'a> { 293 | client: &'a reqwest::Client, 294 | middlewares: &'a [Box], 295 | handlers: Option<&'a RequestHandlers>, 296 | } 297 | 298 | pub type BoxFuture<'a, T> = std::pin::Pin + Send + 'a>>; 299 | 300 | impl<'a> Next<'a> { 301 | pub(crate) fn new( 302 | client: &'a reqwest::Client, 303 | middlewares: &'a [Box], 304 | handlers: Option<&'a RequestHandlers>, 305 | ) -> Self { 306 | Next { 307 | client, 308 | middlewares, 309 | handlers, 310 | } 311 | } 312 | 313 | pub fn run(mut self, req: reqwest::Request) -> BoxFuture<'a, Result> { 314 | if let Some((current, rest)) = self.middlewares.split_first() { 315 | // Run any middleware 316 | self.middlewares = rest; 317 | Box::pin(current.handle(req, self)) 318 | } else if let Some(handlers) = self.handlers { 319 | // If we've got a client with direct request handlers: use those 320 | Box::pin(async move { execute_handlers(req, handlers).await }) 321 | } else { 322 | // Otherwise: execute the actual http request here 323 | Box::pin(async move { Ok(self.client.execute(req).await?) }) 324 | } 325 | } 326 | } 327 | 328 | async fn execute_handlers( 329 | mut req: reqwest::Request, 330 | request_handlers: &RequestHandlers, 331 | ) -> Result { 332 | let req_headers = req.headers_mut(); 333 | for (key, value) in &request_handlers.default_headers { 334 | if let Entry::Vacant(entry) = req_headers.entry(key) { 335 | entry.insert(value.clone()); 336 | } 337 | } 338 | let url = req.url().clone(); 339 | let Some(mut segments) = url.path_segments() else { 340 | return Err(crate::bad_route(format!( 341 | "invalid request to {}: no path segments", 342 | url 343 | ))); 344 | }; 345 | let (Some(method), Some(service)) = (segments.next_back(), segments.next_back()) else { 346 | return Err(crate::bad_route(format!( 347 | "invalid request to {}: method and service required", 348 | url 349 | ))); 350 | }; 351 | let host = url.host().expect("no host in url"); 352 | 353 | if let Some(handler) = request_handlers.get(&host, service) { 354 | handler.handle(method, req).await 355 | } else { 356 | Err(crate::bad_route(format!( 357 | "no handler found for host: '{host}', service: '{service}'" 358 | ))) 359 | } 360 | } 361 | 362 | #[derive(Clone, Default)] 363 | pub struct RequestHandlers { 364 | default_headers: HeaderMap, 365 | /// A map of host/service names to handlers. 366 | handlers: HashMap>, 367 | } 368 | 369 | impl RequestHandlers { 370 | pub fn new() -> Self { 371 | Self { 372 | default_headers: HeaderMap::new(), 373 | handlers: HashMap::new(), 374 | } 375 | } 376 | 377 | pub fn add(&mut self, host: &str, handler: M) { 378 | let key = format!("{}/{}", host, handler.service()); 379 | self.handlers.insert(key, Arc::new(handler)); 380 | } 381 | 382 | pub fn get(&self, host: &Host<&str>, service: &str) -> Option> { 383 | self.handlers.get(&format!("{}/{}", host, service)).cloned() 384 | } 385 | 386 | pub fn len(&self) -> usize { 387 | self.handlers.len() 388 | } 389 | 390 | pub fn is_empty(&self) -> bool { 391 | self.handlers.is_empty() 392 | } 393 | } 394 | 395 | #[async_trait] 396 | pub trait DirectHandler: 'static + Send + Sync { 397 | fn service(&self) -> &str; 398 | async fn handle(&self, path: &str, mut req: reqwest::Request) -> Result; 399 | } 400 | 401 | #[cfg(test)] 402 | mod tests { 403 | use reqwest::{Request, Response}; 404 | 405 | use crate::test::*; 406 | 407 | use super::*; 408 | 409 | struct AssertRouting { 410 | expected_url: &'static str, 411 | } 412 | 413 | #[async_trait] 414 | impl Middleware for AssertRouting { 415 | async fn handle(&self, req: Request, next: Next<'_>) -> Result { 416 | assert_eq!(self.expected_url, &req.url().to_string()); 417 | next.run(req).await 418 | } 419 | } 420 | 421 | #[tokio::test] 422 | async fn test_base_url() { 423 | let url = Url::parse("http://localhost:3001/twirp/").unwrap(); 424 | assert_eq!( 425 | Client::from_base_url(url).base_url().to_string(), 426 | "http://localhost:3001/twirp/" 427 | ); 428 | let url = Url::parse("http://localhost:3001/twirp").unwrap(); 429 | assert_eq!( 430 | Client::from_base_url(url).base_url().to_string(), 431 | "http://localhost:3001/twirp/" 432 | ); 433 | } 434 | 435 | #[tokio::test] 436 | async fn test_routes() { 437 | let base_url = Url::parse("http://localhost:3001/twirp/").unwrap(); 438 | 439 | let client = ClientBuilder::new(base_url) 440 | .with_middleware(AssertRouting { 441 | expected_url: "http://localhost:3001/twirp/test.TestAPI/Ping", 442 | }) 443 | .build(); 444 | assert!(client 445 | .ping(http::Request::new(PingRequest { 446 | name: "hi".to_string(), 447 | })) 448 | .await 449 | .is_err()); // expected connection refused error. 450 | } 451 | 452 | #[tokio::test] 453 | async fn test_standard_client() { 454 | let h = run_test_server(3002).await; 455 | let base_url = Url::parse("http://localhost:3002/twirp/").unwrap(); 456 | let client = Client::from_base_url(base_url); 457 | let resp = client 458 | .ping(http::Request::new(PingRequest { 459 | name: "hi".to_string(), 460 | })) 461 | .await 462 | .unwrap(); 463 | let data = resp.into_body(); 464 | assert_eq!(data.name, "hi"); 465 | h.abort() 466 | } 467 | } 468 | -------------------------------------------------------------------------------- /crates/twirp/src/error.rs: -------------------------------------------------------------------------------- 1 | //! Implement [Twirp](https://twitchtv.github.io/twirp/) error responses 2 | 3 | use std::collections::HashMap; 4 | use std::time::Duration; 5 | 6 | use axum::body::Body; 7 | use axum::response::IntoResponse; 8 | use http::header::{self}; 9 | use hyper::{Response, StatusCode}; 10 | use serde::{Deserialize, Serialize, Serializer}; 11 | use thiserror::Error; 12 | 13 | /// Alias for a generic error 14 | pub type GenericError = Box; 15 | 16 | macro_rules! twirp_error_codes { 17 | ( 18 | $( 19 | $(#[$docs:meta])* 20 | ($konst:ident, $num:expr, $phrase:ident); 21 | )+ 22 | ) => { 23 | /// A Twirp error code as defined by . 24 | #[derive(Clone, Copy, Debug, PartialEq, Eq, Deserialize)] 25 | #[serde(field_identifier, rename_all = "snake_case")] 26 | #[non_exhaustive] 27 | pub enum TwirpErrorCode { 28 | $( 29 | $(#[$docs])* 30 | $konst, 31 | )+ 32 | } 33 | 34 | impl TwirpErrorCode { 35 | pub fn http_status_code(&self) -> StatusCode { 36 | match *self { 37 | $( 38 | TwirpErrorCode::$konst => $num, 39 | )+ 40 | } 41 | } 42 | 43 | pub fn twirp_code(&self) -> &'static str { 44 | match *self { 45 | $( 46 | TwirpErrorCode::$konst => stringify!($phrase), 47 | )+ 48 | } 49 | } 50 | } 51 | 52 | impl From for TwirpErrorCode { 53 | fn from(code: StatusCode) -> Self { 54 | $( 55 | if code == $num { 56 | return TwirpErrorCode::$konst; 57 | } 58 | )+ 59 | return TwirpErrorCode::Unknown 60 | } 61 | } 62 | 63 | $( 64 | pub fn $phrase(msg: T) -> TwirpErrorResponse { 65 | TwirpErrorResponse { 66 | code: TwirpErrorCode::$konst, 67 | msg: msg.to_string(), 68 | meta: Default::default(), 69 | rust_error: None, 70 | retry_after: None, 71 | } 72 | } 73 | )+ 74 | } 75 | } 76 | 77 | // Define all twirp errors. 78 | twirp_error_codes! { 79 | /// The operation was cancelled. 80 | (Canceled, StatusCode::REQUEST_TIMEOUT, canceled); 81 | /// An unknown error occurred. For example, this can be used when handling 82 | /// errors raised by APIs that do not return any error information. 83 | (Unknown, StatusCode::INTERNAL_SERVER_ERROR, unknown); 84 | /// The client specified an invalid argument. This indicates arguments that 85 | /// are invalid regardless of the state of the system (i.e. a malformed file 86 | /// name, required argument, number out of range, etc.). 87 | (InvalidArgument, StatusCode::BAD_REQUEST, invalid_argument); 88 | /// The client sent a message which could not be decoded. This may mean that 89 | /// the message was encoded improperly or that the client and server have 90 | /// incompatible message definitions. 91 | (Malformed, StatusCode::BAD_REQUEST, malformed); 92 | /// Operation expired before completion. For operations that change the 93 | /// state of the system, this error may be returned even if the operation 94 | /// has completed successfully (timeout). 95 | (DeadlineExceeded, StatusCode::REQUEST_TIMEOUT, deadline_exceeded); 96 | /// Some requested entity was not found. 97 | (NotFound, StatusCode::NOT_FOUND, not_found); 98 | /// The requested URL path wasn't routable to a Twirp service and method. 99 | /// This is returned by generated server code and should not be returned by 100 | /// application code (use "not_found" or "unimplemented" instead). 101 | (BadRoute, StatusCode::NOT_FOUND, bad_route); 102 | /// An attempt to create an entity failed because one already exists. 103 | (AlreadyExists, StatusCode::CONFLICT, already_exists); 104 | // The caller does not have permission to execute the specified operation. 105 | // It must not be used if the caller cannot be identified (use 106 | // "unauthenticated" instead). 107 | (PermissionDenied, StatusCode::FORBIDDEN, permission_denied); 108 | // The request does not have valid authentication credentials for the 109 | // operation. 110 | (Unauthenticated, StatusCode::UNAUTHORIZED, unauthenticated); 111 | /// Some resource has been exhausted or rate-limited, perhaps a per-user 112 | /// quota, or perhaps the entire file system is out of space. 113 | (ResourceExhausted, StatusCode::TOO_MANY_REQUESTS, resource_exhausted); 114 | /// The operation was rejected because the system is not in a state required 115 | /// for the operation's execution. For example, doing an rmdir operation on 116 | /// a directory that is non-empty, or on a non-directory object, or when 117 | /// having conflicting read-modify-write on the same resource. 118 | (FailedPrecondition, StatusCode::PRECONDITION_FAILED, failed_precondition); 119 | /// The operation was aborted, typically due to a concurrency issue like 120 | /// sequencer check failures, transaction aborts, etc. 121 | (Aborted, StatusCode::CONFLICT, aborted); 122 | /// The operation was attempted past the valid range. For example, seeking 123 | /// or reading past end of a paginated collection. Unlike 124 | /// "invalid_argument", this error indicates a problem that may be fixed if 125 | /// the system state changes (i.e. adding more items to the collection). 126 | /// There is a fair bit of overlap between "failed_precondition" and 127 | /// "out_of_range". We recommend using "out_of_range" (the more specific 128 | /// error) when it applies so that callers who are iterating through a space 129 | /// can easily look for an "out_of_range" error to detect when they are 130 | /// done. 131 | (OutOfRange, StatusCode::BAD_REQUEST, out_of_range); 132 | /// The operation is not implemented or not supported/enabled in this 133 | /// service. 134 | (Unimplemented, StatusCode::NOT_IMPLEMENTED, unimplemented); 135 | /// When some invariants expected by the underlying system have been broken. 136 | /// In other words, something bad happened in the library or backend 137 | /// service. Twirp specific issues like wire and serialization problems are 138 | /// also reported as "internal" errors. 139 | (Internal, StatusCode::INTERNAL_SERVER_ERROR, internal); 140 | /// The service is currently unavailable. This is most likely a transient 141 | /// condition and may be corrected by retrying with a backoff. 142 | (Unavailable, StatusCode::SERVICE_UNAVAILABLE, unavailable); 143 | /// The operation resulted in unrecoverable data loss or corruption. 144 | (Dataloss, StatusCode::INTERNAL_SERVER_ERROR, dataloss); 145 | } 146 | 147 | impl Serialize for TwirpErrorCode { 148 | fn serialize(&self, serializer: S) -> Result 149 | where 150 | S: Serializer, 151 | { 152 | serializer.serialize_str(self.twirp_code()) 153 | } 154 | } 155 | 156 | /// A Twirp error response meeting the spec: https://twitchtv.github.io/twirp/docs/spec_v7.html#error-codes. 157 | /// 158 | /// NOTE: Twirp error responses are always sent as JSON. 159 | #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Error)] 160 | pub struct TwirpErrorResponse { 161 | /// One of the Twirp error codes. 162 | pub code: TwirpErrorCode, 163 | 164 | /// A human-readable message describing the error. 165 | pub msg: String, 166 | 167 | /// (Optional) An object with string values holding arbitrary additional metadata describing the error. 168 | #[serde(skip_serializing_if = "HashMap::is_empty")] 169 | #[serde(default)] 170 | pub meta: HashMap, 171 | 172 | /// (Optional) How long clients should wait before retrying. If set, will be included in the `Retry-After` response 173 | /// header. Generally only valid for HTTP 429 or 503 responses. NOTE: This is *not* technically part of the twirp 174 | /// spec. 175 | #[serde(skip_serializing)] 176 | retry_after: Option, 177 | 178 | /// Debug form of the underlying Rust error (if any). NOT returned to clients. 179 | #[serde(skip_serializing)] 180 | rust_error: Option, 181 | } 182 | 183 | impl TwirpErrorResponse { 184 | pub fn new(code: TwirpErrorCode, msg: String) -> Self { 185 | Self { 186 | code, 187 | msg, 188 | meta: HashMap::new(), 189 | rust_error: None, 190 | retry_after: None, 191 | } 192 | } 193 | 194 | pub fn http_status_code(&self) -> StatusCode { 195 | self.code.http_status_code() 196 | } 197 | 198 | pub fn meta_mut(&mut self) -> &mut HashMap { 199 | &mut self.meta 200 | } 201 | 202 | pub fn with_meta(mut self, key: S, value: S) -> Self { 203 | self.meta.insert(key.to_string(), value.to_string()); 204 | self 205 | } 206 | 207 | pub fn retry_after(&self) -> Option { 208 | self.retry_after 209 | } 210 | 211 | pub fn with_generic_error(self, err: GenericError) -> Self { 212 | self.with_rust_error_string(format!("{err:?}")) 213 | } 214 | 215 | pub fn with_rust_error(self, err: E) -> Self { 216 | self.with_rust_error_string(format!("{err:?}")) 217 | } 218 | 219 | pub fn with_rust_error_string(mut self, rust_error: String) -> Self { 220 | self.rust_error = Some(rust_error); 221 | self 222 | } 223 | 224 | pub fn rust_error(&self) -> Option<&String> { 225 | self.rust_error.as_ref() 226 | } 227 | 228 | pub fn with_retry_after(mut self, duration: impl Into>) -> Self { 229 | let duration = duration.into(); 230 | self.retry_after = duration.map(|d| { 231 | // Ensure that the duration is at least 1 second, as per HTTP spec. 232 | if d.as_secs() < 1 { 233 | Duration::from_secs(1) 234 | } else { 235 | d 236 | } 237 | }); 238 | self 239 | } 240 | } 241 | 242 | /// Shorthand for an internal server error triggered by a Rust error. 243 | pub fn internal_server_error(err: E) -> TwirpErrorResponse { 244 | internal("internal server error").with_rust_error(err) 245 | } 246 | 247 | // twirp response from server failed to decode 248 | impl From for TwirpErrorResponse { 249 | fn from(e: prost::DecodeError) -> Self { 250 | internal(e.to_string()).with_rust_error(e) 251 | } 252 | } 253 | 254 | // twirp error response from server was invalid 255 | impl From for TwirpErrorResponse { 256 | fn from(e: serde_json::Error) -> Self { 257 | internal(e.to_string()).with_rust_error(e) 258 | } 259 | } 260 | 261 | // unable to build the request 262 | impl From for TwirpErrorResponse { 263 | fn from(e: reqwest::Error) -> Self { 264 | invalid_argument(e.to_string()).with_rust_error(e) 265 | } 266 | } 267 | 268 | // failed modify the request url 269 | impl From for TwirpErrorResponse { 270 | fn from(e: url::ParseError) -> Self { 271 | invalid_argument(e.to_string()).with_rust_error(e) 272 | } 273 | } 274 | 275 | // invalid header value (client middleware examples use this) 276 | impl From for TwirpErrorResponse { 277 | fn from(e: header::InvalidHeaderValue) -> Self { 278 | invalid_argument(e.to_string()) 279 | } 280 | } 281 | 282 | // handy for `?` syntax in implementing servers. 283 | impl From for TwirpErrorResponse { 284 | fn from(err: anyhow::Error) -> Self { 285 | internal("internal server error").with_rust_error_string(format!("{err:#}")) 286 | } 287 | } 288 | 289 | impl IntoResponse for TwirpErrorResponse { 290 | fn into_response(self) -> Response { 291 | let mut resp = Response::builder() 292 | .status(self.http_status_code()) 293 | // NB: Add this in the response extensions so that axum layers can extract (e.g. for logging) 294 | .extension(self.clone()) 295 | .header(header::CONTENT_TYPE, crate::headers::CONTENT_TYPE_JSON); 296 | 297 | if let Some(duration) = self.retry_after { 298 | resp = resp.header(header::RETRY_AFTER, duration.as_secs().to_string()); 299 | } 300 | 301 | let json = serde_json::to_string(&self) 302 | .expect("json serialization of a TwirpErrorResponse should not fail"); 303 | resp.body(Body::new(json)) 304 | .expect("failed to build TwirpErrorResponse") 305 | } 306 | } 307 | 308 | impl std::fmt::Display for TwirpErrorResponse { 309 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 310 | write!(f, "error {:?}: {}", self.code, self.msg)?; 311 | if !self.meta.is_empty() { 312 | write!(f, " (meta: {{")?; 313 | let mut first = true; 314 | for (k, v) in &self.meta { 315 | if !first { 316 | write!(f, ", ")?; 317 | } 318 | write!(f, "{k:?}: {v:?}")?; 319 | first = false; 320 | } 321 | write!(f, "}})")?; 322 | } 323 | if let Some(ref retry_after) = self.retry_after { 324 | write!(f, " (retry_after: {:?})", retry_after)?; 325 | } 326 | if let Some(ref rust_error) = self.rust_error { 327 | write!(f, " (rust_error: {:?})", rust_error)?; 328 | } 329 | Ok(()) 330 | } 331 | } 332 | 333 | #[cfg(test)] 334 | mod test { 335 | use std::collections::HashMap; 336 | 337 | use crate::{TwirpErrorCode, TwirpErrorResponse}; 338 | 339 | #[test] 340 | fn twirp_status_mapping() { 341 | assert_code(TwirpErrorCode::Canceled, "canceled", 408); 342 | assert_code(TwirpErrorCode::Unknown, "unknown", 500); 343 | assert_code(TwirpErrorCode::InvalidArgument, "invalid_argument", 400); 344 | assert_code(TwirpErrorCode::Malformed, "malformed", 400); 345 | assert_code(TwirpErrorCode::Unauthenticated, "unauthenticated", 401); 346 | assert_code(TwirpErrorCode::PermissionDenied, "permission_denied", 403); 347 | assert_code(TwirpErrorCode::DeadlineExceeded, "deadline_exceeded", 408); 348 | assert_code(TwirpErrorCode::NotFound, "not_found", 404); 349 | assert_code(TwirpErrorCode::BadRoute, "bad_route", 404); 350 | assert_code(TwirpErrorCode::Unimplemented, "unimplemented", 501); 351 | assert_code(TwirpErrorCode::Internal, "internal", 500); 352 | assert_code(TwirpErrorCode::Unavailable, "unavailable", 503); 353 | } 354 | 355 | fn assert_code(code: TwirpErrorCode, msg: &str, http: u16) { 356 | assert_eq!( 357 | code.http_status_code(), 358 | http, 359 | "expected http status code {} but got {}", 360 | http, 361 | code.http_status_code() 362 | ); 363 | assert_eq!( 364 | code.twirp_code(), 365 | msg, 366 | "expected error message '{}' but got '{}'", 367 | msg, 368 | code.twirp_code() 369 | ); 370 | } 371 | 372 | #[test] 373 | fn twirp_error_response_serialization() { 374 | let meta = HashMap::from([ 375 | ("key1".to_string(), "value1".to_string()), 376 | ("key2".to_string(), "value2".to_string()), 377 | ]); 378 | let response = TwirpErrorResponse { 379 | code: TwirpErrorCode::DeadlineExceeded, 380 | msg: "test".to_string(), 381 | meta, 382 | rust_error: None, 383 | retry_after: None, 384 | }; 385 | 386 | let result = serde_json::to_string(&response).unwrap(); 387 | assert!(result.contains(r#""code":"deadline_exceeded""#)); 388 | assert!(result.contains(r#""msg":"test""#)); 389 | assert!(result.contains(r#""key1":"value1""#)); 390 | assert!(result.contains(r#""key2":"value2""#)); 391 | 392 | let result = serde_json::from_str(&result).unwrap(); 393 | assert_eq!(response, result); 394 | } 395 | 396 | #[test] 397 | fn twirp_error_response_serialization_skips_fields() { 398 | let response = TwirpErrorResponse { 399 | code: TwirpErrorCode::Unauthenticated, 400 | msg: "test".to_string(), 401 | meta: HashMap::new(), 402 | rust_error: Some("not included".to_string()), 403 | retry_after: None, 404 | }; 405 | 406 | let result = serde_json::to_string(&response).unwrap(); 407 | assert!(result.contains(r#""code":"unauthenticated""#)); 408 | assert!(result.contains(r#""msg":"test""#)); 409 | assert!(!result.contains(r#"rust_error"#)); 410 | } 411 | } 412 | -------------------------------------------------------------------------------- /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 = "aho-corasick" 7 | version = "1.1.3" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" 10 | dependencies = [ 11 | "memchr", 12 | ] 13 | 14 | [[package]] 15 | name = "anyhow" 16 | version = "1.0.100" 17 | source = "registry+https://github.com/rust-lang/crates.io-index" 18 | checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" 19 | 20 | [[package]] 21 | name = "async-trait" 22 | version = "0.1.89" 23 | source = "registry+https://github.com/rust-lang/crates.io-index" 24 | checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" 25 | dependencies = [ 26 | "proc-macro2", 27 | "quote", 28 | "syn", 29 | ] 30 | 31 | [[package]] 32 | name = "atomic-waker" 33 | version = "1.1.2" 34 | source = "registry+https://github.com/rust-lang/crates.io-index" 35 | checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" 36 | 37 | [[package]] 38 | name = "autocfg" 39 | version = "1.5.0" 40 | source = "registry+https://github.com/rust-lang/crates.io-index" 41 | checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" 42 | 43 | [[package]] 44 | name = "axum" 45 | version = "0.8.7" 46 | source = "registry+https://github.com/rust-lang/crates.io-index" 47 | checksum = "5b098575ebe77cb6d14fc7f32749631a6e44edbef6b796f89b020e99ba20d425" 48 | dependencies = [ 49 | "axum-core", 50 | "bytes", 51 | "form_urlencoded", 52 | "futures-util", 53 | "http", 54 | "http-body", 55 | "http-body-util", 56 | "hyper", 57 | "hyper-util", 58 | "itoa", 59 | "matchit", 60 | "memchr", 61 | "mime", 62 | "percent-encoding", 63 | "pin-project-lite", 64 | "serde_core", 65 | "serde_json", 66 | "serde_path_to_error", 67 | "serde_urlencoded", 68 | "sync_wrapper", 69 | "tokio", 70 | "tower", 71 | "tower-layer", 72 | "tower-service", 73 | "tracing", 74 | ] 75 | 76 | [[package]] 77 | name = "axum-core" 78 | version = "0.5.5" 79 | source = "registry+https://github.com/rust-lang/crates.io-index" 80 | checksum = "59446ce19cd142f8833f856eb31f3eb097812d1479ab224f54d72428ca21ea22" 81 | dependencies = [ 82 | "bytes", 83 | "futures-core", 84 | "http", 85 | "http-body", 86 | "http-body-util", 87 | "mime", 88 | "pin-project-lite", 89 | "sync_wrapper", 90 | "tower-layer", 91 | "tower-service", 92 | "tracing", 93 | ] 94 | 95 | [[package]] 96 | name = "base64" 97 | version = "0.22.1" 98 | source = "registry+https://github.com/rust-lang/crates.io-index" 99 | checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" 100 | 101 | [[package]] 102 | name = "bitflags" 103 | version = "2.9.1" 104 | source = "registry+https://github.com/rust-lang/crates.io-index" 105 | checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967" 106 | 107 | [[package]] 108 | name = "bumpalo" 109 | version = "3.19.0" 110 | source = "registry+https://github.com/rust-lang/crates.io-index" 111 | checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" 112 | 113 | [[package]] 114 | name = "bytes" 115 | version = "1.10.1" 116 | source = "registry+https://github.com/rust-lang/crates.io-index" 117 | checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" 118 | 119 | [[package]] 120 | name = "cfg-if" 121 | version = "1.0.1" 122 | source = "registry+https://github.com/rust-lang/crates.io-index" 123 | checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268" 124 | 125 | [[package]] 126 | name = "chrono" 127 | version = "0.4.41" 128 | source = "registry+https://github.com/rust-lang/crates.io-index" 129 | checksum = "c469d952047f47f91b68d1cba3f10d63c11d73e4636f24f08daf0278abf01c4d" 130 | dependencies = [ 131 | "num-traits", 132 | "serde", 133 | ] 134 | 135 | [[package]] 136 | name = "displaydoc" 137 | version = "0.2.5" 138 | source = "registry+https://github.com/rust-lang/crates.io-index" 139 | checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" 140 | dependencies = [ 141 | "proc-macro2", 142 | "quote", 143 | "syn", 144 | ] 145 | 146 | [[package]] 147 | name = "either" 148 | version = "1.15.0" 149 | source = "registry+https://github.com/rust-lang/crates.io-index" 150 | checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" 151 | 152 | [[package]] 153 | name = "equivalent" 154 | version = "1.0.2" 155 | source = "registry+https://github.com/rust-lang/crates.io-index" 156 | checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" 157 | 158 | [[package]] 159 | name = "erased-serde" 160 | version = "0.4.6" 161 | source = "registry+https://github.com/rust-lang/crates.io-index" 162 | checksum = "e004d887f51fcb9fef17317a2f3525c887d8aa3f4f50fed920816a688284a5b7" 163 | dependencies = [ 164 | "serde", 165 | "typeid", 166 | ] 167 | 168 | [[package]] 169 | name = "errno" 170 | version = "0.3.13" 171 | source = "registry+https://github.com/rust-lang/crates.io-index" 172 | checksum = "778e2ac28f6c47af28e4907f13ffd1e1ddbd400980a9abd7c8df189bf578a5ad" 173 | dependencies = [ 174 | "libc", 175 | "windows-sys 0.60.2", 176 | ] 177 | 178 | [[package]] 179 | name = "example" 180 | version = "0.1.0" 181 | dependencies = [ 182 | "fs-err", 183 | "glob", 184 | "http", 185 | "http-body-util", 186 | "prost", 187 | "prost-build", 188 | "prost-wkt", 189 | "prost-wkt-build", 190 | "prost-wkt-types", 191 | "serde", 192 | "thiserror", 193 | "tokio", 194 | "twirp", 195 | "twirp-build", 196 | ] 197 | 198 | [[package]] 199 | name = "fastrand" 200 | version = "2.3.0" 201 | source = "registry+https://github.com/rust-lang/crates.io-index" 202 | checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" 203 | 204 | [[package]] 205 | name = "fixedbitset" 206 | version = "0.5.7" 207 | source = "registry+https://github.com/rust-lang/crates.io-index" 208 | checksum = "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99" 209 | 210 | [[package]] 211 | name = "form_urlencoded" 212 | version = "1.2.2" 213 | source = "registry+https://github.com/rust-lang/crates.io-index" 214 | checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" 215 | dependencies = [ 216 | "percent-encoding", 217 | ] 218 | 219 | [[package]] 220 | name = "fs-err" 221 | version = "3.2.0" 222 | source = "registry+https://github.com/rust-lang/crates.io-index" 223 | checksum = "62d91fd049c123429b018c47887d3f75a265540dd3c30ba9cb7bae9197edb03a" 224 | dependencies = [ 225 | "autocfg", 226 | ] 227 | 228 | [[package]] 229 | name = "futures" 230 | version = "0.3.31" 231 | source = "registry+https://github.com/rust-lang/crates.io-index" 232 | checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" 233 | dependencies = [ 234 | "futures-channel", 235 | "futures-core", 236 | "futures-executor", 237 | "futures-io", 238 | "futures-sink", 239 | "futures-task", 240 | "futures-util", 241 | ] 242 | 243 | [[package]] 244 | name = "futures-channel" 245 | version = "0.3.31" 246 | source = "registry+https://github.com/rust-lang/crates.io-index" 247 | checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" 248 | dependencies = [ 249 | "futures-core", 250 | "futures-sink", 251 | ] 252 | 253 | [[package]] 254 | name = "futures-core" 255 | version = "0.3.31" 256 | source = "registry+https://github.com/rust-lang/crates.io-index" 257 | checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" 258 | 259 | [[package]] 260 | name = "futures-executor" 261 | version = "0.3.31" 262 | source = "registry+https://github.com/rust-lang/crates.io-index" 263 | checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" 264 | dependencies = [ 265 | "futures-core", 266 | "futures-task", 267 | "futures-util", 268 | ] 269 | 270 | [[package]] 271 | name = "futures-io" 272 | version = "0.3.31" 273 | source = "registry+https://github.com/rust-lang/crates.io-index" 274 | checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" 275 | 276 | [[package]] 277 | name = "futures-macro" 278 | version = "0.3.31" 279 | source = "registry+https://github.com/rust-lang/crates.io-index" 280 | checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" 281 | dependencies = [ 282 | "proc-macro2", 283 | "quote", 284 | "syn", 285 | ] 286 | 287 | [[package]] 288 | name = "futures-sink" 289 | version = "0.3.31" 290 | source = "registry+https://github.com/rust-lang/crates.io-index" 291 | checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" 292 | 293 | [[package]] 294 | name = "futures-task" 295 | version = "0.3.31" 296 | source = "registry+https://github.com/rust-lang/crates.io-index" 297 | checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" 298 | 299 | [[package]] 300 | name = "futures-util" 301 | version = "0.3.31" 302 | source = "registry+https://github.com/rust-lang/crates.io-index" 303 | checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" 304 | dependencies = [ 305 | "futures-channel", 306 | "futures-core", 307 | "futures-io", 308 | "futures-macro", 309 | "futures-sink", 310 | "futures-task", 311 | "memchr", 312 | "pin-project-lite", 313 | "pin-utils", 314 | "slab", 315 | ] 316 | 317 | [[package]] 318 | name = "getrandom" 319 | version = "0.3.3" 320 | source = "registry+https://github.com/rust-lang/crates.io-index" 321 | checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" 322 | dependencies = [ 323 | "cfg-if", 324 | "libc", 325 | "r-efi", 326 | "wasi 0.14.2+wasi-0.2.4", 327 | ] 328 | 329 | [[package]] 330 | name = "glob" 331 | version = "0.3.3" 332 | source = "registry+https://github.com/rust-lang/crates.io-index" 333 | checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" 334 | 335 | [[package]] 336 | name = "hashbrown" 337 | version = "0.15.4" 338 | source = "registry+https://github.com/rust-lang/crates.io-index" 339 | checksum = "5971ac85611da7067dbfcabef3c70ebb5606018acd9e2a3903a0da507521e0d5" 340 | 341 | [[package]] 342 | name = "heck" 343 | version = "0.5.0" 344 | source = "registry+https://github.com/rust-lang/crates.io-index" 345 | checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" 346 | 347 | [[package]] 348 | name = "http" 349 | version = "1.4.0" 350 | source = "registry+https://github.com/rust-lang/crates.io-index" 351 | checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" 352 | dependencies = [ 353 | "bytes", 354 | "itoa", 355 | ] 356 | 357 | [[package]] 358 | name = "http-body" 359 | version = "1.0.1" 360 | source = "registry+https://github.com/rust-lang/crates.io-index" 361 | checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" 362 | dependencies = [ 363 | "bytes", 364 | "http", 365 | ] 366 | 367 | [[package]] 368 | name = "http-body-util" 369 | version = "0.1.3" 370 | source = "registry+https://github.com/rust-lang/crates.io-index" 371 | checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" 372 | dependencies = [ 373 | "bytes", 374 | "futures-core", 375 | "http", 376 | "http-body", 377 | "pin-project-lite", 378 | ] 379 | 380 | [[package]] 381 | name = "httparse" 382 | version = "1.10.1" 383 | source = "registry+https://github.com/rust-lang/crates.io-index" 384 | checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" 385 | 386 | [[package]] 387 | name = "httpdate" 388 | version = "1.0.3" 389 | source = "registry+https://github.com/rust-lang/crates.io-index" 390 | checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" 391 | 392 | [[package]] 393 | name = "hyper" 394 | version = "1.8.1" 395 | source = "registry+https://github.com/rust-lang/crates.io-index" 396 | checksum = "2ab2d4f250c3d7b1c9fcdff1cece94ea4e2dfbec68614f7b87cb205f24ca9d11" 397 | dependencies = [ 398 | "atomic-waker", 399 | "bytes", 400 | "futures-channel", 401 | "futures-core", 402 | "http", 403 | "http-body", 404 | "httparse", 405 | "httpdate", 406 | "itoa", 407 | "pin-project-lite", 408 | "pin-utils", 409 | "smallvec", 410 | "tokio", 411 | "want", 412 | ] 413 | 414 | [[package]] 415 | name = "hyper-util" 416 | version = "0.1.16" 417 | source = "registry+https://github.com/rust-lang/crates.io-index" 418 | checksum = "8d9b05277c7e8da2c93a568989bb6207bef0112e8d17df7a6eda4a3cf143bc5e" 419 | dependencies = [ 420 | "base64", 421 | "bytes", 422 | "futures-channel", 423 | "futures-core", 424 | "futures-util", 425 | "http", 426 | "http-body", 427 | "hyper", 428 | "ipnet", 429 | "libc", 430 | "percent-encoding", 431 | "pin-project-lite", 432 | "socket2", 433 | "tokio", 434 | "tower-service", 435 | "tracing", 436 | ] 437 | 438 | [[package]] 439 | name = "icu_collections" 440 | version = "2.0.0" 441 | source = "registry+https://github.com/rust-lang/crates.io-index" 442 | checksum = "200072f5d0e3614556f94a9930d5dc3e0662a652823904c3a75dc3b0af7fee47" 443 | dependencies = [ 444 | "displaydoc", 445 | "potential_utf", 446 | "yoke", 447 | "zerofrom", 448 | "zerovec", 449 | ] 450 | 451 | [[package]] 452 | name = "icu_locale_core" 453 | version = "2.0.0" 454 | source = "registry+https://github.com/rust-lang/crates.io-index" 455 | checksum = "0cde2700ccaed3872079a65fb1a78f6c0a36c91570f28755dda67bc8f7d9f00a" 456 | dependencies = [ 457 | "displaydoc", 458 | "litemap", 459 | "tinystr", 460 | "writeable", 461 | "zerovec", 462 | ] 463 | 464 | [[package]] 465 | name = "icu_normalizer" 466 | version = "2.0.0" 467 | source = "registry+https://github.com/rust-lang/crates.io-index" 468 | checksum = "436880e8e18df4d7bbc06d58432329d6458cc84531f7ac5f024e93deadb37979" 469 | dependencies = [ 470 | "displaydoc", 471 | "icu_collections", 472 | "icu_normalizer_data", 473 | "icu_properties", 474 | "icu_provider", 475 | "smallvec", 476 | "zerovec", 477 | ] 478 | 479 | [[package]] 480 | name = "icu_normalizer_data" 481 | version = "2.0.0" 482 | source = "registry+https://github.com/rust-lang/crates.io-index" 483 | checksum = "00210d6893afc98edb752b664b8890f0ef174c8adbb8d0be9710fa66fbbf72d3" 484 | 485 | [[package]] 486 | name = "icu_properties" 487 | version = "2.0.1" 488 | source = "registry+https://github.com/rust-lang/crates.io-index" 489 | checksum = "016c619c1eeb94efb86809b015c58f479963de65bdb6253345c1a1276f22e32b" 490 | dependencies = [ 491 | "displaydoc", 492 | "icu_collections", 493 | "icu_locale_core", 494 | "icu_properties_data", 495 | "icu_provider", 496 | "potential_utf", 497 | "zerotrie", 498 | "zerovec", 499 | ] 500 | 501 | [[package]] 502 | name = "icu_properties_data" 503 | version = "2.0.1" 504 | source = "registry+https://github.com/rust-lang/crates.io-index" 505 | checksum = "298459143998310acd25ffe6810ed544932242d3f07083eee1084d83a71bd632" 506 | 507 | [[package]] 508 | name = "icu_provider" 509 | version = "2.0.0" 510 | source = "registry+https://github.com/rust-lang/crates.io-index" 511 | checksum = "03c80da27b5f4187909049ee2d72f276f0d9f99a42c306bd0131ecfe04d8e5af" 512 | dependencies = [ 513 | "displaydoc", 514 | "icu_locale_core", 515 | "stable_deref_trait", 516 | "tinystr", 517 | "writeable", 518 | "yoke", 519 | "zerofrom", 520 | "zerotrie", 521 | "zerovec", 522 | ] 523 | 524 | [[package]] 525 | name = "idna" 526 | version = "1.1.0" 527 | source = "registry+https://github.com/rust-lang/crates.io-index" 528 | checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" 529 | dependencies = [ 530 | "idna_adapter", 531 | "smallvec", 532 | "utf8_iter", 533 | ] 534 | 535 | [[package]] 536 | name = "idna_adapter" 537 | version = "1.2.1" 538 | source = "registry+https://github.com/rust-lang/crates.io-index" 539 | checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" 540 | dependencies = [ 541 | "icu_normalizer", 542 | "icu_properties", 543 | ] 544 | 545 | [[package]] 546 | name = "indexmap" 547 | version = "2.10.0" 548 | source = "registry+https://github.com/rust-lang/crates.io-index" 549 | checksum = "fe4cd85333e22411419a0bcae1297d25e58c9443848b11dc6a86fefe8c78a661" 550 | dependencies = [ 551 | "equivalent", 552 | "hashbrown", 553 | ] 554 | 555 | [[package]] 556 | name = "inventory" 557 | version = "0.3.20" 558 | source = "registry+https://github.com/rust-lang/crates.io-index" 559 | checksum = "ab08d7cd2c5897f2c949e5383ea7c7db03fb19130ffcfbf7eda795137ae3cb83" 560 | dependencies = [ 561 | "rustversion", 562 | ] 563 | 564 | [[package]] 565 | name = "ipnet" 566 | version = "2.11.0" 567 | source = "registry+https://github.com/rust-lang/crates.io-index" 568 | checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" 569 | 570 | [[package]] 571 | name = "iri-string" 572 | version = "0.7.8" 573 | source = "registry+https://github.com/rust-lang/crates.io-index" 574 | checksum = "dbc5ebe9c3a1a7a5127f920a418f7585e9e758e911d0466ed004f393b0e380b2" 575 | dependencies = [ 576 | "memchr", 577 | "serde", 578 | ] 579 | 580 | [[package]] 581 | name = "itertools" 582 | version = "0.14.0" 583 | source = "registry+https://github.com/rust-lang/crates.io-index" 584 | checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" 585 | dependencies = [ 586 | "either", 587 | ] 588 | 589 | [[package]] 590 | name = "itoa" 591 | version = "1.0.15" 592 | source = "registry+https://github.com/rust-lang/crates.io-index" 593 | checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" 594 | 595 | [[package]] 596 | name = "js-sys" 597 | version = "0.3.77" 598 | source = "registry+https://github.com/rust-lang/crates.io-index" 599 | checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" 600 | dependencies = [ 601 | "once_cell", 602 | "wasm-bindgen", 603 | ] 604 | 605 | [[package]] 606 | name = "libc" 607 | version = "0.2.174" 608 | source = "registry+https://github.com/rust-lang/crates.io-index" 609 | checksum = "1171693293099992e19cddea4e8b849964e9846f4acee11b3948bcc337be8776" 610 | 611 | [[package]] 612 | name = "linux-raw-sys" 613 | version = "0.9.4" 614 | source = "registry+https://github.com/rust-lang/crates.io-index" 615 | checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12" 616 | 617 | [[package]] 618 | name = "litemap" 619 | version = "0.8.0" 620 | source = "registry+https://github.com/rust-lang/crates.io-index" 621 | checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956" 622 | 623 | [[package]] 624 | name = "log" 625 | version = "0.4.27" 626 | source = "registry+https://github.com/rust-lang/crates.io-index" 627 | checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" 628 | 629 | [[package]] 630 | name = "matchit" 631 | version = "0.8.4" 632 | source = "registry+https://github.com/rust-lang/crates.io-index" 633 | checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" 634 | 635 | [[package]] 636 | name = "memchr" 637 | version = "2.7.5" 638 | source = "registry+https://github.com/rust-lang/crates.io-index" 639 | checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0" 640 | 641 | [[package]] 642 | name = "mime" 643 | version = "0.3.17" 644 | source = "registry+https://github.com/rust-lang/crates.io-index" 645 | checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" 646 | 647 | [[package]] 648 | name = "mio" 649 | version = "1.0.4" 650 | source = "registry+https://github.com/rust-lang/crates.io-index" 651 | checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c" 652 | dependencies = [ 653 | "libc", 654 | "wasi 0.11.1+wasi-snapshot-preview1", 655 | "windows-sys 0.59.0", 656 | ] 657 | 658 | [[package]] 659 | name = "multimap" 660 | version = "0.10.1" 661 | source = "registry+https://github.com/rust-lang/crates.io-index" 662 | checksum = "1d87ecb2933e8aeadb3e3a02b828fed80a7528047e68b4f424523a0981a3a084" 663 | 664 | [[package]] 665 | name = "num-traits" 666 | version = "0.2.19" 667 | source = "registry+https://github.com/rust-lang/crates.io-index" 668 | checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" 669 | dependencies = [ 670 | "autocfg", 671 | ] 672 | 673 | [[package]] 674 | name = "once_cell" 675 | version = "1.21.3" 676 | source = "registry+https://github.com/rust-lang/crates.io-index" 677 | checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" 678 | 679 | [[package]] 680 | name = "percent-encoding" 681 | version = "2.3.2" 682 | source = "registry+https://github.com/rust-lang/crates.io-index" 683 | checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" 684 | 685 | [[package]] 686 | name = "petgraph" 687 | version = "0.7.1" 688 | source = "registry+https://github.com/rust-lang/crates.io-index" 689 | checksum = "3672b37090dbd86368a4145bc067582552b29c27377cad4e0a306c97f9bd7772" 690 | dependencies = [ 691 | "fixedbitset", 692 | "indexmap", 693 | ] 694 | 695 | [[package]] 696 | name = "pin-project-lite" 697 | version = "0.2.16" 698 | source = "registry+https://github.com/rust-lang/crates.io-index" 699 | checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" 700 | 701 | [[package]] 702 | name = "pin-utils" 703 | version = "0.1.0" 704 | source = "registry+https://github.com/rust-lang/crates.io-index" 705 | checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" 706 | 707 | [[package]] 708 | name = "potential_utf" 709 | version = "0.1.2" 710 | source = "registry+https://github.com/rust-lang/crates.io-index" 711 | checksum = "e5a7c30837279ca13e7c867e9e40053bc68740f988cb07f7ca6df43cc734b585" 712 | dependencies = [ 713 | "zerovec", 714 | ] 715 | 716 | [[package]] 717 | name = "prettyplease" 718 | version = "0.2.37" 719 | source = "registry+https://github.com/rust-lang/crates.io-index" 720 | checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" 721 | dependencies = [ 722 | "proc-macro2", 723 | "syn", 724 | ] 725 | 726 | [[package]] 727 | name = "proc-macro2" 728 | version = "1.0.103" 729 | source = "registry+https://github.com/rust-lang/crates.io-index" 730 | checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8" 731 | dependencies = [ 732 | "unicode-ident", 733 | ] 734 | 735 | [[package]] 736 | name = "prost" 737 | version = "0.14.1" 738 | source = "registry+https://github.com/rust-lang/crates.io-index" 739 | checksum = "7231bd9b3d3d33c86b58adbac74b5ec0ad9f496b19d22801d773636feaa95f3d" 740 | dependencies = [ 741 | "bytes", 742 | "prost-derive", 743 | ] 744 | 745 | [[package]] 746 | name = "prost-build" 747 | version = "0.14.1" 748 | source = "registry+https://github.com/rust-lang/crates.io-index" 749 | checksum = "ac6c3320f9abac597dcbc668774ef006702672474aad53c6d596b62e487b40b1" 750 | dependencies = [ 751 | "heck", 752 | "itertools", 753 | "log", 754 | "multimap", 755 | "once_cell", 756 | "petgraph", 757 | "prettyplease", 758 | "prost", 759 | "prost-types", 760 | "regex", 761 | "syn", 762 | "tempfile", 763 | ] 764 | 765 | [[package]] 766 | name = "prost-derive" 767 | version = "0.14.1" 768 | source = "registry+https://github.com/rust-lang/crates.io-index" 769 | checksum = "9120690fafc389a67ba3803df527d0ec9cbbc9cc45e4cc20b332996dfb672425" 770 | dependencies = [ 771 | "anyhow", 772 | "itertools", 773 | "proc-macro2", 774 | "quote", 775 | "syn", 776 | ] 777 | 778 | [[package]] 779 | name = "prost-types" 780 | version = "0.14.1" 781 | source = "registry+https://github.com/rust-lang/crates.io-index" 782 | checksum = "b9b4db3d6da204ed77bb26ba83b6122a73aeb2e87e25fbf7ad2e84c4ccbf8f72" 783 | dependencies = [ 784 | "prost", 785 | ] 786 | 787 | [[package]] 788 | name = "prost-wkt" 789 | version = "0.7.0" 790 | source = "registry+https://github.com/rust-lang/crates.io-index" 791 | checksum = "655944d0ce015e71b3ec21279437e6a09e58433e50c7b0677901f3d5235e74f5" 792 | dependencies = [ 793 | "chrono", 794 | "inventory", 795 | "prost", 796 | "serde", 797 | "serde_derive", 798 | "serde_json", 799 | "typetag", 800 | ] 801 | 802 | [[package]] 803 | name = "prost-wkt-build" 804 | version = "0.7.0" 805 | source = "registry+https://github.com/rust-lang/crates.io-index" 806 | checksum = "f869f1443fee474b785e935d92e1007f57443e485f51668ed41943fc01a321a2" 807 | dependencies = [ 808 | "heck", 809 | "prost", 810 | "prost-build", 811 | "prost-types", 812 | "quote", 813 | ] 814 | 815 | [[package]] 816 | name = "prost-wkt-types" 817 | version = "0.7.0" 818 | source = "registry+https://github.com/rust-lang/crates.io-index" 819 | checksum = "eeeffd6b9becd4600dd461399f3f71aeda2ff0848802a9ed526cf12e8f42902a" 820 | dependencies = [ 821 | "chrono", 822 | "prost", 823 | "prost-build", 824 | "prost-types", 825 | "prost-wkt", 826 | "prost-wkt-build", 827 | "regex", 828 | "serde", 829 | "serde_derive", 830 | "serde_json", 831 | ] 832 | 833 | [[package]] 834 | name = "quote" 835 | version = "1.0.42" 836 | source = "registry+https://github.com/rust-lang/crates.io-index" 837 | checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f" 838 | dependencies = [ 839 | "proc-macro2", 840 | ] 841 | 842 | [[package]] 843 | name = "r-efi" 844 | version = "5.3.0" 845 | source = "registry+https://github.com/rust-lang/crates.io-index" 846 | checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" 847 | 848 | [[package]] 849 | name = "regex" 850 | version = "1.11.1" 851 | source = "registry+https://github.com/rust-lang/crates.io-index" 852 | checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" 853 | dependencies = [ 854 | "aho-corasick", 855 | "memchr", 856 | "regex-automata", 857 | "regex-syntax", 858 | ] 859 | 860 | [[package]] 861 | name = "regex-automata" 862 | version = "0.4.9" 863 | source = "registry+https://github.com/rust-lang/crates.io-index" 864 | checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" 865 | dependencies = [ 866 | "aho-corasick", 867 | "memchr", 868 | "regex-syntax", 869 | ] 870 | 871 | [[package]] 872 | name = "regex-syntax" 873 | version = "0.8.5" 874 | source = "registry+https://github.com/rust-lang/crates.io-index" 875 | checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" 876 | 877 | [[package]] 878 | name = "reqwest" 879 | version = "0.12.24" 880 | source = "registry+https://github.com/rust-lang/crates.io-index" 881 | checksum = "9d0946410b9f7b082a427e4ef5c8ff541a88b357bc6c637c40db3a68ac70a36f" 882 | dependencies = [ 883 | "base64", 884 | "bytes", 885 | "futures-core", 886 | "http", 887 | "http-body", 888 | "http-body-util", 889 | "hyper", 890 | "hyper-util", 891 | "js-sys", 892 | "log", 893 | "percent-encoding", 894 | "pin-project-lite", 895 | "serde", 896 | "serde_json", 897 | "serde_urlencoded", 898 | "sync_wrapper", 899 | "tokio", 900 | "tower", 901 | "tower-http", 902 | "tower-service", 903 | "url", 904 | "wasm-bindgen", 905 | "wasm-bindgen-futures", 906 | "web-sys", 907 | ] 908 | 909 | [[package]] 910 | name = "rustix" 911 | version = "1.0.8" 912 | source = "registry+https://github.com/rust-lang/crates.io-index" 913 | checksum = "11181fbabf243db407ef8df94a6ce0b2f9a733bd8be4ad02b4eda9602296cac8" 914 | dependencies = [ 915 | "bitflags", 916 | "errno", 917 | "libc", 918 | "linux-raw-sys", 919 | "windows-sys 0.60.2", 920 | ] 921 | 922 | [[package]] 923 | name = "rustversion" 924 | version = "1.0.21" 925 | source = "registry+https://github.com/rust-lang/crates.io-index" 926 | checksum = "8a0d197bd2c9dc6e53b84da9556a69ba4cdfab8619eb41a8bd1cc2027a0f6b1d" 927 | 928 | [[package]] 929 | name = "ryu" 930 | version = "1.0.20" 931 | source = "registry+https://github.com/rust-lang/crates.io-index" 932 | checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" 933 | 934 | [[package]] 935 | name = "serde" 936 | version = "1.0.228" 937 | source = "registry+https://github.com/rust-lang/crates.io-index" 938 | checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" 939 | dependencies = [ 940 | "serde_core", 941 | "serde_derive", 942 | ] 943 | 944 | [[package]] 945 | name = "serde_core" 946 | version = "1.0.228" 947 | source = "registry+https://github.com/rust-lang/crates.io-index" 948 | checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" 949 | dependencies = [ 950 | "serde_derive", 951 | ] 952 | 953 | [[package]] 954 | name = "serde_derive" 955 | version = "1.0.228" 956 | source = "registry+https://github.com/rust-lang/crates.io-index" 957 | checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" 958 | dependencies = [ 959 | "proc-macro2", 960 | "quote", 961 | "syn", 962 | ] 963 | 964 | [[package]] 965 | name = "serde_json" 966 | version = "1.0.145" 967 | source = "registry+https://github.com/rust-lang/crates.io-index" 968 | checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c" 969 | dependencies = [ 970 | "itoa", 971 | "memchr", 972 | "ryu", 973 | "serde", 974 | "serde_core", 975 | ] 976 | 977 | [[package]] 978 | name = "serde_path_to_error" 979 | version = "0.1.17" 980 | source = "registry+https://github.com/rust-lang/crates.io-index" 981 | checksum = "59fab13f937fa393d08645bf3a84bdfe86e296747b506ada67bb15f10f218b2a" 982 | dependencies = [ 983 | "itoa", 984 | "serde", 985 | ] 986 | 987 | [[package]] 988 | name = "serde_urlencoded" 989 | version = "0.7.1" 990 | source = "registry+https://github.com/rust-lang/crates.io-index" 991 | checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" 992 | dependencies = [ 993 | "form_urlencoded", 994 | "itoa", 995 | "ryu", 996 | "serde", 997 | ] 998 | 999 | [[package]] 1000 | name = "slab" 1001 | version = "0.4.11" 1002 | source = "registry+https://github.com/rust-lang/crates.io-index" 1003 | checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" 1004 | 1005 | [[package]] 1006 | name = "smallvec" 1007 | version = "1.15.1" 1008 | source = "registry+https://github.com/rust-lang/crates.io-index" 1009 | checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" 1010 | 1011 | [[package]] 1012 | name = "socket2" 1013 | version = "0.6.0" 1014 | source = "registry+https://github.com/rust-lang/crates.io-index" 1015 | checksum = "233504af464074f9d066d7b5416c5f9b894a5862a6506e306f7b816cdd6f1807" 1016 | dependencies = [ 1017 | "libc", 1018 | "windows-sys 0.59.0", 1019 | ] 1020 | 1021 | [[package]] 1022 | name = "stable_deref_trait" 1023 | version = "1.2.0" 1024 | source = "registry+https://github.com/rust-lang/crates.io-index" 1025 | checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" 1026 | 1027 | [[package]] 1028 | name = "syn" 1029 | version = "2.0.111" 1030 | source = "registry+https://github.com/rust-lang/crates.io-index" 1031 | checksum = "390cc9a294ab71bdb1aa2e99d13be9c753cd2d7bd6560c77118597410c4d2e87" 1032 | dependencies = [ 1033 | "proc-macro2", 1034 | "quote", 1035 | "unicode-ident", 1036 | ] 1037 | 1038 | [[package]] 1039 | name = "sync_wrapper" 1040 | version = "1.0.2" 1041 | source = "registry+https://github.com/rust-lang/crates.io-index" 1042 | checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" 1043 | dependencies = [ 1044 | "futures-core", 1045 | ] 1046 | 1047 | [[package]] 1048 | name = "synstructure" 1049 | version = "0.13.2" 1050 | source = "registry+https://github.com/rust-lang/crates.io-index" 1051 | checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" 1052 | dependencies = [ 1053 | "proc-macro2", 1054 | "quote", 1055 | "syn", 1056 | ] 1057 | 1058 | [[package]] 1059 | name = "tempfile" 1060 | version = "3.20.0" 1061 | source = "registry+https://github.com/rust-lang/crates.io-index" 1062 | checksum = "e8a64e3985349f2441a1a9ef0b853f869006c3855f2cda6862a94d26ebb9d6a1" 1063 | dependencies = [ 1064 | "fastrand", 1065 | "getrandom", 1066 | "once_cell", 1067 | "rustix", 1068 | "windows-sys 0.59.0", 1069 | ] 1070 | 1071 | [[package]] 1072 | name = "thiserror" 1073 | version = "2.0.17" 1074 | source = "registry+https://github.com/rust-lang/crates.io-index" 1075 | checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8" 1076 | dependencies = [ 1077 | "thiserror-impl", 1078 | ] 1079 | 1080 | [[package]] 1081 | name = "thiserror-impl" 1082 | version = "2.0.17" 1083 | source = "registry+https://github.com/rust-lang/crates.io-index" 1084 | checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" 1085 | dependencies = [ 1086 | "proc-macro2", 1087 | "quote", 1088 | "syn", 1089 | ] 1090 | 1091 | [[package]] 1092 | name = "tinystr" 1093 | version = "0.8.1" 1094 | source = "registry+https://github.com/rust-lang/crates.io-index" 1095 | checksum = "5d4f6d1145dcb577acf783d4e601bc1d76a13337bb54e6233add580b07344c8b" 1096 | dependencies = [ 1097 | "displaydoc", 1098 | "zerovec", 1099 | ] 1100 | 1101 | [[package]] 1102 | name = "tokio" 1103 | version = "1.48.0" 1104 | source = "registry+https://github.com/rust-lang/crates.io-index" 1105 | checksum = "ff360e02eab121e0bc37a2d3b4d4dc622e6eda3a8e5253d5435ecf5bd4c68408" 1106 | dependencies = [ 1107 | "libc", 1108 | "mio", 1109 | "pin-project-lite", 1110 | "socket2", 1111 | "tokio-macros", 1112 | "windows-sys 0.61.2", 1113 | ] 1114 | 1115 | [[package]] 1116 | name = "tokio-macros" 1117 | version = "2.6.0" 1118 | source = "registry+https://github.com/rust-lang/crates.io-index" 1119 | checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" 1120 | dependencies = [ 1121 | "proc-macro2", 1122 | "quote", 1123 | "syn", 1124 | ] 1125 | 1126 | [[package]] 1127 | name = "tower" 1128 | version = "0.5.2" 1129 | source = "registry+https://github.com/rust-lang/crates.io-index" 1130 | checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" 1131 | dependencies = [ 1132 | "futures-core", 1133 | "futures-util", 1134 | "pin-project-lite", 1135 | "sync_wrapper", 1136 | "tokio", 1137 | "tower-layer", 1138 | "tower-service", 1139 | "tracing", 1140 | ] 1141 | 1142 | [[package]] 1143 | name = "tower-http" 1144 | version = "0.6.6" 1145 | source = "registry+https://github.com/rust-lang/crates.io-index" 1146 | checksum = "adc82fd73de2a9722ac5da747f12383d2bfdb93591ee6c58486e0097890f05f2" 1147 | dependencies = [ 1148 | "bitflags", 1149 | "bytes", 1150 | "futures-util", 1151 | "http", 1152 | "http-body", 1153 | "iri-string", 1154 | "pin-project-lite", 1155 | "tower", 1156 | "tower-layer", 1157 | "tower-service", 1158 | ] 1159 | 1160 | [[package]] 1161 | name = "tower-layer" 1162 | version = "0.3.3" 1163 | source = "registry+https://github.com/rust-lang/crates.io-index" 1164 | checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" 1165 | 1166 | [[package]] 1167 | name = "tower-service" 1168 | version = "0.3.3" 1169 | source = "registry+https://github.com/rust-lang/crates.io-index" 1170 | checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" 1171 | 1172 | [[package]] 1173 | name = "tracing" 1174 | version = "0.1.41" 1175 | source = "registry+https://github.com/rust-lang/crates.io-index" 1176 | checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" 1177 | dependencies = [ 1178 | "log", 1179 | "pin-project-lite", 1180 | "tracing-core", 1181 | ] 1182 | 1183 | [[package]] 1184 | name = "tracing-core" 1185 | version = "0.1.34" 1186 | source = "registry+https://github.com/rust-lang/crates.io-index" 1187 | checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678" 1188 | dependencies = [ 1189 | "once_cell", 1190 | ] 1191 | 1192 | [[package]] 1193 | name = "try-lock" 1194 | version = "0.2.5" 1195 | source = "registry+https://github.com/rust-lang/crates.io-index" 1196 | checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" 1197 | 1198 | [[package]] 1199 | name = "twirp" 1200 | version = "0.10.1" 1201 | dependencies = [ 1202 | "anyhow", 1203 | "async-trait", 1204 | "axum", 1205 | "futures", 1206 | "http", 1207 | "http-body-util", 1208 | "hyper", 1209 | "prost", 1210 | "reqwest", 1211 | "serde", 1212 | "serde_json", 1213 | "thiserror", 1214 | "tokio", 1215 | "tower", 1216 | "url", 1217 | ] 1218 | 1219 | [[package]] 1220 | name = "twirp-build" 1221 | version = "0.10.0" 1222 | dependencies = [ 1223 | "prettyplease", 1224 | "proc-macro2", 1225 | "prost-build", 1226 | "quote", 1227 | "syn", 1228 | ] 1229 | 1230 | [[package]] 1231 | name = "typeid" 1232 | version = "1.0.3" 1233 | source = "registry+https://github.com/rust-lang/crates.io-index" 1234 | checksum = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c" 1235 | 1236 | [[package]] 1237 | name = "typetag" 1238 | version = "0.2.20" 1239 | source = "registry+https://github.com/rust-lang/crates.io-index" 1240 | checksum = "73f22b40dd7bfe8c14230cf9702081366421890435b2d625fa92b4acc4c3de6f" 1241 | dependencies = [ 1242 | "erased-serde", 1243 | "inventory", 1244 | "once_cell", 1245 | "serde", 1246 | "typetag-impl", 1247 | ] 1248 | 1249 | [[package]] 1250 | name = "typetag-impl" 1251 | version = "0.2.20" 1252 | source = "registry+https://github.com/rust-lang/crates.io-index" 1253 | checksum = "35f5380909ffc31b4de4f4bdf96b877175a016aa2ca98cee39fcfd8c4d53d952" 1254 | dependencies = [ 1255 | "proc-macro2", 1256 | "quote", 1257 | "syn", 1258 | ] 1259 | 1260 | [[package]] 1261 | name = "unicode-ident" 1262 | version = "1.0.18" 1263 | source = "registry+https://github.com/rust-lang/crates.io-index" 1264 | checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" 1265 | 1266 | [[package]] 1267 | name = "url" 1268 | version = "2.5.7" 1269 | source = "registry+https://github.com/rust-lang/crates.io-index" 1270 | checksum = "08bc136a29a3d1758e07a9cca267be308aeebf5cfd5a10f3f67ab2097683ef5b" 1271 | dependencies = [ 1272 | "form_urlencoded", 1273 | "idna", 1274 | "percent-encoding", 1275 | "serde", 1276 | ] 1277 | 1278 | [[package]] 1279 | name = "utf8_iter" 1280 | version = "1.0.4" 1281 | source = "registry+https://github.com/rust-lang/crates.io-index" 1282 | checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" 1283 | 1284 | [[package]] 1285 | name = "want" 1286 | version = "0.3.1" 1287 | source = "registry+https://github.com/rust-lang/crates.io-index" 1288 | checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" 1289 | dependencies = [ 1290 | "try-lock", 1291 | ] 1292 | 1293 | [[package]] 1294 | name = "wasi" 1295 | version = "0.11.1+wasi-snapshot-preview1" 1296 | source = "registry+https://github.com/rust-lang/crates.io-index" 1297 | checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" 1298 | 1299 | [[package]] 1300 | name = "wasi" 1301 | version = "0.14.2+wasi-0.2.4" 1302 | source = "registry+https://github.com/rust-lang/crates.io-index" 1303 | checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3" 1304 | dependencies = [ 1305 | "wit-bindgen-rt", 1306 | ] 1307 | 1308 | [[package]] 1309 | name = "wasm-bindgen" 1310 | version = "0.2.100" 1311 | source = "registry+https://github.com/rust-lang/crates.io-index" 1312 | checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" 1313 | dependencies = [ 1314 | "cfg-if", 1315 | "once_cell", 1316 | "rustversion", 1317 | "wasm-bindgen-macro", 1318 | ] 1319 | 1320 | [[package]] 1321 | name = "wasm-bindgen-backend" 1322 | version = "0.2.100" 1323 | source = "registry+https://github.com/rust-lang/crates.io-index" 1324 | checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" 1325 | dependencies = [ 1326 | "bumpalo", 1327 | "log", 1328 | "proc-macro2", 1329 | "quote", 1330 | "syn", 1331 | "wasm-bindgen-shared", 1332 | ] 1333 | 1334 | [[package]] 1335 | name = "wasm-bindgen-futures" 1336 | version = "0.4.50" 1337 | source = "registry+https://github.com/rust-lang/crates.io-index" 1338 | checksum = "555d470ec0bc3bb57890405e5d4322cc9ea83cebb085523ced7be4144dac1e61" 1339 | dependencies = [ 1340 | "cfg-if", 1341 | "js-sys", 1342 | "once_cell", 1343 | "wasm-bindgen", 1344 | "web-sys", 1345 | ] 1346 | 1347 | [[package]] 1348 | name = "wasm-bindgen-macro" 1349 | version = "0.2.100" 1350 | source = "registry+https://github.com/rust-lang/crates.io-index" 1351 | checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" 1352 | dependencies = [ 1353 | "quote", 1354 | "wasm-bindgen-macro-support", 1355 | ] 1356 | 1357 | [[package]] 1358 | name = "wasm-bindgen-macro-support" 1359 | version = "0.2.100" 1360 | source = "registry+https://github.com/rust-lang/crates.io-index" 1361 | checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" 1362 | dependencies = [ 1363 | "proc-macro2", 1364 | "quote", 1365 | "syn", 1366 | "wasm-bindgen-backend", 1367 | "wasm-bindgen-shared", 1368 | ] 1369 | 1370 | [[package]] 1371 | name = "wasm-bindgen-shared" 1372 | version = "0.2.100" 1373 | source = "registry+https://github.com/rust-lang/crates.io-index" 1374 | checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" 1375 | dependencies = [ 1376 | "unicode-ident", 1377 | ] 1378 | 1379 | [[package]] 1380 | name = "web-sys" 1381 | version = "0.3.77" 1382 | source = "registry+https://github.com/rust-lang/crates.io-index" 1383 | checksum = "33b6dd2ef9186f1f2072e409e99cd22a975331a6b3591b12c764e0e55c60d5d2" 1384 | dependencies = [ 1385 | "js-sys", 1386 | "wasm-bindgen", 1387 | ] 1388 | 1389 | [[package]] 1390 | name = "windows-link" 1391 | version = "0.1.3" 1392 | source = "registry+https://github.com/rust-lang/crates.io-index" 1393 | checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" 1394 | 1395 | [[package]] 1396 | name = "windows-link" 1397 | version = "0.2.1" 1398 | source = "registry+https://github.com/rust-lang/crates.io-index" 1399 | checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" 1400 | 1401 | [[package]] 1402 | name = "windows-sys" 1403 | version = "0.59.0" 1404 | source = "registry+https://github.com/rust-lang/crates.io-index" 1405 | checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" 1406 | dependencies = [ 1407 | "windows-targets 0.52.6", 1408 | ] 1409 | 1410 | [[package]] 1411 | name = "windows-sys" 1412 | version = "0.60.2" 1413 | source = "registry+https://github.com/rust-lang/crates.io-index" 1414 | checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" 1415 | dependencies = [ 1416 | "windows-targets 0.53.3", 1417 | ] 1418 | 1419 | [[package]] 1420 | name = "windows-sys" 1421 | version = "0.61.2" 1422 | source = "registry+https://github.com/rust-lang/crates.io-index" 1423 | checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" 1424 | dependencies = [ 1425 | "windows-link 0.2.1", 1426 | ] 1427 | 1428 | [[package]] 1429 | name = "windows-targets" 1430 | version = "0.52.6" 1431 | source = "registry+https://github.com/rust-lang/crates.io-index" 1432 | checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" 1433 | dependencies = [ 1434 | "windows_aarch64_gnullvm 0.52.6", 1435 | "windows_aarch64_msvc 0.52.6", 1436 | "windows_i686_gnu 0.52.6", 1437 | "windows_i686_gnullvm 0.52.6", 1438 | "windows_i686_msvc 0.52.6", 1439 | "windows_x86_64_gnu 0.52.6", 1440 | "windows_x86_64_gnullvm 0.52.6", 1441 | "windows_x86_64_msvc 0.52.6", 1442 | ] 1443 | 1444 | [[package]] 1445 | name = "windows-targets" 1446 | version = "0.53.3" 1447 | source = "registry+https://github.com/rust-lang/crates.io-index" 1448 | checksum = "d5fe6031c4041849d7c496a8ded650796e7b6ecc19df1a431c1a363342e5dc91" 1449 | dependencies = [ 1450 | "windows-link 0.1.3", 1451 | "windows_aarch64_gnullvm 0.53.0", 1452 | "windows_aarch64_msvc 0.53.0", 1453 | "windows_i686_gnu 0.53.0", 1454 | "windows_i686_gnullvm 0.53.0", 1455 | "windows_i686_msvc 0.53.0", 1456 | "windows_x86_64_gnu 0.53.0", 1457 | "windows_x86_64_gnullvm 0.53.0", 1458 | "windows_x86_64_msvc 0.53.0", 1459 | ] 1460 | 1461 | [[package]] 1462 | name = "windows_aarch64_gnullvm" 1463 | version = "0.52.6" 1464 | source = "registry+https://github.com/rust-lang/crates.io-index" 1465 | checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" 1466 | 1467 | [[package]] 1468 | name = "windows_aarch64_gnullvm" 1469 | version = "0.53.0" 1470 | source = "registry+https://github.com/rust-lang/crates.io-index" 1471 | checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764" 1472 | 1473 | [[package]] 1474 | name = "windows_aarch64_msvc" 1475 | version = "0.52.6" 1476 | source = "registry+https://github.com/rust-lang/crates.io-index" 1477 | checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" 1478 | 1479 | [[package]] 1480 | name = "windows_aarch64_msvc" 1481 | version = "0.53.0" 1482 | source = "registry+https://github.com/rust-lang/crates.io-index" 1483 | checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c" 1484 | 1485 | [[package]] 1486 | name = "windows_i686_gnu" 1487 | version = "0.52.6" 1488 | source = "registry+https://github.com/rust-lang/crates.io-index" 1489 | checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" 1490 | 1491 | [[package]] 1492 | name = "windows_i686_gnu" 1493 | version = "0.53.0" 1494 | source = "registry+https://github.com/rust-lang/crates.io-index" 1495 | checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3" 1496 | 1497 | [[package]] 1498 | name = "windows_i686_gnullvm" 1499 | version = "0.52.6" 1500 | source = "registry+https://github.com/rust-lang/crates.io-index" 1501 | checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" 1502 | 1503 | [[package]] 1504 | name = "windows_i686_gnullvm" 1505 | version = "0.53.0" 1506 | source = "registry+https://github.com/rust-lang/crates.io-index" 1507 | checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11" 1508 | 1509 | [[package]] 1510 | name = "windows_i686_msvc" 1511 | version = "0.52.6" 1512 | source = "registry+https://github.com/rust-lang/crates.io-index" 1513 | checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" 1514 | 1515 | [[package]] 1516 | name = "windows_i686_msvc" 1517 | version = "0.53.0" 1518 | source = "registry+https://github.com/rust-lang/crates.io-index" 1519 | checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d" 1520 | 1521 | [[package]] 1522 | name = "windows_x86_64_gnu" 1523 | version = "0.52.6" 1524 | source = "registry+https://github.com/rust-lang/crates.io-index" 1525 | checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" 1526 | 1527 | [[package]] 1528 | name = "windows_x86_64_gnu" 1529 | version = "0.53.0" 1530 | source = "registry+https://github.com/rust-lang/crates.io-index" 1531 | checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba" 1532 | 1533 | [[package]] 1534 | name = "windows_x86_64_gnullvm" 1535 | version = "0.52.6" 1536 | source = "registry+https://github.com/rust-lang/crates.io-index" 1537 | checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" 1538 | 1539 | [[package]] 1540 | name = "windows_x86_64_gnullvm" 1541 | version = "0.53.0" 1542 | source = "registry+https://github.com/rust-lang/crates.io-index" 1543 | checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57" 1544 | 1545 | [[package]] 1546 | name = "windows_x86_64_msvc" 1547 | version = "0.52.6" 1548 | source = "registry+https://github.com/rust-lang/crates.io-index" 1549 | checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" 1550 | 1551 | [[package]] 1552 | name = "windows_x86_64_msvc" 1553 | version = "0.53.0" 1554 | source = "registry+https://github.com/rust-lang/crates.io-index" 1555 | checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" 1556 | 1557 | [[package]] 1558 | name = "wit-bindgen-rt" 1559 | version = "0.39.0" 1560 | source = "registry+https://github.com/rust-lang/crates.io-index" 1561 | checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" 1562 | dependencies = [ 1563 | "bitflags", 1564 | ] 1565 | 1566 | [[package]] 1567 | name = "writeable" 1568 | version = "0.6.1" 1569 | source = "registry+https://github.com/rust-lang/crates.io-index" 1570 | checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb" 1571 | 1572 | [[package]] 1573 | name = "yoke" 1574 | version = "0.8.0" 1575 | source = "registry+https://github.com/rust-lang/crates.io-index" 1576 | checksum = "5f41bb01b8226ef4bfd589436a297c53d118f65921786300e427be8d487695cc" 1577 | dependencies = [ 1578 | "serde", 1579 | "stable_deref_trait", 1580 | "yoke-derive", 1581 | "zerofrom", 1582 | ] 1583 | 1584 | [[package]] 1585 | name = "yoke-derive" 1586 | version = "0.8.0" 1587 | source = "registry+https://github.com/rust-lang/crates.io-index" 1588 | checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6" 1589 | dependencies = [ 1590 | "proc-macro2", 1591 | "quote", 1592 | "syn", 1593 | "synstructure", 1594 | ] 1595 | 1596 | [[package]] 1597 | name = "zerofrom" 1598 | version = "0.1.6" 1599 | source = "registry+https://github.com/rust-lang/crates.io-index" 1600 | checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" 1601 | dependencies = [ 1602 | "zerofrom-derive", 1603 | ] 1604 | 1605 | [[package]] 1606 | name = "zerofrom-derive" 1607 | version = "0.1.6" 1608 | source = "registry+https://github.com/rust-lang/crates.io-index" 1609 | checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" 1610 | dependencies = [ 1611 | "proc-macro2", 1612 | "quote", 1613 | "syn", 1614 | "synstructure", 1615 | ] 1616 | 1617 | [[package]] 1618 | name = "zerotrie" 1619 | version = "0.2.2" 1620 | source = "registry+https://github.com/rust-lang/crates.io-index" 1621 | checksum = "36f0bbd478583f79edad978b407914f61b2972f5af6fa089686016be8f9af595" 1622 | dependencies = [ 1623 | "displaydoc", 1624 | "yoke", 1625 | "zerofrom", 1626 | ] 1627 | 1628 | [[package]] 1629 | name = "zerovec" 1630 | version = "0.11.2" 1631 | source = "registry+https://github.com/rust-lang/crates.io-index" 1632 | checksum = "4a05eb080e015ba39cc9e23bbe5e7fb04d5fb040350f99f34e338d5fdd294428" 1633 | dependencies = [ 1634 | "yoke", 1635 | "zerofrom", 1636 | "zerovec-derive", 1637 | ] 1638 | 1639 | [[package]] 1640 | name = "zerovec-derive" 1641 | version = "0.11.1" 1642 | source = "registry+https://github.com/rust-lang/crates.io-index" 1643 | checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f" 1644 | dependencies = [ 1645 | "proc-macro2", 1646 | "quote", 1647 | "syn", 1648 | ] 1649 | --------------------------------------------------------------------------------