From a539cfde14cf8c9b0025fcbd1425415d3a37573f Mon Sep 17 00:00:00 2001 From: Timothy Clem Date: Wed, 11 Jun 2025 17:58:42 -0700 Subject: [PATCH 1/8] First pass at a design --- Cargo.lock | 1 + crates/twirp-build/Cargo.toml | 1 + crates/twirp-build/src/lib.rs | 32 ++++++++++++++++++++++++++++---- crates/twirp/src/client.rs | 22 +++++++++++++++------- crates/twirp/src/test.rs | 3 ++- example/src/bin/client.rs | 31 ++++++++++++++++++++++++++----- 6 files changed, 73 insertions(+), 17 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b1894de..df694cf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1294,6 +1294,7 @@ dependencies = [ "proc-macro2", "prost-build", "quote", + "reqwest", "syn", ] diff --git a/crates/twirp-build/Cargo.toml b/crates/twirp-build/Cargo.toml index 908c318..67ebe7a 100644 --- a/crates/twirp-build/Cargo.toml +++ b/crates/twirp-build/Cargo.toml @@ -16,6 +16,7 @@ license-file = "./LICENSE" [dependencies] prost-build = "0.13" prettyplease = { version = "0.2" } +reqwest = { version = "0.12", default-features = false } quote = "1.0" syn = "2.0" proc-macro2 = "1.0" diff --git a/crates/twirp-build/src/lib.rs b/crates/twirp-build/src/lib.rs index 0540c67..e614770 100644 --- a/crates/twirp-build/src/lib.rs +++ b/crates/twirp-build/src/lib.rs @@ -164,21 +164,45 @@ impl prost_build::ServiceGenerator for ServiceGenerator { let client_name = service.client_name; let mut client_trait_methods = Vec::with_capacity(service.methods.len()); let mut client_methods = Vec::with_capacity(service.methods.len()); + client_trait_methods.push(quote! { + async fn request(&self, req: twirp::reqwest::RequestBuilder) -> Result where O: prost::Message + Default; + }); + client_methods.push(quote! { + async fn request(&self, req: twirp::reqwest::RequestBuilder) -> Result where O: prost::Message + Default { + self.make_request(req).await + } + }); for m in &service.methods { let name = &m.name; + // let name_ext = format_ident!("{}_ext", name); + let build_name = format_ident!("build_{}", name); let input_type = &m.input_type; let output_type = &m.output_type; let request_path = format!("{}/{}", service.fqn, m.proto_name); client_trait_methods.push(quote! { - async fn #name(&self, req: #input_type) -> Result<#output_type, twirp::ClientError>; + async fn #name(&self, req: #input_type) -> Result<#output_type, twirp::ClientError> { + let builder = self.#build_name(req)?; + self.request(builder).await + } + }); + // client_trait_methods.push(quote! { + // async fn #name_ext(&self, req: twirp::reqwest::RequestBuilder) -> Result<#output_type, twirp::ClientError>; + // }); + client_trait_methods.push(quote! { + fn #build_name(&self, req: #input_type) -> Result; }); + // client_methods.push(quote! { + // async fn #name_ext(&self, req: twirp::reqwest::RequestBuilder) -> Result<#output_type, twirp::ClientError> { + // self.request(req).await + // } + // }); client_methods.push(quote! { - async fn #name(&self, req: #input_type) -> Result<#output_type, twirp::ClientError> { - self.request(#request_path, req).await + fn #build_name(&self, req: #input_type) -> Result { + self.build_request(#request_path, req) } - }) + }); } let client_trait = quote! { #[twirp::async_trait::async_trait] diff --git a/crates/twirp/src/client.rs b/crates/twirp/src/client.rs index 5f8ac5b..cfc84e8 100644 --- a/crates/twirp/src/client.rs +++ b/crates/twirp/src/client.rs @@ -3,7 +3,7 @@ use std::vec; use async_trait::async_trait; use reqwest::header::{InvalidHeaderValue, CONTENT_TYPE}; -use reqwest::StatusCode; +use reqwest::{RequestBuilder, StatusCode}; use thiserror::Error; use url::Url; @@ -155,23 +155,31 @@ impl Client { } } - /// Make an HTTP twirp request. - pub async fn request(&self, path: &str, body: I) -> Result + pub fn build_request(&self, path: &str, body: I) -> Result where I: prost::Message, - O: prost::Message + Default, { let mut url = self.inner.base_url.join(path)?; if let Some(host) = &self.host { url.set_host(Some(host))? }; - let path = url.path().to_string(); + let req = self .http_client .post(url) .header(CONTENT_TYPE, CONTENT_TYPE_PROTOBUF) - .body(serialize_proto_message(body)) - .build()?; + .body(serialize_proto_message(body)); + Ok(req) + } + + /// Make an HTTP twirp request. + // pub async fn request(&self, ctx: Context, path: &str, body: I) -> Result + pub async fn make_request(&self, builder: RequestBuilder) -> Result + where + O: prost::Message + Default, + { + let req = builder.build()?; + let path = req.url().path().to_string(); // Create and execute the middleware handlers let next = Next::new(&self.http_client, &self.inner.middlewares); diff --git a/crates/twirp/src/test.rs b/crates/twirp/src/test.rs index e80effd..6178e91 100644 --- a/crates/twirp/src/test.rs +++ b/crates/twirp/src/test.rs @@ -121,7 +121,8 @@ pub trait TestApiClient { #[async_trait] impl TestApiClient for Client { async fn ping(&self, req: PingRequest) -> Result { - self.request("test.TestAPI/Ping", req).await + let req = self.build_request("test.TestAPI/Ping", req)?; + self.make_request(req).await } async fn boom(&self, _req: PingRequest) -> Result { diff --git a/example/src/bin/client.rs b/example/src/bin/client.rs index 89c6e71..28ea4a3 100644 --- a/example/src/bin/client.rs +++ b/example/src/bin/client.rs @@ -1,6 +1,6 @@ use twirp::async_trait::async_trait; use twirp::client::{Client, ClientBuilder, Middleware, Next}; -use twirp::reqwest::{Request, Response}; +use twirp::reqwest::{self, Request, Response}; use twirp::url::Url; use twirp::GenericError; @@ -38,6 +38,15 @@ pub async fn main() -> Result<(), GenericError> { .await; eprintln!("{:?}", resp); + // TODO: Figure out where `with_host` goes in all this... + let req = client + .with_host("localhost") + .build_make_hat(MakeHatRequest { inches: 1 })? + .header("x-custom-header", "a"); + // Make a request with context + let resp: MakeHatResponse = client.request(req).await?; + eprintln!("{:?}", resp); + Ok(()) } @@ -69,23 +78,35 @@ impl Middleware for PrintResponseHeaders { } } +// NOTE: This is just to demonstrate manually implementing the client trait. You don't need to do this as this code will +// be generated for you by twirp-build. #[allow(dead_code)] #[derive(Debug)] struct MockHaberdasherApiClient; #[async_trait] impl HaberdasherApiClient for MockHaberdasherApiClient { - async fn make_hat( + fn build_make_hat( &self, _req: MakeHatRequest, - ) -> Result { + ) -> Result { todo!() } - async fn get_status( + fn build_get_status( &self, _req: GetStatusRequest, - ) -> Result { + ) -> Result { + todo!() + } + + async fn request( + &self, + _req: reqwest::RequestBuilder, + ) -> Result + where + O: prost::Message + Default, + { todo!() } } From ce4848747de4128416a3a573fbcfc6a19aa6706e Mon Sep 17 00:00:00 2001 From: Timothy Clem Date: Thu, 12 Jun 2025 13:01:57 -0700 Subject: [PATCH 2/8] WIP: handle both types --- crates/twirp-build/src/lib.rs | 37 ++++++++++++++++++---------- crates/twirp/src/client.rs | 46 ++++++++++++++++++++++++++++++++--- crates/twirp/src/lib.rs | 2 +- example/src/bin/client.rs | 22 ++++++++++++----- 4 files changed, 83 insertions(+), 24 deletions(-) diff --git a/crates/twirp-build/src/lib.rs b/crates/twirp-build/src/lib.rs index e614770..a1ab865 100644 --- a/crates/twirp-build/src/lib.rs +++ b/crates/twirp-build/src/lib.rs @@ -165,16 +165,35 @@ impl prost_build::ServiceGenerator for ServiceGenerator { let mut client_trait_methods = Vec::with_capacity(service.methods.len()); let mut client_methods = Vec::with_capacity(service.methods.len()); client_trait_methods.push(quote! { - async fn request(&self, req: twirp::reqwest::RequestBuilder) -> Result where O: prost::Message + Default; + async fn request(&self, req: twirp::RequestBuilder) -> Result + where + I: prost::Message, + O: prost::Message + Default; + }); + client_trait_methods.push(quote! { + fn build(&self, req: I) -> Result, twirp::ClientError> + where + I: prost::Message, + O: prost::Message + Default; }); client_methods.push(quote! { - async fn request(&self, req: twirp::reqwest::RequestBuilder) -> Result where O: prost::Message + Default { + async fn request(&self, req: twirp::RequestBuilder) -> Result + where + I: prost::Message, + O: prost::Message + Default { self.make_request(req).await } }); + client_methods.push(quote! { + fn build(&self, req: I) -> Result, twirp::ClientError> + where + I: prost::Message, + O: prost::Message + Default { + todo!() + } + }); for m in &service.methods { let name = &m.name; - // let name_ext = format_ident!("{}_ext", name); let build_name = format_ident!("build_{}", name); let input_type = &m.input_type; let output_type = &m.output_type; @@ -186,20 +205,12 @@ impl prost_build::ServiceGenerator for ServiceGenerator { self.request(builder).await } }); - // client_trait_methods.push(quote! { - // async fn #name_ext(&self, req: twirp::reqwest::RequestBuilder) -> Result<#output_type, twirp::ClientError>; - // }); client_trait_methods.push(quote! { - fn #build_name(&self, req: #input_type) -> Result; + fn #build_name(&self, req: #input_type) -> Result, twirp::ClientError>; }); - // client_methods.push(quote! { - // async fn #name_ext(&self, req: twirp::reqwest::RequestBuilder) -> Result<#output_type, twirp::ClientError> { - // self.request(req).await - // } - // }); client_methods.push(quote! { - fn #build_name(&self, req: #input_type) -> Result { + fn #build_name(&self, req: #input_type) -> Result, twirp::ClientError> { self.build_request(#request_path, req) } }); diff --git a/crates/twirp/src/client.rs b/crates/twirp/src/client.rs index cfc84e8..9a839b7 100644 --- a/crates/twirp/src/client.rs +++ b/crates/twirp/src/client.rs @@ -2,8 +2,9 @@ use std::sync::Arc; use std::vec; use async_trait::async_trait; +use http::{HeaderName, HeaderValue}; use reqwest::header::{InvalidHeaderValue, CONTENT_TYPE}; -use reqwest::{RequestBuilder, StatusCode}; +use reqwest::StatusCode; use thiserror::Error; use url::Url; @@ -145,6 +146,8 @@ impl Client { &self.inner.base_url } + // TODO: Move this to the `ClientBuilder` + // /// Creates a new `twirp::Client` with the same configuration as the current /// one, but with a different host in the base URL. pub fn with_host(&self, host: &str) -> Self { @@ -155,9 +158,10 @@ impl Client { } } - pub fn build_request(&self, path: &str, body: I) -> Result + pub fn build_request(&self, path: &str, body: I) -> Result> where I: prost::Message, + O: prost::Message + Default, { let mut url = self.inner.base_url.join(path)?; if let Some(host) = &self.host { @@ -169,13 +173,14 @@ impl Client { .post(url) .header(CONTENT_TYPE, CONTENT_TYPE_PROTOBUF) .body(serialize_proto_message(body)); - Ok(req) + Ok(RequestBuilder::new(req)) } /// Make an HTTP twirp request. // pub async fn request(&self, ctx: Context, path: &str, body: I) -> Result - pub async fn make_request(&self, builder: RequestBuilder) -> Result + pub async fn make_request(&self, builder: RequestBuilder) -> Result where + I: prost::Message, O: prost::Message + Default, { let req = builder.build()?; @@ -214,6 +219,39 @@ impl Client { } } +pub struct RequestBuilder { + inner: reqwest::RequestBuilder, + host: Option, + _input: std::marker::PhantomData, + _output: std::marker::PhantomData, +} + +impl RequestBuilder { + pub fn new(inner: reqwest::RequestBuilder) -> Self { + Self { + inner, + host: None, + _input: std::marker::PhantomData, + _output: std::marker::PhantomData, + } + } + + pub fn header(self, key: K, value: V) -> RequestBuilder + where + HeaderName: TryFrom, + >::Error: Into, + HeaderValue: TryFrom, + >::Error: Into, + { + RequestBuilder::new(self.inner.header(key, value)) + } + + /// Builds the request. + pub fn build(self) -> Result { + self.inner.build() + } +} + // This concept of reqwest middleware is taken pretty much directly from: // https://github.com/TrueLayer/reqwest-middleware, but simplified for the // specific needs of this twirp client. diff --git a/crates/twirp/src/lib.rs b/crates/twirp/src/lib.rs index 5b66b2b..6cbbb52 100644 --- a/crates/twirp/src/lib.rs +++ b/crates/twirp/src/lib.rs @@ -10,7 +10,7 @@ pub mod test; #[doc(hidden)] pub mod details; -pub use client::{Client, ClientBuilder, ClientError, Middleware, Next, Result}; +pub use client::{Client, ClientBuilder, ClientError, Middleware, Next, RequestBuilder, Result}; pub use context::Context; pub use error::*; // many constructors like `invalid_argument()` pub use http::Extensions; diff --git a/example/src/bin/client.rs b/example/src/bin/client.rs index 28ea4a3..977e252 100644 --- a/example/src/bin/client.rs +++ b/example/src/bin/client.rs @@ -1,6 +1,6 @@ use twirp::async_trait::async_trait; use twirp::client::{Client, ClientBuilder, Middleware, Next}; -use twirp::reqwest::{self, Request, Response}; +use twirp::reqwest::{Request, Response}; use twirp::url::Url; use twirp::GenericError; @@ -44,7 +44,7 @@ pub async fn main() -> Result<(), GenericError> { .build_make_hat(MakeHatRequest { inches: 1 })? .header("x-custom-header", "a"); // Make a request with context - let resp: MakeHatResponse = client.request(req).await?; + let resp = client.request(req).await?; eprintln!("{:?}", resp); Ok(()) @@ -89,22 +89,32 @@ impl HaberdasherApiClient for MockHaberdasherApiClient { fn build_make_hat( &self, _req: MakeHatRequest, - ) -> Result { + ) -> Result, twirp::ClientError> { todo!() } fn build_get_status( &self, _req: GetStatusRequest, - ) -> Result { + ) -> Result, twirp::ClientError> + { todo!() } - async fn request( + async fn request( &self, - _req: reqwest::RequestBuilder, + _req: twirp::RequestBuilder, ) -> Result where + I: prost::Message, + O: prost::Message + Default, + { + todo!() + } + + fn build(&self, _req: I) -> Result, twirp::ClientError> + where + I: prost::Message, O: prost::Message + Default, { todo!() From 7ea433d683ac3f64886893698d6e6d9c06a72759 Mon Sep 17 00:00:00 2001 From: Timothy Clem Date: Thu, 12 Jun 2025 13:44:56 -0700 Subject: [PATCH 3/8] Clean up --- crates/twirp-build/src/lib.rs | 39 +++++++---------------------------- crates/twirp/src/client.rs | 5 +---- crates/twirp/src/test.rs | 2 +- example/src/bin/client.rs | 24 +++++++-------------- 4 files changed, 17 insertions(+), 53 deletions(-) diff --git a/crates/twirp-build/src/lib.rs b/crates/twirp-build/src/lib.rs index a1ab865..0d69e86 100644 --- a/crates/twirp-build/src/lib.rs +++ b/crates/twirp-build/src/lib.rs @@ -164,34 +164,6 @@ impl prost_build::ServiceGenerator for ServiceGenerator { let client_name = service.client_name; let mut client_trait_methods = Vec::with_capacity(service.methods.len()); let mut client_methods = Vec::with_capacity(service.methods.len()); - client_trait_methods.push(quote! { - async fn request(&self, req: twirp::RequestBuilder) -> Result - where - I: prost::Message, - O: prost::Message + Default; - }); - client_trait_methods.push(quote! { - fn build(&self, req: I) -> Result, twirp::ClientError> - where - I: prost::Message, - O: prost::Message + Default; - }); - client_methods.push(quote! { - async fn request(&self, req: twirp::RequestBuilder) -> Result - where - I: prost::Message, - O: prost::Message + Default { - self.make_request(req).await - } - }); - client_methods.push(quote! { - fn build(&self, req: I) -> Result, twirp::ClientError> - where - I: prost::Message, - O: prost::Message + Default { - todo!() - } - }); for m in &service.methods { let name = &m.name; let build_name = format_ident!("build_{}", name); @@ -200,10 +172,7 @@ impl prost_build::ServiceGenerator for ServiceGenerator { let request_path = format!("{}/{}", service.fqn, m.proto_name); client_trait_methods.push(quote! { - async fn #name(&self, req: #input_type) -> Result<#output_type, twirp::ClientError> { - let builder = self.#build_name(req)?; - self.request(builder).await - } + async fn #name(&self, req: #input_type) -> Result<#output_type, twirp::ClientError>; }); client_trait_methods.push(quote! { fn #build_name(&self, req: #input_type) -> Result, twirp::ClientError>; @@ -214,6 +183,12 @@ impl prost_build::ServiceGenerator for ServiceGenerator { self.build_request(#request_path, req) } }); + client_methods.push(quote! { + async fn #name(&self, req: #input_type) -> Result<#output_type, twirp::ClientError> { + let builder = self.#build_name(req)?; + self.request(builder).await + } + }); } let client_trait = quote! { #[twirp::async_trait::async_trait] diff --git a/crates/twirp/src/client.rs b/crates/twirp/src/client.rs index 9a839b7..38c2e73 100644 --- a/crates/twirp/src/client.rs +++ b/crates/twirp/src/client.rs @@ -177,8 +177,7 @@ impl Client { } /// Make an HTTP twirp request. - // pub async fn request(&self, ctx: Context, path: &str, body: I) -> Result - pub async fn make_request(&self, builder: RequestBuilder) -> Result + pub async fn request(&self, builder: RequestBuilder) -> Result where I: prost::Message, O: prost::Message + Default, @@ -221,7 +220,6 @@ impl Client { pub struct RequestBuilder { inner: reqwest::RequestBuilder, - host: Option, _input: std::marker::PhantomData, _output: std::marker::PhantomData, } @@ -230,7 +228,6 @@ impl RequestBuilder { pub fn new(inner: reqwest::RequestBuilder) -> Self { Self { inner, - host: None, _input: std::marker::PhantomData, _output: std::marker::PhantomData, } diff --git a/crates/twirp/src/test.rs b/crates/twirp/src/test.rs index 6178e91..4a8ecd5 100644 --- a/crates/twirp/src/test.rs +++ b/crates/twirp/src/test.rs @@ -122,7 +122,7 @@ pub trait TestApiClient { impl TestApiClient for Client { async fn ping(&self, req: PingRequest) -> Result { let req = self.build_request("test.TestAPI/Ping", req)?; - self.make_request(req).await + self.request(req).await } async fn boom(&self, _req: PingRequest) -> Result { diff --git a/example/src/bin/client.rs b/example/src/bin/client.rs index 977e252..cff9dd4 100644 --- a/example/src/bin/client.rs +++ b/example/src/bin/client.rs @@ -80,6 +80,8 @@ impl Middleware for PrintResponseHeaders { // NOTE: This is just to demonstrate manually implementing the client trait. You don't need to do this as this code will // be generated for you by twirp-build. +// +// This is here so that we can visualize changes to the generated client code #[allow(dead_code)] #[derive(Debug)] struct MockHaberdasherApiClient; @@ -92,6 +94,9 @@ impl HaberdasherApiClient for MockHaberdasherApiClient { ) -> Result, twirp::ClientError> { todo!() } + async fn make_hat(&self, _req: MakeHatRequest) -> Result { + todo!() + } fn build_get_status( &self, @@ -100,23 +105,10 @@ impl HaberdasherApiClient for MockHaberdasherApiClient { { todo!() } - - async fn request( + async fn get_status( &self, - _req: twirp::RequestBuilder, - ) -> Result - where - I: prost::Message, - O: prost::Message + Default, - { - todo!() - } - - fn build(&self, _req: I) -> Result, twirp::ClientError> - where - I: prost::Message, - O: prost::Message + Default, - { + _req: GetStatusRequest, + ) -> Result { todo!() } } From 05e4f6ad11bf4d89254a16fd4ee6e7ec8a49b9d1 Mon Sep 17 00:00:00 2001 From: Timothy Clem Date: Thu, 12 Jun 2025 13:47:28 -0700 Subject: [PATCH 4/8] Minor cleanup --- Cargo.lock | 2 -- crates/twirp-build/Cargo.toml | 2 -- 2 files changed, 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index df694cf..8f7a016 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1291,10 +1291,8 @@ name = "twirp-build" version = "0.8.0" dependencies = [ "prettyplease", - "proc-macro2", "prost-build", "quote", - "reqwest", "syn", ] diff --git a/crates/twirp-build/Cargo.toml b/crates/twirp-build/Cargo.toml index 67ebe7a..900843e 100644 --- a/crates/twirp-build/Cargo.toml +++ b/crates/twirp-build/Cargo.toml @@ -16,7 +16,5 @@ license-file = "./LICENSE" [dependencies] prost-build = "0.13" prettyplease = { version = "0.2" } -reqwest = { version = "0.12", default-features = false } quote = "1.0" syn = "2.0" -proc-macro2 = "1.0" From 49a738d839ca93abef50f4477aa04a9548666c6c Mon Sep 17 00:00:00 2001 From: Timothy Clem Date: Thu, 12 Jun 2025 20:02:26 -0700 Subject: [PATCH 5/8] second attempt --- crates/twirp-build/src/lib.rs | 11 +++--- crates/twirp/src/client.rs | 67 +++++++++++++++++++---------------- crates/twirp/src/test.rs | 3 +- example/src/bin/client.rs | 15 ++++---- 4 files changed, 50 insertions(+), 46 deletions(-) diff --git a/crates/twirp-build/src/lib.rs b/crates/twirp-build/src/lib.rs index 0d69e86..4506ac9 100644 --- a/crates/twirp-build/src/lib.rs +++ b/crates/twirp-build/src/lib.rs @@ -166,7 +166,7 @@ impl prost_build::ServiceGenerator for ServiceGenerator { let mut client_methods = Vec::with_capacity(service.methods.len()); for m in &service.methods { let name = &m.name; - let build_name = format_ident!("build_{}", name); + let name_request = format_ident!("{}_request", name); let input_type = &m.input_type; let output_type = &m.output_type; let request_path = format!("{}/{}", service.fqn, m.proto_name); @@ -175,18 +175,17 @@ impl prost_build::ServiceGenerator for ServiceGenerator { async fn #name(&self, req: #input_type) -> Result<#output_type, twirp::ClientError>; }); client_trait_methods.push(quote! { - fn #build_name(&self, req: #input_type) -> Result, twirp::ClientError>; + fn #name_request(&self, req: #input_type) -> Result, twirp::ClientError>; }); client_methods.push(quote! { - fn #build_name(&self, req: #input_type) -> Result, twirp::ClientError> { - self.build_request(#request_path, req) + fn #name_request(&self, req: #input_type) -> Result, twirp::ClientError> { + self.request(#request_path, req) } }); client_methods.push(quote! { async fn #name(&self, req: #input_type) -> Result<#output_type, twirp::ClientError> { - let builder = self.#build_name(req)?; - self.request(builder).await + self.#name_request(req)?.send().await } }); } diff --git a/crates/twirp/src/client.rs b/crates/twirp/src/client.rs index 38c2e73..e46bbce 100644 --- a/crates/twirp/src/client.rs +++ b/crates/twirp/src/client.rs @@ -158,31 +158,10 @@ impl Client { } } - pub fn build_request(&self, path: &str, body: I) -> Result> + pub(super) async fn execute(&self, req: reqwest::Request) -> Result where - I: prost::Message, O: prost::Message + Default, { - let mut url = self.inner.base_url.join(path)?; - if let Some(host) = &self.host { - url.set_host(Some(host))? - }; - - let req = self - .http_client - .post(url) - .header(CONTENT_TYPE, CONTENT_TYPE_PROTOBUF) - .body(serialize_proto_message(body)); - Ok(RequestBuilder::new(req)) - } - - /// Make an HTTP twirp request. - pub async fn request(&self, builder: RequestBuilder) -> Result - where - I: prost::Message, - O: prost::Message + Default, - { - let req = builder.build()?; let path = req.url().path().to_string(); // Create and execute the middleware handlers @@ -216,36 +195,64 @@ impl Client { }), } } + + // Start building a request... + pub fn request(&self, path: &str, body: I) -> Result> + where + I: prost::Message, + O: prost::Message + Default, + { + let mut url = self.inner.base_url.join(path)?; + if let Some(host) = &self.host { + url.set_host(Some(host))? + }; + + let req = self + .http_client + .post(url) + .header(CONTENT_TYPE, CONTENT_TYPE_PROTOBUF) + .body(serialize_proto_message(body)); + Ok(RequestBuilder::new(self.clone(), req)) + } } -pub struct RequestBuilder { +pub struct RequestBuilder +where + O: prost::Message + Default, +{ + client: Client, inner: reqwest::RequestBuilder, _input: std::marker::PhantomData, _output: std::marker::PhantomData, } -impl RequestBuilder { - pub fn new(inner: reqwest::RequestBuilder) -> Self { +impl RequestBuilder +where + O: prost::Message + Default, +{ + pub fn new(client: Client, inner: reqwest::RequestBuilder) -> Self { Self { + client, inner, _input: std::marker::PhantomData, _output: std::marker::PhantomData, } } - pub fn header(self, key: K, value: V) -> RequestBuilder + pub fn header(mut self, key: K, value: V) -> RequestBuilder where HeaderName: TryFrom, >::Error: Into, HeaderValue: TryFrom, >::Error: Into, { - RequestBuilder::new(self.inner.header(key, value)) + self.inner = self.inner.header(key, value); + self } - /// Builds the request. - pub fn build(self) -> Result { - self.inner.build() + pub async fn send(self) -> Result { + let req = self.inner.build()?; + self.client.execute(req).await } } diff --git a/crates/twirp/src/test.rs b/crates/twirp/src/test.rs index 4a8ecd5..7489a76 100644 --- a/crates/twirp/src/test.rs +++ b/crates/twirp/src/test.rs @@ -121,8 +121,7 @@ pub trait TestApiClient { #[async_trait] impl TestApiClient for Client { async fn ping(&self, req: PingRequest) -> Result { - let req = self.build_request("test.TestAPI/Ping", req)?; - self.request(req).await + self.request("test.TestAPI/Ping", req)?.send().await } async fn boom(&self, _req: PingRequest) -> Result { diff --git a/example/src/bin/client.rs b/example/src/bin/client.rs index cff9dd4..a959fd8 100644 --- a/example/src/bin/client.rs +++ b/example/src/bin/client.rs @@ -38,13 +38,12 @@ pub async fn main() -> Result<(), GenericError> { .await; eprintln!("{:?}", resp); - // TODO: Figure out where `with_host` goes in all this... - let req = client + let resp = client .with_host("localhost") - .build_make_hat(MakeHatRequest { inches: 1 })? - .header("x-custom-header", "a"); - // Make a request with context - let resp = client.request(req).await?; + .make_hat_request(MakeHatRequest { inches: 1 })? + .header("x-custom-header", "a") + .send() + .await?; eprintln!("{:?}", resp); Ok(()) @@ -88,7 +87,7 @@ struct MockHaberdasherApiClient; #[async_trait] impl HaberdasherApiClient for MockHaberdasherApiClient { - fn build_make_hat( + fn make_hat_request( &self, _req: MakeHatRequest, ) -> Result, twirp::ClientError> { @@ -98,7 +97,7 @@ impl HaberdasherApiClient for MockHaberdasherApiClient { todo!() } - fn build_get_status( + fn get_status_request( &self, _req: GetStatusRequest, ) -> Result, twirp::ClientError> From 3ecd726dfba718a8e2995bffaaa43fe2d228f848 Mon Sep 17 00:00:00 2001 From: Timothy Clem Date: Fri, 13 Jun 2025 10:11:27 -0700 Subject: [PATCH 6/8] Default impl --- crates/twirp-build/src/lib.rs | 9 +++------ example/src/bin/client.rs | 2 ++ 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/crates/twirp-build/src/lib.rs b/crates/twirp-build/src/lib.rs index 4506ac9..78216aa 100644 --- a/crates/twirp-build/src/lib.rs +++ b/crates/twirp-build/src/lib.rs @@ -172,7 +172,9 @@ impl prost_build::ServiceGenerator for ServiceGenerator { let request_path = format!("{}/{}", service.fqn, m.proto_name); client_trait_methods.push(quote! { - async fn #name(&self, req: #input_type) -> Result<#output_type, twirp::ClientError>; + async fn #name(&self, req: #input_type) -> Result<#output_type, twirp::ClientError> { + self.#name_request(req)?.send().await + } }); client_trait_methods.push(quote! { fn #name_request(&self, req: #input_type) -> Result, twirp::ClientError>; @@ -183,11 +185,6 @@ impl prost_build::ServiceGenerator for ServiceGenerator { self.request(#request_path, req) } }); - client_methods.push(quote! { - async fn #name(&self, req: #input_type) -> Result<#output_type, twirp::ClientError> { - self.#name_request(req)?.send().await - } - }); } let client_trait = quote! { #[twirp::async_trait::async_trait] diff --git a/example/src/bin/client.rs b/example/src/bin/client.rs index a959fd8..51b132a 100644 --- a/example/src/bin/client.rs +++ b/example/src/bin/client.rs @@ -93,6 +93,7 @@ impl HaberdasherApiClient for MockHaberdasherApiClient { ) -> Result, twirp::ClientError> { todo!() } + // implementing this one is optional async fn make_hat(&self, _req: MakeHatRequest) -> Result { todo!() } @@ -104,6 +105,7 @@ impl HaberdasherApiClient for MockHaberdasherApiClient { { todo!() } + // implementing this one is optional async fn get_status( &self, _req: GetStatusRequest, From c71285a83bea198fbdb3c9beeaa6083d710810bf Mon Sep 17 00:00:00 2001 From: Timothy Clem Date: Fri, 13 Jun 2025 11:01:27 -0700 Subject: [PATCH 7/8] Some docs --- crates/twirp/src/client.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/twirp/src/client.rs b/crates/twirp/src/client.rs index e46bbce..c6875a7 100644 --- a/crates/twirp/src/client.rs +++ b/crates/twirp/src/client.rs @@ -146,8 +146,6 @@ impl Client { &self.inner.base_url } - // TODO: Move this to the `ClientBuilder` - // /// Creates a new `twirp::Client` with the same configuration as the current /// one, but with a different host in the base URL. pub fn with_host(&self, host: &str) -> Self { @@ -158,6 +156,7 @@ impl Client { } } + /// Executes a `Request`. pub(super) async fn execute(&self, req: reqwest::Request) -> Result where O: prost::Message + Default, @@ -239,6 +238,7 @@ where } } + /// Add a `Header` to this Request. pub fn header(mut self, key: K, value: V) -> RequestBuilder where HeaderName: TryFrom, From f3b0e716e13618650f2a1c64697dde2a881b46f3 Mon Sep 17 00:00:00 2001 From: Timothy Clem Date: Fri, 13 Jun 2025 15:16:21 -0700 Subject: [PATCH 8/8] Docs --- crates/twirp/src/client.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/crates/twirp/src/client.rs b/crates/twirp/src/client.rs index c6875a7..c38176b 100644 --- a/crates/twirp/src/client.rs +++ b/crates/twirp/src/client.rs @@ -195,7 +195,9 @@ impl Client { } } - // Start building a request... + /// Start building a `Request` with a path and a request body. + /// + /// Returns a `RequestBuilder`, which will allow setting headers before sending. pub fn request(&self, path: &str, body: I) -> Result> where I: prost::Message, pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy