Skip to content

Commit 270e71b

Browse files
authored
Merge pull request #185 from blinsay/syn-quote-etc
Use syn/quote for codegen
2 parents 204ea4e + 1e4be22 commit 270e71b

File tree

7 files changed

+237
-107
lines changed

7 files changed

+237
-107
lines changed

Cargo.lock

Lines changed: 5 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/twirp-build/Cargo.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,3 +16,7 @@ license-file = "./LICENSE"
1616

1717
[dependencies]
1818
prost-build = "0.13"
19+
prettyplease = { version = "0.2" }
20+
quote = "1.0"
21+
syn = "2.0"
22+
proc-macro2 = "1.0"

crates/twirp-build/src/lib.rs

Lines changed: 185 additions & 103 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use std::fmt::Write;
1+
use quote::{format_ident, quote};
22

33
/// Generates twirp services for protobuf rpc service definitions.
44
///
@@ -11,123 +11,205 @@ pub fn service_generator() -> Box<ServiceGenerator> {
1111
Box::new(ServiceGenerator {})
1212
}
1313

14+
struct Service {
15+
/// The name of the server trait, as parsed into a Rust identifier.
16+
server_name: syn::Ident,
17+
18+
/// The name of the client trait, as parsed into a Rust identifier.
19+
client_name: syn::Ident,
20+
21+
/// The fully qualified protobuf name of this Service.
22+
fqn: String,
23+
24+
/// The methods that make up this service.
25+
methods: Vec<Method>,
26+
}
27+
28+
struct Method {
29+
/// The name of the method, as parsed into a Rust identifier.
30+
name: syn::Ident,
31+
32+
/// The name of the method as it appears in the protobuf definition.
33+
proto_name: String,
34+
35+
/// The input type of this method.
36+
input_type: syn::Type,
37+
38+
/// The output type of this method.
39+
output_type: syn::Type,
40+
}
41+
42+
impl Service {
43+
fn from_prost(s: prost_build::Service) -> Self {
44+
let fqn = format!("{}.{}", s.package, s.proto_name);
45+
let server_name = format_ident!("{}", &s.name);
46+
let client_name = format_ident!("{}Client", &s.name);
47+
let methods = s
48+
.methods
49+
.into_iter()
50+
.map(|m| Method::from_prost(&s.package, &s.proto_name, m))
51+
.collect();
52+
53+
Self {
54+
server_name,
55+
client_name,
56+
fqn,
57+
methods,
58+
}
59+
}
60+
}
61+
62+
impl Method {
63+
fn from_prost(pkg_name: &str, svc_name: &str, m: prost_build::Method) -> Self {
64+
let as_type = |s| -> syn::Type {
65+
let Ok(typ) = syn::parse_str::<syn::Type>(s) else {
66+
panic!(
67+
"twirp-build failed generated invalid Rust while processing {pkg}.{svc}/{name}). this is a bug in twirp-build, please file a GitHub issue",
68+
pkg = pkg_name,
69+
svc = svc_name,
70+
name = m.proto_name,
71+
);
72+
};
73+
typ
74+
};
75+
76+
let input_type = as_type(&m.input_type);
77+
let output_type = as_type(&m.output_type);
78+
let name = format_ident!("{}", m.name);
79+
let message = m.proto_name;
80+
81+
Self {
82+
name,
83+
proto_name: message,
84+
input_type,
85+
output_type,
86+
}
87+
}
88+
}
89+
1490
pub struct ServiceGenerator;
1591

