Skip to content

Commit b944a5f

Browse files
authored
Generate service trait from proto service definition (#2812)
1 parent 3a84944 commit b944a5f

File tree

10 files changed

+617
-1
lines changed

10 files changed

+617
-1
lines changed

quickwit/Cargo.lock

Lines changed: 21 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

quickwit/Cargo.toml

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ members = [
55
"quickwit-aws",
66
"quickwit-cli",
77
"quickwit-cluster",
8+
"quickwit-codegen",
89
"quickwit-common",
910
"quickwit-config",
1011
"quickwit-control-plane",
@@ -99,6 +100,8 @@ opentelemetry-otlp = "0.11.0"
99100
pin-project-lite = "0.2.9"
100101
pnet = { version = "0.31.0", features = ["std"] }
101102
predicates = "2"
103+
prettyplease = "0.1.23"
104+
proc-macro2 = "1.0.50"
102105
prometheus = { version = "0.13", features = ["process"] }
103106
proptest = "1"
104107
prost = { version = "0.11.6", default-features = false, features = [
@@ -107,7 +110,7 @@ prost = { version = "0.11.6", default-features = false, features = [
107110
prost-build = "0.11.6"
108111
prost-types = "0.11.6"
109112
pulsar = { git = "https://github.com/quickwit-oss/pulsar-rs.git", rev = "f9eff04", default-features = false, features = ["compression", "tokio-runtime", "auth-oauth2"] }
110-
quickwit_elastic_api_generation = { git = "https://github.com/quickwit-oss/elasticsearch-rs", rev = "c157b19" }
113+
quote = "1.0.23"
111114
rand = "0.8"
112115
rand_distr = "0.4"
113116
rayon = "1"
@@ -152,6 +155,7 @@ sqlx = { version = "0.6", features = [
152155
"migrate",
153156
"time",
154157
] }
158+
syn = "1.0.107"
155159
tabled = { version = "0.8", features = ["color"] }
156160
tempfile = "3"
157161
termcolor = "1"
@@ -195,6 +199,7 @@ quickwit-control-plane = { version = "0.4.0", path = "./quickwit-control-plane"
195199
quickwit-core = { version = "0.4.0", path = "./quickwit-core" }
196200
quickwit-directories = { version = "0.4.0", path = "./quickwit-directories" }
197201
quickwit-doc-mapper = { version = "0.4.0", path = "./quickwit-doc-mapper" }
202+
quickwit_elastic_api_generation = { git = "https://github.com/quickwit-oss/elasticsearch-rs", rev = "c157b19" }
198203
quickwit-grpc-clients = { version = "0.4.0", path = "./quickwit-grpc-clients" }
199204
quickwit-indexing = { version = "0.4.0", path = "./quickwit-indexing" }
200205
quickwit-ingest-api = { version = "0.4.0", path = "./quickwit-ingest-api" }

quickwit/quickwit-codegen/Cargo.toml

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
[package]
2+
name = "quickwit-codegen"
3+
version = "0.4.0"
4+
authors = ["Quickwit, Inc. <[email protected]>"]
5+
edition = "2021"
6+
license = "AGPL-3.0-or-later" # For a commercial, license, contact [email protected]
7+
description = "Generate traits, adapters, and gRPC clients/servers from proto files"
8+
repository = "https://github.com/quickwit-oss/quickwit"
9+
homepage = "https://quickwit.io/"
10+
documentation = "https://quickwit.io/docs/"
11+
12+
[dependencies]
13+
anyhow = { workspace = true }
14+
prettyplease = { workspace = true }
15+
proc-macro2 = { workspace = true }
16+
prost = { workspace = true }
17+
prost-build = { workspace = true }
18+
quote = { workspace = true }
19+
syn = { workspace = true }
20+
tonic = { workspace = true }
21+
tonic-build = { workspace = true }
22+
23+
[dev-dependencies]
24+
async-trait = { workspace = true }
25+
dyn-clone = { workspace = true }
26+
serde = { workspace = true }
27+
thiserror = { workspace = true }
28+
tokio = { workspace = true }
29+
utoipa = { workspace = true }
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
// Copyright (C) 2023 Quickwit, Inc.
2+
//
3+
// Quickwit is offered under the AGPL v3.0 and as commercial software.
4+
// For commercial licensing, contact us at [email protected].
5+
//
6+
// AGPL:
7+
// This program is free software: you can redistribute it and/or modify
8+
// it under the terms of the GNU Affero General Public License as
9+
// published by the Free Software Foundation, either version 3 of the
10+
// License, or (at your option) any later version.
11+
//
12+
// This program is distributed in the hope that it will be useful,
13+
// but WITHOUT ANY WARRANTY; without even the implied warranty of
14+
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15+
// GNU Affero General Public License for more details.
16+
//
17+
// You should have received a copy of the GNU Affero General Public License
18+
// along with this program. If not, see <http://www.gnu.org/licenses/>.
19+
20+
use std::path::Path;
21+
22+
use proc_macro2::TokenStream;
23+
use prost_build::{Method, Service, ServiceGenerator};
24+
use quote::{quote, ToTokens};
25+
use syn::Ident;
26+
27+
pub struct Codegen;
28+
29+
impl Codegen {
30+
pub fn run(proto: &Path, out_dir: &Path, result_path: &str) -> anyhow::Result<()> {
31+
let mut prost_config = prost_build::Config::default();
32+
prost_config
33+
.protoc_arg("--experimental_allow_proto3_optional")
34+
.type_attribute(".", "#[derive(Serialize, Deserialize, utoipa::ToSchema)]")
35+
.out_dir(out_dir);
36+
37+
let service_generator = Box::new(QuickwitServiceGenerator::new(result_path));
38+
39+
prost_config
40+
.service_generator(service_generator)
41+
.compile_protos(&[proto], &["protos"])?;
42+
Ok(())
43+
}
44+
}
45+
46+
struct QuickwitServiceGenerator {
47+
result_path: String,
48+
tonic_svc_generator: Box<dyn ServiceGenerator>,
49+
}
50+
51+
impl QuickwitServiceGenerator {
52+
fn new(result_path: &str) -> Self {
53+
Self {
54+
result_path: result_path.to_string(),
55+
tonic_svc_generator: tonic_build::configure().service_generator(),
56+
}
57+
}
58+
}
59+
60+
impl ServiceGenerator for QuickwitServiceGenerator {
61+
fn generate(&mut self, service: Service, buf: &mut String) {
62+
let tokens = generate_all(&service, &self.result_path);
63+
let ast: syn::File = syn::parse2(tokens).expect("Tokenstream should be a valid Syn AST.");
64+
let pretty_code = prettyplease::unparse(&ast);
65+
buf.push_str(&pretty_code);
66+
67+
self.tonic_svc_generator.generate(service, buf)
68+
}
69+
70+
fn finalize(&mut self, buf: &mut String) {
71+
self.tonic_svc_generator.finalize(buf);
72+
}
73+
}
74+
75+
fn generate_all(service: &Service, result_path: &str) -> TokenStream {
76+
let service_trait_name = quote::format_ident!("{}", service.name);
77+
let result_path = syn::parse_str::<syn::Path>(result_path)
78+
.expect("Result path should be a valid result path such as `crate::Result`.");
79+
80+
let service_trait = generate_service_trait(&service_trait_name, &service.methods, &result_path);
81+
82+
quote! {
83+
#service_trait
84+
}
85+
}
86+
87+
fn generate_service_trait(
88+
trait_name: &Ident,
89+
methods: &[Method],
90+
result_path: &syn::Path,
91+
) -> TokenStream {
92+
let trait_methods = generate_service_trait_methods(methods, result_path);
93+
94+
quote! {
95+
#[async_trait]
96+
pub trait #trait_name: std::fmt::Debug + dyn_clone::DynClone + Send + Sync + 'static {
97+
98+
#trait_methods
99+
}
100+
101+
dyn_clone::clone_trait_object!(#trait_name);
102+
}
103+
}
104+
105+
fn generate_service_trait_methods(methods: &[Method], result_path: &syn::Path) -> TokenStream {
106+
let mut stream = TokenStream::new();
107+
for method in methods {
108+
let method_name = quote::format_ident!("{}", method.name);
109+
let request_type = syn::parse_str::<syn::Path>(&method.input_type)
110+
.unwrap()
111+
.to_token_stream();
112+
let response_type = syn::parse_str::<syn::Path>(&method.output_type)
113+
.unwrap()
114+
.to_token_stream();
115+
116+
let method = quote! {
117+
async fn #method_name(&mut self, request: #request_type) -> #result_path<#response_type>;
118+
};
119+
stream.extend(method);
120+
}
121+
stream
122+
}

quickwit/quickwit-codegen/src/lib.rs

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
// Copyright (C) 2023 Quickwit, Inc.
2+
//
3+
// Quickwit is offered under the AGPL v3.0 and as commercial software.
4+
// For commercial licensing, contact us at [email protected].
5+
//
6+
// AGPL:
7+
// This program is free software: you can redistribute it and/or modify
8+
// it under the terms of the GNU Affero General Public License as
9+
// published by the Free Software Foundation, either version 3 of the
10+
// License, or (at your option) any later version.
11+
//
12+
// This program is distributed in the hope that it will be useful,
13+
// but WITHOUT ANY WARRANTY; without even the implied warranty of
14+
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15+
// GNU Affero General Public License for more details.
16+
//
17+
// You should have received a copy of the GNU Affero General Public License
18+
// along with this program. If not, see <http://www.gnu.org/licenses/>.
19+
20+
mod codegen;
21+
22+
pub use codegen::Codegen;
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
// Copyright (C) 2023 Quickwit, Inc.
2+
//
3+
// Quickwit is offered under the AGPL v3.0 and as commercial software.
4+
// For commercial licensing, contact us at [email protected].
5+
//
6+
// AGPL:
7+
// This program is free software: you can redistribute it and/or modify
8+
// it under the terms of the GNU Affero General Public License as
9+
// published by the Free Software Foundation, either version 3 of the
10+
// License, or (at your option) any later version.
11+
//
12+
// This program is distributed in the hope that it will be useful,
13+
// but WITHOUT ANY WARRANTY; without even the implied warranty of
14+
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15+
// GNU Affero General Public License for more details.
16+
//
17+
// You should have received a copy of the GNU Affero General Public License
18+
// along with this program. If not, see <http://www.gnu.org/licenses/>.
19+
20+
use std::path::Path;
21+
22+
use async_trait::async_trait;
23+
use quickwit_codegen::Codegen;
24+
25+
mod hello;
26+
27+
#[tokio::test]
28+
async fn test_hello_codegen() {
29+
let proto = Path::new("tests/hello/hello.proto");
30+
let out_dir = Path::new("tests/hello/");
31+
32+
Codegen::run(proto, out_dir, "crate::hello::HelloResult").unwrap();
33+
34+
use crate::hello::{Hello, HelloRequest, HelloResponse};
35+
36+
#[derive(Debug, Clone)]
37+
struct HelloImpl;
38+
39+
#[async_trait]
40+
impl Hello for HelloImpl {
41+
async fn hello(
42+
&mut self,
43+
request: HelloRequest,
44+
) -> crate::hello::HelloResult<HelloResponse> {
45+
Ok(HelloResponse {
46+
message: format!("Hello, {}!", request.name),
47+
})
48+
}
49+
}
50+
let mut hello = HelloImpl;
51+
assert_eq!(
52+
hello
53+
.hello(HelloRequest {
54+
name: "World".to_string()
55+
})
56+
.await
57+
.unwrap(),
58+
HelloResponse {
59+
message: "Hello, World!".to_string()
60+
}
61+
);
62+
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
// Copyright (C) 2023 Quickwit, Inc.
2+
//
3+
// Quickwit is offered under the AGPL v3.0 and as commercial software.
4+
// For commercial licensing, contact us at [email protected].
5+
//
6+
// AGPL:
7+
// This program is free software: you can redistribute it and/or modify
8+
// it under the terms of the GNU Affero General Public License as
9+
// published by the Free Software Foundation, either version 3 of the
10+
// License, or (at your option) any later version.
11+
//
12+
// This program is distributed in the hope that it will be useful,
13+
// but WITHOUT ANY WARRANTY; without even the implied warranty of
14+
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15+
// GNU Affero General Public License for more details.
16+
//
17+
// You should have received a copy of the GNU Affero General Public License
18+
// along with this program. If not, see <http://www.gnu.org/licenses/>.
19+
20+
// Service errors have to be handwritten before codegen.
21+
#[derive(Debug, thiserror::Error)]
22+
pub enum HelloError {
23+
#[error("Internal error: {0}")]
24+
InternalError(String),
25+
#[error("Transport error: {0}")]
26+
TransportError(#[from] tonic::Status),
27+
}
28+
29+
// Service errors must implement `From<tonic::Status>` and `Into<tonic::Status>`.
30+
impl From<HelloError> for tonic::Status {
31+
fn from(val: HelloError) -> Self {
32+
match val {
33+
HelloError::InternalError(message) => tonic::Status::internal(message),
34+
HelloError::TransportError(status) => status,
35+
}
36+
}
37+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
// Copyright (C) 2023 Quickwit, Inc.
2+
//
3+
// Quickwit is offered under the AGPL v3.0 and as commercial software.
4+
// For commercial licensing, contact us at [email protected].
5+
//
6+
// AGPL:
7+
// This program is free software: you can redistribute it and/or modify
8+
// it under the terms of the GNU Affero General Public License as
9+
// published by the Free Software Foundation, either version 3 of the
10+
// License, or (at your option) any later version.
11+
//
12+
// This program is distributed in the hope that it will be useful,
13+
// but WITHOUT ANY WARRANTY; without even the implied warranty of
14+
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15+
// GNU Affero General Public License for more details.
16+
//
17+
// You should have received a copy of the GNU Affero General Public License
18+
// along with this program. If not, see <http://www.gnu.org/licenses/>.
19+
20+
syntax = "proto3";
21+
22+
package hello;
23+
24+
message HelloRequest {
25+
string name = 1;
26+
}
27+
28+
message HelloResponse {
29+
string message = 1;
30+
}
31+
32+
service Hello {
33+
rpc Hello(HelloRequest) returns (HelloResponse);
34+
}

0 commit comments

Comments
 (0)