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,