1692
impl prost_build::ServiceGenerator for ServiceGenerator {
1793
fn generate(&mut self, service: prost_build::Service, buf: &mut String) {
18-
let service_name = service.name;
19-
let service_fqn = format!("{}.{}", service.package, service.proto_name);
20-
writeln!(buf).unwrap();
94+
let service = Service::from_prost(service);
2195

22-
writeln!(buf, "pub use twirp;").unwrap();
23-
writeln!(buf).unwrap();
24-
writeln!(buf, "pub const SERVICE_FQN: &str = \"/{service_fqn}\";").unwrap();
25-
26-
//
2796
// generate the twirp server
28-
//
29-
writeln!(buf, "#[twirp::async_trait::async_trait]").unwrap();
30-
writeln!(buf, "pub trait {} {{", service_name).unwrap();
31-
writeln!(buf, " type Error;").unwrap();
32-
for m in &service.methods {
33-
writeln!(
34-
buf,
35-
" async fn {}(&self, ctx: twirp::Context, req: {}) -> Result<{}, Self::Error>;",
36-
m.name, m.input_type, m.output_type,
37-
)
38-
.unwrap();
39-
}
40-
writeln!(buf, "}}").unwrap();
41-
42-
writeln!(buf, "#[twirp::async_trait::async_trait]").unwrap();
43-
writeln!(buf, "impl<T> {service_name} for std::sync::Arc<T>").unwrap();
44-
writeln!(buf, "where").unwrap();
45-
writeln!(buf, " T: {service_name} + Sync + Send").unwrap();
46-
writeln!(buf, "{{").unwrap();
47-
writeln!(buf, " type Error = T::Error;\n").unwrap();
97+
let mut trait_methods = Vec::with_capacity(service.methods.len());
98+
let mut proxy_methods = Vec::with_capacity(service.methods.len());
4899
for m in &service.methods {
49-
writeln!(
50-
buf,
51-
" async fn {}(&self, ctx: twirp::Context, req: {}) -> Result<{}, Self::Error> {{",
52-
m.name, m.input_type, m.output_type,
53-
)
54-
.unwrap();
55-
writeln!(buf, " T::{}(&*self, ctx, req).await", m.name).unwrap();
56-
writeln!(buf, " }}").unwrap();
100+
let name = &m.name;
101+
let input_type = &m.input_type;
102+
let output_type = &m.output_type;
103+
104+
trait_methods.push(quote! {
105+
async fn #name(&self, ctx: twirp::Context, req: #input_type) -> Result<#output_type, Self::Error>;
106+
});
107+
108+
proxy_methods.push(quote! {
109+
async fn #name(&self, ctx: twirp::Context, req: #input_type) -> Result<#output_type, Self::Error> {
110+
T::#name(&*self, ctx, req).await
111+
}
112+
});
57113
}
58-
writeln!(buf, "}}").unwrap();
59-
60-
// add_service
61-
writeln!(
62-
buf,
63-
r#"pub fn router<T>(api: T) -> twirp::Router
64-
where
65-
T: {service_name} + Clone + Send + Sync + 'static,
66-
<T as {service_name}>::Error: twirp::IntoTwirpResponse,
67-
{{
68-
twirp::details::TwirpRouterBuilder::new(api)"#,
69-
)
70-
.unwrap();
114+
115+
let server_name = &service.server_name;
116+
let server_trait = quote! {
117+
#[twirp::async_trait::async_trait]
118+
pub trait #server_name {
119+
type Error;
120+
121+
#(#trait_methods)*
122+
}
123+
124+
#[twirp::async_trait::async_trait]
125+
impl<T> #server_name for std::sync::Arc<T>
126+
where
127+
T: #server_name + Sync + Send
128+
{
129+
type Error = T::Error;
130+
131+
#(#proxy_methods)*
132+
}
133+
};
134+
135+
// generate the router
136+
let mut route_calls = Vec::with_capacity(service.methods.len());
71137
for m in &service.methods {
72-
let uri = &m.proto_name;
73-
let req_type = &m.input_type;
74-
let rust_method_name = &m.name;
75-
writeln!(
76-
buf,
77-
r#" .route("/{uri}", |api: T, ctx: twirp::Context, req: {req_type}| async move {{
78-
api.{rust_method_name}(ctx, req).await
79-
}})"#,
80-
)
81-
.unwrap();
138+
let name = &m.name;
139+
let input_type = &m.input_type;
140+
let path = format!("/{uri}", uri = m.proto_name);
141+
142+
route_calls.push(quote! {
143+
.route(#path, |api: T, ctx: twirp::Context, req: #input_type| async move {
144+
api.#name(ctx, req).await
145+
})
146+
});
82147
}
83-
writeln!(
84-
buf,
85-
r#"
86-
.build()
87-
}}"#
88-
)
89-
.unwrap();
148+
let router = quote! {
149+
pub fn router<T>(api: T) -> twirp::Router
150+
where
151+
T: #server_name + Clone + Send + Sync + 'static,
152+
<T as #server_name>::Error: twirp::IntoTwirpResponse
153+
{
154+
twirp::details::TwirpRouterBuilder::new(api)
155+
#(#route_calls)*
156+
.build()
157+
}
158+
};
90159

