Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(build): Better support for custom codecs #999

Merged
merged 3 commits into from May 5, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
8 changes: 8 additions & 0 deletions examples/Cargo.toml
Expand Up @@ -186,6 +186,14 @@ path = "src/streaming/client.rs"
name = "streaming-server"
path = "src/streaming/server.rs"

[[bin]]
name = "json-codec-client"
path = "src/json-codec/client.rs"

[[bin]]
name = "json-codec-server"
path = "src/json-codec/server.rs"

[dependencies]
async-stream = "0.3"
futures = { version = "0.3", default-features = false, features = ["alloc"] }
Expand Down
29 changes: 27 additions & 2 deletions examples/build.rs
@@ -1,5 +1,4 @@
use std::env;
use std::path::PathBuf;
use std::{env, path::PathBuf};

fn main() {
tonic_build::configure()
Expand Down Expand Up @@ -30,4 +29,30 @@ fn main() {
&["proto/googleapis"],
)
.unwrap();

build_json_codec_service();
}

// Manually define the json.helloworld.Greeter service which used a custom JsonCodec to use json
// serialization instead of protobuf for sending messages on the wire.
// This will result in generated client and server code which relies on its request, response and
// codec types being defined in a module `crate::common`.
//
// See the client/server examples defined in `src/json-codec` for more information.
fn build_json_codec_service() {
bmwill marked this conversation as resolved.
Show resolved Hide resolved
let greeter_service = tonic_build::manual::Service::builder()
.name("Greeter")
.package("json.helloworld")
.method(
tonic_build::manual::Method::builder()
.name("say_hello")
.route_name("SayHello")
.input_type("crate::common::HelloRequest")
.output_type("crate::common::HelloResponse")
.codec_path("crate::common::JsonCodec")
.build(),
)
.build();

tonic_build::manual::Builder::new().compile(&[greeter_service]);
}
28 changes: 28 additions & 0 deletions examples/src/json-codec/client.rs
@@ -0,0 +1,28 @@
//! A HelloWorld example that uses JSON instead of protobuf as the message serialization format.
//!
//! Generated code is the output of codegen as defined in the `build_json_codec_service` function
//! in the `examples/build.rs` file. As defined there, the generated code assumes that a module
//! `crate::common` exists which defines `HelloRequest`, `HelloResponse`, and `JsonCodec`.

pub mod common;
use common::HelloRequest;

pub mod hello_world {
include!(concat!(env!("OUT_DIR"), "/json.helloworld.Greeter.rs"));
}
use hello_world::greeter_client::GreeterClient;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let mut client = GreeterClient::connect("http://[::1]:50051").await?;

let request = tonic::Request::new(HelloRequest {
name: "Tonic".into(),
});

let response = client.say_hello(request).await?;

println!("RESPONSE={:?}", response);

Ok(())
}
80 changes: 80 additions & 0 deletions examples/src/json-codec/common.rs
@@ -0,0 +1,80 @@
//! This module defines common request/response types as well as the JsonCodec that is used by the
//! json.helloworld.Greeter service which is defined manually (instead of via proto files) by the
//! `build_json_codec_service` function in the `examples/build.rs` file.

use bytes::{Buf, BufMut};
bmwill marked this conversation as resolved.
Show resolved Hide resolved
use serde::{Deserialize, Serialize};
use std::marker::PhantomData;
use tonic::{
codec::{Codec, DecodeBuf, Decoder, EncodeBuf, Encoder},
Status,
};

#[derive(Debug, Deserialize, Serialize)]
pub struct HelloRequest {
pub name: String,
}

#[derive(Debug, Deserialize, Serialize)]
pub struct HelloResponse {
pub message: String,
}

#[derive(Debug)]
pub struct JsonEncoder<T>(PhantomData<T>);

impl<T: serde::Serialize> Encoder for JsonEncoder<T> {
type Item = T;
type Error = Status;

fn encode(&mut self, item: Self::Item, buf: &mut EncodeBuf<'_>) -> Result<(), Self::Error> {
serde_json::to_writer(buf.writer(), &item).map_err(|e| Status::internal(e.to_string()))
}
}

#[derive(Debug)]
pub struct JsonDecoder<U>(PhantomData<U>);

