├── .github └── workflows │ └── rust.yml ├── .gitignore ├── .gitmodules ├── .gitpod.yml ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── README.md ├── crates ├── hrpc-build │ ├── Cargo.toml │ ├── README.md │ └── src │ │ ├── TONIC_LICENSE │ │ ├── client.rs │ │ ├── lib.rs │ │ ├── prost.rs │ │ └── server.rs ├── hrpc-proc-macro │ ├── Cargo.toml │ └── src │ │ └── lib.rs └── hrpc │ ├── Cargo.toml │ ├── README.md │ ├── build.rs │ └── src │ ├── body.rs │ ├── client │ ├── boxed.rs │ ├── error.rs │ ├── layer │ │ ├── backoff.rs │ │ └── mod.rs │ ├── mod.rs │ ├── socket.rs │ └── transport │ │ ├── http │ │ ├── hyper.rs │ │ ├── mod.rs │ │ └── wasm.rs │ │ ├── mock.rs │ │ └── mod.rs │ ├── common │ ├── buf.rs │ ├── extensions.rs │ ├── future.rs │ ├── layer │ │ ├── mod.rs │ │ ├── modify.rs │ │ └── trace.rs │ ├── mod.rs │ ├── socket.rs │ └── transport │ │ ├── http.rs │ │ ├── mock.rs │ │ ├── mod.rs │ │ ├── tokio_tungstenite.rs │ │ └── ws_wasm.rs │ ├── decode.rs │ ├── encode.rs │ ├── lib.rs │ ├── proto.rs │ ├── request.rs │ ├── response.rs │ └── server │ ├── error.rs │ ├── layer │ ├── mod.rs │ └── ratelimit.rs │ ├── macros.rs │ ├── mod.rs │ ├── router.rs │ ├── service.rs │ ├── socket.rs │ ├── transport │ ├── http │ │ ├── impl.rs │ │ ├── layer │ │ │ ├── errid_to_status.rs │ │ │ └── mod.rs │ │ ├── mod.rs │ │ └── ws.rs │ ├── mock.rs │ └── mod.rs │ └── utils.rs ├── examples ├── chat │ ├── README.md │ ├── client │ │ ├── Cargo.toml │ │ └── src │ │ │ └── main.rs │ ├── common │ │ ├── Cargo.toml │ │ ├── build.rs │ │ └── src │ │ │ ├── chat.proto │ │ │ └── lib.rs │ ├── server │ │ ├── Cargo.toml │ │ └── src │ │ │ └── main.rs │ └── wasm_client │ │ ├── Cargo.toml │ │ ├── index.html │ │ └── src │ │ └── main.rs ├── hello_world │ ├── Cargo.toml │ ├── build.rs │ └── src │ │ ├── bin │ │ ├── client.rs │ │ └── server.rs │ │ ├── hello.proto │ │ └── lib.rs ├── interop │ ├── Cargo.toml │ ├── build.rs │ ├── proto │ │ └── test.proto │ └── src │ │ └── main.rs └── mock │ ├── Cargo.toml │ ├── README.md │ ├── build.rs │ └── src │ ├── hello.proto │ ├── lib.rs │ └── main.rs ├── flake.lock ├── flake.nix ├── rust-toolchain.toml └── shell.nix /.github/workflows/rust.yml: -------------------------------------------------------------------------------- 1 | name: Rust 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | paths-ignore: 7 | - 'README.md' 8 | - '**/*.nix' 9 | - 'nix/envrc' 10 | - 'flake.lock' 11 | pull_request: 12 | branches: [ master ] 13 | paths-ignore: 14 | - 'README.md' 15 | - '**/*.nix' 16 | - 'nix/envrc' 17 | - 'flake.lock' 18 | 19 | env: 20 | CARGO_TERM_COLOR: always 21 | 22 | jobs: 23 | all: 24 | runs-on: ubuntu-latest 25 | steps: 26 | - name: Checkout repo 27 | uses: actions/checkout@v2 28 | with: 29 | submodules: true 30 | 31 | - name: Install dependencies 32 | run: | 33 | sudo apt update -yy 34 | sudo apt install -yy --no-install-recommends protobuf-compiler 35 | - name: Install rust 36 | run: rustup default stable && rustup update && rustup component add rustfmt clippy 37 | 38 | - name: Cache rust 39 | uses: Swatinem/rust-cache@v1 40 | 41 | - name: Build common hrpc 42 | run: cargo build --package hrpc --features server,client --verbose 43 | 44 | - name: Test common hrpc 45 | run: RUST_LOG=info cargo test --package hrpc --features server,client --verbose 46 | 47 | - name: Test all targets 48 | run: RUST_LOG=info cargo test --all-targets --verbose 49 | 50 | - name: Doc 51 | run: cargo doc --no-deps --all-features --verbose 52 | 53 | - name: Finalize documentation 54 | run: | 55 | CRATE_NAME=$(echo '${{ github.repository }}' | tr '[:upper:]' '[:lower:]' | cut -f2 -d"/") 56 | echo "" > target/doc/index.html 57 | touch target/doc/.nojekyll 58 | 59 | - name: Publish docs 60 | if: ${{ github.event_name == 'push' }} 61 | uses: peaceiris/actions-gh-pages@v3 62 | with: 63 | github_token: ${{ secrets.GITHUB_TOKEN }} 64 | publish_dir: target/doc/ 65 | publish_branch: gh-pages 66 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Rust 2 | /target/ 3 | **/*.rs.bk 4 | examples/chat/wasm_client/dist 5 | 6 | # Nix 7 | /result* 8 | /nix/result* 9 | 10 | # Direnv 11 | /.direnv 12 | /.envrc 13 | 14 | # Editor 15 | /.vscode -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "crates/hrpc/hrpc-main"] 2 | path = crates/hrpc/hrpc-main 3 | url = https://github.com/harmony-development/hrpc.git 4 | -------------------------------------------------------------------------------- /.gitpod.yml: -------------------------------------------------------------------------------- 1 | # This configuration file was automatically generated by Gitpod. 2 | # Please adjust to your needs (see https://www.gitpod.io/docs/config-gitpod-file) 3 | # and commit this file to your remote git repository to share the goodness with others. 4 | 5 | tasks: 6 | - init: echo "init" 7 | 8 | 9 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = [ 3 | "crates/*", 4 | "examples/interop", 5 | "examples/hello_world", 6 | "examples/mock", 7 | "examples/chat/*", 8 | ] 9 | resolver = "2" 10 | 11 | [workspace.metadata.nix] 12 | systems = ["x86_64-linux"] 13 | devshell.packages = ["cargo-watch", "trunk"] 14 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2021 Yusuf Bera Ertan 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![crates.io](https://img.shields.io/crates/v/hrpc)](https://crates.io/crates/hrpc) [![release docs](https://img.shields.io/docsrs/hrpc)](https://docs.rs/hrpc) [![docs](https://img.shields.io/badge/docs-master-blue)](https://harmonyapp.io/hrpc-rs) 2 | 3 | # hrpc-rs 4 | 5 | This repo contains an implementation of [hRPC](https://github.com/harmony-development/hrpc) in Rust: 6 | - `crates/hrpc` contains generic server / client code and transports, 7 | - `crates/hrpc-build` is contains codegen code that generates client or server code, 8 | - `examples/interop` is used to test the implementation against itself and other servers. 9 | - `examples` contains commented examples. 10 | - To run an example's server: `cargo run --package example_name --bin server` 11 | - To run an example's client: `cargo run --package example_name --bin client` 12 | 13 | ## Getting started 14 | 15 | Check out the [chat](./examples/chat) example! -------------------------------------------------------------------------------- /crates/hrpc-build/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "hrpc-build" 3 | version = "0.33.1" 4 | authors = ["Yusuf Bera Ertan "] 5 | edition = "2021" 6 | license = "MIT" 7 | repository = "https://github.com/harmony-development/hrpc-rs" 8 | homepage = "https://github.com/harmony-development/hrpc-rs" 9 | description = "Code generation for hRPC." 10 | keywords = ["hrpc", "harmony", "rpc", "build", "protobuf"] 11 | categories = ["development-tools::build-utils"] 12 | 13 | [dependencies] 14 | prost-build = "0.10" 15 | syn = "1.0" 16 | quote = "1.0" 17 | proc-macro2 = "1.0" 18 | 19 | [features] 20 | default = ["rustfmt"] 21 | rustfmt = [] 22 | 23 | server = [] 24 | client = [] 25 | 26 | # Shorthand to enable `server_default_transport_http` and `client_default_transport_hyper_http` 27 | default_transport_http = [ 28 | "server_default_transport_http", 29 | "client_default_transport_hyper_http", 30 | ] 31 | 32 | server_default_transport_http = [] 33 | client_default_transport_hyper_http = [] 34 | client_default_transport_wasm_http = [] 35 | 36 | [package.metadata.docs.rs] 37 | all-features = true 38 | 39 | [package.metadata.nix] 40 | build = true 41 | library = true -------------------------------------------------------------------------------- /crates/hrpc-build/README.md: -------------------------------------------------------------------------------- 1 | # hrpc-build 2 | 3 | Contains code generation (protobuf, server and client) for hRPC. -------------------------------------------------------------------------------- /crates/hrpc-build/src/TONIC_LICENSE: -------------------------------------------------------------------------------- 1 | `hrpc-build` uses code from `tonic-build`. `tonic-build`'s 2 | license is included below: 3 | 4 | Copyright (c) 2020 Lucio Franco 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in 14 | all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /crates/hrpc-build/src/client.rs: -------------------------------------------------------------------------------- 1 | use super::{Method, Service}; 2 | use crate::{generate_doc_comments, naive_snake_case}; 3 | use proc_macro2::TokenStream; 4 | use quote::{format_ident, quote}; 5 | 6 | /// Generate service for client. 7 | /// 8 | /// This takes some `Service` and will generate a `TokenStream` that contains 9 | /// a public module with the generated client. 10 | pub fn generate(service: &T, proto_path: &str) -> TokenStream { 11 | let service_ident = quote::format_ident!("{}Client", service.name()); 12 | let client_mod = quote::format_ident!("{}_client", naive_snake_case(service.name())); 13 | let methods = generate_methods(service, proto_path); 14 | 15 | let service_doc = generate_doc_comments(service.comment()); 16 | 17 | let create_methods = quote! { 18 | impl #service_ident { 19 | /// Create a new client using the provided transport. 20 | pub fn new_transport(transport: Inner) -> Self { 21 | Self { 22 | inner: Client::new(transport) 23 | } 24 | } 25 | 26 | /// Create a new client using the provided generic client. 27 | pub fn new_inner(client: Client) -> Self { 28 | Self { 29 | inner: client, 30 | } 31 | } 32 | } 33 | }; 34 | 35 | #[allow(unused_mut)] 36 | let mut def_transport_impl = TokenStream::new(); 37 | 38 | #[cfg(feature = "client_default_transport_hyper_http")] 39 | def_transport_impl.extend(quote! { 40 | use hrpc::{client::transport::http::{Hyper, HyperError}, exports::http::Uri}; 41 | 42 | impl #service_ident { 43 | /// Create a new client using HTTP transport. 44 | /// 45 | /// Panics if the passed URI is an invalid URI. 46 | pub fn new(server: U) -> ClientResult 47 | where 48 | U: TryInto, 49 | U::Error: Debug, 50 | { 51 | let transport = 52 | Hyper::new(server.try_into().expect("invalid URL")) 53 | .map_err(TransportError::from) 54 | .map_err(ClientError::from)?; 55 | Ok(Self { 56 | inner: Client::new(transport), 57 | }) 58 | } 59 | } 60 | }); 61 | 62 | #[cfg(feature = "client_default_transport_wasm_http")] 63 | def_transport_impl.extend(quote! { 64 | use hrpc::{client::transport::http::{Wasm, WasmError}, exports::http::Uri}; 65 | 66 | impl #service_ident { 67 | /// Create a new client using HTTP transport. 68 | /// 69 | /// Panics if the passed URI is an invalid URI. 70 | pub fn new(server: U) -> ClientResult 71 | where 72 | U: TryInto, 73 | U::Error: Debug, 74 | { 75 | let transport = 76 | Wasm::new(server.try_into().expect("invalid URL")) 77 | .map_err(TransportError::from) 78 | .map_err(ClientError::from)?; 79 | Ok(Self { 80 | inner: Client::new(transport), 81 | }) 82 | } 83 | } 84 | }); 85 | 86 | quote! { 87 | /// Generated client implementations. 88 | #[allow(dead_code, unused_imports)] 89 | pub mod #client_mod { 90 | use hrpc::client::prelude::*; 91 | 92 | #service_doc 93 | #[derive(Debug, Clone)] 94 | pub struct #service_ident { 95 | inner: Client, 96 | } 97 | 98 | impl #service_ident 99 | where 100 | Inner: Service> + 'static, 101 | InnerErr: 'static, 102 | { 103 | #methods 104 | } 105 | 106 | #create_methods 107 | #def_transport_impl 108 | } 109 | } 110 | } 111 | 112 | fn generate_methods(service: &T, proto_path: &str) -> TokenStream { 113 | let mut stream = TokenStream::new(); 114 | 115 | for method in service.methods() { 116 | let path = format!( 117 | "/{}{}{}/{}", 118 | service.package(), 119 | if service.package().is_empty() { 120 | "" 121 | } else { 122 | "." 123 | }, 124 | service.identifier(), 125 | method.identifier() 126 | ); 127 | 128 | let make_method = match (method.client_streaming(), method.server_streaming()) { 129 | (false, false) => generate_unary, 130 | (true, true) => generate_streaming, 131 | (false, true) => generate_server_streaming, 132 | (true, false) => generate_client_streaming, 133 | }; 134 | 135 | stream.extend(generate_doc_comments(method.comment())); 136 | stream.extend(make_method(method, proto_path, path)); 137 | } 138 | 139 | stream 140 | } 141 | 142 | fn generate_unary(method: &T, proto_path: &str, path: String) -> TokenStream { 143 | let ident = format_ident!("{}", method.name()); 144 | let (request, response) = method.request_response_name(proto_path); 145 | 146 | quote! { 147 | pub fn #ident(&mut self, req: Req) -> impl Future, InnerErr>> + 'static 148 | where 149 | Req: IntoRequest<#request>, 150 | { 151 | let mut req = req.into_request(); 152 | *req.endpoint_mut() = Cow::Borrowed(#path); 153 | self.inner.execute_request(req) 154 | } 155 | } 156 | } 157 | 158 | fn generate_streaming(method: &T, proto_path: &str, path: String) -> TokenStream { 159 | let ident = format_ident!("{}", method.name()); 160 | let (request, response) = method.request_response_name(proto_path); 161 | 162 | quote! { 163 | pub fn #ident(&mut self, req: Req) -> impl Future, InnerErr>> + 'static 164 | where 165 | Req: IntoRequest<()>, 166 | { 167 | let mut req = req.into_request(); 168 | *req.endpoint_mut() = Cow::Borrowed(#path); 169 | self.inner.connect_socket(req) 170 | } 171 | } 172 | } 173 | 174 | fn generate_client_streaming(method: &T, proto_path: &str, path: String) -> TokenStream { 175 | let ident = format_ident!("{}", method.name()); 176 | let (request, response) = method.request_response_name(proto_path); 177 | 178 | quote! { 179 | pub fn #ident(&mut self, req: Req) -> impl Future, InnerErr>> + 'static 180 | where 181 | Req: IntoRequest<()>, 182 | { 183 | let mut req = req.into_request(); 184 | *req.endpoint_mut() = Cow::Borrowed(#path); 185 | self.inner.connect_socket(req) 186 | } 187 | } 188 | } 189 | 190 | fn generate_server_streaming(method: &T, proto_path: &str, path: String) -> TokenStream { 191 | let ident = format_ident!("{}", method.name()); 192 | let (request, response) = method.request_response_name(proto_path); 193 | 194 | quote! { 195 | pub fn #ident(&mut self, req: Req) -> impl Future, InnerErr>> + 'static 196 | where 197 | Req: IntoRequest<#request>, 198 | { 199 | let mut req = req.into_request(); 200 | *req.endpoint_mut() = Cow::Borrowed(#path); 201 | self.inner.connect_socket_req(req) 202 | } 203 | } 204 | } 205 | -------------------------------------------------------------------------------- /crates/hrpc-build/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! Code generation for `hrpc`. 2 | #![deny(missing_docs)] 3 | 4 | use proc_macro2::{Delimiter, Group, Ident, Literal, Punct, Spacing, Span, TokenStream}; 5 | use quote::TokenStreamExt; 6 | 7 | #[cfg(feature = "rustfmt")] 8 | #[cfg_attr(docsrs, doc(cfg(feature = "rustfmt")))] 9 | use std::io::{self, Write}; 10 | #[cfg(feature = "rustfmt")] 11 | #[cfg_attr(docsrs, doc(cfg(feature = "rustfmt")))] 12 | use std::process::{exit, Command}; 13 | 14 | /// Prost generator 15 | mod prost; 16 | 17 | pub use self::prost::{compile_protos, configure, Builder}; 18 | 19 | /// Service code generation for client 20 | #[cfg(feature = "client")] 21 | pub mod client; 22 | /// Service code generation for server 23 | #[cfg(feature = "server")] 24 | pub mod server; 25 | 26 | /// Service generation trait. 27 | /// 28 | /// This trait can be implemented and consumed 29 | /// by `client::generate` and `server::generate` 30 | /// to allow any codegen module to generate service 31 | /// abstractions. 32 | pub trait Service { 33 | /// Comment type. 34 | type Comment: AsRef; 35 | 36 | /// Method type. 37 | type Method: Method; 38 | 39 | /// Name of service. 40 | fn name(&self) -> &str; 41 | /// Package name of service. 42 | fn package(&self) -> &str; 43 | /// Identifier used to generate type name. 44 | fn identifier(&self) -> &str; 45 | /// Methods provided by service. 46 | fn methods(&self) -> &[Self::Method]; 47 | /// Get comments about this item. 48 | fn comment(&self) -> &[Self::Comment]; 49 | } 50 | 51 | /// Method generation trait. 52 | /// 53 | /// Each service contains a set of generic 54 | /// `Methods`'s that will be used by codegen 55 | /// to generate abstraction implementations for 56 | /// the provided methods. 57 | pub trait Method { 58 | /// Comment type. 59 | type Comment: AsRef; 60 | 61 | /// Name of method. 62 | fn name(&self) -> &str; 63 | /// Identifier used to generate type name. 64 | fn identifier(&self) -> &str; 65 | /// Method is streamed by client. 66 | fn client_streaming(&self) -> bool; 67 | /// Method is streamed by server. 68 | fn server_streaming(&self) -> bool; 69 | /// Get comments about this item. 70 | fn comment(&self) -> &[Self::Comment]; 71 | /// Get options of this item. 72 | fn options(&self) -> Vec<(String, String)>; 73 | /// Type name of request and response. 74 | fn request_response_name(&self, proto_path: &str) -> (TokenStream, TokenStream); 75 | } 76 | 77 | /// Format files under the out_dir with rustfmt 78 | #[cfg(feature = "rustfmt")] 79 | #[cfg_attr(docsrs, doc(cfg(feature = "rustfmt")))] 80 | pub fn fmt(out_dir: &str) { 81 | let dir = std::fs::read_dir(out_dir).unwrap(); 82 | 83 | for entry in dir { 84 | let file = entry.unwrap().file_name().into_string().unwrap(); 85 | if !file.ends_with(".rs") { 86 | continue; 87 | } 88 | let result = Command::new("rustfmt") 89 | .arg("--emit") 90 | .arg("files") 91 | .arg("--edition") 92 | .arg("2018") 93 | .arg(format!("{}/{}", out_dir, file)) 94 | .output(); 95 | 96 | match result { 97 | Err(e) => { 98 | eprintln!("error running rustfmt: {:?}", e); 99 | exit(1) 100 | } 101 | Ok(output) => { 102 | if !output.status.success() { 103 | io::stderr().write_all(&output.stderr).unwrap(); 104 | exit(output.status.code().unwrap_or(1)) 105 | } 106 | } 107 | } 108 | } 109 | } 110 | 111 | // Generate a singular line of a doc comment 112 | #[allow(dead_code)] 113 | fn generate_doc_comment>(comment: S) -> TokenStream { 114 | let mut doc_stream = TokenStream::new(); 115 | 116 | doc_stream.append(Ident::new("doc", Span::call_site())); 117 | doc_stream.append(Punct::new('=', Spacing::Alone)); 118 | let comment = comment.as_ref(); 119 | let comment_escaped = comment.replace("```", "\\`\\`\\`"); 120 | doc_stream.append(Literal::string(&comment_escaped)); 121 | 122 | let group = Group::new(Delimiter::Bracket, doc_stream); 123 | 124 | let mut stream = TokenStream::new(); 125 | stream.append(Punct::new('#', Spacing::Alone)); 126 | stream.append(group); 127 | stream 128 | } 129 | 130 | // Generate a larger doc comment composed of many lines of doc comments 131 | #[allow(dead_code)] 132 | fn generate_doc_comments>(comments: &[T]) -> TokenStream { 133 | let mut stream = TokenStream::new(); 134 | 135 | for comment in comments { 136 | stream.extend(generate_doc_comment(comment)); 137 | } 138 | 139 | stream 140 | } 141 | 142 | #[allow(dead_code)] 143 | fn naive_snake_case(name: &str) -> String { 144 | let mut s = String::new(); 145 | let mut it = name.chars().peekable(); 146 | 147 | while let Some(x) = it.next() { 148 | s.push(x.to_ascii_lowercase()); 149 | if let Some(y) = it.peek() { 150 | if y.is_uppercase() { 151 | s.push('_'); 152 | } 153 | } 154 | } 155 | 156 | s 157 | } 158 | 159 | #[test] 160 | fn test_snake_case() { 161 | for case in &[ 162 | ("Service", "service"), 163 | ("ThatHasALongName", "that_has_a_long_name"), 164 | ("greeter", "greeter"), 165 | ("ABCServiceX", "a_b_c_service_x"), 166 | ] { 167 | assert_eq!(naive_snake_case(case.0), case.1) 168 | } 169 | } 170 | -------------------------------------------------------------------------------- /crates/hrpc-build/src/server.rs: -------------------------------------------------------------------------------- 1 | use super::{Method, Service}; 2 | use crate::{generate_doc_comment, generate_doc_comments, naive_snake_case}; 3 | use proc_macro2::TokenStream; 4 | use quote::quote; 5 | use syn::Ident; 6 | 7 | /// Generate service for Server. 8 | /// 9 | /// This takes some `Service` and will generate a `TokenStream` that contains 10 | /// a public module containing the server service and handler trait. 11 | pub fn generate(service: &T, proto_path: &str) -> TokenStream { 12 | let server_service = quote::format_ident!("{}Server", service.name()); 13 | let server_trait = quote::format_ident!("{}", service.name()); 14 | let server_mod = quote::format_ident!("{}_server", naive_snake_case(service.name())); 15 | let generated_trait = generate_trait(service, proto_path, server_trait.clone()); 16 | let service_doc = generate_doc_comments(service.comment()); 17 | let (handlers, routes) = generate_handlers(service, proto_path); 18 | 19 | let mut methods = TokenStream::new(); 20 | 21 | methods.extend(quote! { 22 | /// Create a new service server. 23 | pub fn new(service: T) -> Self { 24 | Self { 25 | service: Arc::new(service), 26 | } 27 | } 28 | }); 29 | 30 | #[cfg(feature = "server_default_transport_http")] 31 | methods.extend(quote! { 32 | /// Serves the service with HTTP transport. 33 | pub async fn serve(self, addr: Addr) -> Result<(), as Transport>::Error> { 34 | let transport = hrpc::server::transport::http::Hyper::new(addr)?; 35 | transport.serve(self).await 36 | } 37 | }); 38 | 39 | quote! { 40 | /// Generated server implementations. 41 | #[allow(unused_variables)] 42 | pub mod #server_mod { 43 | use hrpc::server::gen_prelude::*; 44 | 45 | #generated_trait 46 | 47 | #service_doc 48 | pub struct #server_service { 49 | service: Arc, 50 | } 51 | 52 | impl Clone for #server_service { 53 | fn clone(&self) -> Self { 54 | Self { 55 | service: self.service.clone(), 56 | } 57 | } 58 | } 59 | 60 | impl MakeRoutes for #server_service { 61 | fn make_routes(&self) -> Routes { 62 | #handlers 63 | 64 | #routes 65 | } 66 | } 67 | 68 | impl #server_service { 69 | #methods 70 | } 71 | } 72 | } 73 | } 74 | 75 | fn generate_trait(service: &T, proto_path: &str, server_trait: Ident) -> TokenStream { 76 | let methods = generate_trait_methods(service, proto_path); 77 | let trait_doc = generate_doc_comment(&format!( 78 | "Generated trait containing hRPC methods that should be implemented for use with {}Server.", 79 | service.name() 80 | )); 81 | 82 | quote! { 83 | #trait_doc 84 | pub trait #server_trait : Sync + Send + 'static { 85 | #methods 86 | } 87 | } 88 | } 89 | 90 | fn generate_trait_methods(service: &T, proto_path: &str) -> TokenStream { 91 | let mut stream = TokenStream::new(); 92 | 93 | for method in service.methods() { 94 | let streaming = (method.client_streaming(), method.server_streaming()); 95 | 96 | let name = quote::format_ident!("{}", method.name()); 97 | let pre_name = quote::format_ident!("{}_middleware", name); 98 | 99 | let (req_message, res_message) = method.request_response_name(proto_path); 100 | 101 | let method_doc = generate_doc_comments(method.comment()); 102 | stream.extend(quote! { 103 | /// Optional middleware for this RPC. 104 | #[allow(unused_variables)] 105 | fn #pre_name(&self) -> Option { 106 | None 107 | } 108 | }); 109 | 110 | let method = match streaming { 111 | (false, false) => quote! { 112 | #method_doc 113 | fn #name(&self, request: HrpcRequest<#req_message>) -> BoxFuture<'_, ServerResult>>; 114 | }, 115 | (true, false) | (true, true) | (false, true) => quote! { 116 | #method_doc 117 | fn #name(&self, request: HrpcRequest<()>, socket: Socket<#res_message, #req_message>) -> BoxFuture<'_, ServerResult<()>>; 118 | }, 119 | }; 120 | 121 | stream.extend(method); 122 | } 123 | 124 | stream 125 | } 126 | 127 | fn generate_handlers(service: &T, proto_path: &str) -> (TokenStream, TokenStream) { 128 | let mut handlers = TokenStream::new(); 129 | let mut routes = quote! { 130 | Routes::new() 131 | }; 132 | 133 | for method in service.methods().iter() { 134 | let name = quote::format_ident!("{}", method.name()); 135 | let pre_name = quote::format_ident!("{}_middleware", name); 136 | 137 | let package_name = format!( 138 | "{}{}{}", 139 | service.package(), 140 | if service.package().is_empty() { 141 | "" 142 | } else { 143 | "." 144 | }, 145 | service.identifier(), 146 | ); 147 | let method_name = method.identifier(); 148 | let (req_message, res_message) = method.request_response_name(proto_path); 149 | 150 | let streaming = (method.client_streaming(), method.server_streaming()); 151 | let endpoint = format!("/{}/{}", package_name, method_name); 152 | 153 | let handler_body = match streaming { 154 | (false, false) => { 155 | quote! { 156 | let svr = self.service.clone(); 157 | let handler = move |request: HrpcRequest<#req_message>| async move { svr. #name (request).await }; 158 | unary_handler(handler) 159 | } 160 | } 161 | (true, false) | (true, true) | (false, true) => { 162 | quote! { 163 | let svr = self.service.clone(); 164 | let handler = move |request: HrpcRequest<()>, socket: Socket<#res_message, #req_message>| async move { svr. #name (request, socket).await }; 165 | ws_handler(handler) 166 | } 167 | } 168 | }; 169 | 170 | // Apply middleware 171 | let handler = quote! { 172 | let #name = { 173 | #handler_body 174 | }; 175 | let #name = match self.service. #pre_name () { 176 | Some(layer) => layer.layer(#name), 177 | None => HrpcService::new(#name), 178 | }; 179 | }; 180 | 181 | routes.extend(quote! { .route(#endpoint, #name) }); 182 | handlers.extend(handler); 183 | } 184 | 185 | (handlers, routes) 186 | } 187 | -------------------------------------------------------------------------------- /crates/hrpc-proc-macro/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "hrpc-proc-macro" 3 | version = "0.33.0" 4 | edition = "2021" 5 | authors = ["Yusuf Bera Ertan "] 6 | license = "MIT" 7 | repository = "https://github.com/harmony-development/hrpc-rs" 8 | homepage = "https://github.com/harmony-development/hrpc-rs" 9 | description = "Proc macros for hRPC." 10 | keywords = ["hrpc", "harmony", "rpc", "macros", "protobuf"] 11 | 12 | [lib] 13 | proc-macro = true -------------------------------------------------------------------------------- /crates/hrpc-proc-macro/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! Utility proc macros for `hrpc`. 2 | #![deny(missing_docs)] 3 | 4 | use proc_macro::{Delimiter, Group, Ident, Punct, Span, TokenStream, TokenTree}; 5 | 6 | /// An attribute macro that turns an `async fn` into a hRPC handler function. 7 | /// 8 | /// You should import either `hrpc::server::prelude` or `hrpc::make_handler` to be able to 9 | /// use this macro properly. 10 | #[proc_macro_attribute] 11 | pub fn handler(_args: TokenStream, input: TokenStream) -> TokenStream { 12 | TokenStream::from_iter([ 13 | TokenTree::Ident(Ident::new("make_handler", Span::mixed_site())), 14 | TokenTree::Punct(Punct::new('!', proc_macro::Spacing::Joint)), 15 | TokenTree::Group(Group::new(Delimiter::Brace, input)), 16 | ]) 17 | } 18 | -------------------------------------------------------------------------------- /crates/hrpc/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "hrpc" 3 | version = "0.33.29" 4 | authors = ["Yusuf Bera Ertan "] 5 | edition = "2021" 6 | license = "MIT" 7 | repository = "https://github.com/harmony-development/hrpc-rs" 8 | homepage = "https://github.com/harmony-development/hrpc-rs" 9 | description = "Common code for hRPC." 10 | keywords = ["hrpc", "harmony", "rpc", "protobuf"] 11 | categories = ["network-programming"] 12 | include = [ 13 | "/src", 14 | "/build.rs", 15 | "/Cargo.toml", 16 | "/hrpc-main/protocol", 17 | "/README.md", 18 | ] 19 | 20 | [package.metadata.docs.rs] 21 | all-features = true 22 | 23 | [package.metadata.nix] 24 | build = true 25 | library = true 26 | 27 | [features] 28 | default = [] 29 | 30 | # Enable common server code 31 | server = ["_common", "matchit", "hrpc-proc-macro"] 32 | # Enable common client code 33 | client = ["_common"] 34 | # Internal feature used to indicate common server / client is enabled 35 | _common = [ 36 | "futures-util/unstable", 37 | "futures-util/bilock", 38 | "futures-util/sink", 39 | "futures-util/async-await-macro", 40 | "tower", 41 | "pin-project-lite", 42 | ] 43 | 44 | # Enables the mock client 45 | mock_client = ["client", "_common_mock"] 46 | # Enables the mock server 47 | mock_server = ["server", "_common_mock"] 48 | # Internal feature used in mock transport features 49 | _common_mock = [ 50 | "tokio", 51 | "tokio/rt", 52 | "tokio/sync", 53 | "futures-channel", 54 | "futures-channel/sink", 55 | "futures-channel/std", 56 | "client", 57 | ] 58 | 59 | # Enables the HTTP server 60 | http_server = [ 61 | "server", 62 | "_common_http", 63 | "websocket_tokio_tungstenite", 64 | "tokio/rt", 65 | "hyper", 66 | "axum-server", 67 | "sha-1", 68 | "base64", 69 | ] 70 | # Enables the `hyper` HTTP client 71 | http_hyper_client = [ 72 | "_common_http_client", 73 | "websocket_tokio_tungstenite", 74 | "hyper", 75 | "hyper-rustls", 76 | "hyper/client", 77 | ] 78 | # Enables the HTTP client that can compile to WASM 79 | http_wasm_client = [ 80 | "_common_http_client", 81 | "reqwasm", 82 | "js-sys", 83 | "wasm-bindgen", 84 | "websocket_wasm", 85 | "wasm-streams", 86 | "gloo-timers", 87 | ] 88 | # Internal feature used to indicate that a HTTP client is enabled 89 | _common_http_client = ["client", "_common_http"] 90 | # Internal feature used to indicate that HTTP is enabled 91 | _common_http = ["http", "http-body"] 92 | 93 | # Enable the tokio_tungstenite websocket implementation 94 | websocket_tokio_tungstenite = ["tokio-tungstenite", "tokio"] 95 | # Enable the wasm websocket implementation 96 | websocket_wasm = ["ws_stream_wasm"] 97 | 98 | [build-dependencies] 99 | prost-build = "0.10" 100 | 101 | [dependencies] 102 | # Core types, traits and utilities 103 | prost = "0.10" 104 | bytes = "1.0" 105 | futures-util = { version = "0.3", default-features = false, features = ["std"] } 106 | pin-project-lite = { version = "0.2", default-features = false, optional = true } 107 | 108 | # Re-exported by server 109 | hrpc-proc-macro = { version = "0.33.0", path = "../hrpc-proc-macro", optional = true } 110 | # Used by server for it's incredible Service trait and other utilities 111 | tower = { version = "0.4", default-features = false, features = [ 112 | "util", 113 | ], optional = true } 114 | 115 | # Logging 116 | tracing = { version = "0.1", default-features = false, features = ["std"] } 117 | 118 | # Http transport deps 119 | http = { version = "0.2", optional = true } 120 | http-body = { version = "0.4", optional = true } 121 | 122 | # Used by server for routing 123 | matchit = { version = "0.5", default-features = false, optional = true } 124 | 125 | # Used by server to implement WS request handling 126 | sha-1 = { version = "0.10", default-features = false, optional = true } 127 | base64 = { version = "0.13", default-features = false, optional = true } 128 | 129 | # Used by http server impl in ws handling 130 | tokio = { version = "1", default-features = false, optional = true } 131 | # Used by hyper client 132 | hyper = { version = "0.14", default-features = false, features = [ 133 | "http1", 134 | "http2", 135 | "runtime", 136 | "stream", 137 | ], optional = true } 138 | hyper-rustls = { version = "0.23", default-features = false, features = [ 139 | "native-tokio", 140 | "tls12", 141 | "http1", 142 | "http2", 143 | ], optional = true } 144 | tokio-tungstenite = { version = "0.17", default-features = false, features = [ 145 | "rustls-tls-native-roots", 146 | "connect", 147 | ], optional = true } 148 | axum-server = { version = "0.4", default-features = false, features = [ 149 | "tls-rustls", 150 | ], optional = true } 151 | 152 | # Used by wasm client 153 | reqwasm = { version = "0.5", default-features = false, features = [ 154 | "http", 155 | ], optional = true } 156 | ws_stream_wasm = { version = "0.7", default-features = false, optional = true } 157 | js-sys = { version = "0.3", default-features = false, optional = true } 158 | wasm-bindgen = { version = "0.2", default-features = false, optional = true } 159 | wasm-streams = { version = "0.2", default-features = false, optional = true } 160 | gloo-timers = { version = "0.2", default-features = false, features = [ 161 | "futures", 162 | ], optional = true } 163 | 164 | # used by mock client 165 | futures-channel = { version = "0.3", default-features = false, optional = true } 166 | -------------------------------------------------------------------------------- /crates/hrpc/README.md: -------------------------------------------------------------------------------- 1 | # hrpc 2 | 3 | This crate contains generic server and client implementations, and transports 4 | for hRPC. It mainly utilizes `tower`. It uses `matchit` for routing on server. 5 | 6 | Currently implemented transports are: 7 | + HTTP 8 | - `hyper` client and server for use on native platforms 9 | - WASM client for use on web 10 | + Mock client and server (see examples for usage) -------------------------------------------------------------------------------- /crates/hrpc/build.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | let mut conf = prost_build::Config::new(); 3 | conf.bytes([".hrpc.v1.Error"]); 4 | conf.compile_protos(&["hrpc-main/protocol/hrpc.proto"], &["hrpc-main/protocol"]) 5 | .expect("failed to compile hRPC proto"); 6 | } 7 | -------------------------------------------------------------------------------- /crates/hrpc/src/body.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | fmt::{self, Debug, Formatter}, 3 | pin::Pin, 4 | }; 5 | 6 | use bytes::{Buf, Bytes}; 7 | use futures_util::{Stream, StreamExt}; 8 | 9 | use super::BoxError; 10 | use crate::common::buf::BufList; 11 | 12 | /// Type of the item that a [`Body`] produces. 13 | pub type BodyResult = Result; 14 | 15 | /// A request or response body. 16 | pub struct Body { 17 | stream: Pin + Send + Sync + 'static>>, 18 | } 19 | 20 | impl Body { 21 | /// Create a new body by wrapping a stream. 22 | pub fn new(stream: S) -> Self 23 | where 24 | S: Stream + Send + Sync + 'static, 25 | { 26 | Self { 27 | stream: Box::pin(stream), 28 | } 29 | } 30 | 31 | /// Create a new, empty body. 32 | pub fn empty() -> Self { 33 | Self::new(futures_util::stream::empty()) 34 | } 35 | 36 | /// Create a body using a single chunk of data. 37 | pub fn full(data: Data) -> Self 38 | where 39 | Data: Into, 40 | { 41 | Self::new(futures_util::stream::once(futures_util::future::ready(Ok( 42 | data.into(), 43 | )))) 44 | } 45 | 46 | /// Aggregate this body into a single [`Buf`]. 47 | /// 48 | /// Note that this does not do any sort of size check for the body. 49 | pub async fn aggregate(mut self) -> Result { 50 | let mut bufs = BufList::new(); 51 | 52 | while let Some(buf) = self.next().await { 53 | let buf = buf?; 54 | if buf.has_remaining() { 55 | bufs.push(buf); 56 | } 57 | } 58 | 59 | Ok(bufs) 60 | } 61 | } 62 | 63 | impl Stream for Body { 64 | type Item = BodyResult; 65 | 66 | fn poll_next( 67 | mut self: std::pin::Pin<&mut Self>, 68 | cx: &mut std::task::Context<'_>, 69 | ) -> std::task::Poll> { 70 | self.stream.poll_next_unpin(cx) 71 | } 72 | } 73 | 74 | impl Debug for Body { 75 | fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { 76 | f.debug_struct("Body").field("stream", &"").finish() 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /crates/hrpc/src/client/boxed.rs: -------------------------------------------------------------------------------- 1 | use std::{error::Error as StdError, fmt::Display}; 2 | 3 | use futures_util::future::BoxFuture; 4 | use tower::{util::BoxCloneService, Service, ServiceExt}; 5 | 6 | use crate::{box_error, request::BoxRequest, response::BoxResponse, BoxError}; 7 | 8 | use super::transport::TransportError; 9 | 10 | /// A type erased transport. This is useful for storing transports or clients 11 | /// and swapping them at runtime. 12 | #[derive(Clone)] 13 | pub struct BoxedTransport { 14 | inner: BoxCloneService>, 15 | } 16 | 17 | impl BoxedTransport { 18 | /// Create a new boxed transport by wrapping any transport. 19 | pub fn new(svc: Svc) -> Self 20 | where 21 | Svc: Service> 22 | + Send 23 | + Clone 24 | + 'static, 25 | Svc::Future: Send, 26 | SvcErr: StdError + Sync + Send + 'static, 27 | { 28 | let svc = svc.map_err(|err| match err { 29 | TransportError::GenericClient(err) => TransportError::GenericClient(err), 30 | TransportError::Transport(err) => { 31 | TransportError::Transport(BoxedTransportError::new(err)) 32 | } 33 | }); 34 | Self { 35 | inner: BoxCloneService::new(svc), 36 | } 37 | } 38 | } 39 | 40 | impl Service for BoxedTransport { 41 | type Response = BoxResponse; 42 | 43 | type Error = TransportError; 44 | 45 | type Future = BoxFuture<'static, Result>; 46 | 47 | fn poll_ready( 48 | &mut self, 49 | cx: &mut std::task::Context<'_>, 50 | ) -> std::task::Poll> { 51 | Service::poll_ready(&mut self.inner, cx) 52 | } 53 | 54 | fn call(&mut self, req: BoxRequest) -> Self::Future { 55 | Service::call(&mut self.inner, req) 56 | } 57 | } 58 | 59 | /// A type erased boxed transport error. 60 | #[derive(Debug)] 61 | pub struct BoxedTransportError { 62 | inner: BoxError, 63 | } 64 | 65 | impl BoxedTransportError { 66 | /// Create a new boxed transport error. 67 | pub fn new(err: Err) -> Self 68 | where 69 | Err: StdError + Send + Sync + 'static, 70 | { 71 | Self { 72 | inner: box_error(err), 73 | } 74 | } 75 | 76 | /// Extract the inner box error from this error. 77 | pub fn into_box_error(self) -> BoxError { 78 | self.inner 79 | } 80 | } 81 | 82 | impl Display for BoxedTransportError { 83 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 84 | Display::fmt(&self.inner, f) 85 | } 86 | } 87 | 88 | impl StdError for BoxedTransportError { 89 | fn source(&self) -> Option<&(dyn StdError + 'static)> { 90 | Some(self.inner.as_ref()) 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /crates/hrpc/src/client/error.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | borrow::Cow, 3 | error::Error as StdError, 4 | fmt::{self, Display, Formatter}, 5 | }; 6 | 7 | pub use crate::{decode::DecodeBodyError, proto::Error as HrpcError}; 8 | pub use std::io::Error as IoError; 9 | 10 | /// Convenience type for `Client` operation result. 11 | pub type ClientResult = Result>; 12 | 13 | /// Errors that can occur within `Client` operation. 14 | #[derive(Debug)] 15 | pub enum ClientError { 16 | /// Occurs if an endpoint returns an error. 17 | EndpointError { 18 | /// The hRPC error. 19 | hrpc_error: HrpcError, 20 | /// The endpoint for which this error happened. 21 | endpoint: Cow<'static, str>, 22 | }, 23 | /// Occurs if the data server responded with could not be decoded. 24 | MessageDecode(DecodeBodyError), 25 | /// Occurs if the data server responded with is not supported for decoding. 26 | ContentNotSupported, 27 | /// Occures if the underlying transport yields an error. 28 | Transport(TransportError), 29 | /// Occurs if the spec implemented on server doesn't match ours. 30 | /// 31 | /// The value is the version of the server. If it is `unknown`, it means 32 | /// that either no version was provided, or the version couldn't be parsed 33 | /// (ie. if it's an HTTP header). 34 | IncompatibleSpecVersion(String), 35 | } 36 | 37 | impl ClientError { 38 | /// Map the transport error. 39 | pub fn map_transport_err(self, f: F) -> ClientError 40 | where 41 | F: FnOnce(TransportError) -> NewTransportError, 42 | { 43 | match self { 44 | ClientError::Transport(err) => ClientError::Transport(f(err)), 45 | ClientError::EndpointError { 46 | hrpc_error, 47 | endpoint, 48 | } => ClientError::EndpointError { 49 | hrpc_error, 50 | endpoint, 51 | }, 52 | ClientError::ContentNotSupported => ClientError::ContentNotSupported, 53 | ClientError::IncompatibleSpecVersion(server_ver) => { 54 | ClientError::IncompatibleSpecVersion(server_ver) 55 | } 56 | ClientError::MessageDecode(err) => ClientError::MessageDecode(err), 57 | } 58 | } 59 | } 60 | 61 | impl Display for ClientError { 62 | fn fmt(&self, f: &mut Formatter) -> fmt::Result { 63 | match self { 64 | ClientError::EndpointError { 65 | hrpc_error, 66 | endpoint, 67 | } => write!( 68 | f, 69 | "endpoint {} returned an error '{}': {}", 70 | endpoint, hrpc_error.identifier, hrpc_error.human_message, 71 | ), 72 | ClientError::ContentNotSupported => { 73 | write!(f, "server responded with a non protobuf response") 74 | } 75 | ClientError::MessageDecode(err) => write!( 76 | f, 77 | "failed to decode response data as protobuf response: {}", 78 | err 79 | ), 80 | ClientError::Transport(err) => write!(f, "transport error: {}", err), 81 | ClientError::IncompatibleSpecVersion(server_ver) => { 82 | write!( 83 | f, 84 | "server hrpc version ({}) is incompatible with ours ({})", 85 | server_ver, 86 | crate::HRPC_SPEC_VERSION 87 | ) 88 | } 89 | } 90 | } 91 | } 92 | 93 | impl From for ClientError { 94 | fn from(err: DecodeBodyError) -> Self { 95 | ClientError::MessageDecode(err) 96 | } 97 | } 98 | 99 | impl StdError for ClientError { 100 | fn source(&self) -> Option<&(dyn StdError + 'static)> { 101 | match self { 102 | ClientError::MessageDecode(err) => Some(err), 103 | ClientError::Transport(err) => Some(err), 104 | ClientError::EndpointError { hrpc_error, .. } => Some(hrpc_error), 105 | _ => None, 106 | } 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /crates/hrpc/src/client/layer/mod.rs: -------------------------------------------------------------------------------- 1 | #[cfg(any( 2 | feature = "http_hyper_client", 3 | feature = "http_wasm_client", 4 | feature = "mock_client" 5 | ))] 6 | pub mod backoff; 7 | -------------------------------------------------------------------------------- /crates/hrpc/src/client/mod.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | fmt::{self, Debug, Formatter}, 3 | future::Future, 4 | }; 5 | 6 | use crate::{ 7 | body::Body, 8 | decode, 9 | request::{self, BoxRequest}, 10 | response::BoxResponse, 11 | Response, 12 | }; 13 | 14 | use self::{ 15 | boxed::BoxedTransport, 16 | transport::{SocketChannels, SocketRequestMarker, TransportError}, 17 | }; 18 | 19 | use super::Request; 20 | use error::*; 21 | use futures_util::TryFutureExt; 22 | use socket::*; 23 | use tower::{Layer, Service}; 24 | 25 | /// Contains code for a boxed transport. 26 | pub mod boxed; 27 | /// Error types. 28 | pub mod error; 29 | /// Useful layers to use with the generic client. 30 | pub mod layer; 31 | /// hRPC socket used for streaming RPCs. 32 | pub mod socket; 33 | /// hRPC client transports. 34 | /// 35 | /// A client transport is any [`tower::Service`] that has a `BoxRequest` 36 | /// request type, `BoxResponse` response type and [`TransportError`] 37 | /// (where `Err` is the error type the transport uses) error type. This allows 38 | /// [`tower::Layer`]s to be used to compose transports. 39 | /// 40 | /// Currently implemented: 41 | /// - HTTP `hyper` client ([`transport::http::hyper`]), 42 | /// - HTTP WASM web client ([`transport::http::wasm`]), 43 | /// - mock client, useful for testing ([`transport::mock`]). 44 | pub mod transport; 45 | 46 | #[doc(hidden)] 47 | pub mod prelude { 48 | pub use super::{ 49 | error::{ClientError, ClientResult}, 50 | socket::Socket, 51 | transport::TransportError, 52 | Client, 53 | }; 54 | pub use crate::{ 55 | request::{BoxRequest, IntoRequest, Request}, 56 | response::{BoxResponse, Response}, 57 | }; 58 | pub use std::{borrow::Cow, convert::TryInto, fmt::Debug, future::Future}; 59 | pub use tower::Service; 60 | } 61 | 62 | /// Generic client implementation with common methods. 63 | pub struct Client { 64 | transport: Inner, 65 | } 66 | 67 | impl Debug for Client { 68 | fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { 69 | f.debug_struct("Client") 70 | .field("inner", &self.transport) 71 | .finish() 72 | } 73 | } 74 | 75 | impl Clone for Client { 76 | fn clone(&self) -> Self { 77 | Self { 78 | transport: self.transport.clone(), 79 | } 80 | } 81 | } 82 | 83 | impl Client { 84 | /// Create a new client using the provided transport. 85 | pub fn new(transport: Inner) -> Client { 86 | Client { transport } 87 | } 88 | } 89 | 90 | impl Client 91 | where 92 | Inner: Service> 93 | + Send 94 | + Clone 95 | + 'static, 96 | Inner::Future: Send, 97 | InnerErr: std::error::Error + Sync + Send + 'static, 98 | { 99 | /// Box the inner transport. This erases the type, making it easier 100 | /// to store the client in structures. 101 | pub fn boxed(self) -> Client { 102 | Client { 103 | transport: BoxedTransport::new(self.transport), 104 | } 105 | } 106 | } 107 | 108 | impl Client 109 | where 110 | Inner: Service> + 'static, 111 | InnerErr: 'static, 112 | { 113 | /// Layer this client with a new [`Layer`]. 114 | pub fn layer(self, l: L) -> Client 115 | where 116 | L: Layer, 117 | S: Service, 118 | { 119 | Client { 120 | transport: l.layer(self.transport), 121 | } 122 | } 123 | 124 | /// Executes a unary request and returns the decoded response. 125 | pub fn execute_request( 126 | &mut self, 127 | req: Request, 128 | ) -> impl Future, InnerErr>> + 'static 129 | where 130 | Req: prost::Message, 131 | Resp: prost::Message + Default, 132 | { 133 | Service::call(&mut self.transport, req.map::<()>()) 134 | .map_ok(|resp| resp.map::()) 135 | .map_err(ClientError::from) 136 | } 137 | 138 | /// Connect a socket with the server and return it. 139 | pub fn connect_socket( 140 | &mut self, 141 | mut req: Request<()>, 142 | ) -> impl Future, InnerErr>> + 'static 143 | where 144 | Req: prost::Message, 145 | Resp: prost::Message + Default, 146 | { 147 | req.extensions_mut().insert(SocketRequestMarker); 148 | Service::call(&mut self.transport, req) 149 | .map_ok(|mut resp| { 150 | let chans = resp 151 | .extensions_mut() 152 | .remove::() 153 | .expect("transport did not return socket channels - this is a bug"); 154 | 155 | Socket::new( 156 | chans.rx, 157 | chans.tx, 158 | socket::encode_message, 159 | socket::decode_message, 160 | ) 161 | }) 162 | .map_err(ClientError::from) 163 | } 164 | 165 | /// Connect a socket with the server, send a message and return it. 166 | /// 167 | /// Used by the server streaming methods. 168 | pub fn connect_socket_req( 169 | &mut self, 170 | request: Request, 171 | ) -> impl Future, InnerErr>> + 'static 172 | where 173 | Req: prost::Message + Default + 'static, 174 | Resp: prost::Message + Default + 'static, 175 | { 176 | let request::Parts { 177 | body, 178 | extensions, 179 | endpoint, 180 | .. 181 | } = request.into(); 182 | 183 | let request: BoxRequest = Request::from(request::Parts { 184 | body: Body::empty(), 185 | endpoint: endpoint.clone(), 186 | extensions, 187 | }); 188 | 189 | let connect_fut = self.connect_socket(request); 190 | 191 | async move { 192 | let mut socket = connect_fut.await?; 193 | 194 | let message = decode::decode_body(body).await?; 195 | socket 196 | .send_message(message) 197 | .await 198 | .map_err(|err| match err { 199 | SocketError::MessageDecode(err) => ClientError::MessageDecode(err), 200 | SocketError::Protocol(err) => ClientError::EndpointError { 201 | hrpc_error: err, 202 | endpoint, 203 | }, 204 | // TODO: this is not good... we need a proper way to expose this error to the user 205 | // maybe by returning double result? 206 | SocketError::Transport(err) => ClientError::EndpointError { 207 | hrpc_error: HrpcError::from(err).with_identifier("hrpcrs.socket-error"), 208 | endpoint, 209 | }, 210 | })?; 211 | 212 | Ok(socket) 213 | } 214 | } 215 | } 216 | -------------------------------------------------------------------------------- /crates/hrpc/src/client/socket.rs: -------------------------------------------------------------------------------- 1 | use bytes::BytesMut; 2 | use prost::Message as PbMsg; 3 | 4 | use crate::{common::socket::DecodeResult, decode::DecodeBodyError, proto::Error as HrpcError}; 5 | 6 | pub use crate::common::socket::{ReadSocket, Socket, SocketError, WriteSocket}; 7 | 8 | pub(super) fn encode_message(buf: &mut BytesMut, msg: &Msg) -> Vec { 9 | crate::encode::encode_protobuf_message_to(buf, msg); 10 | // TODO: don't allocate here? 11 | buf.to_vec() 12 | } 13 | 14 | pub(super) fn decode_message( 15 | raw: Vec, 16 | ) -> Result, DecodeBodyError> { 17 | if raw.is_empty() { 18 | return Err(DecodeBodyError::InvalidProtoMessage( 19 | prost::DecodeError::new("empty protobuf message"), 20 | )); 21 | } 22 | 23 | let opcode = raw[0]; 24 | 25 | if opcode == 0 { 26 | Msg::decode(&raw[1..]) 27 | .map(DecodeResult::Msg) 28 | .map_err(DecodeBodyError::InvalidProtoMessage) 29 | } else if opcode == 1 { 30 | HrpcError::decode(&raw[1..]) 31 | .map(DecodeResult::Error) 32 | .map_err(DecodeBodyError::InvalidProtoMessage) 33 | } else { 34 | Err(DecodeBodyError::InvalidBody(Box::new( 35 | HrpcError::from(( 36 | "hrpcrs.http.invalid-socket-message-opcode", 37 | "invalid socket binary message opcode", 38 | )) 39 | .with_details(raw), 40 | ))) 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /crates/hrpc/src/client/transport/http/mod.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | error::Error as StdError, 3 | fmt::{self, Display, Formatter}, 4 | }; 5 | 6 | use http::{HeaderMap, Uri}; 7 | 8 | #[cfg(feature = "http_hyper_client")] 9 | pub mod hyper; 10 | use crate::common::extensions::Extensions; 11 | 12 | #[cfg(feature = "http_hyper_client")] 13 | pub use self::hyper::{Hyper, HyperError}; 14 | 15 | #[cfg(feature = "http_wasm_client")] 16 | pub mod wasm; 17 | #[cfg(feature = "http_wasm_client")] 18 | pub use self::wasm::{Wasm, WasmError}; 19 | 20 | /// Clones HTTP extensions that will be added from a hRPC request to a HTTP 21 | /// request. Intended for use with [`crate::client::layer::backoff`]. 22 | pub fn clone_http_extensions(from: &Extensions, to: &mut Extensions) { 23 | if let Some(header_map) = from.get::().cloned() { 24 | to.insert(header_map); 25 | } 26 | } 27 | 28 | /// Check if a URI is a valid server URI or not. 29 | fn check_uri(uri: Uri) -> Result { 30 | matches!(uri.scheme_str(), Some("https" | "http")) 31 | .then(|| uri) 32 | .ok_or(InvalidServerUrl::InvalidScheme) 33 | } 34 | 35 | /// Map a scheme that can be `https` or `http` to `wss` or `ws` respectively. 36 | fn map_scheme_to_ws(scheme: &str) -> Option<&'static str> { 37 | match scheme { 38 | "https" => Some("wss"), 39 | "http" => Some("ws"), 40 | _ => None, 41 | } 42 | } 43 | 44 | #[derive(Debug)] 45 | /// Errors that can occur while parsing a URL when creating a client. 46 | pub enum InvalidServerUrl { 47 | /// Occurs if URL scheme isn't `http` or `https`. 48 | InvalidScheme, 49 | } 50 | 51 | impl Display for InvalidServerUrl { 52 | fn fmt(&self, f: &mut Formatter) -> fmt::Result { 53 | match self { 54 | InvalidServerUrl::InvalidScheme => { 55 | write!(f, "invalid scheme, expected `http` or `https`") 56 | } 57 | } 58 | } 59 | } 60 | 61 | impl StdError for InvalidServerUrl {} 62 | -------------------------------------------------------------------------------- /crates/hrpc/src/client/transport/mock.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | error::Error as StdError, 3 | fmt::{self, Display, Formatter}, 4 | task::Poll, 5 | }; 6 | 7 | use futures_util::{Future, FutureExt}; 8 | use tokio::sync::oneshot::{self, Receiver as OneshotReceiver}; 9 | use tower::Service; 10 | 11 | use crate::{common::transport::mock::MockSender, request::BoxRequest, response::BoxResponse}; 12 | 13 | use super::TransportError; 14 | 15 | /// A client that uses a channel to send requests to a (possibly) 16 | /// mock server. 17 | #[derive(Clone)] 18 | pub struct Mock { 19 | tx: MockSender, 20 | } 21 | 22 | impl Mock { 23 | /// Create a new mock client. 24 | pub fn new(tx: MockSender) -> Self { 25 | Self { tx } 26 | } 27 | } 28 | 29 | impl Service for Mock { 30 | type Response = BoxResponse; 31 | 32 | type Error = TransportError; 33 | 34 | type Future = MockCallFuture; 35 | 36 | fn poll_ready(&mut self, _: &mut std::task::Context<'_>) -> Poll> { 37 | Ok(()).into() 38 | } 39 | 40 | fn call(&mut self, req: BoxRequest) -> Self::Future { 41 | let (resp_tx, resp_rx) = oneshot::channel(); 42 | let send_res = self 43 | .tx 44 | .inner 45 | .send((req, resp_tx)) 46 | .map_err(|err| MockError::Send(err.0 .0)); 47 | 48 | match send_res { 49 | Ok(_) => MockCallFuture { 50 | inner: MockCallFutureInner::Recv(resp_rx), 51 | }, 52 | Err(err) => MockCallFuture { 53 | inner: MockCallFutureInner::Err(Some(err)), 54 | }, 55 | } 56 | } 57 | } 58 | 59 | enum MockCallFutureInner { 60 | Recv(OneshotReceiver), 61 | Err(Option), 62 | } 63 | 64 | /// Future used by [`Mock`]. 65 | pub struct MockCallFuture { 66 | inner: MockCallFutureInner, 67 | } 68 | 69 | impl Future for MockCallFuture { 70 | type Output = Result>; 71 | 72 | fn poll(self: std::pin::Pin<&mut Self>, cx: &mut std::task::Context<'_>) -> Poll { 73 | match &mut self.get_mut().inner { 74 | MockCallFutureInner::Err(err) => Poll::Ready(Err(TransportError::Transport( 75 | err.take().expect("future polled after completion"), 76 | ))), 77 | MockCallFutureInner::Recv(rx) => rx 78 | .poll_unpin(cx) 79 | .map_err(|_| MockError::Receive) 80 | .map_err(TransportError::Transport), 81 | } 82 | } 83 | } 84 | 85 | /// Errors this client can return. 86 | #[derive(Debug)] 87 | pub enum MockError { 88 | /// Occurs if receiving a response fails. Only happens if sender end of the 89 | /// channel is dropped. 90 | Receive, 91 | /// Occurs if sending a request fails. Only happens if receiver end of the 92 | /// channel is dropped. 93 | Send(BoxRequest), 94 | } 95 | 96 | impl Display for MockError { 97 | fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { 98 | match self { 99 | MockError::Receive => f.write_str("failed to receive response"), 100 | MockError::Send(_) => f.write_str("failed to send request"), 101 | } 102 | } 103 | } 104 | 105 | impl StdError for MockError {} 106 | -------------------------------------------------------------------------------- /crates/hrpc/src/client/transport/mod.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | convert::Infallible, 3 | error::Error as StdError, 4 | fmt::{self, Display, Formatter}, 5 | }; 6 | 7 | use futures_util::{Sink, Stream}; 8 | 9 | use crate::{ 10 | common::socket::{BoxedSocketRx, BoxedSocketTx, SocketMessage}, 11 | BoxError, Request, 12 | }; 13 | 14 | use super::error::ClientError; 15 | 16 | /// The HTTP transport. 17 | #[cfg(feature = "_common_http_client")] 18 | pub mod http; 19 | /// The mock transport. Useful for testing. 20 | #[cfg(feature = "mock_client")] 21 | pub mod mock; 22 | 23 | /// Error type that transports need to return. 24 | #[derive(Debug)] 25 | pub enum TransportError { 26 | /// A transport specific error. 27 | Transport(Err), 28 | /// A generic client error. This can be used by transports to reduce 29 | /// duplicated error variants. 30 | GenericClient(ClientError), 31 | } 32 | 33 | impl From> for ClientError { 34 | fn from(err: TransportError) -> Self { 35 | match err { 36 | TransportError::Transport(err) => ClientError::Transport(err), 37 | TransportError::GenericClient(err) => match err { 38 | ClientError::ContentNotSupported => ClientError::ContentNotSupported, 39 | ClientError::EndpointError { 40 | hrpc_error, 41 | endpoint, 42 | } => ClientError::EndpointError { 43 | hrpc_error, 44 | endpoint, 45 | }, 46 | ClientError::IncompatibleSpecVersion(server_ver) => { 47 | ClientError::IncompatibleSpecVersion(server_ver) 48 | } 49 | ClientError::MessageDecode(err) => ClientError::MessageDecode(err), 50 | ClientError::Transport(_) => unreachable!("infallible"), 51 | }, 52 | } 53 | } 54 | } 55 | 56 | impl From> for TransportError { 57 | fn from(err: ClientError) -> Self { 58 | match err { 59 | ClientError::Transport(err) => TransportError::Transport(err), 60 | other => TransportError::GenericClient(match other { 61 | ClientError::ContentNotSupported => ClientError::ContentNotSupported, 62 | ClientError::EndpointError { 63 | hrpc_error, 64 | endpoint, 65 | } => ClientError::EndpointError { 66 | hrpc_error, 67 | endpoint, 68 | }, 69 | ClientError::IncompatibleSpecVersion(server_ver) => { 70 | ClientError::IncompatibleSpecVersion(server_ver) 71 | } 72 | ClientError::MessageDecode(err) => ClientError::MessageDecode(err), 73 | ClientError::Transport(_) => unreachable!("infallible"), 74 | }), 75 | } 76 | } 77 | } 78 | 79 | impl Display for TransportError { 80 | fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { 81 | match self { 82 | TransportError::Transport(err) => write!(f, "transport error: {}", err), 83 | TransportError::GenericClient(err) => write!(f, "{}", err), 84 | } 85 | } 86 | } 87 | 88 | impl StdError for TransportError { 89 | fn source(&self) -> Option<&(dyn StdError + 'static)> { 90 | match self { 91 | TransportError::GenericClient(err) => Some(err), 92 | TransportError::Transport(err) => Some(err), 93 | } 94 | } 95 | } 96 | 97 | /// Struct that should be used by transports to return socket channels 98 | /// to generic client. 99 | pub struct SocketChannels { 100 | pub(super) tx: BoxedSocketTx, 101 | pub(super) rx: BoxedSocketRx, 102 | } 103 | 104 | impl SocketChannels { 105 | /// Create a new socket channels. 106 | pub fn new(tx: Tx, rx: Rx) -> Self 107 | where 108 | Tx: Sink + Send + Sync + 'static, 109 | Rx: Stream> + Send + Sync + 'static, 110 | { 111 | Self { 112 | tx: Box::pin(tx), 113 | rx: Box::pin(rx), 114 | } 115 | } 116 | } 117 | 118 | /// Marker struct that marks a request as a socket request. 119 | #[derive(Clone)] 120 | pub(super) struct SocketRequestMarker; 121 | 122 | /// Returns whether a request is a socket request or not. 123 | pub fn is_socket_request(req: &Request) -> bool { 124 | req.extensions().contains::() 125 | } 126 | -------------------------------------------------------------------------------- /crates/hrpc/src/common/buf.rs: -------------------------------------------------------------------------------- 1 | // This code is taken originally from https://github.com/hyperium/hyper/blob/4d2125c67c8087de863f74278a017c4caf37e6a9/src/common/buf.rs 2 | // and is licensed under the following: 3 | /* 4 | Copyright (c) 2014-2021 Sean McArthur 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in 14 | all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | THE SOFTWARE. 23 | */ 24 | 25 | use std::collections::VecDeque; 26 | use std::io::IoSlice; 27 | 28 | use bytes::{Buf, BufMut, Bytes, BytesMut}; 29 | 30 | pub(crate) struct BufList { 31 | bufs: VecDeque, 32 | } 33 | 34 | impl BufList { 35 | pub(crate) fn new() -> BufList { 36 | BufList { 37 | bufs: VecDeque::new(), 38 | } 39 | } 40 | 41 | #[inline] 42 | pub(crate) fn push(&mut self, buf: T) { 43 | debug_assert!(buf.has_remaining()); 44 | self.bufs.push_back(buf); 45 | } 46 | 47 | #[inline] 48 | #[cfg(feature = "http1")] 49 | pub(crate) fn bufs_cnt(&self) -> usize { 50 | self.bufs.len() 51 | } 52 | } 53 | 54 | impl Buf for BufList { 55 | #[inline] 56 | fn remaining(&self) -> usize { 57 | self.bufs.iter().map(|buf| buf.remaining()).sum() 58 | } 59 | 60 | #[inline] 61 | fn chunk(&self) -> &[u8] { 62 | self.bufs.front().map(Buf::chunk).unwrap_or_default() 63 | } 64 | 65 | #[inline] 66 | fn advance(&mut self, mut cnt: usize) { 67 | while cnt > 0 { 68 | { 69 | let front = &mut self.bufs[0]; 70 | let rem = front.remaining(); 71 | if rem > cnt { 72 | front.advance(cnt); 73 | return; 74 | } else { 75 | front.advance(rem); 76 | cnt -= rem; 77 | } 78 | } 79 | self.bufs.pop_front(); 80 | } 81 | } 82 | 83 | #[inline] 84 | fn chunks_vectored<'t>(&'t self, dst: &mut [IoSlice<'t>]) -> usize { 85 | if dst.is_empty() { 86 | return 0; 87 | } 88 | let mut vecs = 0; 89 | for buf in &self.bufs { 90 | vecs += buf.chunks_vectored(&mut dst[vecs..]); 91 | if vecs == dst.len() { 92 | break; 93 | } 94 | } 95 | vecs 96 | } 97 | 98 | #[inline] 99 | fn copy_to_bytes(&mut self, len: usize) -> Bytes { 100 | // Our inner buffer may have an optimized version of copy_to_bytes, and if the whole 101 | // request can be fulfilled by the front buffer, we can take advantage. 102 | match self.bufs.front_mut() { 103 | Some(front) if front.remaining() == len => { 104 | let b = front.copy_to_bytes(len); 105 | self.bufs.pop_front(); 106 | b 107 | } 108 | Some(front) if front.remaining() > len => front.copy_to_bytes(len), 109 | _ => { 110 | assert!(len <= self.remaining(), "`len` greater than remaining"); 111 | let mut bm = BytesMut::with_capacity(len); 112 | bm.put(self.take(len)); 113 | bm.freeze() 114 | } 115 | } 116 | } 117 | } 118 | 119 | #[cfg(test)] 120 | mod tests { 121 | use std::ptr; 122 | 123 | use super::*; 124 | 125 | fn hello_world_buf() -> BufList { 126 | BufList { 127 | bufs: vec![Bytes::from("Hello"), Bytes::from(" "), Bytes::from("World")].into(), 128 | } 129 | } 130 | 131 | #[test] 132 | fn to_bytes_shorter() { 133 | let mut bufs = hello_world_buf(); 134 | let old_ptr = bufs.chunk().as_ptr(); 135 | let start = bufs.copy_to_bytes(4); 136 | assert_eq!(start, "Hell"); 137 | assert!(ptr::eq(old_ptr, start.as_ptr())); 138 | assert_eq!(bufs.chunk(), b"o"); 139 | assert!(ptr::eq(old_ptr.wrapping_add(4), bufs.chunk().as_ptr())); 140 | assert_eq!(bufs.remaining(), 7); 141 | } 142 | 143 | #[test] 144 | fn to_bytes_eq() { 145 | let mut bufs = hello_world_buf(); 146 | let old_ptr = bufs.chunk().as_ptr(); 147 | let start = bufs.copy_to_bytes(5); 148 | assert_eq!(start, "Hello"); 149 | assert!(ptr::eq(old_ptr, start.as_ptr())); 150 | assert_eq!(bufs.chunk(), b" "); 151 | assert_eq!(bufs.remaining(), 6); 152 | } 153 | 154 | #[test] 155 | fn to_bytes_longer() { 156 | let mut bufs = hello_world_buf(); 157 | let start = bufs.copy_to_bytes(7); 158 | assert_eq!(start, "Hello W"); 159 | assert_eq!(bufs.remaining(), 4); 160 | } 161 | 162 | #[test] 163 | fn one_long_buf_to_bytes() { 164 | let mut buf = BufList::new(); 165 | buf.push(b"Hello World" as &[_]); 166 | assert_eq!(buf.copy_to_bytes(5), "Hello"); 167 | assert_eq!(buf.chunk(), b" World"); 168 | } 169 | 170 | #[test] 171 | #[should_panic(expected = "`len` greater than remaining")] 172 | fn buf_to_bytes_too_many() { 173 | hello_world_buf().copy_to_bytes(42); 174 | } 175 | } 176 | -------------------------------------------------------------------------------- /crates/hrpc/src/common/extensions.rs: -------------------------------------------------------------------------------- 1 | // This code is originally taken from https://github.com/hyperium/http/blob/cc2f3ed7fc7a119f20e6cbe9a6573dbda5eb38df/src/extensions.rs 2 | // and is licensed under the following: 3 | /* 4 | Copyright (c) 2017 http-rs authors 5 | 6 | Permission is hereby granted, free of charge, to any 7 | person obtaining a copy of this software and associated 8 | documentation files (the "Software"), to deal in the 9 | Software without restriction, including without 10 | limitation the rights to use, copy, modify, merge, 11 | publish, distribute, sublicense, and/or sell copies of 12 | the Software, and to permit persons to whom the Software 13 | is furnished to do so, subject to the following 14 | conditions: 15 | 16 | The above copyright notice and this permission notice 17 | shall be included in all copies or substantial portions 18 | of the Software. 19 | 20 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF 21 | ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 22 | TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 23 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT 24 | SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 25 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 26 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR 27 | IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 28 | DEALINGS IN THE SOFTWARE. 29 | */ 30 | 31 | use std::any::{Any, TypeId}; 32 | use std::collections::HashMap; 33 | use std::fmt; 34 | use std::hash::{BuildHasherDefault, Hasher}; 35 | 36 | type AnyMap = HashMap, BuildHasherDefault>; 37 | 38 | // With TypeIds as keys, there's no need to hash them. They are already hashes 39 | // themselves, coming from the compiler. The IdHasher just holds the u64 of 40 | // the TypeId, and then returns it, instead of doing any bit fiddling. 41 | #[derive(Default)] 42 | struct IdHasher(u64); 43 | 44 | impl Hasher for IdHasher { 45 | fn write(&mut self, _: &[u8]) { 46 | unreachable!("TypeId calls write_u64"); 47 | } 48 | 49 | #[inline] 50 | fn write_u64(&mut self, id: u64) { 51 | self.0 = id; 52 | } 53 | 54 | #[inline] 55 | fn finish(&self) -> u64 { 56 | self.0 57 | } 58 | } 59 | 60 | /// A type map of transport extensions. 61 | /// 62 | /// `Extensions` can be used by `Request` and `Response` to store 63 | /// extra data derived from the underlying transport. 64 | #[derive(Default)] 65 | pub struct Extensions { 66 | // If extensions are never used, no need to carry around an empty HashMap. 67 | // That's 3 words. Instead, this is only 1 word. 68 | map: Option>, 69 | } 70 | 71 | impl Extensions { 72 | /// Create an empty `Extensions`. 73 | #[inline] 74 | pub fn new() -> Extensions { 75 | Extensions { map: None } 76 | } 77 | 78 | /// Insert a type into this `Extensions`. 79 | /// 80 | /// If a extension of this type already existed, it will 81 | /// be returned. 82 | /// 83 | /// # Example 84 | /// 85 | /// ``` 86 | /// # use hrpc::common::extensions::Extensions; 87 | /// let mut ext = Extensions::new(); 88 | /// assert!(ext.insert(5i32).is_none()); 89 | /// assert!(ext.insert(4u8).is_none()); 90 | /// assert_eq!(ext.insert(9i32), Some(5i32)); 91 | /// ``` 92 | pub fn insert(&mut self, val: T) -> Option { 93 | self.map 94 | .get_or_insert_with(|| Box::new(HashMap::default())) 95 | .insert(TypeId::of::(), Box::new(val)) 96 | .and_then(|boxed| { 97 | (boxed as Box) 98 | .downcast() 99 | .ok() 100 | .map(|boxed| *boxed) 101 | }) 102 | } 103 | 104 | /// Get a reference to a type previously inserted on this `Extensions`. 105 | /// 106 | /// # Example 107 | /// 108 | /// ``` 109 | /// # use hrpc::common::extensions::Extensions; 110 | /// let mut ext = Extensions::new(); 111 | /// assert!(ext.get::().is_none()); 112 | /// ext.insert(5i32); 113 | /// 114 | /// assert_eq!(ext.get::(), Some(&5i32)); 115 | /// ``` 116 | pub fn get(&self) -> Option<&T> { 117 | self.map 118 | .as_ref() 119 | .and_then(|map| map.get(&TypeId::of::())) 120 | .and_then(|boxed| (&**boxed as &(dyn Any + 'static)).downcast_ref()) 121 | } 122 | 123 | /// Get a mutable reference to a type previously inserted on this `Extensions`. 124 | /// 125 | /// # Example 126 | /// 127 | /// ``` 128 | /// # use hrpc::common::extensions::Extensions; 129 | /// let mut ext = Extensions::new(); 130 | /// ext.insert(String::from("Hello")); 131 | /// ext.get_mut::().unwrap().push_str(" World"); 132 | /// 133 | /// assert_eq!(ext.get::().unwrap(), "Hello World"); 134 | /// ``` 135 | pub fn get_mut(&mut self) -> Option<&mut T> { 136 | self.map 137 | .as_mut() 138 | .and_then(|map| map.get_mut(&TypeId::of::())) 139 | .and_then(|boxed| (&mut **boxed as &mut (dyn Any + 'static)).downcast_mut()) 140 | } 141 | 142 | /// Remove a type from this `Extensions`. 143 | /// 144 | /// If a extension of this type existed, it will be returned. 145 | /// 146 | /// # Example 147 | /// 148 | /// ``` 149 | /// # use hrpc::common::extensions::Extensions; 150 | /// let mut ext = Extensions::new(); 151 | /// ext.insert(5i32); 152 | /// assert_eq!(ext.remove::(), Some(5i32)); 153 | /// assert!(ext.get::().is_none()); 154 | /// ``` 155 | pub fn remove(&mut self) -> Option { 156 | self.map 157 | .as_mut() 158 | .and_then(|map| map.remove(&TypeId::of::())) 159 | .and_then(|boxed| { 160 | (boxed as Box) 161 | .downcast() 162 | .ok() 163 | .map(|boxed| *boxed) 164 | }) 165 | } 166 | 167 | /// Check if a type is contained in this `Extensions`. 168 | /// 169 | /// If a extension of this type existed, it will return `true`. 170 | /// 171 | /// # Example 172 | /// 173 | /// ``` 174 | /// # use hrpc::common::extensions::Extensions; 175 | /// let mut ext = Extensions::new(); 176 | /// ext.insert(5i32); 177 | /// assert!(ext.contains::()); 178 | /// assert_eq!(ext.remove::(), Some(5i32)); 179 | /// assert!(!ext.contains::()); 180 | /// ``` 181 | pub fn contains(&self) -> bool { 182 | self.map 183 | .as_ref() 184 | .map_or(false, |map| map.contains_key(&TypeId::of::())) 185 | } 186 | 187 | /// Clear the `Extensions` of all inserted extensions. 188 | /// 189 | /// # Example 190 | /// 191 | /// ``` 192 | /// # use hrpc::common::extensions::Extensions; 193 | /// let mut ext = Extensions::new(); 194 | /// ext.insert(5i32); 195 | /// ext.clear(); 196 | /// 197 | /// assert!(ext.get::().is_none()); 198 | /// ``` 199 | #[inline] 200 | pub fn clear(&mut self) { 201 | if let Some(ref mut map) = self.map { 202 | map.clear(); 203 | } 204 | } 205 | 206 | /// Check whether the extension set is empty or not. 207 | /// 208 | /// # Example 209 | /// 210 | /// ``` 211 | /// # use hrpc::common::extensions::Extensions; 212 | /// let mut ext = Extensions::new(); 213 | /// assert!(ext.is_empty()); 214 | /// ext.insert(5i32); 215 | /// assert!(!ext.is_empty()); 216 | /// ``` 217 | #[inline] 218 | pub fn is_empty(&self) -> bool { 219 | self.map.as_ref().map_or(true, |map| map.is_empty()) 220 | } 221 | 222 | /// Get the numer of extensions available. 223 | /// 224 | /// # Example 225 | /// 226 | /// ``` 227 | /// # use hrpc::common::extensions::Extensions; 228 | /// let mut ext = Extensions::new(); 229 | /// assert_eq!(ext.len(), 0); 230 | /// ext.insert(5i32); 231 | /// assert_eq!(ext.len(), 1); 232 | /// ``` 233 | #[inline] 234 | pub fn len(&self) -> usize { 235 | self.map.as_ref().map_or(0, |map| map.len()) 236 | } 237 | } 238 | 239 | impl fmt::Debug for Extensions { 240 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 241 | f.debug_struct("Extensions").finish() 242 | } 243 | } 244 | 245 | #[test] 246 | fn test_extensions() { 247 | #[derive(Debug, PartialEq)] 248 | struct MyType(i32); 249 | 250 | let mut extensions = Extensions::new(); 251 | 252 | extensions.insert(5i32); 253 | extensions.insert(MyType(10)); 254 | 255 | assert_eq!(extensions.get(), Some(&5i32)); 256 | assert_eq!(extensions.get_mut(), Some(&mut 5i32)); 257 | 258 | assert_eq!(extensions.remove::(), Some(5i32)); 259 | assert!(extensions.get::().is_none()); 260 | 261 | assert_eq!(extensions.get::(), None); 262 | assert_eq!(extensions.get(), Some(&MyType(10))); 263 | } 264 | -------------------------------------------------------------------------------- /crates/hrpc/src/common/future.rs: -------------------------------------------------------------------------------- 1 | use std::future::Future; 2 | use std::pin::Pin; 3 | use std::task::{Context, Poll}; 4 | 5 | /// Creates a future that is immediately ready with a value. 6 | /// 7 | /// This `struct` is created by [`ready()`]. See its 8 | /// documentation for more. 9 | #[derive(Debug, Clone)] 10 | #[must_use = "futures do nothing unless you `.await` or poll them"] 11 | pub struct Ready(Option); 12 | 13 | impl Ready { 14 | /// Extract the inner value of this [`Ready`]. 15 | pub fn into_inner(self) -> Option { 16 | self.0 17 | } 18 | } 19 | 20 | impl Unpin for Ready {} 21 | 22 | impl Future for Ready { 23 | type Output = T; 24 | 25 | #[inline] 26 | fn poll(mut self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll { 27 | Poll::Ready(self.0.take().expect("`Ready` polled after completion")) 28 | } 29 | } 30 | 31 | /// Creates a future that is immediately ready with a value. 32 | /// 33 | /// Futures created through this function are functionally similar to those 34 | /// created through `async {}`. The main difference is that futures created 35 | /// through this function are named and implement `Unpin`. 36 | /// 37 | /// # Examples 38 | /// 39 | /// ``` 40 | /// use std::future; 41 | /// 42 | /// # async fn run() { 43 | /// let a = future::ready(1); 44 | /// assert_eq!(a.await, 1); 45 | /// # } 46 | /// ``` 47 | pub fn ready(t: T) -> Ready { 48 | Ready(Some(t)) 49 | } 50 | -------------------------------------------------------------------------------- /crates/hrpc/src/common/layer/mod.rs: -------------------------------------------------------------------------------- 1 | /// Layer to modify request and responses. 2 | pub mod modify; 3 | /// Tracing layer for hRPC clients and services. 4 | pub mod trace; 5 | -------------------------------------------------------------------------------- /crates/hrpc/src/common/layer/modify.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | pin::Pin, 3 | task::{Context, Poll}, 4 | }; 5 | 6 | use futures_util::{Future, FutureExt}; 7 | use pin_project_lite::pin_project; 8 | use tower::{Layer, Service}; 9 | 10 | use crate::{request::BoxRequest, response::BoxResponse}; 11 | 12 | /// Layer for creating [`Modify`] instances. 13 | #[derive(Clone)] 14 | pub struct ModifyLayer { 15 | req_fn: ModifyReq, 16 | resp_fn: ModifyResp, 17 | } 18 | 19 | impl ModifyLayer 20 | where 21 | ModifyReq: Fn(&mut BoxRequest), 22 | ModifyResp: Fn(&mut BoxResponse), 23 | { 24 | /// Create a new layer. 25 | pub fn new(req_fn: ModifyReq, resp_fn: ModifyResp) -> Self { 26 | Self { req_fn, resp_fn } 27 | } 28 | } 29 | 30 | impl ModifyLayer 31 | where 32 | ModifyReq: Fn(&mut BoxRequest), 33 | { 34 | /// Create a new layer that only modifies requests. 35 | pub fn new_request(f: ModifyReq) -> Self { 36 | Self::new(f, |_| ()) 37 | } 38 | } 39 | 40 | impl ModifyLayer 41 | where 42 | ModifyResp: Fn(&mut BoxResponse), 43 | { 44 | /// Create a new layer that only modifies responses. 45 | pub fn new_response(f: ModifyResp) -> Self { 46 | Self::new(|_| (), f) 47 | } 48 | } 49 | 50 | impl Layer for ModifyLayer 51 | where 52 | ModifyReq: Clone, 53 | ModifyResp: Clone, 54 | { 55 | type Service = Modify; 56 | 57 | fn layer(&self, inner: S) -> Self::Service { 58 | Modify::new(inner, self.req_fn.clone(), self.resp_fn.clone()) 59 | } 60 | } 61 | 62 | /// Service that lets you modify / inspect requests and responses. 63 | #[derive(Clone)] 64 | pub struct Modify { 65 | inner: S, 66 | req_fn: ModifyReq, 67 | resp_fn: ModifyResp, 68 | } 69 | 70 | impl Modify { 71 | /// Create a new service by wrapping a given service. 72 | pub fn new(inner: S, req_fn: ModifyReq, resp_fn: ModifyResp) -> Self { 73 | Self { 74 | inner, 75 | req_fn, 76 | resp_fn, 77 | } 78 | } 79 | } 80 | 81 | impl Service for Modify 82 | where 83 | S: Service, 84 | ModifyReq: Fn(&mut BoxRequest), 85 | ModifyResp: Fn(&mut BoxResponse) + Clone, 86 | { 87 | type Response = BoxResponse; 88 | 89 | type Error = S::Error; 90 | 91 | type Future = ModifyFuture; 92 | 93 | fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll> { 94 | Service::poll_ready(&mut self.inner, cx) 95 | } 96 | 97 | fn call(&mut self, mut req: BoxRequest) -> Self::Future { 98 | (self.req_fn)(&mut req); 99 | 100 | ModifyFuture { 101 | fut: Service::call(&mut self.inner, req), 102 | resp_fn: self.resp_fn.clone(), 103 | } 104 | } 105 | } 106 | 107 | pin_project! { 108 | /// Future for [`Modify`]. 109 | pub struct ModifyFuture { 110 | #[pin] 111 | fut: Fut, 112 | resp_fn: ModifyResp, 113 | } 114 | } 115 | 116 | impl Future for ModifyFuture 117 | where 118 | Fut: Future>, 119 | ModifyResp: Fn(&mut BoxResponse), 120 | { 121 | type Output = Result; 122 | 123 | fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { 124 | let mut this = self.project(); 125 | 126 | this.fut.poll_unpin(cx).map_ok(|mut resp| { 127 | (this.resp_fn)(&mut resp); 128 | resp 129 | }) 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /crates/hrpc/src/common/layer/trace.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | pin::Pin, 3 | task::{Context, Poll}, 4 | }; 5 | 6 | use futures_util::{Future, FutureExt}; 7 | use tower::{Layer, Service}; 8 | use tracing::Span; 9 | 10 | use crate::{proto::Error as HrpcError, request::BoxRequest, response::BoxResponse}; 11 | 12 | /// Layer for layering hRPC services with [`Trace`]. 13 | #[derive(Debug, Clone)] 14 | pub struct TraceLayer { 15 | span_fn: SpanFn, 16 | on_request: OnRequestFn, 17 | on_success: OnSuccessFn, 18 | on_error: OnErrorFn, 19 | } 20 | 21 | impl 22 | TraceLayer 23 | where 24 | SpanFn: Fn(&BoxRequest) -> Span, 25 | OnRequestFn: Fn(&BoxRequest, &Span), 26 | OnSuccessFn: Fn(&BoxResponse, &Span), 27 | OnErrorFn: Fn(&BoxResponse, &Span, &HrpcError), 28 | { 29 | /// Create a new trace layer. 30 | pub fn new( 31 | span_fn: SpanFn, 32 | on_request: OnRequestFn, 33 | on_success: OnSuccessFn, 34 | on_error: OnErrorFn, 35 | ) -> Self { 36 | Self { 37 | span_fn, 38 | on_request, 39 | on_success, 40 | on_error, 41 | } 42 | } 43 | 44 | /// Change the span function that will be used. 45 | pub fn span_fn( 46 | self, 47 | span_fn: NewSpanFn, 48 | ) -> TraceLayer 49 | where 50 | NewSpanFn: Fn(&BoxRequest) -> Span, 51 | { 52 | TraceLayer { 53 | span_fn, 54 | on_error: self.on_error, 55 | on_request: self.on_request, 56 | on_success: self.on_success, 57 | } 58 | } 59 | 60 | /// Change the on request function that will be used. 61 | pub fn on_request( 62 | self, 63 | on_request: NewOnRequestFn, 64 | ) -> TraceLayer 65 | where 66 | NewOnRequestFn: Fn(&BoxRequest, &Span), 67 | { 68 | TraceLayer { 69 | span_fn: self.span_fn, 70 | on_error: self.on_error, 71 | on_request, 72 | on_success: self.on_success, 73 | } 74 | } 75 | 76 | /// Change the on success function that will be used. 77 | pub fn on_success( 78 | self, 79 | on_success: NewOnSuccessFn, 80 | ) -> TraceLayer 81 | where 82 | NewOnSuccessFn: Fn(&BoxResponse, &Span), 83 | { 84 | TraceLayer { 85 | span_fn: self.span_fn, 86 | on_error: self.on_error, 87 | on_request: self.on_request, 88 | on_success, 89 | } 90 | } 91 | 92 | /// Change the on error function that will be used. 93 | pub fn on_error( 94 | self, 95 | on_error: NewOnErrorFn, 96 | ) -> TraceLayer 97 | where 98 | NewOnErrorFn: Fn(&BoxResponse, &Span, &HrpcError), 99 | { 100 | TraceLayer { 101 | span_fn: self.span_fn, 102 | on_error, 103 | on_request: self.on_request, 104 | on_success: self.on_success, 105 | } 106 | } 107 | } 108 | 109 | type SpanFnPtr = fn(&BoxRequest) -> Span; 110 | type OnRequestFnPtr = fn(&BoxRequest, &Span); 111 | type OnSuccessFnPtr = fn(&BoxResponse, &Span); 112 | type OnErrorFnPtr = fn(&BoxResponse, &Span, &HrpcError); 113 | 114 | impl TraceLayer { 115 | /// Create a trace layer with a default configuration that logs on the info 116 | /// level of `tracing`. Errors are logged with error level. 117 | pub fn default() -> Self { 118 | Self { 119 | span_fn: |req| tracing::info_span!("request", endpoint = %req.endpoint()), 120 | on_request: |_, _| tracing::info!("processing request"), 121 | on_success: |_, _| tracing::info!("request successful"), 122 | on_error: |_, _, err| tracing::error!("request failed: {}", err), 123 | } 124 | } 125 | 126 | /// Create a trace layer with a default configuration that logs on the debug 127 | /// level of `tracing`. Errors are logged with error level. 128 | pub fn default_debug() -> Self { 129 | Self { 130 | span_fn: |req| tracing::debug_span!("request", endpoint = %req.endpoint()), 131 | on_request: |_, _| tracing::debug!("processing request"), 132 | on_success: |_, _| tracing::debug!("request successful"), 133 | on_error: |_, _, err| tracing::error!("request failed: {}", err), 134 | } 135 | } 136 | } 137 | 138 | impl Layer 139 | for TraceLayer 140 | where 141 | SpanFn: Fn(&BoxRequest) -> Span + Clone, 142 | OnRequestFn: Fn(&BoxRequest, &Span) + Clone, 143 | OnSuccessFn: Fn(&BoxResponse, &Span) + Clone, 144 | OnErrorFn: Fn(&BoxResponse, &Span, &HrpcError) + Clone, 145 | { 146 | type Service = Trace; 147 | 148 | fn layer(&self, inner: S) -> Self::Service { 149 | Trace::new( 150 | inner, 151 | self.span_fn.clone(), 152 | self.on_request.clone(), 153 | self.on_success.clone(), 154 | self.on_error.clone(), 155 | ) 156 | } 157 | } 158 | 159 | /// Service that can be used to trace request and responses. 160 | #[derive(Debug, Clone)] 161 | pub struct Trace { 162 | inner: S, 163 | span_fn: SpanFn, 164 | on_request: OnRequestFn, 165 | on_success: OnSuccessFn, 166 | on_error: OnErrorFn, 167 | } 168 | 169 | impl 170 | Trace 171 | where 172 | SpanFn: Fn(&BoxRequest) -> Span, 173 | OnRequestFn: Fn(&BoxRequest, &Span), 174 | OnSuccessFn: Fn(&BoxResponse, &Span), 175 | OnErrorFn: Fn(&BoxResponse, &Span, &HrpcError), 176 | { 177 | /// Create a new trace service. 178 | pub fn new( 179 | inner: S, 180 | span_fn: SpanFn, 181 | on_request: OnRequestFn, 182 | on_success: OnSuccessFn, 183 | on_error: OnErrorFn, 184 | ) -> Self { 185 | Self { 186 | inner, 187 | span_fn, 188 | on_request, 189 | on_success, 190 | on_error, 191 | } 192 | } 193 | } 194 | 195 | impl Service 196 | for Trace 197 | where 198 | S: Service, 199 | SpanFn: Fn(&BoxRequest) -> Span, 200 | OnRequestFn: Fn(&BoxRequest, &Span), 201 | OnSuccessFn: Fn(&BoxResponse, &Span) + Clone, 202 | OnErrorFn: Fn(&BoxResponse, &Span, &HrpcError) + Clone, 203 | { 204 | type Response = S::Response; 205 | 206 | type Error = S::Error; 207 | 208 | type Future = TraceFuture; 209 | 210 | fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll> { 211 | Service::poll_ready(&mut self.inner, cx) 212 | } 213 | 214 | fn call(&mut self, req: BoxRequest) -> Self::Future { 215 | let span = (self.span_fn)(&req); 216 | 217 | let fut = { 218 | let _guard = span.enter(); 219 | (self.on_request)(&req, &span); 220 | Service::call(&mut self.inner, req) 221 | }; 222 | 223 | TraceFuture { 224 | span, 225 | on_error: self.on_error.clone(), 226 | on_success: self.on_success.clone(), 227 | fut, 228 | } 229 | } 230 | } 231 | 232 | pin_project_lite::pin_project! { 233 | /// Future used by [`Trace`]. 234 | pub struct TraceFuture { 235 | #[pin] 236 | fut: Fut, 237 | span: Span, 238 | on_success: OnSuccessFn, 239 | on_error: OnErrorFn, 240 | } 241 | } 242 | 243 | impl Future for TraceFuture 244 | where 245 | Fut: Future>, 246 | OnSuccessFn: Fn(&BoxResponse, &Span), 247 | OnErrorFn: Fn(&BoxResponse, &Span, &HrpcError), 248 | { 249 | type Output = Fut::Output; 250 | 251 | fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { 252 | let mut project = self.project(); 253 | let _guard = project.span.enter(); 254 | 255 | let resp = futures_util::ready!(project.fut.poll_unpin(cx)?); 256 | 257 | if let Some(err) = resp.extensions().get::() { 258 | (project.on_error)(&resp, project.span, err); 259 | } else { 260 | (project.on_success)(&resp, project.span); 261 | } 262 | 263 | Poll::Ready(Ok(resp)) 264 | } 265 | } 266 | -------------------------------------------------------------------------------- /crates/hrpc/src/common/mod.rs: -------------------------------------------------------------------------------- 1 | pub(crate) mod buf; 2 | 3 | /// Extension type used by [`crate::Request`] and [`crate::Response`]. 4 | pub mod extensions; 5 | /// Common future types used by `hrpc-rs`. 6 | pub mod future; 7 | /// Common layers that can be used in both clients and servers. 8 | #[cfg(feature = "_common")] 9 | pub mod layer; 10 | /// Common code to work with sockets. 11 | #[cfg(feature = "_common")] 12 | pub mod socket; 13 | /// Common code to work with transports. 14 | #[cfg(feature = "_common")] 15 | pub mod transport; 16 | -------------------------------------------------------------------------------- /crates/hrpc/src/common/transport/http.rs: -------------------------------------------------------------------------------- 1 | use bytes::Bytes; 2 | use futures_util::StreamExt; 3 | use http::{header::HeaderName, HeaderMap, HeaderValue, StatusCode}; 4 | 5 | use crate::{ 6 | body::Body, proto::HrpcErrorIdentifier, BoxError, Request, Response, HRPC_CONTENT_MIMETYPE, 7 | HRPC_SPEC_VERSION, 8 | }; 9 | 10 | /// The hRPC version header used in unary requests. 11 | pub const HRPC_VERSION_HEADER: &str = "hrpc-version"; 12 | 13 | /// Create a header value for the hRPC content type. 14 | pub fn content_header_value() -> HeaderValue { 15 | unsafe { 16 | HeaderValue::from_maybe_shared_unchecked(Bytes::from_static( 17 | HRPC_CONTENT_MIMETYPE.as_bytes(), 18 | )) 19 | } 20 | } 21 | 22 | /// Create the spec compliant WS protocol with hRPC version, as a header value. 23 | pub fn ws_version_header_value() -> HeaderValue { 24 | unsafe { HeaderValue::from_maybe_shared_unchecked(ws_version().into_bytes()) } 25 | } 26 | 27 | /// Create the spec compliant WS protocol with hRPC version. 28 | pub fn ws_version() -> String { 29 | format!("hrpc{}", HRPC_SPEC_VERSION) 30 | } 31 | 32 | /// Create a header value with hRPC version, for the [`HRPC_VERSION_HEADER`] header. 33 | pub fn version_header_value() -> HeaderValue { 34 | unsafe { 35 | HeaderValue::from_maybe_shared_unchecked(Bytes::from_static(HRPC_SPEC_VERSION.as_bytes())) 36 | } 37 | } 38 | 39 | /// Create a header name for the hRPC version header (see [`HRPC_VERSION_HEADER`]). 40 | pub fn version_header_name() -> HeaderName { 41 | HeaderName::from_static(HRPC_VERSION_HEADER) 42 | } 43 | 44 | /// Helper methods for working with `HeaderMap`. 45 | pub trait HeaderMapExt { 46 | /// Check if a header is equal to a bytes array. Ignores casing. 47 | fn header_eq(&self, key: &HeaderName, value: &[u8]) -> bool; 48 | /// Check if a header contains a string. Ignores casing for the header value. 49 | fn header_contains_str(&self, key: &HeaderName, value: &str) -> bool; 50 | } 51 | 52 | impl HeaderMapExt for HeaderMap { 53 | fn header_eq(&self, key: &HeaderName, value: &[u8]) -> bool { 54 | self.get(key).map_or(false, |header| { 55 | header.as_bytes().eq_ignore_ascii_case(value) 56 | }) 57 | } 58 | 59 | fn header_contains_str(&self, key: &HeaderName, pat: &str) -> bool { 60 | self.get(key).map_or(false, |header| { 61 | header 62 | .to_str() 63 | .map_or(false, |value| value.to_ascii_lowercase().contains(pat)) 64 | }) 65 | } 66 | } 67 | 68 | impl_exts::impl_exts!(Request); 69 | impl_exts::impl_exts!(Response); 70 | 71 | impl Request { 72 | /// Get an immutable reference to the HTTP method. 73 | #[inline] 74 | pub fn http_method(&self) -> Option<&http::Method> { 75 | self.extensions().get::() 76 | } 77 | 78 | /// Get an immutable reference to the HTTP version. 79 | #[inline] 80 | pub fn http_version(&self) -> Option<&http::Version> { 81 | self.extensions().get::() 82 | } 83 | 84 | /// Get an immutable reference to the HTTP URI. 85 | #[inline] 86 | pub fn http_uri(&self) -> Option<&http::Uri> { 87 | self.extensions().get::() 88 | } 89 | } 90 | 91 | mod impl_exts { 92 | macro_rules! impl_exts { 93 | ($t:ty) => { 94 | impl $t { 95 | /// Get an immutable reference to the header map. 96 | #[inline] 97 | pub fn header_map(&self) -> Option<&HeaderMap> { 98 | self.extensions().get::() 99 | } 100 | 101 | /// Get a mutable reference to the header map. 102 | #[inline] 103 | pub fn header_map_mut(&mut self) -> Option<&mut HeaderMap> { 104 | self.extensions_mut().get_mut::() 105 | } 106 | 107 | /// Get a mutable reference to the header map, inserting a new one 108 | /// if it doesn't already exist. 109 | #[inline] 110 | pub fn get_or_insert_header_map(&mut self) -> &mut HeaderMap { 111 | if !self.extensions().contains::() { 112 | self.extensions_mut().insert(HeaderMap::new()); 113 | } 114 | self.extensions_mut().get_mut::().unwrap() 115 | } 116 | 117 | /// Get an immutable reference to the HTTP extensions. 118 | #[inline] 119 | pub fn http_extensions(&self) -> Option<&http::Extensions> { 120 | self.extensions().get::() 121 | } 122 | 123 | /// Get a mutable reference to the HTTP extensions. 124 | #[inline] 125 | pub fn http_extensions_mut(&mut self) -> Option<&mut http::Extensions> { 126 | self.extensions_mut().get_mut::() 127 | } 128 | 129 | /// Get a mutable reference to the HTTP extensions, inserting a new one 130 | /// if it doesn't already exist. 131 | #[inline] 132 | pub fn get_or_insert_http_extensions(&mut self) -> &mut http::Extensions { 133 | if !self.extensions().contains::() { 134 | self.extensions_mut().insert(http::Extensions::new()); 135 | } 136 | self.extensions_mut().get_mut::().unwrap() 137 | } 138 | } 139 | }; 140 | } 141 | 142 | pub(crate) use impl_exts; 143 | } 144 | 145 | impl http_body::Body for Body { 146 | type Data = Bytes; 147 | 148 | type Error = BoxError; 149 | 150 | fn poll_data( 151 | mut self: std::pin::Pin<&mut Self>, 152 | cx: &mut std::task::Context<'_>, 153 | ) -> std::task::Poll>> { 154 | self.poll_next_unpin(cx) 155 | } 156 | 157 | fn poll_trailers( 158 | self: std::pin::Pin<&mut Self>, 159 | _cx: &mut std::task::Context<'_>, 160 | ) -> std::task::Poll, Self::Error>> { 161 | std::task::Poll::Ready(Ok(None)) 162 | } 163 | } 164 | 165 | impl From for StatusCode { 166 | fn from(id: HrpcErrorIdentifier) -> Self { 167 | match id { 168 | HrpcErrorIdentifier::InternalServerError => StatusCode::INTERNAL_SERVER_ERROR, 169 | HrpcErrorIdentifier::NotFound => StatusCode::NOT_FOUND, 170 | HrpcErrorIdentifier::NotImplemented => StatusCode::NOT_IMPLEMENTED, 171 | HrpcErrorIdentifier::ResourceExhausted => StatusCode::TOO_MANY_REQUESTS, 172 | HrpcErrorIdentifier::Unavailable => StatusCode::SERVICE_UNAVAILABLE, 173 | } 174 | } 175 | } 176 | 177 | #[cfg(feature = "hyper")] 178 | mod hyper { 179 | use futures_util::TryStreamExt; 180 | 181 | use crate::{body::Body, BoxError}; 182 | 183 | impl From for hyper::Body { 184 | fn from(body: Body) -> Self { 185 | hyper::Body::wrap_stream(body) 186 | } 187 | } 188 | 189 | impl From for Body { 190 | fn from(hbody: hyper::Body) -> Self { 191 | Body::new( 192 | hbody 193 | .into_stream() 194 | .map_err(|err| -> BoxError { Box::new(err) }), 195 | ) 196 | } 197 | } 198 | } 199 | -------------------------------------------------------------------------------- /crates/hrpc/src/common/transport/mock.rs: -------------------------------------------------------------------------------- 1 | use tokio::sync::{ 2 | mpsc::{self, UnboundedReceiver as MpscReceiver, UnboundedSender as MpscSender}, 3 | oneshot::Sender as OneshotSender, 4 | }; 5 | 6 | use crate::{request::BoxRequest, response::BoxResponse}; 7 | 8 | /// A mock sender. 9 | #[derive(Clone)] 10 | pub struct MockSender { 11 | pub(crate) inner: MpscSender<(BoxRequest, OneshotSender)>, 12 | } 13 | 14 | /// A mock receiver. 15 | pub struct MockReceiver { 16 | pub(crate) inner: MpscReceiver<(BoxRequest, OneshotSender)>, 17 | } 18 | 19 | /// Create a new pair of mock channels. 20 | pub fn new_mock_channels() -> (MockSender, MockReceiver) { 21 | let (tx, rx) = mpsc::unbounded_channel(); 22 | (MockSender { inner: tx }, MockReceiver { inner: rx }) 23 | } 24 | -------------------------------------------------------------------------------- /crates/hrpc/src/common/transport/mod.rs: -------------------------------------------------------------------------------- 1 | /// Common code to work with the HTTP transport. 2 | #[cfg(feature = "_common_http")] 3 | pub mod http; 4 | 5 | /// Common code to work with tokio_tungstenite WebSockets. 6 | #[cfg(feature = "websocket_tokio_tungstenite")] 7 | pub mod tokio_tungstenite; 8 | 9 | /// Common code to work with ws_stream_wasm WebSockets. 10 | #[cfg(feature = "websocket_wasm")] 11 | pub mod ws_wasm; 12 | 13 | /// Common code to work with the mock transport. 14 | #[cfg(feature = "_common_mock")] 15 | pub mod mock; 16 | -------------------------------------------------------------------------------- /crates/hrpc/src/common/transport/tokio_tungstenite.rs: -------------------------------------------------------------------------------- 1 | use futures_util::{Sink, SinkExt, Stream, StreamExt}; 2 | use tokio::io::{AsyncRead, AsyncWrite}; 3 | use tokio_tungstenite::{tungstenite::Message as WsMessage, WebSocketStream}; 4 | 5 | use crate::{common::socket::SocketMessage, BoxError}; 6 | 7 | /// Wrapper over a [`tokio_tungstenite::WebSocketStream`] that produces 8 | /// and takes [`SocketMessage`]. 9 | pub struct WebSocket { 10 | inner: WebSocketStream, 11 | } 12 | 13 | impl WebSocket { 14 | /// Create a new web socket by wrapping a [`tokio_tungstenite::WebSocketStream`]. 15 | pub fn new(inner: WebSocketStream) -> Self { 16 | Self { inner } 17 | } 18 | } 19 | 20 | impl Stream for WebSocket 21 | where 22 | S: AsyncRead + AsyncWrite + Unpin, 23 | { 24 | type Item = Result; 25 | 26 | fn poll_next( 27 | mut self: std::pin::Pin<&mut Self>, 28 | cx: &mut std::task::Context<'_>, 29 | ) -> std::task::Poll> { 30 | self.inner 31 | .poll_next_unpin(cx) 32 | .map_ok(SocketMessage::from) 33 | .map_err(BoxError::from) 34 | } 35 | } 36 | 37 | impl Sink for WebSocket 38 | where 39 | S: AsyncRead + AsyncWrite + Unpin, 40 | { 41 | type Error = BoxError; 42 | 43 | fn poll_ready( 44 | mut self: std::pin::Pin<&mut Self>, 45 | cx: &mut std::task::Context<'_>, 46 | ) -> std::task::Poll> { 47 | self.inner.poll_ready_unpin(cx).map_err(BoxError::from) 48 | } 49 | 50 | fn start_send( 51 | mut self: std::pin::Pin<&mut Self>, 52 | item: SocketMessage, 53 | ) -> Result<(), Self::Error> { 54 | self.inner 55 | .start_send_unpin(item.into()) 56 | .map_err(BoxError::from) 57 | } 58 | 59 | fn poll_flush( 60 | mut self: std::pin::Pin<&mut Self>, 61 | cx: &mut std::task::Context<'_>, 62 | ) -> std::task::Poll> { 63 | self.inner.poll_flush_unpin(cx).map_err(BoxError::from) 64 | } 65 | 66 | fn poll_close( 67 | mut self: std::pin::Pin<&mut Self>, 68 | cx: &mut std::task::Context<'_>, 69 | ) -> std::task::Poll> { 70 | self.inner.poll_close_unpin(cx).map_err(BoxError::from) 71 | } 72 | } 73 | 74 | impl From for SocketMessage { 75 | fn from(msg: WsMessage) -> Self { 76 | match msg { 77 | // TODO: do we actually need to handle this? 78 | WsMessage::Frame(data) => Self::Binary(data.into_data()), 79 | WsMessage::Binary(data) => Self::Binary(data), 80 | WsMessage::Close(_) => Self::Close, 81 | WsMessage::Text(data) => Self::Text(data), 82 | WsMessage::Pong(data) => Self::Pong(data), 83 | WsMessage::Ping(data) => Self::Ping(data), 84 | } 85 | } 86 | } 87 | 88 | impl From for WsMessage { 89 | fn from(msg: SocketMessage) -> WsMessage { 90 | match msg { 91 | SocketMessage::Binary(data) => Self::Binary(data), 92 | SocketMessage::Close => Self::Close(None), 93 | SocketMessage::Text(data) => Self::Text(data), 94 | SocketMessage::Pong(data) => Self::Pong(data), 95 | SocketMessage::Ping(data) => Self::Ping(data), 96 | } 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /crates/hrpc/src/common/transport/ws_wasm.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | pin::Pin, 3 | task::{Context, Poll}, 4 | }; 5 | 6 | use futures_util::{Sink, SinkExt, Stream, StreamExt}; 7 | use ws_stream_wasm::{WsMessage, WsStream}; 8 | 9 | use crate::{common::socket::SocketMessage, proto::Error as HrpcError, BoxError}; 10 | 11 | /// Type that wraps a [`WsStream`] and implements [`Sink`] and [`Stream`] 12 | /// for working with [`SocketMessage`]s. 13 | /// 14 | /// # Limitations 15 | /// - This does not support sending or receiving [`SocketMessage::Ping`], 16 | /// [`SocketMessage::Pong`], [`SocketMessage::Close`] messages. 17 | pub struct WebSocket { 18 | stream: WsStream, 19 | } 20 | 21 | impl WebSocket { 22 | /// Create a new websocket from a stream. 23 | pub fn new(stream: WsStream) -> Self { 24 | Self { stream } 25 | } 26 | } 27 | 28 | impl Stream for WebSocket { 29 | type Item = Result; 30 | 31 | fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { 32 | self.stream 33 | .poll_next_unpin(cx) 34 | .map(|a| a.map(SocketMessage::from).map(Ok)) 35 | } 36 | } 37 | 38 | impl Sink for WebSocket { 39 | type Error = BoxError; 40 | 41 | fn poll_ready(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { 42 | self.stream.poll_ready_unpin(cx).map_err(BoxError::from) 43 | } 44 | 45 | fn start_send(mut self: Pin<&mut Self>, item: SocketMessage) -> Result<(), Self::Error> { 46 | self.stream 47 | .start_send_unpin(WsMessage::try_from(item)?) 48 | .map_err(BoxError::from) 49 | } 50 | 51 | fn poll_flush(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { 52 | self.stream.poll_flush_unpin(cx).map_err(BoxError::from) 53 | } 54 | 55 | fn poll_close(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { 56 | self.stream.poll_close_unpin(cx).map_err(BoxError::from) 57 | } 58 | } 59 | 60 | impl TryFrom for WsMessage { 61 | type Error = HrpcError; 62 | 63 | fn try_from(msg: SocketMessage) -> Result { 64 | match msg { 65 | SocketMessage::Binary(data) => Ok(WsMessage::Binary(data)), 66 | SocketMessage::Text(data) => Ok(WsMessage::Text(data)), 67 | msg => Err(( 68 | "hrpcrs.socket-error.ws-wasm", 69 | format!("invalid socket message passed: {:?}", msg), 70 | ) 71 | .into()), 72 | } 73 | } 74 | } 75 | 76 | impl From for SocketMessage { 77 | fn from(msg: WsMessage) -> Self { 78 | match msg { 79 | WsMessage::Binary(data) => SocketMessage::Binary(data), 80 | WsMessage::Text(data) => SocketMessage::Text(data), 81 | } 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /crates/hrpc/src/decode.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | error::Error as StdError, 3 | fmt::{self, Display, Formatter}, 4 | }; 5 | 6 | use prost::Message as PbMsg; 7 | 8 | use crate::{body::Body, proto::Error as HrpcError, BoxError}; 9 | 10 | pub(crate) async fn decode_body(body: Body) -> Result { 11 | let buf = body 12 | .aggregate() 13 | .await 14 | .map_err(DecodeBodyError::InvalidBody)?; 15 | let decoded = T::decode(buf).map_err(DecodeBodyError::InvalidProtoMessage)?; 16 | Ok(decoded) 17 | } 18 | 19 | /// Errors that can occur while decoding the body of a [`crate::Request`]. 20 | #[derive(Debug)] 21 | pub enum DecodeBodyError { 22 | /// The body contained an invalid protobuf message. 23 | InvalidProtoMessage(prost::DecodeError), 24 | /// An error occured while reading the body. 25 | InvalidBody(BoxError), 26 | } 27 | 28 | impl Display for DecodeBodyError { 29 | fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { 30 | match self { 31 | Self::InvalidProtoMessage(err) => write!( 32 | f, 33 | "body was detected to be protobuf, but contains invalid protobuf message: {}", 34 | err 35 | ), 36 | Self::InvalidBody(err) => write!(f, "error occured while aggregating body: {}", err), 37 | } 38 | } 39 | } 40 | 41 | impl StdError for DecodeBodyError { 42 | fn source(&self) -> Option<&(dyn StdError + 'static)> { 43 | match self { 44 | Self::InvalidProtoMessage(err) => Some(err), 45 | Self::InvalidBody(err) => err.source(), 46 | } 47 | } 48 | } 49 | 50 | impl From for HrpcError { 51 | fn from(err: DecodeBodyError) -> Self { 52 | let hrpc_err = HrpcError::default().with_message(err.to_string()); 53 | match err { 54 | DecodeBodyError::InvalidBody(_) => hrpc_err.with_identifier("hrpcrs.invalid-body"), 55 | DecodeBodyError::InvalidProtoMessage(_) => { 56 | hrpc_err.with_identifier("hrpcrs.invalid-proto-message") 57 | } 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /crates/hrpc/src/encode.rs: -------------------------------------------------------------------------------- 1 | use bytes::BytesMut; 2 | 3 | /// Encodes a protobuf message into the given `BytesMut` buffer. 4 | pub fn encode_protobuf_message_to(buf: &mut BytesMut, msg: &Msg) { 5 | buf.reserve(msg.encoded_len().saturating_sub(buf.len())); 6 | buf.clear(); 7 | // ignore the error since this can never fail 8 | let _ = msg.encode(buf); 9 | } 10 | 11 | /// Encodes a protobuf message into a new `BytesMut` buffer. 12 | pub fn encode_protobuf_message(msg: &Msg) -> BytesMut { 13 | let mut buf = BytesMut::new(); 14 | encode_protobuf_message_to(&mut buf, msg); 15 | buf 16 | } 17 | -------------------------------------------------------------------------------- /crates/hrpc/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! Generic client and server implementations and transport implementations for hRPC. 2 | //! 3 | //! - For generic client and client implementations, see the [`client`] module. 4 | //! - For generic server and server implementations, see the [`server`] module. 5 | //! - For common code shared by client and servers, see the [`common`] module. 6 | //! - Modules named `transport` contain transport specific code. 7 | //! - Modules named `layer` contain layers for use. These can be generic, or 8 | //! transport specific. 9 | #![deny(missing_docs)] 10 | #![allow(clippy::blocks_in_if_conditions)] 11 | 12 | /// Some re-exported crates that might be useful while writing software with `hrpc`. 13 | pub mod exports { 14 | pub use bytes; 15 | pub use futures_util; 16 | pub use prost; 17 | pub use tracing; 18 | 19 | #[cfg(feature = "_common_http")] 20 | pub use http; 21 | #[cfg(feature = "_common")] 22 | pub use tower; 23 | } 24 | 25 | /// Common client types and functions. 26 | #[cfg(feature = "client")] 27 | pub mod client; 28 | /// Common server types and functions. 29 | #[cfg(feature = "server")] 30 | pub mod server; 31 | 32 | /// Body utitilies and types. 33 | pub mod body; 34 | /// Common utilities. 35 | pub mod common; 36 | /// Decoding utilities. 37 | pub mod decode; 38 | /// Encoding utilities. 39 | pub mod encode; 40 | /// The hRPC generated protocol. 41 | pub mod proto; 42 | /// The `Request` type used by hRPC. 43 | pub mod request; 44 | /// The `Response` type used by hRPC. 45 | pub mod response; 46 | 47 | use std::error::Error; 48 | 49 | #[doc(inline)] 50 | pub use request::Request; 51 | #[doc(inline)] 52 | pub use response::Response; 53 | 54 | /// Alias for a type-erased error type. 55 | pub type BoxError = Box; 56 | 57 | /// Convenience function for converting some error to a boxed error. 58 | pub fn box_error(err: Err) -> BoxError 59 | where 60 | Err: Error + Send + Sync + 'static, 61 | { 62 | Box::new(err) 63 | } 64 | 65 | /// The hRPC protobuf mimetype. 66 | pub const HRPC_CONTENT_MIMETYPE: &str = "application/hrpc"; 67 | /// The hRPC spec version this version of `hrpc-rs` implements. 68 | pub const HRPC_SPEC_VERSION: &str = "1"; 69 | 70 | /// Include generated proto server and client items. 71 | /// 72 | /// You must specify the hRPC package name. 73 | /// 74 | /// ```rust,ignore 75 | /// mod pb { 76 | /// hrpc::include_proto!("helloworld"); 77 | /// } 78 | /// ``` 79 | /// 80 | /// # Note: 81 | /// **This only works if the hrpc-build output directory has been unmodified**. 82 | /// The default output directory is set to the [`OUT_DIR`] environment variable. 83 | /// If the output directory has been modified, the following pattern may be used 84 | /// instead of this macro. 85 | /// 86 | /// ```rust,ignore 87 | /// mod pb { 88 | /// include!("/relative/protobuf/directory/helloworld.rs"); 89 | /// } 90 | /// ``` 91 | /// You can also use a custom environment variable using the following pattern. 92 | /// ```rust,ignore 93 | /// mod pb { 94 | /// include!(concat!(env!("PROTOBUFS"), "/helloworld.rs")); 95 | /// } 96 | /// ``` 97 | /// 98 | /// [`OUT_DIR`]: https://doc.rust-lang.org/cargo/reference/environment-variables.html#environment-variables-cargo-sets-for-build-scripts 99 | #[macro_export] 100 | macro_rules! include_proto { 101 | ($package: tt) => { 102 | include!(concat!(env!("OUT_DIR"), concat!("/", $package, ".rs"))); 103 | }; 104 | } 105 | -------------------------------------------------------------------------------- /crates/hrpc/src/proto.rs: -------------------------------------------------------------------------------- 1 | use std::{error::Error as StdError, fmt::Display, str::FromStr}; 2 | 3 | use bytes::Bytes; 4 | 5 | use crate::{response::BoxResponse, BoxError, Response}; 6 | 7 | crate::include_proto!("hrpc.v1"); 8 | 9 | /// Represents a hRPC error identifier. 10 | #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] 11 | pub enum HrpcErrorIdentifier { 12 | /// Endpoint was not implemented by server. 13 | NotImplemented, 14 | /// Reached resource quota or rate limited by server. 15 | ResourceExhausted, 16 | /// An error occured in server. 17 | InternalServerError, 18 | /// The server could not be reached, most likely means the server is down. 19 | Unavailable, 20 | /// Specified endpoint was not found on server. 21 | NotFound, 22 | } 23 | 24 | impl From for String { 25 | fn from(id: HrpcErrorIdentifier) -> Self { 26 | id.as_id().to_string() 27 | } 28 | } 29 | 30 | impl AsRef for HrpcErrorIdentifier { 31 | fn as_ref(&self) -> &str { 32 | self.as_id() 33 | } 34 | } 35 | 36 | /// Error produced when trying to parse a string as a [`HrpcErrorIdentifier`]. 37 | #[derive(Debug)] 38 | #[non_exhaustive] 39 | pub struct NotHrpcErrorIdentifier; 40 | 41 | impl FromStr for HrpcErrorIdentifier { 42 | type Err = NotHrpcErrorIdentifier; 43 | 44 | fn from_str(s: &str) -> Result { 45 | compare_if_ret::compare_if_ret! { 46 | s, 47 | Self::NotImplemented, 48 | Self::ResourceExhausted, 49 | Self::InternalServerError, 50 | Self::Unavailable, 51 | Self::NotFound, 52 | } 53 | } 54 | } 55 | 56 | mod compare_if_ret { 57 | macro_rules! compare_if_ret { 58 | ($var:ident, $variant:expr, $( $variant2:expr, )+) => { 59 | if $variant.compare($var) { 60 | return Ok($variant); 61 | } $( 62 | else if $variant2.compare($var) { 63 | return Ok($variant2); 64 | } 65 | )* else { 66 | return Err(NotHrpcErrorIdentifier); 67 | } 68 | }; 69 | } 70 | 71 | pub(crate) use compare_if_ret; 72 | } 73 | 74 | impl HrpcErrorIdentifier { 75 | /// Return the string version of this hRPC identifier. 76 | pub const fn as_id(&self) -> &'static str { 77 | match self { 78 | Self::InternalServerError => "hrpc.internal-server-error", 79 | Self::ResourceExhausted => "hrpc.resource-exhausted", 80 | Self::NotImplemented => "hrpc.not-implemented", 81 | Self::Unavailable => "hrpc.unavailable", 82 | Self::NotFound => "hrpc.not-found", 83 | } 84 | } 85 | 86 | /// Compare this hRPC identifier with some string identifier to see if they match. 87 | pub fn compare(&self, identifier: impl AsRef) -> bool { 88 | identifier.as_ref() == self.as_id() 89 | } 90 | } 91 | 92 | impl Error { 93 | /// Create a new hRPC error representing a not implemented endpoint ([`HrpcErrorIdentifier::NotImplemented`]). 94 | pub fn new_not_implemented(message: impl Into) -> Self { 95 | Self::default() 96 | .with_identifier(HrpcErrorIdentifier::NotImplemented) 97 | .with_message(message) 98 | } 99 | 100 | /// Create a new hRPC error representing resource exhaustion by a client ([`HrpcErrorIdentifier::ResourceExhausted`]). 101 | pub fn new_resource_exhausted(message: impl Into) -> Self { 102 | Self::default() 103 | .with_identifier(HrpcErrorIdentifier::ResourceExhausted) 104 | .with_message(message) 105 | } 106 | 107 | /// Create a new hRPC error representing an internal server error ([`HrpcErrorIdentifier::InternalServerError`]). 108 | pub fn new_internal_server_error(message: impl Into) -> Self { 109 | Self::default() 110 | .with_identifier(HrpcErrorIdentifier::InternalServerError) 111 | .with_message(message) 112 | } 113 | 114 | /// Create a new hRPC error representing a not found error ([`HrpcErrorIdentifier::NotFound`]). 115 | pub fn new_not_found(message: impl Into) -> Self { 116 | Self::default() 117 | .with_identifier(HrpcErrorIdentifier::NotFound) 118 | .with_message(message) 119 | } 120 | 121 | /// Set the "more details" of this hRPC error. 122 | pub fn with_details(mut self, details: impl Into) -> Self { 123 | self.details = details.into(); 124 | self 125 | } 126 | 127 | /// Set the "identifier" of this hRPC error. 128 | pub fn with_identifier(mut self, identifier: impl Into) -> Self { 129 | self.identifier = identifier.into(); 130 | self 131 | } 132 | 133 | /// Set the "human message" of this hRPC error. 134 | pub fn with_message(mut self, message: impl Into) -> Self { 135 | self.human_message = message.into(); 136 | self 137 | } 138 | 139 | pub(crate) fn invalid_hrpc_error(details: impl Into) -> Self { 140 | Self { 141 | human_message: 142 | "the server error was an invalid hRPC error, check more_details field for the error" 143 | .to_string(), 144 | identifier: "hrpcrs.invalid-hrpc-error".to_string(), 145 | details: details.into(), 146 | } 147 | } 148 | } 149 | 150 | impl<'a> From<&'a str> for Error { 151 | fn from(msg: &'a str) -> Self { 152 | Error::default() 153 | .with_identifier(HrpcErrorIdentifier::InternalServerError) 154 | .with_message(msg) 155 | } 156 | } 157 | 158 | impl<'a, 'b> From<(&'a str, &'b str)> for Error { 159 | fn from((id, msg): (&'a str, &'b str)) -> Self { 160 | Error::default().with_identifier(id).with_message(msg) 161 | } 162 | } 163 | 164 | impl From for Error { 165 | fn from(msg: String) -> Self { 166 | Error::default() 167 | .with_identifier(HrpcErrorIdentifier::InternalServerError) 168 | .with_message(msg) 169 | } 170 | } 171 | 172 | impl<'a> From<(&'a str, String)> for Error { 173 | fn from((id, msg): (&'a str, String)) -> Self { 174 | Error::default().with_identifier(id).with_message(msg) 175 | } 176 | } 177 | 178 | impl From for Error { 179 | fn from(err: BoxError) -> Self { 180 | Error::default().with_message(err.to_string()) 181 | } 182 | } 183 | 184 | impl From<(&'static str, BoxError)> for Error { 185 | fn from((id, err): (&'static str, BoxError)) -> Self { 186 | Error::default() 187 | .with_identifier(id) 188 | .with_message(err.to_string()) 189 | } 190 | } 191 | 192 | impl From for Response { 193 | fn from(err: Error) -> Self { 194 | let mut resp = Response::new(&err); 195 | resp.extensions_mut().insert(err); 196 | resp 197 | } 198 | } 199 | 200 | impl From for BoxResponse { 201 | fn from(err: Error) -> Self { 202 | Response::::from(err).map::<()>() 203 | } 204 | } 205 | 206 | impl Display for Error { 207 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 208 | write!(f, "error '{}': {}", self.identifier, self.human_message) 209 | } 210 | } 211 | 212 | impl StdError for Error {} 213 | -------------------------------------------------------------------------------- /crates/hrpc/src/request.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | borrow::Cow, 3 | fmt::{self, Debug, Formatter}, 4 | marker::PhantomData, 5 | }; 6 | 7 | use prost::Message as PbMsg; 8 | 9 | use crate::{ 10 | body::Body, common::extensions::Extensions, decode::*, encode::encode_protobuf_message, 11 | }; 12 | 13 | /// Request parts. 14 | #[non_exhaustive] 15 | #[derive(Debug)] 16 | pub struct Parts { 17 | /// Body of a request. 18 | pub body: Body, 19 | /// Extensions of a request. 20 | pub extensions: Extensions, 21 | /// Endpoint of a request. 22 | pub endpoint: Cow<'static, str>, 23 | } 24 | 25 | impl From> for Parts { 26 | fn from(req: Request) -> Self { 27 | req.parts 28 | } 29 | } 30 | 31 | impl From for Request { 32 | fn from(parts: Parts) -> Self { 33 | Self { 34 | parts, 35 | message: PhantomData, 36 | } 37 | } 38 | } 39 | 40 | /// A hRPC request. 41 | pub struct Request { 42 | parts: Parts, 43 | message: PhantomData, 44 | } 45 | 46 | impl Debug for Request { 47 | fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { 48 | f.debug_struct("Request") 49 | .field("body", &self.parts.body) 50 | .field("extensions", &self.parts.extensions) 51 | .field("endpoint", &self.parts.endpoint) 52 | .finish() 53 | } 54 | } 55 | 56 | impl Request<()> { 57 | /// Create a request with an empty body. 58 | /// 59 | /// This is useful for hRPC socket requests, since they don't send any messages. 60 | pub fn empty() -> Request<()> { 61 | Self::new_with_body(Body::empty()) 62 | } 63 | } 64 | 65 | impl Request { 66 | /// Creates a new request using the provided body. 67 | pub fn new_with_body(body: Body) -> Self { 68 | Self { 69 | parts: Parts { 70 | body, 71 | extensions: Extensions::new(), 72 | endpoint: Cow::Borrowed(""), 73 | }, 74 | message: PhantomData, 75 | } 76 | } 77 | 78 | /// Get a mutable reference to the extensions of this response. 79 | #[inline] 80 | pub fn extensions_mut(&mut self) -> &mut Extensions { 81 | &mut self.parts.extensions 82 | } 83 | 84 | /// Get an immutable reference to the extensions of this response. 85 | #[inline] 86 | pub fn extensions(&self) -> &Extensions { 87 | &self.parts.extensions 88 | } 89 | 90 | /// Get a mutable reference to the endpoint of this response. 91 | #[inline] 92 | pub fn endpoint_mut(&mut self) -> &mut Cow<'static, str> { 93 | &mut self.parts.endpoint 94 | } 95 | 96 | /// Get an immutable reference to the endpoint of this response. 97 | #[inline] 98 | pub fn endpoint(&self) -> &str { 99 | self.parts.endpoint.as_ref() 100 | } 101 | 102 | #[allow(dead_code)] 103 | pub(crate) fn map(self) -> Request { 104 | Request { 105 | parts: self.parts, 106 | message: PhantomData, 107 | } 108 | } 109 | } 110 | 111 | impl Request { 112 | /// Create a new request with the specified message. 113 | pub fn new(msg: &T) -> Self { 114 | let encoded = encode_protobuf_message(msg).freeze(); 115 | Self::new_with_body(Body::full(encoded)) 116 | } 117 | } 118 | 119 | impl Request { 120 | /// Extract the body from the request and decode it into the message. 121 | #[inline] 122 | pub async fn into_message(self) -> Result { 123 | decode_body(self.parts.body).await 124 | } 125 | } 126 | 127 | /// Trait used for blanket impls on generated protobuf types. 128 | pub trait IntoRequest { 129 | /// Convert this to a hRPC request. 130 | fn into_request(self) -> Request; 131 | } 132 | 133 | impl IntoRequest for T { 134 | fn into_request(self) -> Request { 135 | Request::new(&self) 136 | } 137 | } 138 | 139 | impl IntoRequest for Request { 140 | fn into_request(self) -> Request { 141 | self 142 | } 143 | } 144 | 145 | /// A request that has a message type of `()`. Used in places where the 146 | /// request message type is not important. 147 | pub type BoxRequest = Request<()>; 148 | -------------------------------------------------------------------------------- /crates/hrpc/src/response.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | fmt::{self, Debug, Formatter}, 3 | marker::PhantomData, 4 | }; 5 | 6 | use prost::Message as PbMsg; 7 | 8 | use crate::{ 9 | body::Body, 10 | common::extensions::Extensions, 11 | decode::{decode_body, DecodeBodyError}, 12 | encode::encode_protobuf_message, 13 | }; 14 | 15 | /// Response parts. 16 | #[non_exhaustive] 17 | #[derive(Debug)] 18 | pub struct Parts { 19 | /// Body of a response. 20 | pub body: Body, 21 | /// Extensions of a response. 22 | pub extensions: Extensions, 23 | } 24 | 25 | impl From> for Parts { 26 | fn from(resp: Response) -> Self { 27 | resp.parts 28 | } 29 | } 30 | 31 | impl From for Response { 32 | fn from(parts: Parts) -> Self { 33 | Self { 34 | parts, 35 | msg: PhantomData, 36 | } 37 | } 38 | } 39 | 40 | /// hRPC response type. 41 | pub struct Response { 42 | parts: Parts, 43 | msg: PhantomData, 44 | } 45 | 46 | impl Response<()> { 47 | /// Creates a response with an empty body. 48 | pub fn empty() -> Self { 49 | Self::new_with_body(Body::empty()) 50 | } 51 | } 52 | 53 | impl Debug for Response { 54 | fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { 55 | f.debug_struct("Response") 56 | .field("body", &self.parts.body) 57 | .field("extensions", &self.parts.extensions) 58 | .finish() 59 | } 60 | } 61 | 62 | impl Response { 63 | /// Creates a response with the provided body. 64 | pub fn new_with_body(body: Body) -> Self { 65 | Self { 66 | parts: Parts { 67 | body, 68 | extensions: Extensions::new(), 69 | }, 70 | msg: PhantomData, 71 | } 72 | } 73 | 74 | /// Get a mutable reference to the extensions of this response. 75 | #[inline] 76 | pub fn extensions_mut(&mut self) -> &mut Extensions { 77 | &mut self.parts.extensions 78 | } 79 | 80 | /// Get an immutable reference to the extensions of this response. 81 | #[inline] 82 | pub fn extensions(&self) -> &Extensions { 83 | &self.parts.extensions 84 | } 85 | 86 | #[allow(dead_code)] 87 | pub(crate) fn map(self) -> Response { 88 | Response { 89 | parts: self.parts, 90 | msg: PhantomData, 91 | } 92 | } 93 | } 94 | 95 | impl Response { 96 | /// Create a new hRPC response. 97 | pub fn new(msg: &T) -> Response { 98 | Self::new_with_body(Body::full(encode_protobuf_message(msg))) 99 | } 100 | } 101 | 102 | impl Response { 103 | /// Extract the body from the response and decode it into the message. 104 | #[inline] 105 | pub async fn into_message(self) -> Result { 106 | decode_body(self.parts.body).await 107 | } 108 | } 109 | 110 | /// Trait used for converting any type to a Response type. 111 | pub trait IntoResponse { 112 | /// Convert this to a hRPC response. 113 | fn into_response(self) -> Response; 114 | } 115 | 116 | impl IntoResponse for T { 117 | fn into_response(self) -> Response { 118 | Response::new(&self) 119 | } 120 | } 121 | 122 | impl IntoResponse for Response { 123 | fn into_response(self) -> Response { 124 | self 125 | } 126 | } 127 | 128 | /// A response that has a message type of `()`. Used in places where the 129 | /// response message type is not important. 130 | pub type BoxResponse = Response<()>; 131 | -------------------------------------------------------------------------------- /crates/hrpc/src/server/error.rs: -------------------------------------------------------------------------------- 1 | pub use crate::proto::Error as HrpcError; 2 | 3 | /// Shorthand type for `Result. 4 | pub type ServerResult = Result; 5 | -------------------------------------------------------------------------------- /crates/hrpc/src/server/layer/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod ratelimit; 2 | -------------------------------------------------------------------------------- /crates/hrpc/src/server/macros.rs: -------------------------------------------------------------------------------- 1 | /// Bails with an error. 2 | /// 3 | /// # Example 4 | /// ```rust,no_run 5 | /// # use hrpc::{bail, proto::Error as HrpcError}; 6 | /// # fn main() -> Result<(), HrpcError> { 7 | /// bail!(("some-error", "a very bad error occured!")); 8 | /// # Ok(()) 9 | /// # } 10 | /// ``` 11 | #[macro_export] 12 | macro_rules! bail { 13 | ($err:expr) => { 14 | return Err($err.into()); 15 | }; 16 | } 17 | 18 | /// Takes a `Result`, returns the error if it's `Err`, otherwise returns the 19 | /// `Ok` value. 20 | /// 21 | /// # Example 22 | /// ```rust,ignore 23 | /// async fn handler(&mut self, request: Request) -> ServerResult> { 24 | /// // ... 25 | /// bail_result!(some_computation()); 26 | /// // ... 27 | /// } 28 | /// ``` 29 | #[macro_export] 30 | macro_rules! bail_result { 31 | ($res:expr) => { 32 | match $res { 33 | Ok(val) => val, 34 | Err(err) => return Err(err.into()), 35 | } 36 | }; 37 | ($res:expr, |$err:ident| $func:expr) => { 38 | match $res { 39 | Ok(val) => val, 40 | Err($err) => { 41 | $func 42 | return Err($err.into()); 43 | }, 44 | } 45 | }; 46 | } 47 | 48 | /// Combines a list of services that implement `MakeRoutes`. 49 | /// 50 | /// # Example 51 | /// ```rust,ignore 52 | /// let hello = HelloServer::new(HelloService); 53 | /// let welcome = WelcomeServer::new(WelcomeService); 54 | /// combine_services!(hello, welcome).serve("127.0.0.1:2289").await?; 55 | /// ``` 56 | #[macro_export] 57 | macro_rules! combine_services { 58 | ( 59 | $( #[$fattr:meta] )* 60 | $fsvc:ident, 61 | $( 62 | $( #[$attr:meta] )* 63 | $svc:ident 64 | ),+ 65 | ) => { 66 | { 67 | use $crate::server::MakeRoutes; 68 | 69 | $( #[$fattr] )* 70 | let svc = $fsvc; 71 | $( 72 | $( #[$attr] )* 73 | let svc = MakeRoutes::combine_with(svc, $svc); 74 | )+ 75 | svc 76 | } 77 | }; 78 | } 79 | 80 | /// Macro to workaround `async fn`s not being allowed in traits. You do not 81 | /// need to use this directly, instead you should use the `handler` macro 82 | /// attribute provided in the server prelude. 83 | #[macro_export] 84 | macro_rules! make_handler { 85 | ( 86 | $( #[$attr:meta] )* 87 | $pub:vis 88 | async 89 | fn $fname:ident ( $($args:tt)* ) $(-> $Ret:ty)? 90 | { 91 | $($body:tt)* 92 | } 93 | ) => { 94 | $( #[$attr] )* 95 | $pub 96 | fn $fname ( $($args)* ) -> $crate::exports::futures_util::future::BoxFuture<'_, ($($Ret)?)> { 97 | Box::pin(async move { $($body)* }) 98 | } 99 | }; 100 | } 101 | -------------------------------------------------------------------------------- /crates/hrpc/src/server/mod.rs: -------------------------------------------------------------------------------- 1 | use std::convert::Infallible; 2 | 3 | use tower::{Layer, Service as TowerService}; 4 | 5 | use router::Routes; 6 | use service::HrpcService; 7 | 8 | use crate::{common::future, request::BoxRequest, response::BoxResponse}; 9 | 10 | use self::router::RoutesFinalized; 11 | 12 | /// Error types used by hRPC. 13 | pub mod error; 14 | /// Layers for hRPC services. 15 | pub mod layer; 16 | /// The router used by hRPC. 17 | pub mod router; 18 | /// Handler type and handlers used by hRPC. 19 | pub mod service; 20 | /// Socket used by hRPC for "streaming" RPCs. 21 | pub mod socket; 22 | /// Transports for hRPC services. 23 | pub mod transport; 24 | /// Other useful types, traits and functions used by hRPC. 25 | pub mod utils; 26 | 27 | mod macros; 28 | 29 | pub use service::HrpcLayer; 30 | 31 | // Prelude used by generated code. It is not meant to be used by users. 32 | #[doc(hidden)] 33 | pub mod gen_prelude { 34 | pub use super::{ 35 | error::ServerResult, 36 | router::Routes, 37 | service::{unary_handler, ws_handler, HrpcLayer, HrpcService}, 38 | socket::Socket, 39 | transport::Transport, 40 | MakeRoutes, 41 | }; 42 | pub use crate::{BoxError, Request as HrpcRequest, Response as HrpcResponse}; 43 | pub use bytes::Bytes; 44 | pub use futures_util::future::BoxFuture; 45 | pub use std::{convert::Infallible, future::Future, net::ToSocketAddrs, sync::Arc}; 46 | pub use tower::{layer::util::Identity, Layer, Service as TowerService}; 47 | } 48 | 49 | /// Prelude that exports commonly used server types. 50 | pub mod prelude { 51 | pub use super::{ 52 | error::{HrpcError, ServerResult}, 53 | service::{HrpcLayer, HrpcLayerExt}, 54 | socket::Socket, 55 | transport::Transport, 56 | MakeRoutes, 57 | }; 58 | pub use crate::{make_handler, response::IntoResponse, Request, Response}; 59 | pub use hrpc_proc_macro::handler; 60 | } 61 | 62 | /// The core trait of `hrpc-rs` servers. This trait acts as a `tower::MakeService`, 63 | /// it can produce a set of [`Routes`] and can be combined with other [`MakeRoutes`]s. 64 | /// 65 | /// Not to be confused with [`tower::Service`]. 66 | pub trait MakeRoutes: Send + 'static { 67 | /// Creates a [`Routes`], which will be used to build a [`RoutesFinalized`] instance. 68 | fn make_routes(&self) -> Routes; 69 | 70 | /// Combines this server with another server. 71 | fn combine_with(self, other: Other) -> ServiceStack 72 | where 73 | Other: MakeRoutes, 74 | Self: Sized, 75 | { 76 | ServiceStack { 77 | outer: other, 78 | inner: self, 79 | } 80 | } 81 | 82 | /// Turns this server into a type that implements `MakeService`, so that 83 | /// it can be used with [`hyper`]. 84 | fn into_make_service(self) -> IntoMakeService 85 | where 86 | Self: Sized, 87 | { 88 | IntoMakeService { mk_router: self } 89 | } 90 | 91 | /// Layers this server with a layer. 92 | /// 93 | /// If your layer does not implement [`Clone`], you can wrap it using 94 | /// [`HrpcLayer::new`]. 95 | fn layer(self, layer: L) -> LayeredService 96 | where 97 | L: Layer + Clone + Sync + Send + 'static, 98 | S: tower::Service + Send + 'static, 99 | S::Future: Send, 100 | Self: Sized, 101 | { 102 | LayeredService { inner: self, layer } 103 | } 104 | } 105 | 106 | /// Type that layers the handlers that are produced by a [`MakeRoutes`]. 107 | pub struct LayeredService 108 | where 109 | L: Layer + Clone + Sync + Send + 'static, 110 | S: tower::Service + Send + 'static, 111 | S::Future: Send, 112 | M: MakeRoutes, 113 | { 114 | inner: M, 115 | layer: L, 116 | } 117 | 118 | impl Clone for LayeredService 119 | where 120 | L: Layer + Clone + Sync + Send + 'static, 121 | S: tower::Service + Send + 'static, 122 | S::Future: Send, 123 | M: MakeRoutes + Clone, 124 | { 125 | fn clone(&self) -> Self { 126 | Self { 127 | inner: self.inner.clone(), 128 | layer: self.layer.clone(), 129 | } 130 | } 131 | } 132 | 133 | impl MakeRoutes for LayeredService 134 | where 135 | L: Layer + Clone + Sync + Send + 'static, 136 | S: tower::Service + Send + 'static, 137 | S::Future: Send, 138 | M: MakeRoutes, 139 | { 140 | fn make_routes(&self) -> Routes { 141 | let rb = MakeRoutes::make_routes(&self.inner); 142 | rb.layer_all(self.layer.clone()) 143 | } 144 | } 145 | 146 | /// Type that contains two [`MakeRoutes`]s and stacks (combines) them. 147 | pub struct ServiceStack 148 | where 149 | Outer: MakeRoutes, 150 | Inner: MakeRoutes, 151 | { 152 | outer: Outer, 153 | inner: Inner, 154 | } 155 | 156 | impl Clone for ServiceStack 157 | where 158 | Outer: MakeRoutes + Clone, 159 | Inner: MakeRoutes + Clone, 160 | { 161 | fn clone(&self) -> Self { 162 | Self { 163 | outer: self.outer.clone(), 164 | inner: self.inner.clone(), 165 | } 166 | } 167 | } 168 | 169 | impl MakeRoutes for ServiceStack 170 | where 171 | Outer: MakeRoutes, 172 | Inner: MakeRoutes, 173 | { 174 | fn make_routes(&self) -> Routes { 175 | let outer_rb = MakeRoutes::make_routes(&self.outer); 176 | let inner_rb = MakeRoutes::make_routes(&self.inner); 177 | outer_rb.combine_with(inner_rb) 178 | } 179 | } 180 | 181 | /// Type that contains a [`MakeRoutes`] and implements `tower::MakeService` to 182 | /// create [`RoutesFinalized`] instances. 183 | pub struct IntoMakeService { 184 | mk_router: S, 185 | } 186 | 187 | impl Clone for IntoMakeService { 188 | fn clone(&self) -> Self { 189 | Self { 190 | mk_router: self.mk_router.clone(), 191 | } 192 | } 193 | } 194 | 195 | impl TowerService for IntoMakeService { 196 | type Response = RoutesFinalized; 197 | 198 | type Error = Infallible; 199 | 200 | type Future = future::Ready>; 201 | 202 | fn poll_ready( 203 | &mut self, 204 | _cx: &mut std::task::Context<'_>, 205 | ) -> std::task::Poll> { 206 | Ok(()).into() 207 | } 208 | 209 | fn call(&mut self, _req: T) -> Self::Future { 210 | future::ready(Ok(self.mk_router.make_routes().build())) 211 | } 212 | } 213 | 214 | #[cfg(test)] 215 | mod tests { 216 | use tower::layer::util::Identity; 217 | 218 | use crate::server::{router::Routes, HrpcLayer, MakeRoutes}; 219 | 220 | struct TestServer; 221 | 222 | impl MakeRoutes for TestServer { 223 | fn make_routes(&self) -> Routes { 224 | Routes::new() 225 | } 226 | } 227 | 228 | #[test] 229 | fn layered_identity() { 230 | let s = TestServer; 231 | 232 | // we can't poll it, and we don't want to anyways 233 | let _ = s.layer(HrpcLayer::new(Identity::new())); 234 | } 235 | } 236 | -------------------------------------------------------------------------------- /crates/hrpc/src/server/router.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | borrow::Cow, convert::Infallible, mem::ManuallyDrop, ops::DerefMut, ptr::NonNull, task::Poll, 3 | }; 4 | 5 | use crate::{request::BoxRequest, response::BoxResponse}; 6 | 7 | use super::{ 8 | service::{not_found, CallFuture, HrpcService}, 9 | HrpcLayer, 10 | }; 11 | 12 | use matchit::{MatchError, Router as Matcher}; 13 | use tower::{Layer, Service}; 14 | use tracing::Span; 15 | 16 | /// Builder type for inserting [`HrpcService`]s before building a [`RoutesFinalized`]. 17 | pub struct Routes { 18 | handlers: Vec<(Cow<'static, str>, HrpcService)>, 19 | any: Option, 20 | all_layer: Option, 21 | } 22 | 23 | impl Default for Routes { 24 | fn default() -> Self { 25 | Self::new() 26 | } 27 | } 28 | 29 | impl Routes { 30 | /// Create a new [`Routes`]. 31 | pub fn new() -> Self { 32 | Self { 33 | handlers: Vec::new(), 34 | any: None, 35 | all_layer: None, 36 | } 37 | } 38 | 39 | /// Add a new route. 40 | pub fn route(mut self, path: impl Into>, handler: S) -> Self 41 | where 42 | S: Service + Send + 'static, 43 | S::Future: Send, 44 | { 45 | self.handlers.push((path.into(), HrpcService::new(handler))); 46 | self 47 | } 48 | 49 | /// Layer the routes that were added until this was called. 50 | pub fn layer(mut self, layer: L) -> Self 51 | where 52 | L: Layer, 53 | S: Service + Send + 'static, 54 | S::Future: Send, 55 | { 56 | for (path, handler) in self.handlers.drain(..).collect::>() { 57 | let layered = handler.layer(&layer); 58 | self.handlers.push((path, layered)); 59 | } 60 | self 61 | } 62 | 63 | /// Set layer for the finalized router service. This applies to all routes. 64 | pub fn layer_all(mut self, layer: L) -> Self 65 | where 66 | L: Layer + Sync + Send + 'static, 67 | S: Service + Send + 'static, 68 | S::Future: Send, 69 | { 70 | let outer = HrpcLayer::new(layer); 71 | self.all_layer = match self.all_layer { 72 | Some(inner) => Some(HrpcLayer::stack(inner, outer)), 73 | None => Some(outer), 74 | }; 75 | self 76 | } 77 | 78 | /// Combine this with another [`Routes`]. Note that this cannot combine the `any` handler. 79 | pub fn combine_with(mut self, other_routes: impl Into) -> Self { 80 | let mut other_routes = other_routes.into(); 81 | self.handlers.append(&mut other_routes.handlers); 82 | self.all_layer = if let Some(layer) = self.all_layer { 83 | let layer = if let Some(other_layer) = other_routes.all_layer { 84 | HrpcLayer::stack(layer, other_layer) 85 | } else { 86 | layer 87 | }; 88 | Some(layer) 89 | } else { 90 | other_routes.all_layer 91 | }; 92 | self 93 | } 94 | 95 | /// Set the handler that will be used if no routes are matched. 96 | pub fn any(mut self, handler: S) -> Self 97 | where 98 | S: Service + Send + 'static, 99 | S::Future: Send, 100 | { 101 | self.any = Some(HrpcService::new(handler)); 102 | self 103 | } 104 | 105 | /// Build the routes. 106 | /// 107 | /// ## Panics 108 | /// - This can panic if one of the paths of the inserted routes is invalid. 109 | pub fn build(self) -> RoutesFinalized { 110 | let mut matcher = Matcher::new(); 111 | let mut all_routes = Vec::with_capacity(self.handlers.len()); 112 | 113 | for (path, handler) in self.handlers { 114 | let manual_drop = ManuallyDrop::new(handler); 115 | let mut_ptr = Box::leak(Box::new(manual_drop)) as *mut ManuallyDrop; 116 | let handler_ptr = NonNull::new(mut_ptr).unwrap(); 117 | matcher 118 | .insert(path, handler_ptr) 119 | .expect("invalid route path"); 120 | all_routes.push(handler_ptr); 121 | } 122 | 123 | let internal = RoutesInternal { 124 | matcher, 125 | all_routes, 126 | any: self.any.unwrap_or_else(not_found), 127 | }; 128 | 129 | let inner = match self.all_layer { 130 | Some(layer) => layer.layer(internal), 131 | None => HrpcService::new(internal), 132 | }; 133 | 134 | RoutesFinalized { inner } 135 | } 136 | } 137 | 138 | struct RoutesInternal { 139 | matcher: Matcher>>, 140 | all_routes: Vec>>, 141 | any: HrpcService, 142 | } 143 | 144 | // SAFETY: this impl is safe since our NonNull is not aliased while `Send`ing 145 | unsafe impl Send for RoutesInternal {} 146 | 147 | impl Drop for RoutesInternal { 148 | fn drop(&mut self) { 149 | for route_ptr in &mut self.all_routes { 150 | // SAFETY: this is required since we are manually dropping the items 151 | unsafe { ManuallyDrop::drop(route_ptr.as_mut()) } 152 | } 153 | } 154 | } 155 | 156 | impl RoutesInternal { 157 | fn call_redirected( 158 | &mut self, 159 | mut req: BoxRequest, 160 | redirect_to: String, 161 | ) -> >::Future { 162 | *req.endpoint_mut() = Cow::Owned(redirect_to); 163 | Span::current().record("redirected_to", &req.endpoint()); 164 | let matched = self 165 | .matcher 166 | .at_mut(req.endpoint()) 167 | .expect("the path must be correct"); 168 | Service::call( 169 | // SAFETY: our ptr is always initialized 170 | unsafe { matched.value.as_mut().deref_mut() }, 171 | req, 172 | ) 173 | } 174 | } 175 | 176 | impl Service for RoutesInternal { 177 | type Response = BoxResponse; 178 | 179 | type Error = Infallible; 180 | 181 | type Future = CallFuture<'static>; 182 | 183 | fn poll_ready(&mut self, cx: &mut std::task::Context<'_>) -> Poll> { 184 | for route_ptr in &mut self.all_routes { 185 | // SAFETY: our ptr is always initialized 186 | let route = unsafe { route_ptr.as_mut() }; 187 | match Service::poll_ready(route.deref_mut(), cx) { 188 | Poll::Pending => return Poll::Pending, 189 | Poll::Ready(Err(err)) => return Poll::Ready(Err(err)), 190 | _ => continue, 191 | } 192 | } 193 | Service::poll_ready(&mut self.any, cx) 194 | } 195 | 196 | fn call(&mut self, req: BoxRequest) -> Self::Future { 197 | let path = req.endpoint(); 198 | 199 | match self.matcher.at_mut(path) { 200 | Ok(matched) => Service::call( 201 | // SAFETY: our ptr is always initialized 202 | unsafe { matched.value.as_mut().deref_mut() }, 203 | req, 204 | ), 205 | Err(MatchError::ExtraTrailingSlash) => { 206 | let redirect_to = path.trim_end_matches('/').to_string(); 207 | self.call_redirected(req, redirect_to) 208 | } 209 | Err(MatchError::MissingTrailingSlash) => { 210 | let redirect_to = format!("{}/", path); 211 | self.call_redirected(req, redirect_to) 212 | } 213 | _ => Service::call(&mut self.any, req), 214 | } 215 | } 216 | } 217 | 218 | /// Finalized [`Routes`], ready for serving as a [`Service`]. 219 | pub struct RoutesFinalized { 220 | pub(crate) inner: HrpcService, 221 | } 222 | 223 | impl Service for RoutesFinalized { 224 | type Response = BoxResponse; 225 | 226 | type Error = Infallible; 227 | 228 | type Future = CallFuture<'static>; 229 | 230 | fn poll_ready( 231 | &mut self, 232 | cx: &mut std::task::Context<'_>, 233 | ) -> std::task::Poll> { 234 | self.inner.poll_ready(cx) 235 | } 236 | 237 | fn call(&mut self, req: BoxRequest) -> Self::Future { 238 | Service::call(&mut self.inner, req) 239 | } 240 | } 241 | -------------------------------------------------------------------------------- /crates/hrpc/src/server/service.rs: -------------------------------------------------------------------------------- 1 | use futures_util::{future::BoxFuture, Future, FutureExt}; 2 | use std::{convert::Infallible, sync::Arc}; 3 | use tower::{ 4 | layer::{layer_fn, util::Stack}, 5 | service_fn, 6 | util::BoxService, 7 | Layer, Service, 8 | }; 9 | 10 | use super::{ 11 | error::HrpcError, 12 | socket::{self, Socket, SocketHandler}, 13 | }; 14 | use crate::{request::BoxRequest, response::BoxResponse, Request, Response}; 15 | 16 | /// Call future used by [`HrpcService`]. 17 | pub(crate) type CallFuture<'a> = BoxFuture<'a, Result>; 18 | 19 | /// A hRPC handler. 20 | pub struct HrpcService { 21 | svc: BoxService, 22 | } 23 | 24 | impl HrpcService { 25 | /// Create a new handler from a [`tower::Service`]. 26 | pub fn new(svc: S) -> Self 27 | where 28 | S: Service + Send + 'static, 29 | S::Future: Send, 30 | { 31 | // If it's already a `HrpcService`, just use the service in that. 32 | super::utils::downcast::if_downcast_into!(S, HrpcService, svc, { 33 | return Self { svc: svc.svc }; 34 | }); 35 | 36 | Self { 37 | svc: BoxService::new(svc), 38 | } 39 | } 40 | 41 | /// Layer this handler. 42 | pub fn layer(self, layer: L) -> Self 43 | where 44 | L: Layer, 45 | S: Service + Send + 'static, 46 | S::Future: Send, 47 | { 48 | HrpcService::new(layer.layer(self)) 49 | } 50 | } 51 | 52 | impl Service for HrpcService { 53 | type Response = BoxResponse; 54 | 55 | type Error = Infallible; 56 | 57 | type Future = CallFuture<'static>; 58 | 59 | fn poll_ready( 60 | &mut self, 61 | cx: &mut std::task::Context<'_>, 62 | ) -> std::task::Poll> { 63 | self.svc.poll_ready(cx) 64 | } 65 | 66 | fn call(&mut self, req: BoxRequest) -> Self::Future { 67 | Service::call(&mut self.svc, req) 68 | } 69 | } 70 | 71 | /// Layer type that produces [`HrpcService`]s. 72 | #[derive(Clone)] 73 | pub struct HrpcLayer { 74 | inner: Arc + Sync + Send + 'static>, 75 | } 76 | 77 | impl HrpcLayer { 78 | /// Create a new [`HrpcLayer`] from a [`tower::Layer`]. 79 | pub fn new(layer: L) -> Self 80 | where 81 | L: Layer + Sync + Send + 'static, 82 | S: Service + Send + 'static, 83 | S::Future: Send, 84 | { 85 | // If it's already a HrpcLayer, no need to wrap it in stuff 86 | super::utils::downcast::if_downcast_into!(S, HrpcLayer, layer, { 87 | return Self { inner: layer.inner }; 88 | }); 89 | 90 | let layer = layer_fn(move |svc| { 91 | let new_svc = layer.layer(svc); 92 | HrpcService::new(new_svc) 93 | }); 94 | 95 | Self { 96 | inner: Arc::new(layer), 97 | } 98 | } 99 | 100 | pub(crate) fn stack(inner: HrpcLayer, outer: HrpcLayer) -> Self { 101 | Self { 102 | inner: Arc::new(Stack::new(inner, outer)), 103 | } 104 | } 105 | } 106 | 107 | impl Layer for HrpcLayer 108 | where 109 | S: Service + Send + 'static, 110 | S::Future: Send, 111 | { 112 | type Service = HrpcService; 113 | 114 | fn layer(&self, inner: S) -> Self::Service { 115 | self.inner.layer(HrpcService::new(inner)) 116 | } 117 | } 118 | 119 | /// Extension trait for [`HrpcLayer`]. 120 | pub trait HrpcLayerExt: Sized { 121 | /// Convert this into a [`HrpcLayer`]. 122 | fn into_hrpc_layer(self) -> HrpcLayer; 123 | } 124 | 125 | impl HrpcLayerExt for L 126 | where 127 | L: Layer + Sync + Send + 'static, 128 | S: Service + Send + 'static, 129 | S::Future: Send, 130 | { 131 | fn into_hrpc_layer(self) -> HrpcLayer { 132 | HrpcLayer::new(self) 133 | } 134 | } 135 | 136 | /// A handler that responses to any request with not found. 137 | pub fn not_found() -> HrpcService { 138 | HrpcService::new(tower::service_fn(|_| { 139 | futures_util::future::ready(Ok(HrpcError::new_not_found("not found").into())) 140 | })) 141 | } 142 | 143 | #[doc(hidden)] 144 | pub fn unary_handler(handler: HandlerFn) -> HrpcService 145 | where 146 | Req: prost::Message + Default, 147 | Resp: prost::Message, 148 | HandlerFut: Future, HrpcError>> + Send, 149 | HandlerFn: FnOnce(Request) -> HandlerFut + Clone + Send + 'static, 150 | { 151 | let service = service_fn(move |req: BoxRequest| { 152 | (handler.clone())(req.map::()) 153 | .map(|res| Ok(res.map_or_else(HrpcError::into, |resp| resp.map::<()>()))) 154 | }); 155 | HrpcService::new(service) 156 | } 157 | 158 | #[doc(hidden)] 159 | pub fn ws_handler(handler: HandlerFn) -> HrpcService 160 | where 161 | Req: prost::Message + Default + 'static, 162 | Resp: prost::Message + 'static, 163 | HandlerFut: Future> + Send, 164 | HandlerFn: FnOnce(Request<()>, Socket) -> HandlerFut + Clone + Send + Sync + 'static, 165 | { 166 | let service = service_fn(move |req: BoxRequest| { 167 | let handler = handler.clone(); 168 | let socket_handler = SocketHandler { 169 | inner: Box::new(move |rx, tx| { 170 | Box::pin(async move { 171 | let socket = 172 | Socket::new(rx, tx, socket::encode_message, socket::decode_message); 173 | let res = handler(req, socket).await; 174 | if let Err(err) = res { 175 | tracing::error!("{}", err); 176 | } 177 | }) 178 | }), 179 | }; 180 | 181 | let mut response = Response::empty(); 182 | response.extensions_mut().insert(socket_handler); 183 | 184 | futures_util::future::ready(Ok(response)) 185 | }); 186 | 187 | HrpcService::new(service) 188 | } 189 | -------------------------------------------------------------------------------- /crates/hrpc/src/server/socket.rs: -------------------------------------------------------------------------------- 1 | use bytes::BytesMut; 2 | use futures_util::{future::BoxFuture, SinkExt}; 3 | use prost::Message as PbMsg; 4 | 5 | use crate::{common::socket::*, decode::DecodeBodyError, proto::Error as HrpcError}; 6 | 7 | pub use crate::common::socket::{ReadSocket, Socket, SocketError, WriteSocket}; 8 | 9 | impl Socket { 10 | /// Send an error over the socket. 11 | #[inline] 12 | pub async fn send_error(&mut self, err: HrpcError) -> Result<(), SocketError> { 13 | self.write.send_error(err).await 14 | } 15 | } 16 | 17 | impl WriteSocket { 18 | /// Send an error over the socket. 19 | pub async fn send_error(&mut self, err: HrpcError) -> Result<(), SocketError> { 20 | let data = encode_hrpc_error(&mut self.buf, &err); 21 | self.tx 22 | .lock() 23 | .await 24 | .send(SocketMessage::Binary(data)) 25 | .await 26 | .map_err(SocketError::Transport) 27 | } 28 | } 29 | 30 | pub(super) struct SocketHandler { 31 | #[allow(dead_code)] 32 | pub(crate) inner: Box< 33 | dyn FnOnce(BoxedSocketRx, BoxedSocketTx) -> BoxFuture<'static, ()> + Send + Sync + 'static, 34 | >, 35 | } 36 | 37 | pub(super) fn encode_message(buf: &mut BytesMut, msg: &Msg) -> Vec { 38 | crate::encode::encode_protobuf_message_to(buf, msg); 39 | // TODO: don't allocate here? 40 | let mut data = buf.to_vec(); 41 | data.insert(0, 0); 42 | data 43 | } 44 | 45 | fn encode_hrpc_error(buf: &mut BytesMut, err: &HrpcError) -> Vec { 46 | crate::encode::encode_protobuf_message_to(buf, err); 47 | // TODO: don't allocate here? 48 | let mut data = buf.to_vec(); 49 | data.insert(0, 1); 50 | data 51 | } 52 | 53 | pub(super) fn decode_message( 54 | raw: Vec, 55 | ) -> Result, DecodeBodyError> { 56 | if raw.is_empty() { 57 | return Err(DecodeBodyError::InvalidProtoMessage( 58 | prost::DecodeError::new("empty protobuf message"), 59 | )); 60 | } 61 | 62 | Msg::decode(raw.as_slice()) 63 | .map(DecodeResult::Msg) 64 | .map_err(DecodeBodyError::InvalidProtoMessage) 65 | } 66 | -------------------------------------------------------------------------------- /crates/hrpc/src/server/transport/http/layer/errid_to_status.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | pin::Pin, 3 | task::{Context, Poll}, 4 | }; 5 | 6 | use futures_util::{Future, FutureExt}; 7 | use http::StatusCode; 8 | use pin_project_lite::pin_project; 9 | use tower::{Layer, Service}; 10 | 11 | use crate::{proto::Error as HrpcError, request::BoxRequest, response::BoxResponse}; 12 | 13 | /// Layer for layering services with [`ErrorIdentifierToStatus`]. 14 | #[derive(Clone)] 15 | pub struct ErrorIdentifierToStatusLayer { 16 | to_status: ToStatus, 17 | } 18 | 19 | impl ErrorIdentifierToStatusLayer 20 | where 21 | ToStatus: Fn(&str) -> Option, 22 | { 23 | /// Create a new layer using the provided [`ToStatus`] function. 24 | pub fn new(to_status: ToStatus) -> Self { 25 | Self { to_status } 26 | } 27 | } 28 | 29 | impl Layer for ErrorIdentifierToStatusLayer 30 | where 31 | ToStatus: Clone, 32 | { 33 | type Service = ErrorIdentifierToStatus; 34 | 35 | fn layer(&self, inner: S) -> Self::Service { 36 | ErrorIdentifierToStatus::new(self.to_status.clone(), inner) 37 | } 38 | } 39 | 40 | /// Service to set response status from possible errors. 41 | #[derive(Clone)] 42 | pub struct ErrorIdentifierToStatus { 43 | inner: S, 44 | to_status: ToStatus, 45 | } 46 | 47 | impl ErrorIdentifierToStatus { 48 | /// Create a new service by wrapping another service, and converting to 49 | /// status using the provided function. 50 | pub fn new(to_status: ToStatus, inner: S) -> Self { 51 | Self { inner, to_status } 52 | } 53 | } 54 | 55 | impl Service for ErrorIdentifierToStatus 56 | where 57 | S: Service, 58 | ToStatus: Fn(&str) -> Option + Clone, 59 | { 60 | type Response = BoxResponse; 61 | 62 | type Error = S::Error; 63 | 64 | type Future = ErrorIdentifierToStatusFuture; 65 | 66 | fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll> { 67 | Service::poll_ready(&mut self.inner, cx) 68 | } 69 | 70 | fn call(&mut self, req: BoxRequest) -> Self::Future { 71 | ErrorIdentifierToStatusFuture { 72 | resp_fut: Service::call(&mut self.inner, req), 73 | to_status: self.to_status.clone(), 74 | } 75 | } 76 | } 77 | 78 | pin_project! { 79 | /// Future for [`ErrorIdentifierToStatus`]. 80 | pub struct ErrorIdentifierToStatusFuture { 81 | #[pin] 82 | resp_fut: Fut, 83 | to_status: ToStatus, 84 | } 85 | } 86 | 87 | impl Future for ErrorIdentifierToStatusFuture 88 | where 89 | Fut: Future>, 90 | ToStatus: Fn(&str) -> Option, 91 | { 92 | type Output = Result; 93 | 94 | fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { 95 | let mut this = self.project(); 96 | 97 | this.resp_fut.poll_unpin(cx).map_ok(|mut resp| { 98 | if let Some(status) = resp 99 | .extensions() 100 | .get::() 101 | .and_then(|err| (this.to_status)(&err.identifier)) 102 | { 103 | resp.extensions_mut().insert(status); 104 | } 105 | resp 106 | }) 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /crates/hrpc/src/server/transport/http/layer/mod.rs: -------------------------------------------------------------------------------- 1 | /// Layer for converting error identifers to HTTP statuses. 2 | pub mod errid_to_status; 3 | -------------------------------------------------------------------------------- /crates/hrpc/src/server/transport/http/mod.rs: -------------------------------------------------------------------------------- 1 | use bytes::Bytes; 2 | use futures_util::future::BoxFuture; 3 | use std::{ 4 | convert::Infallible, 5 | io, 6 | net::{SocketAddr, ToSocketAddrs}, 7 | path::PathBuf, 8 | }; 9 | use tower::{ 10 | layer::util::{Identity, Stack}, 11 | Layer, Service, 12 | }; 13 | 14 | use super::Transport; 15 | use crate::{server::MakeRoutes, BoxError}; 16 | 17 | /// Underlying implementation of this transport. 18 | pub mod r#impl; 19 | /// Useful layers for HTTP. 20 | pub mod layer; 21 | mod ws; 22 | 23 | #[doc(inline)] 24 | pub use axum_server::HttpConfig; 25 | /// A boxed HTTP body. This is used to unify response bodies. 26 | pub type BoxBody = http_body::combinators::UnsyncBoxBody; 27 | /// A HTTP request. 28 | pub type HttpRequest = http::Request; 29 | /// A HTTP response. 30 | pub type HttpResponse = http::Response; 31 | 32 | /// Convert a body with the correct attributes to a [`BoxBody`]. 33 | pub fn box_body(body: B) -> BoxBody 34 | where 35 | B: http_body::Body + Send + 'static, 36 | B::Error: Into, 37 | { 38 | BoxBody::new(body.map_err(Into::into)) 39 | } 40 | 41 | /// A transport based on [`hyper`] that supports TLS. 42 | #[non_exhaustive] 43 | pub struct Hyper { 44 | addr: SocketAddr, 45 | layer: L, 46 | tls: Option<(PathBuf, PathBuf)>, 47 | config: HttpConfig, 48 | } 49 | 50 | impl Hyper { 51 | /// Create a new `hyper` transport. 52 | pub fn new(addr: Addr) -> Result { 53 | Ok(Self { 54 | addr: addr.to_socket_addrs()?.next().ok_or_else(|| { 55 | io::Error::new(io::ErrorKind::Other, "no socket addresses passed") 56 | })?, 57 | layer: Identity::new(), 58 | tls: None, 59 | config: HttpConfig::new(), 60 | }) 61 | } 62 | } 63 | 64 | impl Hyper { 65 | /// Layer this [`hyper`] server with a [`Layer`]. 66 | pub fn layer(self, layer: Layer) -> Hyper> { 67 | Hyper { 68 | addr: self.addr, 69 | layer: Stack::new(layer, self.layer), 70 | tls: self.tls, 71 | config: self.config, 72 | } 73 | } 74 | 75 | /// Configure TLS for this server with paths to certificate and private key 76 | /// files. 77 | pub fn configure_tls_files( 78 | mut self, 79 | cert_path: impl Into, 80 | key_path: impl Into, 81 | ) -> Self { 82 | self.tls = Some((cert_path.into(), key_path.into())); 83 | self 84 | } 85 | 86 | /// Set new configuration for the [`hyper`] HTTP server. 87 | pub fn configure_hyper(mut self, config: HttpConfig) -> Self { 88 | self.config = config; 89 | self 90 | } 91 | } 92 | 93 | impl Transport for Hyper 94 | where 95 | L: Layer + Clone + Send + 'static, 96 | S: Service + Send + 'static, 97 | S::Future: Send, 98 | { 99 | type Error = std::io::Error; 100 | 101 | fn serve(self, mk_routes: M) -> BoxFuture<'static, Result<(), Self::Error>> 102 | where 103 | M: MakeRoutes, 104 | { 105 | let service = 106 | r#impl::MakeRoutesToHttp::new(mk_routes.into_make_service()).layer(self.layer); 107 | 108 | if let Some((cert_path, key_path)) = self.tls { 109 | Box::pin(async move { 110 | let rustls_conf = 111 | axum_server::tls_rustls::RustlsConfig::from_pem_file(cert_path, key_path) 112 | .await?; 113 | axum_server::bind_rustls(self.addr, rustls_conf) 114 | .http_config(self.config) 115 | .serve(service) 116 | .await 117 | }) 118 | } else { 119 | Box::pin( 120 | axum_server::bind(self.addr) 121 | .http_config(self.config) 122 | .serve(service), 123 | ) 124 | } 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /crates/hrpc/src/server/transport/http/ws.rs: -------------------------------------------------------------------------------- 1 | // The code in this file originally comes from https://github.com/tokio-rs/axum/blob/main/src/extract/ws.rs 2 | // and is under the following license: 3 | /* 4 | Copyright (c) 2019 Tower Contributors 5 | 6 | Permission is hereby granted, free of charge, to any 7 | person obtaining a copy of this software and associated 8 | documentation files (the "Software"), to deal in the 9 | Software without restriction, including without 10 | limitation the rights to use, copy, modify, merge, 11 | publish, distribute, sublicense, and/or sell copies of 12 | the Software, and to permit persons to whom the Software 13 | is furnished to do so, subject to the following 14 | conditions: 15 | 16 | The above copyright notice and this permission notice 17 | shall be included in all copies or substantial portions 18 | of the Software. 19 | 20 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF 21 | ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 22 | TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 23 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT 24 | SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 25 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 26 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR 27 | IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 28 | DEALINGS IN THE SOFTWARE. 29 | */ 30 | 31 | use bytes::Bytes; 32 | use http::{ 33 | header::{self, HeaderValue}, 34 | Method, StatusCode, 35 | }; 36 | use hyper::upgrade::{OnUpgrade, Upgraded}; 37 | use sha1::{Digest, Sha1}; 38 | use tokio_tungstenite::{ 39 | tungstenite::protocol::{self, WebSocketConfig}, 40 | WebSocketStream, 41 | }; 42 | 43 | use std::{ 44 | borrow::Cow, 45 | error::Error as StdError, 46 | fmt::{self, Display, Formatter}, 47 | future::Future, 48 | }; 49 | 50 | use crate::common::transport::{http::HeaderMapExt, tokio_tungstenite::WebSocket}; 51 | 52 | use super::{box_body, HttpRequest, HttpResponse}; 53 | 54 | /// Extractor for establishing WebSocket connections. 55 | #[derive(Debug)] 56 | pub(crate) struct WebSocketUpgrade { 57 | config: WebSocketConfig, 58 | protocols: Option]>>, 59 | extensions: Option]>>, 60 | sec_websocket_key: HeaderValue, 61 | on_upgrade: OnUpgrade, 62 | sec_websocket_protocol: Option, 63 | } 64 | 65 | impl WebSocketUpgrade { 66 | #[allow(dead_code)] 67 | /// Set the size of the internal message send queue. 68 | pub(crate) fn max_send_queue(mut self, max: usize) -> Self { 69 | self.config.max_send_queue = Some(max); 70 | self 71 | } 72 | 73 | #[allow(dead_code)] 74 | /// Set the maximum message size (defaults to 64 megabytes) 75 | pub(crate) fn max_message_size(mut self, max: usize) -> Self { 76 | self.config.max_message_size = Some(max); 77 | self 78 | } 79 | 80 | #[allow(dead_code)] 81 | /// Set the maximum frame size (defaults to 16 megabytes) 82 | pub(crate) fn max_frame_size(mut self, max: usize) -> Self { 83 | self.config.max_frame_size = Some(max); 84 | self 85 | } 86 | 87 | #[allow(dead_code)] 88 | /// Set the known protocols. 89 | pub(crate) fn protocols(mut self, protocols: I) -> Self 90 | where 91 | I: IntoIterator, 92 | I::Item: Into>, 93 | { 94 | self.protocols = Some( 95 | protocols 96 | .into_iter() 97 | .map(Into::into) 98 | .collect::>() 99 | .into(), 100 | ); 101 | self 102 | } 103 | 104 | #[allow(dead_code)] 105 | pub(crate) fn extensions(mut self, extensions: I) -> Self 106 | where 107 | I: IntoIterator, 108 | I::Item: Into>, 109 | { 110 | self.extensions = Some( 111 | extensions 112 | .into_iter() 113 | .map(Into::into) 114 | .collect::>() 115 | .into(), 116 | ); 117 | self 118 | } 119 | 120 | /// Finalize upgrading the connection and call the provided callback with 121 | /// the stream. 122 | pub(crate) fn on_upgrade(self, callback: F) -> WebSocketUpgradeResponse 123 | where 124 | F: FnOnce(WebSocket) -> Fut + Send + 'static, 125 | Fut: Future + Send + 'static, 126 | { 127 | WebSocketUpgradeResponse { 128 | extractor: self, 129 | callback, 130 | } 131 | } 132 | 133 | pub(crate) fn from_request(req: &mut HttpRequest) -> Result { 134 | if req.method() != Method::GET { 135 | return Err(WebSocketUpgradeError::MethodNotGet); 136 | } 137 | 138 | if !req 139 | .headers() 140 | .header_contains_str(&header::CONNECTION, "upgrade") 141 | { 142 | return Err(WebSocketUpgradeError::InvalidConnectionHeader); 143 | } 144 | 145 | if !req.headers().header_eq(&header::UPGRADE, b"websocket") { 146 | return Err(WebSocketUpgradeError::InvalidUpgradeHeader); 147 | } 148 | 149 | if !req 150 | .headers() 151 | .header_eq(&header::SEC_WEBSOCKET_VERSION, b"13") 152 | { 153 | return Err(WebSocketUpgradeError::InvalidWebsocketVersionHeader); 154 | } 155 | 156 | let sec_websocket_key = 157 | if let Some(key) = req.headers_mut().remove(header::SEC_WEBSOCKET_KEY) { 158 | key 159 | } else { 160 | return Err(WebSocketUpgradeError::WebsocketKeyHeaderMissing); 161 | }; 162 | let on_upgrade = req.extensions_mut().remove::().unwrap(); 163 | let sec_websocket_protocol = req.headers().get(header::SEC_WEBSOCKET_PROTOCOL).cloned(); 164 | 165 | Ok(Self { 166 | config: Default::default(), 167 | protocols: None, 168 | extensions: None, 169 | sec_websocket_key, 170 | on_upgrade, 171 | sec_websocket_protocol, 172 | }) 173 | } 174 | } 175 | 176 | #[derive(Debug)] 177 | pub(crate) enum WebSocketUpgradeError { 178 | MethodNotGet, 179 | InvalidConnectionHeader, 180 | InvalidUpgradeHeader, 181 | InvalidWebsocketVersionHeader, 182 | WebsocketKeyHeaderMissing, 183 | } 184 | 185 | impl Display for WebSocketUpgradeError { 186 | fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { 187 | match self { 188 | Self::MethodNotGet => f.write_str("request method is not get"), 189 | Self::InvalidConnectionHeader => f.write_str("request connection header is invalid"), 190 | Self::InvalidUpgradeHeader => f.write_str("request upgrade header is invalid"), 191 | Self::InvalidWebsocketVersionHeader => { 192 | f.write_str("request websocket version is invalid") 193 | } 194 | Self::WebsocketKeyHeaderMissing => { 195 | f.write_str("no websocket key header in request header") 196 | } 197 | } 198 | } 199 | } 200 | 201 | impl StdError for WebSocketUpgradeError {} 202 | 203 | pub(crate) struct WebSocketUpgradeResponse { 204 | extractor: WebSocketUpgrade, 205 | callback: F, 206 | } 207 | 208 | impl WebSocketUpgradeResponse 209 | where 210 | F: FnOnce(WebSocket) -> Fut + Send + 'static, 211 | Fut: Future + Send + 'static, 212 | { 213 | pub(crate) fn into_response(self) -> HttpResponse { 214 | // check requested protocols 215 | let protocol = self 216 | .extractor 217 | .sec_websocket_protocol 218 | .as_ref() 219 | .and_then(|req_protocols| { 220 | let req_protocols = req_protocols.to_str().ok()?; 221 | let protocols = self.extractor.protocols.as_ref()?; 222 | req_protocols 223 | .split(',') 224 | .map(|req_p| req_p.trim()) 225 | .find(|req_p| protocols.iter().any(|p| p == req_p)) 226 | }); 227 | 228 | let protocol = match protocol { 229 | Some(protocol) => { 230 | if let Ok(protocol) = HeaderValue::from_str(protocol) { 231 | Some(protocol) 232 | } else { 233 | return http::Response::builder() 234 | .status(StatusCode::BAD_REQUEST) 235 | .body(box_body(http_body::Full::new(Bytes::from_static( 236 | b"`Sec-Websocket-Protocol` header is invalid", 237 | )))) 238 | .unwrap(); 239 | } 240 | } 241 | None => None, 242 | }; 243 | 244 | let callback = self.callback; 245 | let on_upgrade = self.extractor.on_upgrade; 246 | let config = self.extractor.config; 247 | 248 | tokio::spawn(async move { 249 | let upgraded = on_upgrade.await.expect("connection upgrade failed"); 250 | let socket = 251 | WebSocketStream::from_raw_socket(upgraded, protocol::Role::Server, Some(config)) 252 | .await; 253 | callback(WebSocket::new(socket)).await; 254 | }); 255 | 256 | let mut builder = http::Response::builder() 257 | .status(StatusCode::SWITCHING_PROTOCOLS) 258 | .header( 259 | header::CONNECTION, 260 | HeaderValue::from_str("upgrade").unwrap(), 261 | ) 262 | .header(header::UPGRADE, HeaderValue::from_str("websocket").unwrap()) 263 | .header( 264 | header::SEC_WEBSOCKET_ACCEPT, 265 | sign(self.extractor.sec_websocket_key.as_bytes()), 266 | ); 267 | 268 | if let Some(protocol) = protocol { 269 | builder = builder.header(header::SEC_WEBSOCKET_PROTOCOL, protocol); 270 | } 271 | 272 | let mut new_exts = String::new(); 273 | 274 | if let Some(exts) = self.extractor.extensions { 275 | for ext in exts.iter() { 276 | if !new_exts.is_empty() { 277 | new_exts.push_str("; "); 278 | } 279 | new_exts.push_str(ext.as_ref()); 280 | } 281 | } 282 | 283 | if !new_exts.is_empty() { 284 | builder = builder.header( 285 | header::SEC_WEBSOCKET_EXTENSIONS, 286 | HeaderValue::from_str(&new_exts).expect("invalid websocket extensions"), 287 | ); 288 | } 289 | 290 | builder.body(box_body(http_body::Empty::new())).unwrap() 291 | } 292 | } 293 | 294 | fn sign(key: &[u8]) -> HeaderValue { 295 | let mut sha1 = Sha1::default(); 296 | sha1.update(key); 297 | sha1.update(&b"258EAFA5-E914-47DA-95CA-C5AB0DC85B11"[..]); 298 | let b64 = Bytes::from(base64::encode(&sha1.finalize())); 299 | HeaderValue::from_maybe_shared(b64).expect("base64 is a valid value") 300 | } 301 | -------------------------------------------------------------------------------- /crates/hrpc/src/server/transport/mock.rs: -------------------------------------------------------------------------------- 1 | use std::convert::Infallible; 2 | 3 | use futures_util::{future::BoxFuture, SinkExt, StreamExt}; 4 | use tower::Service; 5 | 6 | use crate::{ 7 | box_error, 8 | client::transport::SocketChannels, 9 | common::{socket::SocketMessage, transport::mock::MockReceiver}, 10 | response::BoxResponse, 11 | server::{socket::SocketHandler, MakeRoutes}, 12 | }; 13 | 14 | use super::Transport; 15 | 16 | /// Mock transport for the server. 17 | pub struct Mock { 18 | rx: MockReceiver, 19 | } 20 | 21 | impl Mock { 22 | /// Create a new mock transport. 23 | pub fn new(rx: MockReceiver) -> Self { 24 | Self { rx } 25 | } 26 | } 27 | 28 | impl Transport for Mock { 29 | type Error = Infallible; 30 | 31 | fn serve(mut self, mk_routes: S) -> BoxFuture<'static, Result<(), Self::Error>> 32 | where 33 | S: MakeRoutes, 34 | { 35 | let mut svc = mk_routes 36 | .into_make_service() 37 | .call(()) 38 | .into_inner() 39 | .unwrap() 40 | .unwrap(); 41 | 42 | Box::pin(async move { 43 | while let Some((req, sender)) = self.rx.inner.recv().await { 44 | let fut = Service::call(&mut svc, req); 45 | 46 | tokio::spawn(async move { 47 | let mut resp = fut.await.expect("cant fail"); 48 | 49 | if let Some(socket_handler) = resp.extensions_mut().remove::() { 50 | let (client_tx, server_rx) = 51 | futures_channel::mpsc::unbounded::(); 52 | let (server_tx, client_rx) = futures_channel::mpsc::unbounded(); 53 | 54 | { 55 | let client_socket_chans = SocketChannels::new( 56 | Box::pin(client_tx.sink_map_err(box_error)), 57 | Box::pin(client_rx.map(Ok)), 58 | ); 59 | 60 | let mut resp = BoxResponse::empty(); 61 | resp.extensions_mut().insert(client_socket_chans); 62 | 63 | sender.send(resp).expect("sender dropped"); 64 | } 65 | 66 | let fut = (socket_handler.inner)( 67 | Box::pin(server_rx.map(Ok)), 68 | Box::pin(server_tx.sink_map_err(box_error)), 69 | ); 70 | 71 | tokio::spawn(fut); 72 | } else { 73 | sender.send(resp).expect("sender dropped"); 74 | } 75 | }); 76 | } 77 | 78 | Ok(()) 79 | }) 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /crates/hrpc/src/server/transport/mod.rs: -------------------------------------------------------------------------------- 1 | use futures_util::future::BoxFuture; 2 | 3 | use super::MakeRoutes; 4 | 5 | /// Trait for enabling generic transport implementations over a [`MakeRoutes`]. 6 | pub trait Transport: Sized { 7 | /// The type of the error returned by a transport if it fails. 8 | type Error; 9 | 10 | /// Start serving a [`MakeRoutes`]. 11 | fn serve(self, mk_routes: S) -> BoxFuture<'static, Result<(), Self::Error>> 12 | where 13 | S: MakeRoutes; 14 | } 15 | 16 | /// Server implementation for a HTTP transport. 17 | #[cfg(feature = "http_server")] 18 | pub mod http; 19 | 20 | /// The mock transport. Useful for testing. 21 | #[cfg(feature = "mock_server")] 22 | pub mod mock; 23 | -------------------------------------------------------------------------------- /crates/hrpc/src/server/utils.rs: -------------------------------------------------------------------------------- 1 | pub(crate) mod downcast { 2 | /// Code originally from https://github.com/hyperium/http/blob/master/src/convert.rs, 3 | /// licensed under the following license: 4 | /* 5 | Copyright (c) 2017 http-rs authors 6 | 7 | Permission is hereby granted, free of charge, to any 8 | person obtaining a copy of this software and associated 9 | documentation files (the "Software"), to deal in the 10 | Software without restriction, including without 11 | limitation the rights to use, copy, modify, merge, 12 | publish, distribute, sublicense, and/or sell copies of 13 | the Software, and to permit persons to whom the Software 14 | is furnished to do so, subject to the following 15 | conditions: 16 | 17 | The above copyright notice and this permission notice 18 | shall be included in all copies or substantial portions 19 | of the Software. 20 | 21 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF 22 | ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 23 | TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 24 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT 25 | SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 26 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 27 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR 28 | IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 29 | DEALINGS IN THE SOFTWARE. 30 | */ 31 | macro_rules! if_downcast_into { 32 | ($in_ty:ty, $out_ty:ty, $val:ident, $body:expr) => {{ 33 | if std::any::TypeId::of::<$in_ty>() == std::any::TypeId::of::<$out_ty>() { 34 | // Store the value in an `Option` so we can `take` 35 | // it after casting to `&mut dyn Any`. 36 | let mut slot = Some($val); 37 | // Re-write the `$val` ident with the downcasted value. 38 | let $val = (&mut slot as &mut dyn std::any::Any) 39 | .downcast_mut::>() 40 | .unwrap() 41 | .take() 42 | .unwrap(); 43 | // Run the $body in scope of the replaced val. 44 | $body 45 | } 46 | }}; 47 | } 48 | 49 | pub(crate) use if_downcast_into; 50 | } 51 | -------------------------------------------------------------------------------- /examples/chat/README.md: -------------------------------------------------------------------------------- 1 | # `chat` 2 | 3 | This is an example showcasing unary and streaming requests, creating and using 4 | transports and layering as a very simple chatting app. 5 | 6 | To run the server: 7 | ```console 8 | $ cargo run --package chat_server 9 | ``` 10 | 11 | To run the CLI client: 12 | ```console 13 | $ cargo run --package chat_client 14 | ``` 15 | 16 | To run the WASM web client, you will need [`trunk`](https://trunkrs.dev): 17 | ```console 18 | $ trunk serve wasm_client/index.html 19 | ``` -------------------------------------------------------------------------------- /examples/chat/client/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "chat_client" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | chat_common = { path = "../common" } 10 | # Enable hrpc's server and client features, and the recommended transport 11 | hrpc = { path = "../../../crates/hrpc", features = ["http_hyper_client"] } 12 | # Enable tokio's macros so we can mark our main function, and multi threaded 13 | # runtime 14 | tokio = { version = "1", features = [ 15 | "rt", 16 | "rt-multi-thread", 17 | "macros", 18 | ] } 19 | # We will use rustyline to read messages from user on our client 20 | rustyline = "9" -------------------------------------------------------------------------------- /examples/chat/client/src/main.rs: -------------------------------------------------------------------------------- 1 | use chat_common::{ 2 | chat::{chat_client::ChatClient, *}, 3 | BoxError, 4 | }; 5 | 6 | use hrpc::client::transport::http::Hyper; 7 | use rustyline::{error::ReadlineError, Editor as Rustyline}; 8 | 9 | #[tokio::main] 10 | async fn main() -> Result<(), BoxError> { 11 | // Create a new chat client 12 | let transport = Hyper::new("http://localhost:2289".parse()?)?; 13 | let mut client = ChatClient::new_transport(transport); 14 | 15 | // Connect to message socket 16 | let mut socket = client.stream_messages(Empty {}).await?; 17 | 18 | // Send a message 19 | client 20 | .send_message(Message { 21 | content: "hello world!".to_string(), 22 | }) 23 | .await?; 24 | 25 | // Wait for messages and post them, in a seperate task 26 | tokio::spawn(async move { 27 | while let Ok(message) = socket.receive_message().await { 28 | println!("got: {}", message.content); 29 | } 30 | }); 31 | 32 | // Create our rustyline instance which we will use to read messages 33 | // from stdin 34 | let mut rustyline = Rustyline::<()>::new(); 35 | loop { 36 | let readline = rustyline.readline("(write your message)> "); 37 | match readline { 38 | Ok(line) => { 39 | client.send_message(Message { content: line }).await?; 40 | } 41 | Err(ReadlineError::Interrupted | ReadlineError::Eof) => { 42 | break; 43 | } 44 | Err(err) => { 45 | println!("rustyline error: {}", err); 46 | break; 47 | } 48 | } 49 | } 50 | 51 | Ok(()) 52 | } 53 | -------------------------------------------------------------------------------- /examples/chat/common/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "chat_common" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | prost = "0.10" 10 | hrpc = { path = "../../../crates/hrpc", features = ["client", "server"] } 11 | 12 | [build-dependencies] 13 | hrpc-build = { path = "../../../crates/hrpc-build", features = ["client", "server"] } -------------------------------------------------------------------------------- /examples/chat/common/build.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | hrpc_build::compile_protos("src/chat.proto").expect("could not compile the proto"); 3 | } 4 | -------------------------------------------------------------------------------- /examples/chat/common/src/chat.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package chat; 4 | 5 | // A chat message. 6 | message Message { string content = 1; } 7 | 8 | // An empty message. 9 | message Empty {} 10 | 11 | // Chat service. 12 | service Chat { 13 | // Endpoint to send a chat message. 14 | rpc SendMessage(Message) returns (Empty); 15 | // Endpoint to stream chat messages. 16 | rpc StreamMessages(Empty) returns (stream Message); 17 | } -------------------------------------------------------------------------------- /examples/chat/common/src/lib.rs: -------------------------------------------------------------------------------- 1 | /// `chat` package protobuf definitions and service. 2 | pub mod chat { 3 | hrpc::include_proto!("chat"); 4 | } 5 | 6 | /// A boxed error. 7 | pub type BoxError = Box; 8 | -------------------------------------------------------------------------------- /examples/chat/server/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "chat_server" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | chat_common = { path = "../common" } 10 | # Enable hrpc's server and client features, and the recommended transport 11 | hrpc = { path = "../../../crates/hrpc", features = ["http_server"] } 12 | # Enable tokio's macros so we can mark our main function, and multi threaded 13 | # runtime 14 | tokio = { version = "1", features = [ 15 | "rt", 16 | "rt-multi-thread", 17 | "macros", 18 | ] } 19 | 20 | # Server dependencies 21 | # We will use tower's RateLimitLayer to rate limit our endpoints 22 | tower = { version = "0.4", features = ["limit"] } 23 | # tower-http for CORS layer and tracing 24 | tower-http = { version = "0.3", features = ["cors"] } 25 | # Logging utilities 26 | tracing = "0.1" 27 | tracing-subscriber = { version = "0.3", features = ["env-filter"] } -------------------------------------------------------------------------------- /examples/chat/server/src/main.rs: -------------------------------------------------------------------------------- 1 | use std::time::Duration; 2 | 3 | use chat_common::{ 4 | chat::{chat_server::*, *}, 5 | BoxError, 6 | }; 7 | use hrpc::{ 8 | bail, 9 | common::layer::trace::TraceLayer, 10 | exports::http::StatusCode, 11 | server::{ 12 | layer::ratelimit::RateLimitLayer, 13 | prelude::*, 14 | transport::http::{layer::errid_to_status::ErrorIdentifierToStatusLayer, Hyper}, 15 | }, 16 | }; 17 | use tokio::sync::broadcast; 18 | use tower_http::cors::CorsLayer; 19 | use tracing_subscriber::EnvFilter; 20 | 21 | pub struct ChatService { 22 | message_broadcast: broadcast::Sender, 23 | } 24 | 25 | impl ChatService { 26 | fn new() -> Self { 27 | let (tx, _) = broadcast::channel(100); 28 | Self { 29 | message_broadcast: tx, 30 | } 31 | } 32 | } 33 | 34 | impl Chat for ChatService { 35 | fn send_message_middleware(&self) -> Option { 36 | // Limit send_message calls to 5 per 2 seconds 37 | Some(RateLimitLayer::new(5, Duration::from_secs(2)).into_hrpc_layer()) 38 | } 39 | 40 | #[handler] 41 | async fn send_message(&self, request: Request) -> ServerResult> { 42 | // Extract the message from the request 43 | let message = request.into_message().await?; 44 | 45 | if message.content.is_empty() { 46 | bail!(("empty-message", "empty messages aren't allowed")); 47 | } 48 | 49 | // Log message content 50 | tracing::info!("got message: {}", message.content); 51 | 52 | // Try to broadcast the message, if it fails return an error 53 | self.message_broadcast 54 | .send(message) 55 | .map_err(|_| HrpcError::new_internal_server_error("couldn't broadcast message"))?; 56 | 57 | Ok((Empty {}).into_response()) 58 | } 59 | 60 | fn stream_messages_middleware(&self) -> Option { 61 | // Limit stream_messages calls to 1 per 5 seconds 62 | Some(RateLimitLayer::new(1, Duration::from_secs(5)).into_hrpc_layer()) 63 | } 64 | 65 | #[handler] 66 | async fn stream_messages( 67 | &self, 68 | _request: Request<()>, 69 | mut socket: Socket, 70 | ) -> ServerResult<()> { 71 | // Subscribe to the message broadcaster 72 | let mut message_receiver = self.message_broadcast.subscribe(); 73 | 74 | // Process the received messages and send them to client 75 | while let Ok(message) = message_receiver.recv().await { 76 | socket.send_message(message).await?; 77 | } 78 | 79 | Ok(()) 80 | } 81 | } 82 | 83 | #[tokio::main] 84 | async fn main() -> Result<(), BoxError> { 85 | // Set up logging 86 | tracing_subscriber::fmt() 87 | .with_env_filter(EnvFilter::from_default_env()) 88 | .init(); 89 | 90 | // Create our chat service 91 | let service = ChatServer::new(ChatService::new()); 92 | // Layer our service with error identifier to HTTP status layer, which helps us convert our error identifers 93 | // to HTTP statuses. 94 | let service = service.layer(ErrorIdentifierToStatusLayer::new(|id| { 95 | id.eq("empty-message").then(|| StatusCode::BAD_REQUEST) 96 | })); 97 | // Layer our service with a tracing layer. 98 | let service = service.layer(TraceLayer::default_debug()); 99 | 100 | // Create our transport that we will use to serve our service 101 | let transport = Hyper::new("127.0.0.1:2289")?; 102 | 103 | // Layer our transport with a CORS header 104 | // 105 | // Since this is specific to HTTP, we use the transport's layer method! 106 | let transport = transport.layer(CorsLayer::permissive()); 107 | 108 | // Serve our service with our transport 109 | transport.serve(service).await?; 110 | 111 | Ok(()) 112 | } 113 | -------------------------------------------------------------------------------- /examples/chat/wasm_client/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "chat_wasm_client" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | chat_common = { path = "../common" } 8 | hrpc = { path = "../../../crates/hrpc", features = ["http_wasm_client"] } 9 | yew = "0.19" 10 | web-sys = { version = "0.3", features = ["HtmlInputElement"] } -------------------------------------------------------------------------------- /examples/chat/wasm_client/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | hRPC Example - Chat Client 6 | 7 | -------------------------------------------------------------------------------- /examples/chat/wasm_client/src/main.rs: -------------------------------------------------------------------------------- 1 | use std::cell::RefCell; 2 | 3 | use web_sys::HtmlInputElement as InputElement; 4 | use yew::prelude::*; 5 | 6 | use chat_common::{ 7 | chat::{chat_client::*, *}, 8 | *, 9 | }; 10 | use hrpc::{ 11 | client::{ 12 | error::ClientResult as HrpcClientResult, 13 | socket::{Socket, SocketError}, 14 | transport::http::{Wasm, WasmError}, 15 | }, 16 | exports::futures_util::FutureExt, 17 | Response, 18 | }; 19 | 20 | type ClientResult = HrpcClientResult; 21 | 22 | enum Msg { 23 | SendMessage(String), 24 | SendMessageResult(ClientResult>), 25 | PollMessagesResult { 26 | socket: Socket, 27 | res: Result, 28 | }, 29 | SocketCreateResult(ClientResult>), 30 | Nothing, 31 | } 32 | 33 | struct Model { 34 | messages: Vec, 35 | last_error: String, 36 | current_message: String, 37 | } 38 | 39 | #[derive(Clone)] 40 | struct Props { 41 | client: RefCell>, 42 | } 43 | 44 | impl PartialEq for Props { 45 | fn eq(&self, _other: &Self) -> bool { 46 | // There is no notion of "equality" for client provided by hRPC 47 | // and since we won't change the server URL we can just they are 48 | // always equal 49 | true 50 | } 51 | } 52 | 53 | impl Properties for Props { 54 | type Builder = (); 55 | 56 | fn builder() -> Self::Builder {} 57 | } 58 | 59 | impl Component for Model { 60 | type Message = Msg; 61 | type Properties = Props; 62 | 63 | fn create(_ctx: &Context) -> Self { 64 | Self { 65 | messages: Vec::new(), 66 | last_error: String::new(), 67 | current_message: String::new(), 68 | } 69 | } 70 | 71 | fn update(&mut self, ctx: &Context, msg: Self::Message) -> bool { 72 | match msg { 73 | Msg::SendMessage(content) => { 74 | let fut = ctx 75 | .props() 76 | .client 77 | .borrow_mut() 78 | .send_message(Message { content }) 79 | .map(Msg::SendMessageResult); 80 | ctx.link().send_future(fut); 81 | false 82 | } 83 | Msg::SendMessageResult(res) => match res { 84 | Ok(_) => false, 85 | Err(err) => { 86 | self.last_error = err.to_string(); 87 | true 88 | } 89 | }, 90 | Msg::SocketCreateResult(res) => match res { 91 | Ok(mut socket) => { 92 | let fut = async move { 93 | let res = socket.receive_message().await; 94 | Msg::PollMessagesResult { socket, res } 95 | }; 96 | ctx.link().send_future(fut); 97 | false 98 | } 99 | Err(err) => { 100 | self.last_error = err.to_string(); 101 | true 102 | } 103 | }, 104 | Msg::PollMessagesResult { mut socket, res } => { 105 | match res { 106 | Ok(msg) => self.messages.push(msg.content), 107 | Err(err) => self.last_error = err.to_string(), 108 | } 109 | 110 | let fut = async move { 111 | let res = socket.receive_message().await; 112 | Msg::PollMessagesResult { socket, res } 113 | }; 114 | ctx.link().send_future(fut); 115 | true 116 | } 117 | Msg::Nothing => false, 118 | } 119 | } 120 | 121 | fn view(&self, ctx: &Context) -> Html { 122 | let on_input_enter = ctx.link().callback(move |e: KeyboardEvent| { 123 | (e.key() == "Enter") 124 | .then(|| { 125 | let input: InputElement = e.target_unchecked_into(); 126 | let value = input.value(); 127 | input.set_value(""); 128 | Msg::SendMessage(value) 129 | }) 130 | .unwrap_or(Msg::Nothing) 131 | }); 132 | 133 | html! { 134 |
135 | { for self.messages.iter().map(|msg| html! {

{msg}

}) } 136 | 137 |

{format!("last error: {}", self.last_error.is_empty().then(|| "nothing").unwrap_or(&self.last_error))}

138 |
139 | } 140 | } 141 | } 142 | 143 | fn main() -> Result<(), BoxError> { 144 | let transport = Wasm::new("http://0.0.0.0:2289".parse().unwrap())?; 145 | 146 | let mut client = ChatClient::new_transport(transport); 147 | let connect_socket_fut = client 148 | .stream_messages(Empty {}) 149 | .map(Msg::SocketCreateResult); 150 | 151 | let app = yew::start_app_with_props::(Props { 152 | client: RefCell::new(client), 153 | }); 154 | app.send_future(connect_socket_fut); 155 | 156 | Ok(()) 157 | } 158 | -------------------------------------------------------------------------------- /examples/hello_world/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "hello_world" 3 | description = "Simple 'hello world' example for hrpc-rs." 4 | authors = ["Yusuf Bera Ertan "] 5 | version = "0.1.0" 6 | edition = "2021" 7 | 8 | [dependencies] 9 | prost = "0.10" 10 | hrpc = { path = "../../crates/hrpc", features = [ 11 | "http_hyper_client", 12 | "http_server", 13 | ] } 14 | tokio = { version = "1", features = ["rt", "rt-multi-thread", "macros"] } 15 | 16 | [build-dependencies] 17 | hrpc-build = { path = "../../crates/hrpc-build", features = [ 18 | "server", 19 | "client", 20 | "default_transport_http", 21 | ] } 22 | 23 | [[bin]] 24 | name = "client" 25 | doc = false 26 | 27 | [[bin]] 28 | name = "server" 29 | doc = false 30 | -------------------------------------------------------------------------------- /examples/hello_world/build.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | hrpc_build::compile_protos("src/hello.proto").expect("could not compile the proto"); 3 | } 4 | -------------------------------------------------------------------------------- /examples/hello_world/src/bin/client.rs: -------------------------------------------------------------------------------- 1 | use hello_world::{ 2 | hello::{greeter_client::GreeterClient, WelcomeUserRequest}, 3 | BoxError, 4 | }; 5 | 6 | #[tokio::main] 7 | async fn main() -> Result<(), BoxError> { 8 | // Create a new greeter client 9 | let mut client = GreeterClient::new("http://localhost:2289")?; 10 | 11 | // Call the welcome user RPC 12 | let response = client 13 | .welcome_user(WelcomeUserRequest { 14 | user_name: "joe".to_string(), 15 | }) 16 | .await? 17 | .into_message() 18 | .await?; 19 | 20 | // Print the welcome message 21 | println!("{}", response.welcome_message); 22 | 23 | Ok(()) 24 | } 25 | -------------------------------------------------------------------------------- /examples/hello_world/src/bin/server.rs: -------------------------------------------------------------------------------- 1 | use hello_world::{ 2 | hello::{greeter_server::*, *}, 3 | BoxError, 4 | }; 5 | use hrpc::server::prelude::*; 6 | 7 | pub struct GreeterService; 8 | 9 | impl Greeter for GreeterService { 10 | #[handler] 11 | async fn welcome_user( 12 | &self, 13 | request: Request, 14 | ) -> ServerResult> { 15 | // Extract the message from the request 16 | let message = request.into_message().await?; 17 | 18 | // Craft a response message using the `user_name` we got in the request message 19 | let response_message = WelcomeUserResponse { 20 | welcome_message: format!("Welcome, {}!", message.user_name), 21 | }; 22 | 23 | Ok(response_message.into_response()) 24 | } 25 | } 26 | 27 | #[tokio::main] 28 | async fn main() -> Result<(), BoxError> { 29 | // Create a new greeter server and start serving it 30 | GreeterServer::new(GreeterService) 31 | .serve("127.0.0.1:2289") 32 | .await?; 33 | 34 | Ok(()) 35 | } 36 | -------------------------------------------------------------------------------- /examples/hello_world/src/hello.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package hello; 4 | 5 | // Request used in `WelcomeUser` endpoint. 6 | message WelcomeUserRequest { 7 | // The user name of the user you want to welcome. 8 | string user_name = 1; 9 | } 10 | 11 | // Response used in `WelcomeUser` endpoint. 12 | message WelcomeUserResponse { 13 | // The welcome message. 14 | string welcome_message = 1; 15 | } 16 | 17 | // The greeter service. 18 | service Greeter { 19 | // Endpoint to welcome a user. 20 | rpc WelcomeUser(WelcomeUserRequest) returns (WelcomeUserResponse); 21 | } -------------------------------------------------------------------------------- /examples/hello_world/src/lib.rs: -------------------------------------------------------------------------------- 1 | /// `hello` package protobuf definitions and service. 2 | pub mod hello { 3 | hrpc::include_proto!("hello"); 4 | } 5 | 6 | /// A boxed error. 7 | pub type BoxError = Box; 8 | -------------------------------------------------------------------------------- /examples/interop/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "interop" 3 | version = "0.1.0" 4 | authors = ["Yusuf Bera Ertan "] 5 | edition = "2021" 6 | license = "MIT" 7 | 8 | [dependencies] 9 | prost = "0.10" 10 | hrpc = { path = "../../crates/hrpc", features = [ 11 | "http_hyper_client", 12 | "http_server", 13 | ] } 14 | tokio = { version = "1.8", features = [ 15 | "rt", 16 | "rt-multi-thread", 17 | "macros", 18 | "fs", 19 | ] } 20 | futures = "0.3" 21 | tracing-subscriber = "0.3" 22 | tower = { version = "0.4", features = ["limit", "load"] } 23 | 24 | [build-dependencies] 25 | hrpc-build = { path = "../../crates/hrpc-build", features = [ 26 | "server", 27 | "client", 28 | "default_transport_http", 29 | ] } 30 | 31 | [package.metadata.nix] 32 | build = true 33 | app = true 34 | -------------------------------------------------------------------------------- /examples/interop/build.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | hrpc_build::compile_protos("proto/test.proto").unwrap(); 3 | } 4 | -------------------------------------------------------------------------------- /examples/interop/proto/test.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package test; 4 | 5 | option go_package = "hrpc/jiti"; 6 | 7 | // Ping message. 8 | message Ping { 9 | string mu = 1; 10 | } 11 | // Pong message. 12 | message Pong { 13 | string mu = 1; 14 | } 15 | 16 | service Mu { 17 | // Mu RPC. 18 | rpc Mu(Ping) returns (Pong); 19 | // MuMute RPC. 20 | rpc MuMute(stream Ping) returns (stream Pong); 21 | // MuMu RPC. 22 | // ``` 23 | // test 24 | // ``` 25 | rpc MuMu(Ping) returns (stream Pong); 26 | } -------------------------------------------------------------------------------- /examples/interop/src/main.rs: -------------------------------------------------------------------------------- 1 | use hrpc::{bail, bail_result, exports::tracing::Level, server::prelude::*, BoxError}; 2 | use tower::limit::RateLimitLayer; 3 | use tracing_subscriber::{filter::Targets, prelude::*}; 4 | 5 | use std::time::{Duration, Instant}; 6 | 7 | hrpc::include_proto!("test"); 8 | 9 | #[tokio::main] 10 | async fn main() { 11 | let layer = tracing_subscriber::fmt::layer().with_filter( 12 | Targets::default() 13 | .with_target("hyper", Level::ERROR) 14 | .with_default(Level::DEBUG), 15 | ); 16 | 17 | tracing_subscriber::registry().with(layer).init(); 18 | 19 | let is_server = std::env::args().nth(1).map_or(false, |t| t == "server"); 20 | if is_server { 21 | server().await 22 | } else { 23 | client().await.unwrap() 24 | } 25 | } 26 | 27 | async fn client() -> Result<(), BoxError> { 28 | let mut client = mu_client::MuClient::new("http://localhost:2289").unwrap(); 29 | 30 | let resp = client 31 | .mu(Ping { 32 | mu: "123".to_string(), 33 | }) 34 | .await? 35 | .into_message() 36 | .await?; 37 | println!("{:#?}", resp); 38 | 39 | let resp = client.mu(Ping { mu: "".to_string() }).await; 40 | println!("{:#?}", resp); 41 | 42 | let (mut write, mut read) = client.mu_mute(()).await.unwrap().split(); 43 | let _ = client.mu_mu(Ping::default()).await.unwrap(); 44 | 45 | tokio::spawn(async move { 46 | while let Ok(msg) = read.receive_message().await { 47 | println!("got: {:#?}", msg); 48 | } 49 | }); 50 | 51 | for i in 0..100 { 52 | let ins = Instant::now(); 53 | if let Err(err) = write.send_message(Ping { mu: i.to_string() }).await { 54 | eprintln!("failed to send message: {}", err); 55 | } 56 | println!("sent in {}", ins.elapsed().as_secs_f64()); 57 | let ins = Instant::now(); 58 | match client.mu(Ping { mu: i.to_string() }).await { 59 | Err(err) => { 60 | eprintln!("failed to send message: {}", err); 61 | } 62 | Ok(resp) => { 63 | println!("got {:#?}", resp.into_message().await?); 64 | } 65 | } 66 | println!("sent in {}", ins.elapsed().as_secs_f64()); 67 | 68 | tokio::time::sleep(Duration::from_secs(1)).await; 69 | } 70 | 71 | write.close().await?; 72 | 73 | Ok(()) 74 | } 75 | 76 | async fn server() { 77 | mu_server::MuServer::new(MuService) 78 | .serve("127.0.0.1:2289") 79 | .await 80 | .unwrap(); 81 | } 82 | 83 | struct MuService; 84 | 85 | impl mu_server::Mu for MuService { 86 | #[handler] 87 | async fn mu_mu(&self, req: Request<()>, _: Socket) -> ServerResult<()> { 88 | println!("mu_mu: {:?}", req); 89 | Ok(()) 90 | } 91 | 92 | fn mu_middleware(&self) -> Option { 93 | Some(HrpcLayer::new(RateLimitLayer::new( 94 | 5, 95 | Duration::from_secs(10), 96 | ))) 97 | } 98 | 99 | #[handler] 100 | async fn mu(&self, request: Request) -> ServerResult> { 101 | let msg = request.into_message().await?; 102 | if msg.mu.is_empty() { 103 | bail!(("interop.empty-ping", "empty ping")); 104 | } 105 | Ok((Pong { mu: msg.mu }).into_response()) 106 | } 107 | 108 | #[handler] 109 | async fn mu_mute(&self, _: Request<()>, mut sock: Socket) -> ServerResult<()> { 110 | let mut int = tokio::time::interval(Duration::from_secs(10)); 111 | int.tick().await; 112 | 113 | loop { 114 | tokio::select! { 115 | res = sock.receive_message() => { 116 | match res { 117 | Ok(msg) => println!("{:?}", msg), 118 | Err(err) => return Err(err.into()), 119 | } 120 | } 121 | _ = int.tick() => { 122 | bail_result!( 123 | sock.send_message(Pong { 124 | mu: "been 10 seconds".to_string(), 125 | }) 126 | .await 127 | ); 128 | } 129 | } 130 | } 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /examples/mock/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "mock" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | prost = "0.10" 8 | hrpc = { path = "../../crates/hrpc", features = ["mock_client", "mock_server"] } 9 | tokio = { version = "1", features = ["rt", "rt-multi-thread", "macros"] } 10 | 11 | [build-dependencies] 12 | hrpc-build = { path = "../../crates/hrpc-build", features = [ 13 | "server", 14 | "client", 15 | ] } 16 | -------------------------------------------------------------------------------- /examples/mock/README.md: -------------------------------------------------------------------------------- 1 | # mock 2 | 3 | This example showcases the mock transport. The mock transport is intended 4 | for tests, where networking might not be available. It uses a simple 5 | `tokio` MPSC channel for communication between client and server. -------------------------------------------------------------------------------- /examples/mock/build.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | hrpc_build::compile_protos("src/hello.proto").expect("could not compile the proto"); 3 | } 4 | -------------------------------------------------------------------------------- /examples/mock/src/hello.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package hello; 4 | 5 | // Request used in `WelcomeUser` endpoint. 6 | message WelcomeUserRequest { 7 | // The user name of the user you want to welcome. 8 | string user_name = 1; 9 | } 10 | 11 | // Response used in `WelcomeUser` endpoint. 12 | message WelcomeUserResponse { 13 | // The welcome message. 14 | string welcome_message = 1; 15 | } 16 | 17 | // The greeter service. 18 | service Greeter { 19 | // Endpoint to welcome a user. 20 | rpc WelcomeUser(WelcomeUserRequest) returns (WelcomeUserResponse); 21 | } -------------------------------------------------------------------------------- /examples/mock/src/lib.rs: -------------------------------------------------------------------------------- 1 | /// `hello` package protobuf definitions and service. 2 | pub mod hello { 3 | hrpc::include_proto!("hello"); 4 | } 5 | 6 | /// A boxed error. 7 | pub type BoxError = Box; 8 | 9 | /// Server implementation. 10 | pub mod server { 11 | use super::hello::{greeter_server::*, *}; 12 | use hrpc::server::prelude::*; 13 | 14 | pub struct GreeterService; 15 | 16 | impl Greeter for GreeterService { 17 | #[handler] 18 | async fn welcome_user( 19 | &self, 20 | request: Request, 21 | ) -> ServerResult> { 22 | // Extract the message from the request 23 | let message = request.into_message().await?; 24 | 25 | // Craft a response message using the `user_name` we got in the request message 26 | let response_message = WelcomeUserResponse { 27 | welcome_message: format!("Welcome, {}!", message.user_name), 28 | }; 29 | 30 | Ok(response_message.into_response()) 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /examples/mock/src/main.rs: -------------------------------------------------------------------------------- 1 | use hrpc::{ 2 | client::transport::mock::Mock as MockClient, 3 | common::transport::mock::{new_mock_channels, MockReceiver, MockSender}, 4 | server::transport::{mock::Mock as MockServer, Transport}, 5 | }; 6 | use mock::{ 7 | hello::{greeter_client::GreeterClient, greeter_server::GreeterServer, WelcomeUserRequest}, 8 | server::GreeterService, 9 | BoxError, 10 | }; 11 | 12 | #[tokio::main] 13 | async fn main() -> Result<(), BoxError> { 14 | // First create mock channels 15 | let (tx, rx) = new_mock_channels(); 16 | 17 | // Then we spawn our server 18 | spawn_server(rx); 19 | 20 | // Afterwards we make a client 21 | let mut client = make_client(tx); 22 | 23 | // Now we can call some methods! 24 | let response = client 25 | .welcome_user(WelcomeUserRequest { 26 | user_name: "mock".to_string(), 27 | }) 28 | .await?; 29 | let welcome_message = response.into_message().await?.welcome_message; 30 | 31 | println!("mock response: {}", welcome_message); 32 | 33 | Ok(()) 34 | } 35 | 36 | fn spawn_server(rx: MockReceiver) { 37 | let transport = MockServer::new(rx); 38 | let server = GreeterServer::new(GreeterService); 39 | let fut = transport.serve(server); 40 | 41 | tokio::spawn(fut); 42 | } 43 | 44 | fn make_client(tx: MockSender) -> GreeterClient { 45 | let transport = MockClient::new(tx); 46 | GreeterClient::new_transport(transport) 47 | } 48 | -------------------------------------------------------------------------------- /flake.lock: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": { 3 | "crane": { 4 | "flake": false, 5 | "locked": { 6 | "lastModified": 1644785799, 7 | "narHash": "sha256-VpAJO1L0XeBvtCuNGK4IDKp6ENHIpTrlaZT7yfBCvwo=", 8 | "owner": "ipetkov", 9 | "repo": "crane", 10 | "rev": "fc7a94f841347c88f2cb44217b2a3faa93e2a0b2", 11 | "type": "github" 12 | }, 13 | "original": { 14 | "owner": "ipetkov", 15 | "repo": "crane", 16 | "type": "github" 17 | } 18 | }, 19 | "devshell": { 20 | "inputs": { 21 | "flake-utils": "flake-utils", 22 | "nixpkgs": [ 23 | "nci", 24 | "nixpkgs" 25 | ] 26 | }, 27 | "locked": { 28 | "lastModified": 1650900878, 29 | "narHash": "sha256-qhNncMBSa9STnhiLfELEQpYC1L4GrYHNIzyCZ/pilsI=", 30 | "owner": "numtide", 31 | "repo": "devshell", 32 | "rev": "d97df53b5ddaa1cfbea7cddbd207eb2634304733", 33 | "type": "github" 34 | }, 35 | "original": { 36 | "owner": "numtide", 37 | "repo": "devshell", 38 | "type": "github" 39 | } 40 | }, 41 | "dream2nix": { 42 | "inputs": { 43 | "alejandra": [ 44 | "nci", 45 | "nixpkgs" 46 | ], 47 | "crane": "crane", 48 | "flake-utils-pre-commit": [ 49 | "nci", 50 | "nixpkgs" 51 | ], 52 | "gomod2nix": [ 53 | "nci", 54 | "nixpkgs" 55 | ], 56 | "mach-nix": [ 57 | "nci", 58 | "nixpkgs" 59 | ], 60 | "nixpkgs": [ 61 | "nci", 62 | "nixpkgs" 63 | ], 64 | "node2nix": [ 65 | "nci", 66 | "nixpkgs" 67 | ], 68 | "poetry2nix": [ 69 | "nci", 70 | "nixpkgs" 71 | ], 72 | "pre-commit-hooks": [ 73 | "nci", 74 | "nixpkgs" 75 | ] 76 | }, 77 | "locked": { 78 | "lastModified": 1651223854, 79 | "narHash": "sha256-VFeEmzUiTSvilzKKNVPbfgXTn7EvrQol//9PDbLba+8=", 80 | "owner": "nix-community", 81 | "repo": "dream2nix", 82 | "rev": "97d32e314e4621306adbbc2cdc71c0e84dbbd9ed", 83 | "type": "github" 84 | }, 85 | "original": { 86 | "owner": "nix-community", 87 | "repo": "dream2nix", 88 | "type": "github" 89 | } 90 | }, 91 | "flake-utils": { 92 | "locked": { 93 | "lastModified": 1642700792, 94 | "narHash": "sha256-XqHrk7hFb+zBvRg6Ghl+AZDq03ov6OshJLiSWOoX5es=", 95 | "owner": "numtide", 96 | "repo": "flake-utils", 97 | "rev": "846b2ae0fc4cc943637d3d1def4454213e203cba", 98 | "type": "github" 99 | }, 100 | "original": { 101 | "owner": "numtide", 102 | "repo": "flake-utils", 103 | "type": "github" 104 | } 105 | }, 106 | "nci": { 107 | "inputs": { 108 | "devshell": "devshell", 109 | "dream2nix": "dream2nix", 110 | "nixpkgs": "nixpkgs", 111 | "rustOverlay": "rustOverlay" 112 | }, 113 | "locked": { 114 | "lastModified": 1651299115, 115 | "narHash": "sha256-CYIBsYOvQxbxLvuS6tTGa9/fbYMt++bqqxKI6D1mTfE=", 116 | "owner": "yusdacra", 117 | "repo": "nix-cargo-integration", 118 | "rev": "84ca864e64a7c31a879279f18a64a610d19a10e6", 119 | "type": "github" 120 | }, 121 | "original": { 122 | "owner": "yusdacra", 123 | "repo": "nix-cargo-integration", 124 | "type": "github" 125 | } 126 | }, 127 | "nixpkgs": { 128 | "locked": { 129 | "lastModified": 1651007983, 130 | "narHash": "sha256-GNay7yDPtLcRcKCNHldug85AhAvBpTtPEJWSSDYBw8U=", 131 | "owner": "NixOS", 132 | "repo": "nixpkgs", 133 | "rev": "e10da1c7f542515b609f8dfbcf788f3d85b14936", 134 | "type": "github" 135 | }, 136 | "original": { 137 | "owner": "NixOS", 138 | "ref": "nixos-unstable", 139 | "repo": "nixpkgs", 140 | "type": "github" 141 | } 142 | }, 143 | "root": { 144 | "inputs": { 145 | "nci": "nci" 146 | } 147 | }, 148 | "rustOverlay": { 149 | "flake": false, 150 | "locked": { 151 | "lastModified": 1651286718, 152 | "narHash": "sha256-sPGOKDL6TNRfLnwarbdlmeD0FW4BmPfOoB/AMax91pg=", 153 | "owner": "oxalica", 154 | "repo": "rust-overlay", 155 | "rev": "8a687a6e5dc1f5c39715b01521a7aa0122529a05", 156 | "type": "github" 157 | }, 158 | "original": { 159 | "owner": "oxalica", 160 | "repo": "rust-overlay", 161 | "type": "github" 162 | } 163 | } 164 | }, 165 | "root": "root", 166 | "version": 7 167 | } 168 | -------------------------------------------------------------------------------- /flake.nix: -------------------------------------------------------------------------------- 1 | { 2 | inputs.nci.url = "github:yusdacra/nix-cargo-integration"; 3 | 4 | outputs = inputs: 5 | inputs.nci.lib.makeOutputs { 6 | root = ./.; 7 | overrides = { 8 | crateOverrides = common: prev: { 9 | interop = old: let 10 | env = {PROTOC = "protoc";}; 11 | in 12 | { 13 | buildInputs = (old.buildInputs or []) ++ [common.pkgs.protobuf]; 14 | propagatedEnv = env; 15 | } 16 | // env; 17 | }; 18 | }; 19 | }; 20 | } 21 | -------------------------------------------------------------------------------- /rust-toolchain.toml: -------------------------------------------------------------------------------- 1 | [toolchain] 2 | channel = "stable" 3 | targets = ["wasm32-unknown-unknown", "x86_64-unknown-linux-gnu"] -------------------------------------------------------------------------------- /shell.nix: -------------------------------------------------------------------------------- 1 | (import 2 | (fetchTarball { 3 | url = "https://github.com/edolstra/flake-compat/archive/99f1c2157fba4bfe6211a321fd0ee43199025dbf.tar.gz"; 4 | sha256 = "sha256:0x2jn3vrawwv9xp15674wjz9pixwjyj3j771izayl962zziivbx2"; 5 | }) 6 | {src = ./.;}) 7 | .shellNix 8 | --------------------------------------------------------------------------------