91160
//
92161
// generate the twirp client
93162
//
94-
writeln!(buf).unwrap();
95-
writeln!(buf, "#[twirp::async_trait::async_trait]").unwrap();
96-
writeln!(buf, "pub trait {service_name}Client: Send + Sync {{",).unwrap();
97-
for m in &service.methods {
98-
// Define: <METHOD>
99-
writeln!(
100-
buf,
101-
" async fn {}(&self, req: {}) -> Result<{}, twirp::ClientError>;",
102-
m.name, m.input_type, m.output_type,
103-
)
104-
.unwrap();
105-
}
106-
writeln!(buf, "}}").unwrap();
107-
108-
// Implement the rpc traits for: `twirp::client::Client`
109-
writeln!(buf, "#[twirp::async_trait::async_trait]").unwrap();
110-
writeln!(
111-
buf,
112-
"impl {service_name}Client for twirp::client::Client {{",
113-
)
114-
.unwrap();
163+
164+
let client_name = service.client_name;
165+
let mut client_trait_methods = Vec::with_capacity(service.methods.len());
166+
let mut client_methods = Vec::with_capacity(service.methods.len());
115167
for m in &service.methods {
116-
// Define the rpc `<METHOD>`
117-
writeln!(
118-
buf,
119-
" async fn {}(&self, req: {}) -> Result<{}, twirp::ClientError> {{",
120-
m.name, m.input_type, m.output_type,
121-
)
122-
.unwrap();
123-
writeln!(
124-
buf,
125-
r#" self.request("{}/{}", req).await"#,
126-
service_fqn, m.proto_name
127-
)
128-
.unwrap();
129-
writeln!(buf, " }}").unwrap();
168+
let name = &m.name;
169+
let input_type = &m.input_type;
170+
let output_type = &m.output_type;
171+
let request_path = format!("{}/{}", service.fqn, m.proto_name);
172+
173+
client_trait_methods.push(quote! {
174+
async fn #name(&self, req: #input_type) -> Result<#output_type, twirp::ClientError>;
175+
});
176+
177+
client_methods.push(quote! {
178+
async fn #name(&self, req: #input_type) -> Result<#output_type, twirp::ClientError> {
179+
self.request(#request_path, req).await
180+
}
181+
})
130182
}
131-
writeln!(buf, "}}").unwrap();
183+
let client_trait = quote! {
184+
#[twirp::async_trait::async_trait]
185+
pub trait #client_name: Send + Sync {
186+
#(#client_trait_methods)*
187+
}
188+
189+
#[twirp::async_trait::async_trait]
190+
impl #client_name for twirp::client::Client {
191+
#(#client_methods)*
192+
}
193+
};
194+
195+
// generate the service and client as a single file. run it through
196+
// prettyplease before outputting it.
197+
let service_fqn_path = format!("/{}", service.fqn);
198+
let generated = quote! {
199+
pub use twirp;
200+
201+
pub const SERVICE_FQN: &str = #service_fqn_path;
202+
203+
#server_trait
204+
205+
#router
206+
207+
#client_trait
208+
};
209+
210+
let ast: syn::File = syn::parse2(generated)
211+
.expect("twirp-build generated invalid Rust. this is a bug in twirp-build, please file an issue");
212+
let code = prettyplease::unparse(&ast);
213+
buf.push_str(&code);
132214
}
133215
}

example/proto/haberdash/v1/haberdash_api.proto

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ option go_package = "haberdash.v1";
99
service HaberdasherAPI {
1010
// MakeHat produces a hat of mysterious, randomly-selected color!
1111
rpc MakeHat(MakeHatRequest) returns (MakeHatResponse);
12+
rpc GetStatus(GetStatusRequest) returns (GetStatusResponse);
1213
}
1314

1415
// Size is passed when requesting a new hat to be made. It's always
@@ -32,3 +33,9 @@ message MakeHatResponse {
3233
// Demonstrate importing an external message.
3334
google.protobuf.Timestamp timestamp = 4;
3435
}
36+
37+
message GetStatusRequest {}
38+
39+
message GetStatusResponse {
40+
string status = 1;
41+
}

example/src/bin/advanced-server.rs

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,9 @@ pub mod service {
1717
}
1818
}
1919
}
20-
use service::haberdash::v1::{self as haberdash, MakeHatRequest, MakeHatResponse};
20+
use service::haberdash::v1::{
21+
self as haberdash, GetStatusRequest, GetStatusResponse, MakeHatRequest, MakeHatResponse,
22+
};
2123

2224
async fn ping() -> &'static str {
2325
"Pong\n"
@@ -95,6 +97,16 @@ impl haberdash::HaberdasherApi for HaberdasherApiServer {
9597
}),
9698
})
9799
}
100+
101+
async fn get_status(
102+
&self,
103+
_ctx: Context,
104+
_req: GetStatusRequest,
105+
) -> Result<GetStatusResponse, HatError> {
106+
Ok(GetStatusResponse {
107+
status: "making hats".to_string(),
108+
})
109+
}
98110
}
99111

100112
// Demonstrate sending back custom extensions from the handlers.

0 commit comments

Comments
 (0)
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