impl<U: serde::de::DeserializeOwned> Decoder for JsonDecoder<U> {
type Item = U;
type Error = Status;

fn decode(&mut self, buf: &mut DecodeBuf<'_>) -> Result<Option<Self::Item>, Self::Error> {
if !buf.has_remaining() {
return Ok(None);
}

let item: Self::Item =
serde_json::from_reader(buf.reader()).map_err(|e| Status::internal(e.to_string()))?;
Ok(Some(item))
}
}

/// A [`Codec`] that implements `application/grpc+json` via the serde library.
#[derive(Debug, Clone)]
pub struct JsonCodec<T, U>(PhantomData<(T, U)>);

impl<T, U> Default for JsonCodec<T, U> {
fn default() -> Self {
Self(PhantomData)
}
}

impl<T, U> Codec for JsonCodec<T, U>
where
T: serde::Serialize + Send + 'static,
U: serde::de::DeserializeOwned + Send + 'static,
{
type Encode = T;
type Decode = U;
type Encoder = JsonEncoder<T>;
type Decoder = JsonDecoder<U>;

fn encoder(&mut self) -> Self::Encoder {
JsonEncoder(PhantomData)
}

fn decoder(&mut self) -> Self::Decoder {
JsonDecoder(PhantomData)
}
}
48 changes: 48 additions & 0 deletions examples/src/json-codec/server.rs
@@ -0,0 +1,48 @@
//! A HelloWorld example that uses JSON instead of protobuf as the message serialization format.
//!
//! Generated code is the output of codegen as defined in the `build_json_codec_service` function
//! in the `examples/build.rs` file. As defined there, the generated code assumes that a module
//! `crate::common` exists which defines `HelloRequest`, `HelloResponse`, and `JsonCodec`.

use tonic::{transport::Server, Request, Response, Status};

pub mod common;
use common::{HelloRequest, HelloResponse};

pub mod hello_world {
include!(concat!(env!("OUT_DIR"), "/json.helloworld.Greeter.rs"));
}
use hello_world::greeter_server::{Greeter, GreeterServer};

#[derive(Default)]
pub struct MyGreeter {}

#[tonic::async_trait]
impl Greeter for MyGreeter {
async fn say_hello(
&self,
request: Request<HelloRequest>,
) -> Result<Response<HelloResponse>, Status> {
println!("Got a request from {:?}", request.remote_addr());

let reply = HelloResponse {
message: format!("Hello {}!", request.into_inner().name),
};
Ok(Response::new(reply))
}
}

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let addr = "[::1]:50051".parse().unwrap();
let greeter = MyGreeter::default();

println!("GreeterServer listening on {}", addr);

Server::builder()
.add_service(GreeterServer::new(greeter))
.serve(addr)
.await?;

Ok(())
}
8 changes: 4 additions & 4 deletions tonic-build/src/client.rs
Expand Up @@ -167,7 +167,7 @@ fn generate_unary<T: Method>(
compile_well_known_types: bool,
path: String,
) -> TokenStream {
let codec_name = syn::parse_str::<syn::Path>(T::CODEC_PATH).unwrap();
let codec_name = syn::parse_str::<syn::Path>(method.codec_path()).unwrap();
let ident = format_ident!("{}", method.name());
let (request, response) = method.request_response_name(proto_path, compile_well_known_types);

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

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

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

let (request, response) = method.request_response_name(proto_path, compile_well_known_types);
Expand Down
9 changes: 4 additions & 5 deletions tonic-build/src/lib.rs
Expand Up @@ -79,6 +79,8 @@ mod prost;
#[cfg_attr(docsrs, doc(cfg(feature = "prost")))]
pub use prost::{compile_protos, configure, Builder};

pub mod manual;

/// Service code generation for client
pub mod client;
/// Service code generation for Server
Expand All @@ -91,9 +93,6 @@ pub mod server;
/// to allow any codegen module to generate service
/// abstractions.
pub trait Service {
/// Path to the codec.
const CODEC_PATH: &'static str;

/// Comment type.
type Comment: AsRef<str>;

Expand All @@ -119,15 +118,15 @@ pub trait Service {
/// to generate abstraction implementations for
/// the provided methods.
pub trait Method {
/// Path to the codec.
const CODEC_PATH: &'static str;
/// Comment type.
type Comment: AsRef<str>;

/// Name of method.
fn name(&self) -> &str;
/// Identifier used to generate type name.
fn identifier(&self) -> &str;
/// Path to the codec.
fn codec_path(&self) -> &str;
/// Method is streamed by client.
fn client_streaming(&self) -> bool;
/// Method is streamed by server.
Expand Down