Skip to content

Commit de2e4ac

Browse files
authoredMay 5, 2022
feat(build): Better support for custom codecs (#999)
BREAKING CHANGE: `CODEC_PATH` moved from const to fn
1 parent 1b0b525 commit de2e4ac

File tree

10 files changed

+694
-21
lines changed

10 files changed

+694
-21
lines changed
 

‎examples/Cargo.toml

+8
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,14 @@ path = "src/streaming/client.rs"
186186
name = "streaming-server"
187187
path = "src/streaming/server.rs"
188188

189+
[[bin]]
190+
name = "json-codec-client"
191+
path = "src/json-codec/client.rs"
192+
193+
[[bin]]
194+
name = "json-codec-server"
195+
path = "src/json-codec/server.rs"
196+
189197
[dependencies]
190198
async-stream = "0.3"
191199
futures = { version = "0.3", default-features = false, features = ["alloc"] }

‎examples/build.rs

+27-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
1-
use std::env;
2-
use std::path::PathBuf;
1+
use std::{env, path::PathBuf};
32

43
fn main() {
54
tonic_build::configure()
@@ -30,4 +29,30 @@ fn main() {
3029
&["proto/googleapis"],
3130
)
3231
.unwrap();
32+
33+
build_json_codec_service();
34+
}
35+
36+
// Manually define the json.helloworld.Greeter service which used a custom JsonCodec to use json
37+
// serialization instead of protobuf for sending messages on the wire.
38+
// This will result in generated client and server code which relies on its request, response and
39+
// codec types being defined in a module `crate::common`.
40+
//
41+
// See the client/server examples defined in `src/json-codec` for more information.
42+
fn build_json_codec_service() {
43+
let greeter_service = tonic_build::manual::Service::builder()
44+
.name("Greeter")
45+
.package("json.helloworld")
46+
.method(
47+
tonic_build::manual::Method::builder()
48+
.name("say_hello")
49+
.route_name("SayHello")
50+
.input_type("crate::common::HelloRequest")
51+
.output_type("crate::common::HelloResponse")
52+
.codec_path("crate::common::JsonCodec")
53+
.build(),
54+
)
55+
.build();
56+
57+
tonic_build::manual::Builder::new().compile(&[greeter_service]);
3358
}

‎examples/src/json-codec/client.rs

+28
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
//! A HelloWorld example that uses JSON instead of protobuf as the message serialization format.
2+
//!
3+
//! Generated code is the output of codegen as defined in the `build_json_codec_service` function
4+
//! in the `examples/build.rs` file. As defined there, the generated code assumes that a module
5+
//! `crate::common` exists which defines `HelloRequest`, `HelloResponse`, and `JsonCodec`.
6+
7+
pub mod common;
8+
use common::HelloRequest;
9+
10+
pub mod hello_world {
11+
include!(concat!(env!("OUT_DIR"), "/json.helloworld.Greeter.rs"));
12+
}
13+
use hello_world::greeter_client::GreeterClient;
14+
15+
#[tokio::main]
16+
async fn main() -> Result<(), Box<dyn std::error::Error>> {
17+
let mut client = GreeterClient::connect("http://[::1]:50051").await?;
18+
19+
let request = tonic::Request::new(HelloRequest {
20+
name: "Tonic".into(),
21+
});
22+
23+
let response = client.say_hello(request).await?;
24+
25+
println!("RESPONSE={:?}", response);
26+
27+
Ok(())
28+
}

‎examples/src/json-codec/common.rs

+80
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
//! This module defines common request/response types as well as the JsonCodec that is used by the
2+
//! json.helloworld.Greeter service which is defined manually (instead of via proto files) by the
3+
//! `build_json_codec_service` function in the `examples/build.rs` file.
4+
5+
use bytes::{Buf, BufMut};
6+
use serde::{Deserialize, Serialize};
7+
use std::marker::PhantomData;
8+
use tonic::{
9+
codec::{Codec, DecodeBuf, Decoder, EncodeBuf, Encoder},
10+
Status,
11+
};
12+
13+
#[derive(Debug, Deserialize, Serialize)]
14+
pub struct HelloRequest {
15+
pub name: String,
16+
}
17+
18+
#[derive(Debug, Deserialize, Serialize)]
19+
pub struct HelloResponse {
20+
pub message: String,
21+
}
22+
23+
#[derive(Debug)]
24+
pub struct JsonEncoder<T>(PhantomData<T>);
25+
26+
impl<T: serde::Serialize> Encoder for JsonEncoder<T> {
27+
type Item = T;
28+
type Error = Status;
29+
30+
fn encode(&mut self, item: Self::Item, buf: &mut EncodeBuf<'_>) -> Result<(), Self::Error> {
31+
serde_json::to_writer(buf.writer(), &item).map_err(|e| Status::internal(e.to_string()))
32+
}
33+
}
34+
35+
#[derive(Debug)]
36+
pub struct JsonDecoder<U>(PhantomData<U>);
37+
38+
impl<U: serde::de::DeserializeOwned> Decoder for JsonDecoder<U> {
39+
type Item = U;
40+
type Error = Status;
41+
42+
fn decode(&mut self, buf: &mut DecodeBuf<'_>) -> Result<Option<Self::Item>, Self::Error> {
43+
if !buf.has_remaining() {
44+
return Ok(None);
45+
}
46+
47+
let item: Self::Item =
48+
serde_json::from_reader(buf.reader()).map_err(|e| Status::internal(e.to_string()))?;
49+
Ok(Some(item))
50+
}
51+
}
52+
53+
/// A [`Codec`] that implements `application/grpc+json` via the serde library.
54+
#[derive(Debug, Clone)]
55+
pub struct JsonCodec<T, U>(PhantomData<(T, U)>);
56+
57+
impl<T, U> Default for JsonCodec<T, U> {
58+
fn default() -> Self {
59+
Self(PhantomData)
60+
}
61+
}
62+
63+
impl<T, U> Codec for JsonCodec<T, U>
64+
where
65+
T: serde::Serialize + Send + 'static,
66+
U: serde::de::DeserializeOwned + Send + 'static,
67+
{
68+
type Encode = T;
69+
type Decode = U;
70+
type Encoder = JsonEncoder<T>;
71+
type Decoder = JsonDecoder<U>;
72+
73+
fn encoder(&mut self) -> Self::Encoder {
74+
JsonEncoder(PhantomData)
75+
}
76+
77+
fn decoder(&mut self) -> Self::Decoder {
78+
JsonDecoder(PhantomData)
79+
}
80+
}

‎examples/src/json-codec/server.rs

+48
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
//! A HelloWorld example that uses JSON instead of protobuf as the message serialization format.
2+
//!
3+
//! Generated code is the output of codegen as defined in the `build_json_codec_service` function
4+
//! in the `examples/build.rs` file. As defined there, the generated code assumes that a module
5+
//! `crate::common` exists which defines `HelloRequest`, `HelloResponse`, and `JsonCodec`.
6+
7+
use tonic::{transport::Server, Request, Response, Status};
8+
9+
pub mod common;
10+
use common::{HelloRequest, HelloResponse};
11+
12+
pub mod hello_world {
13+
include!(concat!(env!("OUT_DIR"), "/json.helloworld.Greeter.rs"));
14+
}
15+
use hello_world::greeter_server::{Greeter, GreeterServer};
16+
17+
#[derive(Default)]
18+
pub struct MyGreeter {}
19+
20+
#[tonic::async_trait]
21+
impl Greeter for MyGreeter {
22+
async fn say_hello(
23+
&self,
24+
request: Request<HelloRequest>,
25+
) -> Result<Response<HelloResponse>, Status> {
26+
println!("Got a request from {:?}", request.remote_addr());
27+
28+
let reply = HelloResponse {
29+
message: format!("Hello {}!", request.into_inner().name),
30+
};
31+
Ok(Response::new(reply))
32+
}
33+
}
34+
35+
#[tokio::main]
36+
async fn main() -> Result<(), Box<dyn std::error::Error>> {
37+
let addr = "[::1]:50051".parse().unwrap();
38+
let greeter = MyGreeter::default();
39+
40+
println!("GreeterServer listening on {}", addr);
41+
42+
Server::builder()
43+
.add_service(GreeterServer::new(greeter))
44+
.serve(addr)
45+
.await?;
46+
47+
Ok(())
48+
}

‎tonic-build/src/client.rs

+4-4
Original file line numberDiff line numberDiff line change
@@ -167,7 +167,7 @@ fn generate_unary<T: Method>(
167167
compile_well_known_types: bool,
168168
path: String,
169169
) -> TokenStream {
170-
let codec_name = syn::parse_str::<syn::Path>(T::CODEC_PATH).unwrap();
170+
let codec_name = syn::parse_str::<syn::Path>(method.codec_path()).unwrap();
171171
let ident = format_ident!("{}", method.name());
172172
let (request, response) = method.request_response_name(proto_path, compile_well_known_types);
173173

@@ -192,7 +192,7 @@ fn generate_server_streaming<T: Method>(
192192
compile_well_known_types: bool,
193193
path: String,
194194
) -> TokenStream {
195-
let codec_name = syn::parse_str::<syn::Path>(T::CODEC_PATH).unwrap();
195+
let codec_name = syn::parse_str::<syn::Path>(method.codec_path()).unwrap();
196196
let ident = format_ident!("{}", method.name());
197197

198198
let (request, response) = method.request_response_name(proto_path, compile_well_known_types);
@@ -218,7 +218,7 @@ fn generate_client_streaming<T: Method>(
218218
compile_well_known_types: bool,
219219
path: String,
220220
) -> TokenStream {
221-
let codec_name = syn::parse_str::<syn::Path>(T::CODEC_PATH).unwrap();
221+
let codec_name = syn::parse_str::<syn::Path>(method.codec_path()).unwrap();
222222
let ident = format_ident!("{}", method.name());
223223

224224
let (request, response) = method.request_response_name(proto_path, compile_well_known_types);
@@ -244,7 +244,7 @@ fn generate_streaming<T: Method>(
244244
compile_well_known_types: bool,
245245
path: String,
246246
) -> TokenStream {
247-
let codec_name = syn::parse_str::<syn::Path>(T::CODEC_PATH).unwrap();
247+
let codec_name = syn::parse_str::<syn::Path>(method.codec_path()).unwrap();
248248
let ident = format_ident!("{}", method.name());
249249

250250
let (request, response) = method.request_response_name(proto_path, compile_well_known_types);

‎tonic-build/src/lib.rs

+4-5
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,8 @@ mod prost;
7979
#[cfg_attr(docsrs, doc(cfg(feature = "prost")))]
8080
pub use prost::{compile_protos, configure, Builder};
8181

82+
pub mod manual;
83+
8284
/// Service code generation for client
8385
pub mod client;
8486
/// Service code generation for Server
@@ -91,9 +93,6 @@ pub mod server;
9193
/// to allow any codegen module to generate service
9294
/// abstractions.
9395
pub trait Service {
94-
/// Path to the codec.
95-
const CODEC_PATH: &'static str;
96-
9796
/// Comment type.
9897
type Comment: AsRef<str>;
9998

@@ -119,15 +118,15 @@ pub trait Service {
119118
/// to generate abstraction implementations for
120119
/// the provided methods.
121120
pub trait Method {
122-
/// Path to the codec.
123-
const CODEC_PATH: &'static str;
124121
/// Comment type.
125122
type Comment: AsRef<str>;
126123

127124
/// Name of method.
128125
fn name(&self) -> &str;
129126
/// Identifier used to generate type name.
130127
fn identifier(&self) -> &str;
128+
/// Path to the codec.
129+
fn codec_path(&self) -> &str;
131130
/// Method is streamed by client.
132131
fn client_streaming(&self) -> bool;
133132
/// Method is streamed by server.

‎tonic-build/src/manual.rs

+482
Large diffs are not rendered by default.

‎tonic-build/src/prost.rs

+9-6
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,11 @@ use super::{client, server, Attributes};
22
use proc_macro2::TokenStream;
33
use prost_build::{Config, Method, Service};
44
use quote::ToTokens;
5-
use std::ffi::OsString;
6-
use std::io;
7-
use std::path::{Path, PathBuf};
5+
use std::{
6+
ffi::OsString,
7+
io,
8+
path::{Path, PathBuf},
9+
};
810

911
/// Configure `tonic-build` code generation.
1012
///
@@ -51,8 +53,6 @@ const PROST_CODEC_PATH: &str = "tonic::codec::ProstCodec";
5153
const NON_PATH_TYPE_ALLOWLIST: &[&str] = &["()"];
5254

5355
impl crate::Service for Service {
54-
const CODEC_PATH: &'static str = PROST_CODEC_PATH;
55-
5656
type Method = Method;
5757
type Comment = String;
5858

@@ -78,7 +78,6 @@ impl crate::Service for Service {
7878
}
7979

8080
impl crate::Method for Method {
81-
const CODEC_PATH: &'static str = PROST_CODEC_PATH;
8281
type Comment = String;
8382

8483
fn name(&self) -> &str {
@@ -89,6 +88,10 @@ impl crate::Method for Method {
8988
&self.proto_name
9089
}
9190

91+
fn codec_path(&self) -> &str {
92+
PROST_CODEC_PATH
93+
}
94+
9295
fn client_streaming(&self) -> bool {
9396
self.client_streaming
9497
}

‎tonic-build/src/server.rs

+4-4
Original file line numberDiff line numberDiff line change
@@ -366,7 +366,7 @@ fn generate_unary<T: Method>(
366366
method_ident: Ident,
367367
server_trait: Ident,
368368
) -> TokenStream {
369-
let codec_name = syn::parse_str::<syn::Path>(T::CODEC_PATH).unwrap();
369+
let codec_name = syn::parse_str::<syn::Path>(method.codec_path()).unwrap();
370370

371371
let service_ident = quote::format_ident!("{}Svc", method.identifier());
372372

@@ -415,7 +415,7 @@ fn generate_server_streaming<T: Method>(
415415
method_ident: Ident,
416416
server_trait: Ident,
417417
) -> TokenStream {
418-
let codec_name = syn::parse_str::<syn::Path>(T::CODEC_PATH).unwrap();
418+
let codec_name = syn::parse_str::<syn::Path>(method.codec_path()).unwrap();
419419

420420
let service_ident = quote::format_ident!("{}Svc", method.identifier());
421421

@@ -470,7 +470,7 @@ fn generate_client_streaming<T: Method>(
470470
let service_ident = quote::format_ident!("{}Svc", method.identifier());
471471

472472
let (request, response) = method.request_response_name(proto_path, compile_well_known_types);
473-
let codec_name = syn::parse_str::<syn::Path>(T::CODEC_PATH).unwrap();
473+
let codec_name = syn::parse_str::<syn::Path>(method.codec_path()).unwrap();
474474

475475
quote! {
476476
#[allow(non_camel_case_types)]
@@ -517,7 +517,7 @@ fn generate_streaming<T: Method>(
517517
method_ident: Ident,
518518
server_trait: Ident,
519519
) -> TokenStream {
520-
let codec_name = syn::parse_str::<syn::Path>(T::CODEC_PATH).unwrap();
520+
let codec_name = syn::parse_str::<syn::Path>(method.codec_path()).unwrap();
521521

522522
let service_ident = quote::format_ident!("{}Svc", method.identifier());
523523

0 commit comments

Comments
 (0)
Please sign in to comment.