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)