From e8cb7acf9b0def52d89ef7d5b4776ad7666a245e Mon Sep 17 00:00:00 2001 From: Lucio Franco Date: Wed, 2 Jul 2025 14:42:00 -0400 Subject: [PATCH] feat: Extract prost into its own tonic based crates --- .github/workflows/CI.yml | 2 +- Cargo.toml | 2 + codegen/Cargo.toml | 2 +- codegen/src/main.rs | 12 +- examples/Cargo.toml | 3 +- examples/build.rs | 20 +- examples/src/codec_buffers/common.rs | 3 +- grpc/Cargo.toml | 2 +- interop/Cargo.toml | 3 +- interop/build.rs | 2 +- tests/ambiguous_methods/Cargo.toml | 3 +- tests/ambiguous_methods/build.rs | 2 +- tests/compression/Cargo.toml | 3 +- tests/compression/build.rs | 2 +- tests/default_stubs/Cargo.toml | 3 +- tests/default_stubs/build.rs | 4 +- tests/deprecated_methods/Cargo.toml | 3 +- tests/deprecated_methods/build.rs | 2 +- tests/disable_comments/Cargo.toml | 3 +- tests/disable_comments/build.rs | 10 +- tests/extern_path/my_application/Cargo.toml | 3 +- tests/extern_path/my_application/build.rs | 2 +- tests/included_service/Cargo.toml | 3 +- tests/included_service/build.rs | 2 +- tests/integration_tests/Cargo.toml | 3 +- tests/integration_tests/build.rs | 4 +- tests/root-crate-path/Cargo.toml | 3 +- tests/root-crate-path/build.rs | 2 +- tests/same_name/Cargo.toml | 3 +- tests/same_name/build.rs | 2 +- tests/service_named_result/Cargo.toml | 3 +- tests/service_named_result/build.rs | 2 +- tests/service_named_service/Cargo.toml | 3 +- tests/service_named_service/build.rs | 2 +- tests/skip_debug/Cargo.toml | 3 +- tests/skip_debug/build.rs | 6 +- tests/stream_conflict/Cargo.toml | 3 +- tests/stream_conflict/build.rs | 2 +- tests/use_arc_self/Cargo.toml | 3 +- tests/use_arc_self/build.rs | 2 +- tests/web/Cargo.toml | 3 +- tests/web/build.rs | 2 +- tests/wellknown-compiled/Cargo.toml | 3 +- tests/wellknown-compiled/build.rs | 2 +- tests/wellknown/Cargo.toml | 3 +- tests/wellknown/build.rs | 2 +- tonic-build/Cargo.toml | 10 +- tonic-build/README.md | 18 +- tonic-build/src/code_gen.rs | 5 + tonic-build/src/compile_settings.rs | 14 - tonic-build/src/lib.rs | 13 +- tonic-build/src/prost.rs | 708 -------------- tonic-health/Cargo.toml | 3 +- tonic-health/src/generated/grpc_health_v1.rs | 8 +- tonic-prost-build/Cargo.toml | 37 + tonic-prost-build/README.md | 29 + tonic-prost-build/src/lib.rs | 902 ++++++++++++++++++ tonic-prost-build/src/tests.rs | 281 ++++++ tonic-prost/Cargo.toml | 30 + tonic-prost/README.md | 19 + .../prost.rs => tonic-prost/src/codec.rs | 25 +- tonic-prost/src/lib.rs | 31 + tonic-reflection/Cargo.toml | 3 +- .../src/generated/grpc_reflection_v1.rs | 4 +- .../src/generated/grpc_reflection_v1alpha.rs | 4 +- tonic/Cargo.toml | 5 +- tonic/src/client/grpc.rs | 2 +- tonic/src/codec/compression.rs | 1 + tonic/src/codec/mod.rs | 16 +- 69 files changed, 1473 insertions(+), 852 deletions(-) delete mode 100644 tonic-build/src/compile_settings.rs delete mode 100644 tonic-build/src/prost.rs create mode 100644 tonic-prost-build/Cargo.toml create mode 100644 tonic-prost-build/README.md create mode 100644 tonic-prost-build/src/lib.rs create mode 100644 tonic-prost-build/src/tests.rs create mode 100644 tonic-prost/Cargo.toml create mode 100644 tonic-prost/README.md rename tonic/src/codec/prost.rs => tonic-prost/src/codec.rs (94%) create mode 100644 tonic-prost/src/lib.rs diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 49dfd0da8..2a6368c23 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -150,13 +150,13 @@ jobs: shell: bash semver: + if: false runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: obi1kenobi/cargo-semver-checks-action@v2 with: feature-group: all-features - exclude: grpc external-types: runs-on: ubuntu-latest diff --git a/Cargo.toml b/Cargo.toml index 7b2aaddd8..f72e4746a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,6 +5,8 @@ members = [ "tonic-health", "tonic-types", "tonic-reflection", + "tonic-prost", + "tonic-prost-build", "tonic-web", # Non-published crates "examples", "codegen", diff --git a/codegen/Cargo.toml b/codegen/Cargo.toml index b4994a5e8..63a71f1cb 100644 --- a/codegen/Cargo.toml +++ b/codegen/Cargo.toml @@ -10,4 +10,4 @@ prettyplease = "0.2" quote = "1" syn = "2" tempfile = "3.8.0" -tonic-build = {path = "../tonic-build", default-features = false, features = ["prost", "cleanup-markdown"]} +tonic-prost-build = {path = "../tonic-prost-build", default-features = false, features = ["cleanup-markdown"]} diff --git a/codegen/src/main.rs b/codegen/src/main.rs index 9dd7f322a..6cc193421 100644 --- a/codegen/src/main.rs +++ b/codegen/src/main.rs @@ -2,13 +2,18 @@ use std::{ fs::File, io::{BufWriter, Write as _}, path::{Path, PathBuf}, + time::Instant, }; use protox::prost::Message as _; use quote::quote; -use tonic_build::FileDescriptorSet; +use tonic_prost_build::FileDescriptorSet; fn main() { + println!("Running codegen..."); + + let start = Instant::now(); + // tonic-health codegen( &PathBuf::from(std::env!("CARGO_MANIFEST_DIR")) @@ -62,6 +67,8 @@ fn main() { false, false, ); + + println!("Codgen completed: {}ms", start.elapsed().as_millis()); } fn codegen( @@ -87,9 +94,10 @@ fn codegen( write_fds(&fds, &file_descriptor_set_path); - tonic_build::configure() + tonic_prost_build::configure() .build_client(build_client) .build_server(build_server) + .build_transport(false) .out_dir(&tempdir) .compile_fds(fds) .unwrap(); diff --git a/examples/Cargo.toml b/examples/Cargo.toml index 24e71261a..b2f6d0ccf 100644 --- a/examples/Cargo.toml +++ b/examples/Cargo.toml @@ -281,6 +281,7 @@ default = ["full"] tokio = { version = "1.0", features = ["rt-multi-thread", "macros"] } prost = "0.14" tonic = { path = "../tonic" } +tonic-prost = { path = "../tonic-prost" } # Optional dependencies tonic-web = { path = "../tonic-web", optional = true } tonic-health = { path = "../tonic-health", optional = true } @@ -307,4 +308,4 @@ hyper-rustls = { version = "0.27.0", features = ["http2", "ring", "tls12"], opti tower-http = { version = "0.6", optional = true } [build-dependencies] -tonic-build = { path = "../tonic-build", features = ["prost"] } +tonic-prost-build = { path = "../tonic-prost-build" } diff --git a/examples/build.rs b/examples/build.rs index 84b9e2c6c..7cb50ae77 100644 --- a/examples/build.rs +++ b/examples/build.rs @@ -1,21 +1,21 @@ use std::{env, path::PathBuf}; fn main() { - tonic_build::configure() + tonic_prost_build::configure() .compile_protos(&["proto/routeguide/route_guide.proto"], &["proto"]) .unwrap(); let out_dir = PathBuf::from(env::var("OUT_DIR").unwrap()); - tonic_build::configure() + tonic_prost_build::configure() .file_descriptor_set_path(out_dir.join("helloworld_descriptor.bin")) .compile_protos(&["proto/helloworld/helloworld.proto"], &["proto"]) .unwrap(); - tonic_build::compile_protos("proto/echo/echo.proto").unwrap(); + tonic_prost_build::compile_protos("proto/echo/echo.proto").unwrap(); - tonic_build::compile_protos("proto/unaryecho/echo.proto").unwrap(); + tonic_prost_build::compile_protos("proto/unaryecho/echo.proto").unwrap(); - tonic_build::configure() + tonic_prost_build::configure() .server_mod_attribute("attrs", "#[cfg(feature = \"server\")]") .server_attribute("Echo", "#[derive(PartialEq)]") .client_mod_attribute("attrs", "#[cfg(feature = \"client\")]") @@ -23,7 +23,7 @@ fn main() { .compile_protos(&["proto/attrs/attrs.proto"], &["proto"]) .unwrap(); - tonic_build::configure() + tonic_prost_build::configure() .build_server(false) .compile_protos( &["proto/googleapis/google/pubsub/v1/pubsub.proto"], @@ -35,7 +35,7 @@ fn main() { let smallbuff_copy = out_dir.join("smallbuf"); let _ = std::fs::create_dir(smallbuff_copy.clone()); // This will panic below if the directory failed to create - tonic_build::configure() + tonic_prost_build::configure() .out_dir(smallbuff_copy) .codec_path("crate::common::SmallBufferCodec") .compile_protos(&["proto/helloworld/helloworld.proto"], &["proto"]) @@ -49,11 +49,11 @@ fn main() { // // See the client/server examples defined in `src/json-codec` for more information. fn build_json_codec_service() { - let greeter_service = tonic_build::manual::Service::builder() + let greeter_service = tonic_prost_build::manual::Service::builder() .name("Greeter") .package("json.helloworld") .method( - tonic_build::manual::Method::builder() + tonic_prost_build::manual::Method::builder() .name("say_hello") .route_name("SayHello") .input_type("crate::common::HelloRequest") @@ -63,5 +63,5 @@ fn build_json_codec_service() { ) .build(); - tonic_build::manual::Builder::new().compile(&[greeter_service]); + tonic_prost_build::manual::Builder::new().compile(&[greeter_service]); } diff --git a/examples/src/codec_buffers/common.rs b/examples/src/codec_buffers/common.rs index c8f9ed777..2785d108c 100644 --- a/examples/src/codec_buffers/common.rs +++ b/examples/src/codec_buffers/common.rs @@ -12,7 +12,8 @@ use std::marker::PhantomData; use prost::Message; -use tonic::codec::{BufferSettings, Codec, ProstCodec}; +use tonic::codec::{BufferSettings, Codec}; +use tonic_prost::ProstCodec; #[derive(Debug, Clone, Copy, Default)] pub struct SmallBufferCodec(PhantomData<(T, U)>); diff --git a/grpc/Cargo.toml b/grpc/Cargo.toml index 86f1e8c22..b20e0a7ad 100644 --- a/grpc/Cargo.toml +++ b/grpc/Cargo.toml @@ -30,7 +30,7 @@ url = "2.5.0" [dev-dependencies] async-stream = "0.3.6" -tonic = { version = "0.14.0", path = "../tonic", default-features = false, features = ["prost", "server", "router"] } +tonic = { version = "0.14.0", path = "../tonic", default-features = false, features = ["server", "router"] } hickory-server = "0.25.2" prost = "0.13.5" diff --git a/interop/Cargo.toml b/interop/Cargo.toml index 9994895a4..ba080667d 100644 --- a/interop/Cargo.toml +++ b/interop/Cargo.toml @@ -23,8 +23,9 @@ prost = "0.14" tokio = {version = "1.0", features = ["rt-multi-thread", "time", "macros"]} tokio-stream = "0.1" tonic = {path = "../tonic", features = ["tls-ring"]} +tonic-prost = {path = "../tonic-prost"} tower = "0.5" tracing-subscriber = {version = "0.3"} [build-dependencies] -tonic-build = {path = "../tonic-build", features = ["prost"]} +tonic-prost-build = {path = "../tonic-prost-build"} diff --git a/interop/build.rs b/interop/build.rs index 295d5e2a3..a73f69f34 100644 --- a/interop/build.rs +++ b/interop/build.rs @@ -1,7 +1,7 @@ fn main() { let proto = "proto/grpc/testing/test.proto"; - tonic_build::compile_protos(proto).unwrap(); + tonic_prost_build::compile_protos(proto).unwrap(); // prevent needing to rebuild if files (or deps) haven't changed println!("cargo:rerun-if-changed={proto}"); diff --git a/tests/ambiguous_methods/Cargo.toml b/tests/ambiguous_methods/Cargo.toml index dbd7b1946..7a1dc9c36 100644 --- a/tests/ambiguous_methods/Cargo.toml +++ b/tests/ambiguous_methods/Cargo.toml @@ -9,6 +9,7 @@ name = "ambiguous_methods" [dependencies] prost = "0.14" tonic = {path = "../../tonic"} +tonic-prost = {path = "../../tonic-prost"} [build-dependencies] -tonic-build = {path = "../../tonic-build"} +tonic-prost-build = {path = "../../tonic-prost-build"} diff --git a/tests/ambiguous_methods/build.rs b/tests/ambiguous_methods/build.rs index c8a4f961c..1804d36da 100644 --- a/tests/ambiguous_methods/build.rs +++ b/tests/ambiguous_methods/build.rs @@ -1,3 +1,3 @@ fn main() { - tonic_build::compile_protos("proto/ambiguous_methods.proto").unwrap(); + tonic_prost_build::compile_protos("proto/ambiguous_methods.proto").unwrap(); } diff --git a/tests/compression/Cargo.toml b/tests/compression/Cargo.toml index 13616d0d7..b6a57a3cf 100644 --- a/tests/compression/Cargo.toml +++ b/tests/compression/Cargo.toml @@ -16,8 +16,9 @@ prost = "0.14" tokio = {version = "1.0", features = ["macros", "rt-multi-thread", "net"]} tokio-stream = "0.1" tonic = {path = "../../tonic", features = ["gzip", "deflate", "zstd"]} +tonic-prost = {path = "../../tonic-prost"} tower = "0.5" tower-http = {version = "0.6", features = ["map-response-body", "map-request-body"]} [build-dependencies] -tonic-build = {path = "../../tonic-build" } +tonic-prost-build = {path = "../../tonic-prost-build" } diff --git a/tests/compression/build.rs b/tests/compression/build.rs index a091e9483..034524e6a 100644 --- a/tests/compression/build.rs +++ b/tests/compression/build.rs @@ -1,3 +1,3 @@ fn main() { - tonic_build::compile_protos("proto/test.proto").unwrap(); + tonic_prost_build::compile_protos("proto/test.proto").unwrap(); } diff --git a/tests/default_stubs/Cargo.toml b/tests/default_stubs/Cargo.toml index da878513c..ffa09cfbe 100644 --- a/tests/default_stubs/Cargo.toml +++ b/tests/default_stubs/Cargo.toml @@ -8,9 +8,10 @@ name = "default_stubs" tokio = {version = "1.0", features = ["macros", "rt-multi-thread", "net"]} tokio-stream = {version = "0.1", features = ["net"]} tonic = {path = "../../tonic"} +tonic-prost = {path = "../../tonic-prost"} [dev-dependencies] tempfile = "3.20" [build-dependencies] -tonic-build = {path = "../../tonic-build" } +tonic-prost-build = {path = "../../tonic-prost-build" } diff --git a/tests/default_stubs/build.rs b/tests/default_stubs/build.rs index bb6c83495..7d56494a7 100644 --- a/tests/default_stubs/build.rs +++ b/tests/default_stubs/build.rs @@ -1,8 +1,8 @@ fn main() { - tonic_build::configure() + tonic_prost_build::configure() .compile_protos(&["proto/test.proto"], &["proto"]) .unwrap(); - tonic_build::configure() + tonic_prost_build::configure() .generate_default_stubs(true) .compile_protos(&["proto/test_default.proto"], &["proto"]) .unwrap(); diff --git a/tests/deprecated_methods/Cargo.toml b/tests/deprecated_methods/Cargo.toml index 720069330..bb7a89813 100644 --- a/tests/deprecated_methods/Cargo.toml +++ b/tests/deprecated_methods/Cargo.toml @@ -8,6 +8,7 @@ license = "MIT" [dependencies] prost = "0.14" tonic = { path = "../../tonic" } +tonic-prost = { path = "../../tonic-prost" } [build-dependencies] -tonic-build = { path = "../../tonic-build" } +tonic-prost-build = { path = "../../tonic-prost-build" } diff --git a/tests/deprecated_methods/build.rs b/tests/deprecated_methods/build.rs index a091e9483..034524e6a 100644 --- a/tests/deprecated_methods/build.rs +++ b/tests/deprecated_methods/build.rs @@ -1,3 +1,3 @@ fn main() { - tonic_build::compile_protos("proto/test.proto").unwrap(); + tonic_prost_build::compile_protos("proto/test.proto").unwrap(); } diff --git a/tests/disable_comments/Cargo.toml b/tests/disable_comments/Cargo.toml index 582f9626f..c0708b005 100644 --- a/tests/disable_comments/Cargo.toml +++ b/tests/disable_comments/Cargo.toml @@ -9,6 +9,7 @@ name = "disable-comments" [dependencies] prost = "0.14" tonic = { path = "../../tonic" } +tonic-prost = { path = "../../tonic-prost" } [build-dependencies] -tonic-build = { path = "../../tonic-build" } +tonic-prost-build = { path = "../../tonic-prost-build" } diff --git a/tests/disable_comments/build.rs b/tests/disable_comments/build.rs index 6fcdc0afd..80df90531 100644 --- a/tests/disable_comments/build.rs +++ b/tests/disable_comments/build.rs @@ -1,11 +1,11 @@ fn main() { - let mut config = tonic_build::Config::default(); + let mut config = tonic_prost_build::Config::default(); config.disable_comments(["test.Input1", "test.Output1"]); - tonic_build::configure() - .disable_comments("test.Service1") - .disable_comments("test.Service1.Rpc1") + tonic_prost_build::configure() + .disable_comments(["test.Service1"]) + .disable_comments(["test.Service1.Rpc1"]) .build_client(true) .build_server(true) - .compile_protos_with_config(config, &["proto/test.proto"], &["proto"]) + .compile_with_config(config, &["proto/test.proto"], &["proto"]) .unwrap(); } diff --git a/tests/extern_path/my_application/Cargo.toml b/tests/extern_path/my_application/Cargo.toml index a63cf16d8..05dcb5e26 100644 --- a/tests/extern_path/my_application/Cargo.toml +++ b/tests/extern_path/my_application/Cargo.toml @@ -9,7 +9,8 @@ name = "my_application" [dependencies] prost = "0.14" tonic = {path = "../../../tonic"} +tonic-prost = {path = "../../../tonic-prost"} uuid = {package = "uuid1", path = "../uuid"} [build-dependencies] -tonic-build = {path = "../../../tonic-build"} +tonic-prost-build = {path = "../../../tonic-prost-build"} diff --git a/tests/extern_path/my_application/build.rs b/tests/extern_path/my_application/build.rs index f4d467cdf..8ff9d0193 100644 --- a/tests/extern_path/my_application/build.rs +++ b/tests/extern_path/my_application/build.rs @@ -1,5 +1,5 @@ fn main() -> Result<(), std::io::Error> { - tonic_build::configure() + tonic_prost_build::configure() .build_server(false) .build_client(true) .extern_path(".uuid", "::uuid") diff --git a/tests/included_service/Cargo.toml b/tests/included_service/Cargo.toml index 4b2bc3aa6..bea9a1517 100644 --- a/tests/included_service/Cargo.toml +++ b/tests/included_service/Cargo.toml @@ -9,6 +9,7 @@ name = "included_service" [dependencies] prost = "0.14" tonic = {path = "../../tonic"} +tonic-prost = {path = "../../tonic-prost"} [build-dependencies] -tonic-build = {path = "../../tonic-build"} +tonic-prost-build = {path = "../../tonic-prost-build"} diff --git a/tests/included_service/build.rs b/tests/included_service/build.rs index 1c29065b1..580f49d83 100644 --- a/tests/included_service/build.rs +++ b/tests/included_service/build.rs @@ -1,3 +1,3 @@ fn main() { - tonic_build::compile_protos("proto/includer.proto").unwrap(); + tonic_prost_build::compile_protos("proto/includer.proto").unwrap(); } diff --git a/tests/integration_tests/Cargo.toml b/tests/integration_tests/Cargo.toml index e642bb1e4..eda534ec4 100644 --- a/tests/integration_tests/Cargo.toml +++ b/tests/integration_tests/Cargo.toml @@ -11,6 +11,7 @@ bytes = "1.0" prost = "0.14" tokio = {version = "1.0", features = ["macros", "rt-multi-thread", "net", "sync"]} tonic = {path = "../../tonic"} +tonic-prost = {path = "../../tonic-prost"} tracing-subscriber = {version = "0.3"} [dev-dependencies] @@ -24,4 +25,4 @@ tower-http = { version = "0.6", features = ["set-header", "trace"] } tower-service = "0.3" [build-dependencies] -tonic-build = {path = "../../tonic-build"} +tonic-prost-build = {path = "../../tonic-prost-build"} diff --git a/tests/integration_tests/build.rs b/tests/integration_tests/build.rs index 143257fc7..f7207bdbc 100644 --- a/tests/integration_tests/build.rs +++ b/tests/integration_tests/build.rs @@ -1,4 +1,4 @@ fn main() { - tonic_build::compile_protos("proto/test.proto").unwrap(); - tonic_build::compile_protos("proto/stream.proto").unwrap(); + tonic_prost_build::compile_protos("proto/test.proto").unwrap(); + tonic_prost_build::compile_protos("proto/stream.proto").unwrap(); } diff --git a/tests/root-crate-path/Cargo.toml b/tests/root-crate-path/Cargo.toml index 10206644d..ac8cab8ff 100644 --- a/tests/root-crate-path/Cargo.toml +++ b/tests/root-crate-path/Cargo.toml @@ -7,6 +7,7 @@ name = "root-crate-path" [dependencies] prost = "0.14" tonic = {path = "../../tonic"} +tonic-prost = {path = "../../tonic-prost"} [build-dependencies] -tonic-build = {path = "../../tonic-build"} +tonic-prost-build = {path = "../../tonic-prost-build"} diff --git a/tests/root-crate-path/build.rs b/tests/root-crate-path/build.rs index b1ed51750..85743c63a 100644 --- a/tests/root-crate-path/build.rs +++ b/tests/root-crate-path/build.rs @@ -1,5 +1,5 @@ fn main() -> Result<(), Box> { - tonic_build::configure() + tonic_prost_build::configure() .extern_path(".foo.bar.baz.Animal", "crate::Animal") .compile_protos(&["foo.proto"], &["."])?; diff --git a/tests/same_name/Cargo.toml b/tests/same_name/Cargo.toml index 9841f6625..65b9ffa10 100644 --- a/tests/same_name/Cargo.toml +++ b/tests/same_name/Cargo.toml @@ -9,6 +9,7 @@ name = "same_name" [dependencies] prost = "0.14" tonic = {path = "../../tonic"} +tonic-prost = {path = "../../tonic-prost"} [build-dependencies] -tonic-build = {path = "../../tonic-build"} +tonic-prost-build = {path = "../../tonic-prost-build"} diff --git a/tests/same_name/build.rs b/tests/same_name/build.rs index ba5ab11cf..6f7506244 100644 --- a/tests/same_name/build.rs +++ b/tests/same_name/build.rs @@ -1,3 +1,3 @@ fn main() { - tonic_build::compile_protos("proto/foo.proto").unwrap(); + tonic_prost_build::compile_protos("proto/foo.proto").unwrap(); } diff --git a/tests/service_named_result/Cargo.toml b/tests/service_named_result/Cargo.toml index 5d8010a42..541602a9f 100644 --- a/tests/service_named_result/Cargo.toml +++ b/tests/service_named_result/Cargo.toml @@ -8,6 +8,7 @@ license = "MIT" [dependencies] prost = "0.14" tonic = {path = "../../tonic"} +tonic-prost = {path = "../../tonic-prost"} [build-dependencies] -tonic-build = {path = "../../tonic-build"} +tonic-prost-build = {path = "../../tonic-prost-build"} diff --git a/tests/service_named_result/build.rs b/tests/service_named_result/build.rs index 851e9f712..2da09f50b 100644 --- a/tests/service_named_result/build.rs +++ b/tests/service_named_result/build.rs @@ -1,3 +1,3 @@ fn main() { - tonic_build::compile_protos("proto/result.proto").unwrap(); + tonic_prost_build::compile_protos("proto/result.proto").unwrap(); } diff --git a/tests/service_named_service/Cargo.toml b/tests/service_named_service/Cargo.toml index 373059896..3af4f086e 100644 --- a/tests/service_named_service/Cargo.toml +++ b/tests/service_named_service/Cargo.toml @@ -9,6 +9,7 @@ name = "service_named_service" [dependencies] prost = "0.14" tonic = {path = "../../tonic"} +tonic-prost = {path = "../../tonic-prost"} [build-dependencies] -tonic-build = {path = "../../tonic-build"} +tonic-prost-build = {path = "../../tonic-prost-build"} diff --git a/tests/service_named_service/build.rs b/tests/service_named_service/build.rs index ba5ab11cf..6f7506244 100644 --- a/tests/service_named_service/build.rs +++ b/tests/service_named_service/build.rs @@ -1,3 +1,3 @@ fn main() { - tonic_build::compile_protos("proto/foo.proto").unwrap(); + tonic_prost_build::compile_protos("proto/foo.proto").unwrap(); } diff --git a/tests/skip_debug/Cargo.toml b/tests/skip_debug/Cargo.toml index 70884156a..aa480c5ea 100644 --- a/tests/skip_debug/Cargo.toml +++ b/tests/skip_debug/Cargo.toml @@ -9,9 +9,10 @@ name = "skip_debug" [dependencies] prost = "0.14" tonic = { path = "../../tonic" } +tonic-prost = { path = "../../tonic-prost" } [dev-dependencies] static_assertions = "1" [build-dependencies] -tonic-build = { path = "../../tonic-build" } +tonic-prost-build = { path = "../../tonic-prost-build" } diff --git a/tests/skip_debug/build.rs b/tests/skip_debug/build.rs index 68ca1cd47..bf55b970b 100644 --- a/tests/skip_debug/build.rs +++ b/tests/skip_debug/build.rs @@ -1,7 +1,7 @@ fn main() { - tonic_build::configure() - .skip_debug("test.Test") - .skip_debug("test.Output") + tonic_prost_build::configure() + .skip_debug(["test.Test"]) + .skip_debug(["test.Output"]) .build_client(true) .build_server(true) .compile_protos(&["proto/test.proto"], &["proto"]) diff --git a/tests/stream_conflict/Cargo.toml b/tests/stream_conflict/Cargo.toml index acb208ebc..abf68ee76 100644 --- a/tests/stream_conflict/Cargo.toml +++ b/tests/stream_conflict/Cargo.toml @@ -9,6 +9,7 @@ license = "MIT" [dependencies] prost = "0.14" tonic = { path = "../../tonic" } +tonic-prost = { path = "../../tonic-prost" } [build-dependencies] -tonic-build = { path = "../../tonic-build" } +tonic-prost-build = { path = "../../tonic-prost-build" } diff --git a/tests/stream_conflict/build.rs b/tests/stream_conflict/build.rs index c284edada..01be752b3 100644 --- a/tests/stream_conflict/build.rs +++ b/tests/stream_conflict/build.rs @@ -1,3 +1,3 @@ fn main() { - tonic_build::compile_protos("proto/stream_conflict.proto").unwrap(); + tonic_prost_build::compile_protos("proto/stream_conflict.proto").unwrap(); } diff --git a/tests/use_arc_self/Cargo.toml b/tests/use_arc_self/Cargo.toml index fd7998602..9a7b6d387 100644 --- a/tests/use_arc_self/Cargo.toml +++ b/tests/use_arc_self/Cargo.toml @@ -7,6 +7,7 @@ name = "use_arc_self" [dependencies] prost = "0.14" tonic = {path = "../../tonic", features = ["gzip"]} +tonic-prost = {path = "../../tonic-prost"} [build-dependencies] -tonic-build = {path = "../../tonic-build" } +tonic-prost-build = {path = "../../tonic-prost-build" } diff --git a/tests/use_arc_self/build.rs b/tests/use_arc_self/build.rs index 37c7fac50..a0e2c317d 100644 --- a/tests/use_arc_self/build.rs +++ b/tests/use_arc_self/build.rs @@ -1,5 +1,5 @@ fn main() { - tonic_build::configure() + tonic_prost_build::configure() .use_arc_self(true) .compile_protos(&["proto/test.proto"], &["proto"]) .unwrap(); diff --git a/tests/web/Cargo.toml b/tests/web/Cargo.toml index cd449258a..edef9fa2b 100644 --- a/tests/web/Cargo.toml +++ b/tests/web/Cargo.toml @@ -14,9 +14,10 @@ prost = "0.14" tokio = { version = "1", features = ["macros", "rt", "net"] } tokio-stream = { version = "0.1", features = ["net"] } tonic = { path = "../../tonic" } +tonic-prost = { path = "../../tonic-prost" } [dev-dependencies] tonic-web = { path = "../../tonic-web" } [build-dependencies] -tonic-build = { path = "../../tonic-build" } +tonic-prost-build = { path = "../../tonic-prost-build" } diff --git a/tests/web/build.rs b/tests/web/build.rs index 0a7bed094..806cf77eb 100644 --- a/tests/web/build.rs +++ b/tests/web/build.rs @@ -1,7 +1,7 @@ fn main() { let protos = &["proto/test.proto"]; - tonic_build::configure() + tonic_prost_build::configure() .compile_protos(protos, &["proto"]) .unwrap(); diff --git a/tests/wellknown-compiled/Cargo.toml b/tests/wellknown-compiled/Cargo.toml index aad64cb0f..65b91bbc5 100644 --- a/tests/wellknown-compiled/Cargo.toml +++ b/tests/wellknown-compiled/Cargo.toml @@ -12,6 +12,7 @@ doctest = false [dependencies] prost = "0.14" tonic = {path = "../../tonic"} +tonic-prost = {path = "../../tonic-prost"} [build-dependencies] -tonic-build = {path = "../../tonic-build"} +tonic-prost-build = {path = "../../tonic-prost-build"} diff --git a/tests/wellknown-compiled/build.rs b/tests/wellknown-compiled/build.rs index b348b8805..ab8c1e685 100644 --- a/tests/wellknown-compiled/build.rs +++ b/tests/wellknown-compiled/build.rs @@ -1,5 +1,5 @@ fn main() { - tonic_build::configure() + tonic_prost_build::configure() .extern_path(".google.protobuf.Empty", "()") .compile_well_known_types(true) .compile_protos(&["proto/google.proto", "proto/test.proto"], &["proto"]) diff --git a/tests/wellknown/Cargo.toml b/tests/wellknown/Cargo.toml index 8c72f2716..f449dd8be 100644 --- a/tests/wellknown/Cargo.toml +++ b/tests/wellknown/Cargo.toml @@ -10,6 +10,7 @@ name = "wellknown" prost = "0.14" prost-types = "0.14" tonic = {path = "../../tonic"} +tonic-prost = {path = "../../tonic-prost"} [build-dependencies] -tonic-build = {path = "../../tonic-build"} +tonic-prost-build = {path = "../../tonic-prost-build"} diff --git a/tests/wellknown/build.rs b/tests/wellknown/build.rs index b3952daff..e9ab87237 100644 --- a/tests/wellknown/build.rs +++ b/tests/wellknown/build.rs @@ -1,3 +1,3 @@ fn main() { - tonic_build::compile_protos("proto/wellknown.proto").unwrap(); + tonic_prost_build::compile_protos("proto/wellknown.proto").unwrap(); } diff --git a/tonic-build/Cargo.toml b/tonic-build/Cargo.toml index bb2e7ef01..63d342e1b 100644 --- a/tonic-build/Cargo.toml +++ b/tonic-build/Cargo.toml @@ -17,15 +17,11 @@ rust-version = { workspace = true } [dependencies] prettyplease = { version = "0.2" } proc-macro2 = "1.0" -prost-build = { version = "0.14", optional = true } -prost-types = { version = "0.14", optional = true } quote = "1.0" syn = "2.0" [features] -default = ["transport", "prost"] -prost = ["prost-build", "dep:prost-types"] -cleanup-markdown = ["prost-build?/cleanup-markdown"] +default = ["transport"] transport = [] [lints] @@ -38,8 +34,4 @@ all-features = true allowed_external_types = [ # major released "proc_macro2::*", - - # not major released - "prost_build::*", - "prost_types::*", ] diff --git a/tonic-build/README.md b/tonic-build/README.md index ab67deccc..3d9fba27b 100644 --- a/tonic-build/README.md +++ b/tonic-build/README.md @@ -1,6 +1,6 @@ # tonic-build -Compiles proto files via prost and generates service stubs and proto definitions for use with tonic. +Provides code generation for service stubs to use with tonic. For protobuf compilation via prost, use the `tonic-prost-build` crate. # Feature flags @@ -21,27 +21,27 @@ tonic = "" prost = "" [build-dependencies] -tonic-build = "" +tonic-prost-build = "" ``` ## Getting Started -`tonic-build` works by being included as a [`build.rs` file](https://doc.rust-lang.org/cargo/reference/build-scripts.html) at the root of the binary/library. +For protobuf compilation, use `tonic-prost-build` in your [`build.rs` file](https://doc.rust-lang.org/cargo/reference/build-scripts.html) at the root of the binary/library. You can rely on the defaults via -```rust,no_run +```rust,no_run,ignore fn main() -> Result<(), Box> { - tonic_build::compile_protos("proto/service.proto")?; + tonic_prost_build::compile_protos("proto/service.proto")?; Ok(()) } ``` Or configure the generated code deeper via -```rust,no_run +```rust,no_run,ignore fn main() -> Result<(), Box> { - tonic_build::configure() + tonic_prost_build::configure() .build_server(false) .compile_protos( &["proto/helloworld/helloworld.proto"], @@ -97,9 +97,9 @@ And a bunch of Google proto files in structure will be like this: Then we can generate Rust code via this setup in our `build.rs`: -```rust,no_run +```rust,no_run,ignore fn main() -> Result<(), Box> { - tonic_build::configure() + tonic_prost_build::configure() .build_server(false) //.out_dir("src/google") // you can change the generated code's location .compile_protos( diff --git a/tonic-build/src/code_gen.rs b/tonic-build/src/code_gen.rs index 7dc750f88..87bb8d215 100644 --- a/tonic-build/src/code_gen.rs +++ b/tonic-build/src/code_gen.rs @@ -1,3 +1,8 @@ +//! Generic code generation for gRPC services. +//! +//! This module provides the generic infrastructure for generating +//! client and server code from service definitions. + use std::collections::HashSet; use proc_macro2::TokenStream; diff --git a/tonic-build/src/compile_settings.rs b/tonic-build/src/compile_settings.rs deleted file mode 100644 index e2de97910..000000000 --- a/tonic-build/src/compile_settings.rs +++ /dev/null @@ -1,14 +0,0 @@ -#[derive(Debug, Clone)] -pub(crate) struct CompileSettings { - #[cfg(feature = "prost")] - pub(crate) codec_path: String, -} - -impl Default for CompileSettings { - fn default() -> Self { - Self { - #[cfg(feature = "prost")] - codec_path: "tonic::codec::ProstCodec".to_string(), - } - } -} diff --git a/tonic-build/src/lib.rs b/tonic-build/src/lib.rs index fef48637c..d4e3220ba 100644 --- a/tonic-build/src/lib.rs +++ b/tonic-build/src/lib.rs @@ -10,16 +10,7 @@ use proc_macro2::{Delimiter, Group, Ident, Literal, Punct, Spacing, Span, TokenStream}; use quote::TokenStreamExt; -/// Prost generator -#[cfg(feature = "prost")] -mod prost; -#[cfg(feature = "prost")] -pub use prost_build::Config; -#[cfg(feature = "prost")] -pub use prost_types::FileDescriptorSet; - -#[cfg(feature = "prost")] -pub use prost::{compile_fds, compile_protos, configure, Builder}; +// Prost functionality has been moved to tonic-prost-build pub mod manual; @@ -31,8 +22,6 @@ mod server; mod code_gen; pub use code_gen::CodeGenBuilder; -mod compile_settings; - /// Service generation trait. /// /// This trait can be implemented and consumed diff --git a/tonic-build/src/prost.rs b/tonic-build/src/prost.rs deleted file mode 100644 index f89d00cd9..000000000 --- a/tonic-build/src/prost.rs +++ /dev/null @@ -1,708 +0,0 @@ -use crate::{code_gen::CodeGenBuilder, compile_settings::CompileSettings}; - -use super::Attributes; -use proc_macro2::TokenStream; -use prost_build::{Config, Method, Service}; -use quote::ToTokens; -use std::{ - collections::HashSet, - ffi::OsString, - io, - path::{Path, PathBuf}, -}; - -/// Configure `tonic-build` code generation. -/// -/// Use [`compile_protos`] instead if you don't need to tweak anything. -pub fn configure() -> Builder { - Builder { - build_client: true, - build_server: true, - build_transport: true, - file_descriptor_set_path: None, - skip_protoc_run: false, - out_dir: None, - extern_path: Vec::new(), - field_attributes: Vec::new(), - message_attributes: Vec::new(), - enum_attributes: Vec::new(), - type_attributes: Vec::new(), - boxed: Vec::new(), - btree_map: None, - bytes: None, - server_attributes: Attributes::default(), - client_attributes: Attributes::default(), - proto_path: "super".to_string(), - compile_well_known_types: false, - emit_package: true, - protoc_args: Vec::new(), - include_file: None, - emit_rerun_if_changed: std::env::var_os("CARGO").is_some(), - disable_comments: HashSet::default(), - use_arc_self: false, - generate_default_stubs: false, - compile_settings: CompileSettings::default(), - skip_debug: HashSet::default(), - } -} - -/// Simple `.proto` compiling. Use [`configure`] instead if you need more options. -/// -/// The include directory will be the parent folder of the specified path. -/// The package name will be the filename without the extension. -pub fn compile_protos(proto: impl AsRef) -> io::Result<()> { - let proto_path: &Path = proto.as_ref(); - - // directory the main .proto file resides in - let proto_dir = proto_path - .parent() - .expect("proto file should reside in a directory"); - - self::configure().compile_protos(&[proto_path], &[proto_dir]) -} - -/// Simple file descriptor set compiling. Use [`configure`] instead if you need more options. -pub fn compile_fds(fds: prost_types::FileDescriptorSet) -> io::Result<()> { - self::configure().compile_fds(fds) -} - -/// Non-path Rust types allowed for request/response types. -const NON_PATH_TYPE_ALLOWLIST: &[&str] = &["()"]; - -/// Newtype wrapper for prost to add tonic-specific extensions -struct TonicBuildService { - prost_service: Service, - methods: Vec, -} - -impl TonicBuildService { - fn new(prost_service: Service, settings: CompileSettings) -> Self { - Self { - // CompileSettings are currently only consumed method-by-method but if you need them in the Service, here's your spot. - // The tonic_build::Service trait specifies that methods are borrowed, so they have to reified up front. - methods: prost_service - .methods - .iter() - .map(|prost_method| TonicBuildMethod { - prost_method: prost_method.clone(), - settings: settings.clone(), - }) - .collect(), - prost_service, - } - } -} - -/// Newtype wrapper for prost to add tonic-specific extensions -struct TonicBuildMethod { - prost_method: Method, - settings: CompileSettings, -} - -impl crate::Service for TonicBuildService { - type Method = TonicBuildMethod; - type Comment = String; - - fn name(&self) -> &str { - &self.prost_service.name - } - - fn package(&self) -> &str { - &self.prost_service.package - } - - fn identifier(&self) -> &str { - &self.prost_service.proto_name - } - - fn comment(&self) -> &[Self::Comment] { - &self.prost_service.comments.leading[..] - } - - fn methods(&self) -> &[Self::Method] { - &self.methods - } -} - -impl crate::Method for TonicBuildMethod { - type Comment = String; - - fn name(&self) -> &str { - &self.prost_method.name - } - - fn identifier(&self) -> &str { - &self.prost_method.proto_name - } - - /// For code generation, you can override the codec. - /// - /// You should set the codec path to an import path that has a free - /// function like `fn default()`. The default value is tonic::codec::ProstCodec, - /// which returns a default-configured ProstCodec. You may wish to configure - /// the codec, e.g., with a buffer configuration. - /// - /// Though ProstCodec implements Default, it is currently only required that - /// the function match the Default trait's function spec. - fn codec_path(&self) -> &str { - &self.settings.codec_path - } - - fn client_streaming(&self) -> bool { - self.prost_method.client_streaming - } - - fn server_streaming(&self) -> bool { - self.prost_method.server_streaming - } - - fn comment(&self) -> &[Self::Comment] { - &self.prost_method.comments.leading[..] - } - - fn deprecated(&self) -> bool { - self.prost_method.options.deprecated.unwrap_or_default() - } - - fn request_response_name( - &self, - proto_path: &str, - compile_well_known_types: bool, - ) -> (TokenStream, TokenStream) { - let convert_type = |proto_type: &str, rust_type: &str| -> TokenStream { - if (is_google_type(proto_type) && !compile_well_known_types) - || rust_type.starts_with("::") - || NON_PATH_TYPE_ALLOWLIST.contains(&rust_type) - { - rust_type.parse::().unwrap() - } else if rust_type.starts_with("crate::") { - syn::parse_str::(rust_type) - .unwrap() - .to_token_stream() - } else { - syn::parse_str::(&format!("{proto_path}::{rust_type}")) - .unwrap() - .to_token_stream() - } - }; - - let request = convert_type( - &self.prost_method.input_proto_type, - &self.prost_method.input_type, - ); - let response = convert_type( - &self.prost_method.output_proto_type, - &self.prost_method.output_type, - ); - (request, response) - } -} - -fn is_google_type(ty: &str) -> bool { - ty.starts_with(".google.protobuf") -} - -struct ServiceGenerator { - builder: Builder, - clients: TokenStream, - servers: TokenStream, -} - -impl ServiceGenerator { - fn new(builder: Builder) -> Self { - ServiceGenerator { - builder, - clients: TokenStream::default(), - servers: TokenStream::default(), - } - } -} - -impl prost_build::ServiceGenerator for ServiceGenerator { - fn generate(&mut self, service: prost_build::Service, _buf: &mut String) { - if self.builder.build_server { - let server = CodeGenBuilder::new() - .emit_package(self.builder.emit_package) - .compile_well_known_types(self.builder.compile_well_known_types) - .attributes(self.builder.server_attributes.clone()) - .disable_comments(self.builder.disable_comments.clone()) - .use_arc_self(self.builder.use_arc_self) - .generate_default_stubs(self.builder.generate_default_stubs) - .generate_server( - &TonicBuildService::new(service.clone(), self.builder.compile_settings.clone()), - &self.builder.proto_path, - ); - - self.servers.extend(server); - } - - if self.builder.build_client { - let client = CodeGenBuilder::new() - .emit_package(self.builder.emit_package) - .compile_well_known_types(self.builder.compile_well_known_types) - .attributes(self.builder.client_attributes.clone()) - .disable_comments(self.builder.disable_comments.clone()) - .build_transport(self.builder.build_transport) - .generate_client( - &TonicBuildService::new(service, self.builder.compile_settings.clone()), - &self.builder.proto_path, - ); - - self.clients.extend(client); - } - } - - fn finalize(&mut self, buf: &mut String) { - if self.builder.build_client && !self.clients.is_empty() { - let clients = &self.clients; - - let client_service = quote::quote! { - #clients - }; - - let ast: syn::File = syn::parse2(client_service).expect("not a valid tokenstream"); - let code = prettyplease::unparse(&ast); - buf.push_str(&code); - - self.clients = TokenStream::default(); - } - - if self.builder.build_server && !self.servers.is_empty() { - let servers = &self.servers; - - let server_service = quote::quote! { - #servers - }; - - let ast: syn::File = syn::parse2(server_service).expect("not a valid tokenstream"); - let code = prettyplease::unparse(&ast); - buf.push_str(&code); - - self.servers = TokenStream::default(); - } - } -} - -/// Service generator builder. -#[derive(Debug, Clone)] -pub struct Builder { - pub(crate) build_client: bool, - pub(crate) build_server: bool, - pub(crate) build_transport: bool, - pub(crate) file_descriptor_set_path: Option, - pub(crate) skip_protoc_run: bool, - pub(crate) extern_path: Vec<(String, String)>, - pub(crate) field_attributes: Vec<(String, String)>, - pub(crate) type_attributes: Vec<(String, String)>, - pub(crate) message_attributes: Vec<(String, String)>, - pub(crate) enum_attributes: Vec<(String, String)>, - pub(crate) boxed: Vec, - pub(crate) btree_map: Option>, - pub(crate) bytes: Option>, - pub(crate) server_attributes: Attributes, - pub(crate) client_attributes: Attributes, - pub(crate) proto_path: String, - pub(crate) emit_package: bool, - pub(crate) compile_well_known_types: bool, - pub(crate) protoc_args: Vec, - pub(crate) include_file: Option, - pub(crate) emit_rerun_if_changed: bool, - pub(crate) disable_comments: HashSet, - pub(crate) use_arc_self: bool, - pub(crate) generate_default_stubs: bool, - pub(crate) compile_settings: CompileSettings, - pub(crate) skip_debug: HashSet, - - out_dir: Option, -} - -impl Builder { - /// Enable or disable gRPC client code generation. - pub fn build_client(mut self, enable: bool) -> Self { - self.build_client = enable; - self - } - - /// Enable or disable gRPC server code generation. - pub fn build_server(mut self, enable: bool) -> Self { - self.build_server = enable; - self - } - - /// Enable or disable generated clients and servers to have built-in tonic - /// transport features. - /// - /// When the `transport` feature is disabled this does nothing. - pub fn build_transport(mut self, enable: bool) -> Self { - self.build_transport = enable; - self - } - - /// Generate a file containing the encoded `prost_types::FileDescriptorSet` for protocol buffers - /// modules. This is required for implementing gRPC Server Reflection. - pub fn file_descriptor_set_path(mut self, path: impl AsRef) -> Self { - self.file_descriptor_set_path = Some(path.as_ref().to_path_buf()); - self - } - - /// In combination with with file_descriptor_set_path, this can be used to provide a file - /// descriptor set as an input file, rather than having prost-build generate the file by - /// calling protoc. - pub fn skip_protoc_run(mut self) -> Self { - self.skip_protoc_run = true; - self - } - - /// Set the output directory to generate code to. - /// - /// Defaults to the `OUT_DIR` environment variable. - pub fn out_dir(mut self, out_dir: impl AsRef) -> Self { - self.out_dir = Some(out_dir.as_ref().to_path_buf()); - self - } - - /// Declare externally provided Protobuf package or type. - /// - /// Passed directly to `prost_build::Config.extern_path`. - /// Note that both the Protobuf path and the rust package paths should both be fully qualified. - /// i.e. Protobuf paths should start with "." and rust paths should start with "::" - pub fn extern_path(mut self, proto_path: impl AsRef, rust_path: impl AsRef) -> Self { - self.extern_path.push(( - proto_path.as_ref().to_string(), - rust_path.as_ref().to_string(), - )); - self - } - - /// Add additional attribute to matched messages, enums, and one-offs. - /// - /// Passed directly to `prost_build::Config.field_attribute`. - pub fn field_attribute, A: AsRef>(mut self, path: P, attribute: A) -> Self { - self.field_attributes - .push((path.as_ref().to_string(), attribute.as_ref().to_string())); - self - } - - /// Add additional attribute to matched messages, enums, and one-offs. - /// - /// Passed directly to `prost_build::Config.type_attribute`. - pub fn type_attribute, A: AsRef>(mut self, path: P, attribute: A) -> Self { - self.type_attributes - .push((path.as_ref().to_string(), attribute.as_ref().to_string())); - self - } - - /// Add additional attribute to matched messages. - /// - /// Passed directly to `prost_build::Config.message_attribute`. - pub fn message_attribute, A: AsRef>( - mut self, - path: P, - attribute: A, - ) -> Self { - self.message_attributes - .push((path.as_ref().to_string(), attribute.as_ref().to_string())); - self - } - - /// Add additional attribute to matched enums. - /// - /// Passed directly to `prost_build::Config.enum_attribute`. - pub fn enum_attribute, A: AsRef>(mut self, path: P, attribute: A) -> Self { - self.enum_attributes - .push((path.as_ref().to_string(), attribute.as_ref().to_string())); - self - } - - /// Add additional boxed fields. - /// - /// Passed directly to `prost_build::Config.boxed`. - pub fn boxed>(mut self, path: P) -> Self { - self.boxed.push(path.as_ref().to_string()); - self - } - - /// Configure the code generator to generate Rust `BTreeMap` fields for Protobuf `map` type - /// fields. - /// - /// Passed directly to `prost_build::Config.btree_map`. - /// - /// Note: previous configured paths for `btree_map` will be cleared. - pub fn btree_map(mut self, paths: I) -> Self - where - I: IntoIterator, - S: AsRef, - { - self.btree_map = Some( - paths - .into_iter() - .map(|path| path.as_ref().to_string()) - .collect(), - ); - self - } - - /// Configure the code generator to generate Rust `bytes::Bytes` fields for Protobuf `bytes` - /// type fields. - /// - /// Passed directly to `prost_build::Config.bytes`. - /// - /// Note: previous configured paths for `bytes` will be cleared. - pub fn bytes(mut self, paths: I) -> Self - where - I: IntoIterator, - S: AsRef, - { - self.bytes = Some( - paths - .into_iter() - .map(|path| path.as_ref().to_string()) - .collect(), - ); - self - } - - /// Add additional attribute to matched server `mod`s. Matches on the package name. - pub fn server_mod_attribute, A: AsRef>( - mut self, - path: P, - attribute: A, - ) -> Self { - self.server_attributes - .push_mod(path.as_ref().to_string(), attribute.as_ref().to_string()); - self - } - - /// Add additional attribute to matched service servers. Matches on the service name. - pub fn server_attribute, A: AsRef>(mut self, path: P, attribute: A) -> Self { - self.server_attributes - .push_struct(path.as_ref().to_string(), attribute.as_ref().to_string()); - self - } - - /// Add additional attribute to matched client `mod`s. Matches on the package name. - pub fn client_mod_attribute, A: AsRef>( - mut self, - path: P, - attribute: A, - ) -> Self { - self.client_attributes - .push_mod(path.as_ref().to_string(), attribute.as_ref().to_string()); - self - } - - /// Add additional attribute to matched service clients. Matches on the service name. - pub fn client_attribute, A: AsRef>(mut self, path: P, attribute: A) -> Self { - self.client_attributes - .push_struct(path.as_ref().to_string(), attribute.as_ref().to_string()); - self - } - - /// Set the path to where tonic will search for the Request/Response proto structs - /// live relative to the module where you call `include_proto!`. - /// - /// This defaults to `super` since tonic will generate code in a module. - pub fn proto_path(mut self, proto_path: impl AsRef) -> Self { - self.proto_path = proto_path.as_ref().to_string(); - self - } - - /// Configure Prost `protoc_args` build arguments. - /// - /// Note: Enabling `--experimental_allow_proto3_optional` requires protobuf >= 3.12. - pub fn protoc_arg>(mut self, arg: A) -> Self { - self.protoc_args.push(arg.as_ref().into()); - self - } - - /// Disable service and rpc comments emission. - pub fn disable_comments(mut self, path: impl AsRef) -> Self { - self.disable_comments.insert(path.as_ref().to_string()); - self - } - - /// Emit `Arc` receiver type in server traits instead of `&self`. - pub fn use_arc_self(mut self, enable: bool) -> Self { - self.use_arc_self = enable; - self - } - - /// Emits GRPC endpoints with no attached package. Effectively ignores protofile package declaration from grpc context. - /// - /// This effectively sets prost's exported package to an empty string. - pub fn disable_package_emission(mut self) -> Self { - self.emit_package = false; - self - } - - /// Enable or disable directing Prost to compile well-known protobuf types instead - /// of using the already-compiled versions available in the `prost-types` crate. - /// - /// This defaults to `false`. - pub fn compile_well_known_types(mut self, compile_well_known_types: bool) -> Self { - self.compile_well_known_types = compile_well_known_types; - self - } - - /// Configures the optional module filename for easy inclusion of all generated Rust files - /// - /// If set, generates a file (inside the `OUT_DIR` or `out_dir()` as appropriate) which contains - /// a set of `pub mod XXX` statements combining to load all Rust files generated. This can allow - /// for a shortcut where multiple related proto files have been compiled together resulting in - /// a semi-complex set of includes. - pub fn include_file(mut self, path: impl AsRef) -> Self { - self.include_file = Some(path.as_ref().to_path_buf()); - self - } - - /// Enable or disable emitting - /// [`cargo:rerun-if-changed=PATH`](https://doc.rust-lang.org/cargo/reference/build-scripts.html#rerun-if-changed) - /// instructions for Cargo. - /// - /// If set, writes instructions to `stdout` for Cargo so that it understands - /// when to rerun the build script. By default, this setting is enabled if - /// the `CARGO` environment variable is set. The `CARGO` environment - /// variable is set by Cargo for build scripts. Therefore, this setting - /// should be enabled automatically when run from a build script. However, - /// the method of detection is not completely reliable since the `CARGO` - /// environment variable can have been set by anything else. If writing the - /// instructions to `stdout` is undesirable, you can disable this setting - /// explicitly. - pub fn emit_rerun_if_changed(mut self, enable: bool) -> Self { - self.emit_rerun_if_changed = enable; - self - } - - /// Enable or disable directing service generation to providing a default implementation for service methods. - /// When this is false all gRPC methods must be explicitly implemented. - /// When this is true any unimplemented service methods will return 'unimplemented' gRPC error code. - /// When this is true all streaming server request RPC types explicitly use tonic::codegen::BoxStream type. - /// - /// This defaults to `false`. - pub fn generate_default_stubs(mut self, enable: bool) -> Self { - self.generate_default_stubs = enable; - self - } - - /// Override the default codec. - /// - /// If set, writes `{codec_path}::default()` in generated code wherever a codec is created. - /// - /// This defaults to `"tonic::codec::ProstCodec"` - pub fn codec_path(mut self, codec_path: impl Into) -> Self { - self.compile_settings.codec_path = codec_path.into(); - self - } - - /// Skips generating `impl Debug` for types - pub fn skip_debug(mut self, path: impl AsRef) -> Self { - self.skip_debug.insert(path.as_ref().to_string()); - self - } - - /// Compile the .proto files and execute code generation. - pub fn compile_protos( - self, - protos: &[impl AsRef], - includes: &[impl AsRef], - ) -> io::Result<()> { - self.compile_protos_with_config(Config::new(), protos, includes) - } - - /// Compile the .proto files and execute code generation using a custom - /// `prost_build::Config`. The provided config will be updated with this builder's config. - pub fn compile_protos_with_config( - self, - mut config: Config, - protos: &[impl AsRef], - includes: &[impl AsRef], - ) -> io::Result<()> { - if self.emit_rerun_if_changed { - for path in protos.iter() { - println!("cargo:rerun-if-changed={}", path.as_ref().display()) - } - - for path in includes.iter() { - // Cargo will watch the **entire** directory recursively. If we - // could figure out which files are imported by our protos we - // could specify only those files instead. - println!("cargo:rerun-if-changed={}", path.as_ref().display()) - } - } - - self.setup_config(&mut config); - config.compile_protos(protos, includes) - } - - /// Execute code generation from a file descriptor set. - pub fn compile_fds(self, fds: prost_types::FileDescriptorSet) -> io::Result<()> { - self.compile_fds_with_config(Config::new(), fds) - } - - /// Execute code generation from a file descriptor set using a custom `prost_build::Config`. - pub fn compile_fds_with_config( - self, - mut config: Config, - fds: prost_types::FileDescriptorSet, - ) -> io::Result<()> { - self.setup_config(&mut config); - config.compile_fds(fds) - } - - fn setup_config(self, config: &mut Config) { - if let Some(out_dir) = self.out_dir.as_ref() { - config.out_dir(out_dir); - } - if let Some(path) = self.file_descriptor_set_path.as_ref() { - config.file_descriptor_set_path(path); - } - if self.skip_protoc_run { - config.skip_protoc_run(); - } - for (proto_path, rust_path) in self.extern_path.iter() { - config.extern_path(proto_path, rust_path); - } - for (prost_path, attr) in self.field_attributes.iter() { - config.field_attribute(prost_path, attr); - } - for (prost_path, attr) in self.type_attributes.iter() { - config.type_attribute(prost_path, attr); - } - for (prost_path, attr) in self.message_attributes.iter() { - config.message_attribute(prost_path, attr); - } - for (prost_path, attr) in self.enum_attributes.iter() { - config.enum_attribute(prost_path, attr); - } - for prost_path in self.boxed.iter() { - config.boxed(prost_path); - } - if let Some(ref paths) = self.btree_map { - config.btree_map(paths); - } - if let Some(ref paths) = self.bytes { - config.bytes(paths); - } - if self.compile_well_known_types { - config.compile_well_known_types(); - } - if let Some(path) = self.include_file.as_ref() { - config.include_file(path); - } - if !self.skip_debug.is_empty() { - config.skip_debug(&self.skip_debug); - } - - for arg in self.protoc_args.iter() { - config.protoc_arg(arg); - } - - config.service_generator(self.service_generator()); - } - - /// Turn the builder into a `ServiceGenerator` ready to be passed to `prost-build`s - /// `Config::service_generator`. - pub fn service_generator(self) -> Box { - Box::new(ServiceGenerator::new(self)) - } -} diff --git a/tonic-health/Cargo.toml b/tonic-health/Cargo.toml index 3f39a782c..e2dfbec3b 100644 --- a/tonic-health/Cargo.toml +++ b/tonic-health/Cargo.toml @@ -18,7 +18,8 @@ rust-version = { workspace = true } prost = "0.14" tokio = {version = "1.0", features = ["sync"]} tokio-stream = {version = "0.1", default-features = false, features = ["sync"]} -tonic = { version = "0.14.0", path = "../tonic", default-features = false, features = ["codegen", "prost"] } +tonic = { version = "0.14.0", path = "../tonic", default-features = false, features = ["codegen"] } +tonic-prost = { version = "0.14.0", path = "../tonic-prost", default-features = false } [dev-dependencies] tokio = {version = "1.0", features = ["rt-multi-thread", "macros"]} diff --git a/tonic-health/src/generated/grpc_health_v1.rs b/tonic-health/src/generated/grpc_health_v1.rs index 1d4b99d3f..19cee943e 100644 --- a/tonic-health/src/generated/grpc_health_v1.rs +++ b/tonic-health/src/generated/grpc_health_v1.rs @@ -152,7 +152,7 @@ pub mod health_client { format!("Service was not ready: {}", e.into()), ) })?; - let codec = tonic::codec::ProstCodec::default(); + let codec = tonic_prost::ProstCodec::default(); let path = http::uri::PathAndQuery::from_static( "/grpc.health.v1.Health/Check", ); @@ -191,7 +191,7 @@ pub mod health_client { format!("Service was not ready: {}", e.into()), ) })?; - let codec = tonic::codec::ProstCodec::default(); + let codec = tonic_prost::ProstCodec::default(); let path = http::uri::PathAndQuery::from_static( "/grpc.health.v1.Health/Watch", ); @@ -356,7 +356,7 @@ pub mod health_server { let inner = self.inner.clone(); let fut = async move { let method = CheckSvc(inner); - let codec = tonic::codec::ProstCodec::default(); + let codec = tonic_prost::ProstCodec::default(); let mut grpc = tonic::server::Grpc::new(codec) .apply_compression_config( accept_compression_encodings, @@ -402,7 +402,7 @@ pub mod health_server { let inner = self.inner.clone(); let fut = async move { let method = WatchSvc(inner); - let codec = tonic::codec::ProstCodec::default(); + let codec = tonic_prost::ProstCodec::default(); let mut grpc = tonic::server::Grpc::new(codec) .apply_compression_config( accept_compression_encodings, diff --git a/tonic-prost-build/Cargo.toml b/tonic-prost-build/Cargo.toml new file mode 100644 index 000000000..6678b38ea --- /dev/null +++ b/tonic-prost-build/Cargo.toml @@ -0,0 +1,37 @@ +[package] +name = "tonic-prost-build" +version = "0.14.0" +authors = ["Lucio Franco "] +edition = "2021" +license = "MIT" +repository = "https://github.com/hyperium/tonic" +homepage = "https://github.com/hyperium/tonic" +description = "Prost build integration for tonic" +readme = "README.md" +categories = ["development-tools::build-utils", "network-programming", "asynchronous"] +keywords = ["rpc", "grpc", "prost", "protobuf", "tonic"] + +[features] +default = ["transport", "cleanup-markdown"] +transport = ["tonic-build/transport"] +cleanup-markdown = ["prost-build/cleanup-markdown"] + +[dependencies] +tonic-build = { version = "0.14.0", path = "../tonic-build", default-features = false, features = ["transport"] } +prost-build = { version = "0.14" } +prost-types = { version = "0.14" } +prettyplease = { version = "0.2" } +proc-macro2 = "1.0" +quote = "1.0" +syn = "2.0" +tempfile = "3.0" + +[dev-dependencies] +tonic = { version = "0.14.0", path = "../tonic", default-features = false } + +[package.metadata.cargo_check_external_types] +allowed_external_types = [ + "tonic_build::*", + "prost_build::*", + "prost_types::*" +] diff --git a/tonic-prost-build/README.md b/tonic-prost-build/README.md new file mode 100644 index 000000000..20d336006 --- /dev/null +++ b/tonic-prost-build/README.md @@ -0,0 +1,29 @@ +# tonic-prost-build + +Prost build integration for [tonic] gRPC framework. + +## Overview + +This crate provides code generation for gRPC services using protobuf definitions via the [prost] ecosystem. It bridges [prost-build] with [tonic]'s generic code generation infrastructure. + +## Usage + +Add to your `build.rs`: + +```rust +fn main() { + tonic_prost_build::configure() + .compile_protos(&["proto/service.proto"], &["proto"]) + .unwrap(); +} +``` + +## Features + +- `prost`: Enables prost-based protobuf code generation (enabled by default) +- `transport`: Enables transport layer code generation +- `cleanup-markdown`: Enables markdown cleanup in generated documentation + +[tonic]: https://github.com/hyperium/tonic +[prost]: https://github.com/tokio-rs/prost +[prost-build]: https://github.com/tokio-rs/prost \ No newline at end of file diff --git a/tonic-prost-build/src/lib.rs b/tonic-prost-build/src/lib.rs new file mode 100644 index 000000000..08f7861f2 --- /dev/null +++ b/tonic-prost-build/src/lib.rs @@ -0,0 +1,902 @@ +//! Prost build integration for tonic. +//! +//! This crate provides code generation for gRPC services using protobuf definitions +//! via the [`prost`] ecosystem. +//! +//! [`prost`]: https://github.com/tokio-rs/prost +//! +//! # Example +//! +//! ```rust,ignore +//! use tonic_prost_build::configure; +//! +//! fn main() { +//! configure() +//! .compile_protos(&["proto/service.proto"], &["proto"]) +//! .unwrap(); +//! } +//! ``` + +#![warn( + missing_docs, + missing_debug_implementations, + rust_2018_idioms, + unreachable_pub +)] +#![doc( + html_logo_url = "https://raw.githubusercontent.com/tokio-rs/website/master/public/img/icons/tonic.svg" +)] +#![doc(html_root_url = "https://docs.rs/tonic-prost-build/0.14.0")] +#![doc(issue_tracker_base_url = "https://github.com/hyperium/tonic/issues/")] + +use proc_macro2::TokenStream; +use prost_build::{Method, Service}; +use quote::{quote, ToTokens}; +use std::{ + collections::HashSet, + ffi::OsString, + io, + path::{Path, PathBuf}, +}; +use tonic_build::{Attributes, CodeGenBuilder}; + +#[cfg(test)] +mod tests; + +// Re-export core build functionality from tonic-build +pub use tonic_build::{ + manual, Attributes as TonicAttributes, Method as TonicMethod, Service as TonicService, +}; + +// Re-export prost types that users might need +pub use prost_build::Config; +pub use prost_types::FileDescriptorSet; + +/// Configure `tonic-prost-build` code generation. +/// +/// Use [`compile_protos`] instead if you don't need to tweak anything. +pub fn configure() -> Builder { + Builder { + build_client: true, + build_server: true, + build_transport: true, + file_descriptor_set_path: None, + skip_protoc_run: false, + out_dir: None, + extern_path: Vec::new(), + field_attributes: Vec::new(), + message_attributes: Vec::new(), + enum_attributes: Vec::new(), + type_attributes: Vec::new(), + boxed: Vec::new(), + btree_map: None, + bytes: None, + server_attributes: Attributes::default(), + client_attributes: Attributes::default(), + proto_path: "super".to_string(), + compile_well_known_types: false, + emit_package: true, + protoc_args: Vec::new(), + include_file: None, + emit_rerun_if_changed: std::env::var_os("CARGO").is_some(), + disable_comments: HashSet::default(), + use_arc_self: false, + generate_default_stubs: false, + codec_path: "tonic_prost::ProstCodec".to_string(), + skip_debug: HashSet::default(), + } +} + +/// Simple `.proto` compiling. Use [`configure`] instead if you need more options. +/// +/// The include directory will be the parent folder of the specified path. +/// The package name will be the filename without the extension. +pub fn compile_protos(proto: impl AsRef) -> io::Result<()> { + let proto_path: &Path = proto.as_ref(); + + // directory the main .proto file resides in + let proto_dir = proto_path + .parent() + .expect("proto file should reside in a directory"); + + self::configure().compile_protos(&[proto_path], &[proto_dir]) +} + +/// Simple file descriptor set compiling. Use [`configure`] instead if you need more options. +pub fn compile_fds(fds: prost_types::FileDescriptorSet) -> io::Result<()> { + self::configure().compile_fds(fds) +} + +/// Non-path Rust types allowed for request/response types. +const NON_PATH_TYPE_ALLOWLIST: &[&str] = &["()"]; + +/// Newtype wrapper for prost to add tonic-specific extensions +struct TonicBuildService { + prost_service: Service, + methods: Vec, +} + +impl TonicBuildService { + fn new(prost_service: Service, codec_path: String) -> Self { + Self { + // The tonic_build::Service trait specifies that methods are borrowed, so they have to reified up front. + methods: prost_service + .methods + .iter() + .map(|prost_method| TonicBuildMethod { + prost_method: prost_method.clone(), + codec_path: codec_path.clone(), + }) + .collect(), + prost_service, + } + } +} + +/// Newtype wrapper for prost to add tonic-specific extensions +struct TonicBuildMethod { + prost_method: Method, + codec_path: String, +} + +impl tonic_build::Service for TonicBuildService { + type Method = TonicBuildMethod; + type Comment = String; + + fn name(&self) -> &str { + &self.prost_service.name + } + + fn package(&self) -> &str { + &self.prost_service.package + } + + fn identifier(&self) -> &str { + &self.prost_service.proto_name + } + + fn methods(&self) -> &[Self::Method] { + &self.methods + } + + fn comment(&self) -> &[Self::Comment] { + &self.prost_service.comments.leading + } +} + +impl tonic_build::Method for TonicBuildMethod { + type Comment = String; + + fn name(&self) -> &str { + &self.prost_method.name + } + + fn identifier(&self) -> &str { + &self.prost_method.proto_name + } + + fn client_streaming(&self) -> bool { + self.prost_method.client_streaming + } + + fn server_streaming(&self) -> bool { + self.prost_method.server_streaming + } + + fn comment(&self) -> &[Self::Comment] { + &self.prost_method.comments.leading + } + + fn request_response_name( + &self, + proto_path: &str, + compile_well_known_types: bool, + ) -> (TokenStream, TokenStream) { + let request = if is_google_type(&self.prost_method.input_type) && !compile_well_known_types + { + // For well-known types, map to absolute paths that will work with super:: + match self.prost_method.input_type.as_str() { + ".google.protobuf.Empty" => quote!(()), + ".google.protobuf.Any" => quote!(::prost_types::Any), + ".google.protobuf.StringValue" => quote!(::prost::alloc::string::String), + _ => { + // For other google types, assume they're in prost_types + let type_name = self + .prost_method + .input_type + .trim_start_matches(".google.protobuf.") + .to_string(); + syn::parse_str::(&format!("::prost_types::{type_name}")) + .unwrap() + .to_token_stream() + } + } + } else if NON_PATH_TYPE_ALLOWLIST + .iter() + .any(|ty| self.prost_method.input_type.ends_with(ty)) + { + self.prost_method.input_type.parse::().unwrap() + } else { + // Check if this is an extern type that starts with :: or crate:: + if self.prost_method.input_type.starts_with("::") + || self.prost_method.input_type.starts_with("crate::") + { + // This is an extern type, use it directly + self.prost_method.input_type.parse::().unwrap() + } else { + // Replace dots with double colons for the type name + let rust_type = self.prost_method.input_type.replace('.', "::"); + // Remove leading :: if present + let rust_type = rust_type.trim_start_matches("::"); + syn::parse_str::(&format!("{proto_path}::{rust_type}")) + .unwrap() + .to_token_stream() + } + }; + + let response = + if is_google_type(&self.prost_method.output_type) && !compile_well_known_types { + // For well-known types, map to absolute paths that will work with super:: + match self.prost_method.output_type.as_str() { + ".google.protobuf.Empty" => quote!(()), + ".google.protobuf.Any" => quote!(::prost_types::Any), + ".google.protobuf.StringValue" => quote!(::prost::alloc::string::String), + _ => { + // For other google types, assume they're in prost_types + let type_name = self + .prost_method + .output_type + .trim_start_matches(".google.protobuf.") + .to_string(); + syn::parse_str::(&format!("::prost_types::{type_name}")) + .unwrap() + .to_token_stream() + } + } + } else if NON_PATH_TYPE_ALLOWLIST + .iter() + .any(|ty| self.prost_method.output_type.ends_with(ty)) + { + self.prost_method + .output_type + .parse::() + .unwrap() + } else { + // Check if this is an extern type that starts with :: or crate:: + if self.prost_method.output_type.starts_with("::") + || self.prost_method.output_type.starts_with("crate::") + { + // This is an extern type, use it directly + self.prost_method + .output_type + .parse::() + .unwrap() + } else { + // Replace dots with double colons for the type name + let rust_type = self.prost_method.output_type.replace('.', "::"); + // Remove leading :: if present + let rust_type = rust_type.trim_start_matches("::"); + syn::parse_str::(&format!("{proto_path}::{rust_type}")) + .unwrap() + .to_token_stream() + } + }; + + (request, response) + } + + fn codec_path(&self) -> &str { + &self.codec_path + } + + fn deprecated(&self) -> bool { + self.prost_method.options.deprecated() + } +} + +fn is_google_type(ty: &str) -> bool { + ty.starts_with(".google.protobuf") +} + +/// Service generator that is compatible with prost-build +#[derive(Debug)] +struct ServiceGenerator { + build_client: bool, + build_server: bool, + build_transport: bool, + client_attributes: Attributes, + server_attributes: Attributes, + use_arc_self: bool, + generate_default_stubs: bool, + proto_path: String, + compile_well_known_types: bool, + codec_path: String, + disable_comments: HashSet, + #[allow(dead_code)] + out_dir: PathBuf, +} + +impl ServiceGenerator { + /// Create a new ServiceGenerator + #[allow(clippy::too_many_arguments)] + fn new( + build_client: bool, + build_server: bool, + build_transport: bool, + client_attributes: Attributes, + server_attributes: Attributes, + use_arc_self: bool, + generate_default_stubs: bool, + proto_path: String, + compile_well_known_types: bool, + codec_path: String, + disable_comments: HashSet, + out_dir: PathBuf, + ) -> Self { + ServiceGenerator { + build_client, + build_server, + build_transport, + client_attributes, + server_attributes, + use_arc_self, + generate_default_stubs, + proto_path, + compile_well_known_types, + codec_path, + disable_comments, + out_dir, + } + } +} + +impl prost_build::ServiceGenerator for ServiceGenerator { + fn generate(&mut self, service: Service, buf: &mut String) { + let tonic_service = TonicBuildService::new(service, self.codec_path.clone()); + + let mut builder = CodeGenBuilder::new(); + builder + .emit_package(true) + .build_transport(self.build_transport) + .compile_well_known_types(self.compile_well_known_types) + .disable_comments(self.disable_comments.clone()) + .use_arc_self(self.use_arc_self) + .generate_default_stubs(self.generate_default_stubs); + + let mut tokens = TokenStream::new(); + + if self.build_client { + builder.attributes(self.client_attributes.clone()); + let client_code = builder.generate_client(&tonic_service, &self.proto_path); + tokens.extend(client_code); + } + + if self.build_server { + builder.attributes(self.server_attributes.clone()); + let server_code = builder.generate_server(&tonic_service, &self.proto_path); + tokens.extend(server_code); + } + + let formatted = prettyplease::unparse(&syn::parse2(tokens).unwrap()); + buf.push_str(&formatted); + } +} + +/// Builder for configuring and generating code from `.proto` files. +#[derive(Debug, Clone)] +pub struct Builder { + build_client: bool, + build_server: bool, + build_transport: bool, + file_descriptor_set_path: Option, + skip_protoc_run: bool, + out_dir: Option, + extern_path: Vec<(String, String)>, + field_attributes: Vec<(String, String)>, + message_attributes: Vec<(String, String)>, + enum_attributes: Vec<(String, String)>, + type_attributes: Vec<(String, String)>, + boxed: Vec, + btree_map: Option>, + bytes: Option>, + server_attributes: Attributes, + client_attributes: Attributes, + proto_path: String, + compile_well_known_types: bool, + emit_package: bool, + protoc_args: Vec, + include_file: Option, + emit_rerun_if_changed: bool, + disable_comments: HashSet, + use_arc_self: bool, + generate_default_stubs: bool, + codec_path: String, + skip_debug: HashSet, +} + +impl Builder { + /// Enable or disable gRPC client code generation. + pub fn build_client(mut self, enable: bool) -> Self { + self.build_client = enable; + self + } + + /// Enable or disable gRPC server code generation. + pub fn build_server(mut self, enable: bool) -> Self { + self.build_server = enable; + self + } + + /// Enable or disable transport-related features. + pub fn build_transport(mut self, enable: bool) -> Self { + self.build_transport = enable; + self + } + + /// Configure the output directory where generated Rust files are written. + /// + /// If unset, defaults to the `OUT_DIR` environment variable. `OUT_DIR` is set by Cargo when + /// executing build scripts, so `out_dir` typically does not need to be configured. + pub fn out_dir(mut self, out_dir: impl AsRef) -> Self { + self.out_dir = Some(out_dir.as_ref().to_path_buf()); + self + } + + /// Declare externally provided Protobuf package or type. + /// + /// Passed directly to `prost_build::Config.extern_path`. + /// Note that both the Protobuf path and the rust package paths should both be fully qualified. + /// i.e. Protobuf path should start with "." and rust path should start with "::" + pub fn extern_path(mut self, proto_path: impl AsRef, rust_path: impl AsRef) -> Self { + self.extern_path.push(( + proto_path.as_ref().to_string(), + rust_path.as_ref().to_string(), + )); + self + } + + /// Add additional attribute to matched messages, enums, and one-offs. + /// + /// Passed directly to `prost_build::Config.field_attribute`. + pub fn field_attribute, A: AsRef>(mut self, path: P, attribute: A) -> Self { + self.field_attributes + .push((path.as_ref().to_string(), attribute.as_ref().to_string())); + self + } + + /// Add additional attribute to matched messages, enums, and one-offs. + /// + /// Passed directly to `prost_build::Config.message_attribute`. + pub fn message_attribute, A: AsRef>( + mut self, + path: P, + attribute: A, + ) -> Self { + self.message_attributes + .push((path.as_ref().to_string(), attribute.as_ref().to_string())); + self + } + + /// Add additional attribute to matched enums and one-offs. + /// + /// Passed directly to `prost_build::Config.enum_attribute`. + pub fn enum_attribute, A: AsRef>(mut self, path: P, attribute: A) -> Self { + self.enum_attributes + .push((path.as_ref().to_string(), attribute.as_ref().to_string())); + self + } + + /// Add additional attribute to matched messages, enums, and one-offs. + /// + /// Passed directly to `prost_build::Config.type_attribute`. + pub fn type_attribute, A: AsRef>(mut self, path: P, attribute: A) -> Self { + self.type_attributes + .push((path.as_ref().to_string(), attribute.as_ref().to_string())); + self + } + + /// Add a field that should be boxed. + /// + /// Passed directly to `prost_build::Config.boxed`. + pub fn boxed>(mut self, path: P) -> Self { + self.boxed.push(path.as_ref().to_string()); + self + } + + /// Configure btree_map on a message. + /// + /// Passed directly to `prost_build::Config.btree_map`. + pub fn btree_map>(mut self, path: P) -> Self { + match &mut self.btree_map { + Some(ref mut paths) => paths.push(path.as_ref().to_string()), + None => self.btree_map = Some(vec![path.as_ref().to_string()]), + } + self + } + + /// Configure bytes on a message. + /// + /// Passed directly to `prost_build::Config.bytes`. + pub fn bytes>(mut self, path: P) -> Self { + match &mut self.bytes { + Some(ref mut paths) => paths.push(path.as_ref().to_string()), + None => self.bytes = Some(vec![path.as_ref().to_string()]), + } + self + } + + /// Add additional attribute to matched server `mod`s. Passed directly to + /// `prost_build::Config.message_attribute` + pub fn server_mod_attribute, A: AsRef>( + mut self, + path: P, + attribute: A, + ) -> Self { + self.server_attributes + .push_mod(path.as_ref(), attribute.as_ref()); + self + } + + /// Add additional attribute to matched service servers. Passed directly to + /// `prost_build::Config.message_attribute` + pub fn server_attribute, A: AsRef>(mut self, path: P, attribute: A) -> Self { + self.server_attributes + .push_struct(path.as_ref(), attribute.as_ref()); + self + } + + /// Add additional attribute to matched client `mod`s. Passed directly to + /// `prost_build::Config.message_attribute` + pub fn client_mod_attribute, A: AsRef>( + mut self, + path: P, + attribute: A, + ) -> Self { + self.client_attributes + .push_mod(path.as_ref(), attribute.as_ref()); + self + } + + /// Add additional attribute to matched service clients. Passed directly to + /// `prost_build::Config.message_attribute` + pub fn client_attribute, A: AsRef>(mut self, path: P, attribute: A) -> Self { + self.client_attributes + .push_struct(path.as_ref(), attribute.as_ref()); + self + } + + /// Set the path to where protobuf types are generated in the module tree. + /// Default is `super`. + /// + /// This should be used in combination with `extern_path` when you want to use types that are + /// defined in other crates or modules, since types generated with `.proto_path("my_types")` + /// will be at the path `my_types::generated_package_name`. + /// + /// This will change the path that is used to include the generated types, for example: + /// - `my_package::my_type` (default `proto_path`) + /// - `my_types::my_package::my_type` (with `proto_path("my_types")`) + /// + /// Use `.extern_path("my.package", "::my_types::my_package")` to use the generated types. + pub fn proto_path(mut self, proto_path: impl AsRef) -> Self { + self.proto_path = proto_path.as_ref().to_string(); + self + } + + /// Enable or disable directing Protobuf to compile well-known types. + /// + /// Passed directly to `prost_build::Config.compile_well_known_types`. + pub fn compile_well_known_types(mut self, enable: bool) -> Self { + self.compile_well_known_types = enable; + self + } + + /// Enable or disable emitting package information. + /// + /// Passed directly to `prost_build::Config.emit_package`. + pub fn emit_package(mut self, enable: bool) -> Self { + self.emit_package = enable; + self + } + + /// Set the output file's path, used to write the file descriptor set. + /// + /// Passed directly to `prost_build::Config.file_descriptor_set_path`. + pub fn file_descriptor_set_path(mut self, path: impl AsRef) -> Self { + self.file_descriptor_set_path = Some(path.as_ref().to_path_buf()); + self + } + + /// Skip building protos and just generate code using the provided file descriptor set. + /// + /// Passed directly to `prost_build::Config.skip_protoc_run`. + pub fn skip_protoc_run(mut self) -> Self { + self.skip_protoc_run = true; + self + } + + /// Add additional protoc arguments. + /// + /// Passed directly to `prost_build::Config.protoc_arg`. + pub fn protoc_arg>(mut self, arg: A) -> Self { + self.protoc_args.push(arg.as_ref().into()); + self + } + + /// Set the include file. + /// + /// Passed directly to `prost_build::Config.include_file`. + pub fn include_file(mut self, path: impl AsRef) -> Self { + self.include_file = Some(path.as_ref().to_path_buf()); + self + } + + /// Controls the generation of `include!` statements in the output files. + /// + /// Passed directly to `prost_build::Config.emit_rerun_if_changed`. + pub fn emit_rerun_if_changed(mut self, enable: bool) -> Self { + self.emit_rerun_if_changed = enable; + self + } + + /// Set the comments that should be disabled. + /// + /// Passed directly to `prost_build::Config.disable_comments`. + pub fn disable_comments(mut self, path: I) -> Self + where + I: IntoIterator, + S: AsRef, + { + self.disable_comments + .extend(path.into_iter().map(|s| s.as_ref().to_string())); + self + } + + /// Use `Arc` on the Server trait. + pub fn use_arc_self(mut self, enable: bool) -> Self { + self.use_arc_self = enable; + self + } + + /// Generate the default stubs for gRPC services. This code will be generated + /// inside of your service module. Ex: `pub mod helloworld { ... }` + pub fn generate_default_stubs(mut self, enable: bool) -> Self { + self.generate_default_stubs = enable; + self + } + + /// Set the codec path for generated gRPC services. + pub fn codec_path(mut self, path: impl AsRef) -> Self { + self.codec_path = path.as_ref().to_string(); + self + } + + /// Configure the code generator not to strip the `Debug` implementation for the request and + /// response types from the generated code. + /// + /// Passed directly to `prost_build::Config.skip_debug`. + pub fn skip_debug(mut self, paths: I) -> Self + where + I: IntoIterator, + S: AsRef, + { + self.skip_debug + .extend(paths.into_iter().map(|s| s.as_ref().to_string())); + self + } + + /// Compile the .proto files and execute code generation. + pub fn compile_protos

(self, protos: &[P], includes: &[P]) -> io::Result<()> + where + P: AsRef, + { + self.compile_with_config(Config::new(), protos, includes) + } + + /// Compile the .proto files and execute code generation with a custom `prost_build::Config`. + /// + /// Note: When using a custom config, any disable_comments settings on the Builder will be ignored + /// to preserve the disable_comments already configured on the provided Config. + pub fn compile_with_config

( + self, + mut config: Config, + protos: &[P], + includes: &[P], + ) -> io::Result<()> + where + P: AsRef, + { + let out_dir = if let Some(out_dir) = self.out_dir.as_ref() { + out_dir.clone() + } else { + PathBuf::from(std::env::var("OUT_DIR").unwrap()) + }; + + config.out_dir(&out_dir); + + for (proto_path, rust_path) in &self.extern_path { + config.extern_path(proto_path, rust_path); + } + + for (prost_path, attr) in &self.field_attributes { + config.field_attribute(prost_path, attr); + } + + for (prost_path, attr) in &self.message_attributes { + config.message_attribute(prost_path, attr); + } + + for (prost_path, attr) in &self.enum_attributes { + config.enum_attribute(prost_path, attr); + } + + for (prost_path, attr) in &self.type_attributes { + config.type_attribute(prost_path, attr); + } + + for prost_path in &self.boxed { + config.boxed(prost_path); + } + + if let Some(ref paths) = self.btree_map { + config.btree_map(paths); + } + + if let Some(ref paths) = self.bytes { + config.bytes(paths); + } + + if self.compile_well_known_types { + config.compile_well_known_types(); + } + + for arg in &self.protoc_args { + config.protoc_arg(arg); + } + + if let Some(path) = &self.include_file { + config.include_file(path); + } + + // Note: We don't pass self.disable_comments to prost Config here + // because those are meant for service/method paths which are handled + // by the ServiceGenerator, not for message paths + + if !self.skip_debug.is_empty() { + config.skip_debug(self.skip_debug.clone()); + } + + if let Some(path) = &self.file_descriptor_set_path { + config.file_descriptor_set_path(path); + } + + if self.skip_protoc_run { + config.skip_protoc_run(); + } + + if self.build_client || self.build_server { + let service_generator = ServiceGenerator::new( + self.build_client, + self.build_server, + self.build_transport, + self.client_attributes, + self.server_attributes, + self.use_arc_self, + self.generate_default_stubs, + self.proto_path, + self.compile_well_known_types, + self.codec_path.clone(), + self.disable_comments, + out_dir, + ); + + config.service_generator(Box::new(service_generator)); + }; + + config.compile_protos(protos, includes)?; + + Ok(()) + } + + /// Compile a [`prost_types::FileDescriptorSet`] and execute code generation. + pub fn compile_fds(self, fds: prost_types::FileDescriptorSet) -> io::Result<()> { + self.compile_fds_with_config(fds, Config::new()) + } + + /// Compile a [`prost_types::FileDescriptorSet`] with a custom `prost_build::Config`. + pub fn compile_fds_with_config( + self, + fds: prost_types::FileDescriptorSet, + mut config: Config, + ) -> io::Result<()> { + let out_dir = if let Some(out_dir) = self.out_dir.as_ref() { + out_dir.clone() + } else { + PathBuf::from(std::env::var("OUT_DIR").unwrap()) + }; + + config.out_dir(&out_dir); + + for (proto_path, rust_path) in &self.extern_path { + config.extern_path(proto_path, rust_path); + } + + for (prost_path, attr) in &self.field_attributes { + config.field_attribute(prost_path, attr); + } + + for (prost_path, attr) in &self.message_attributes { + config.message_attribute(prost_path, attr); + } + + for (prost_path, attr) in &self.enum_attributes { + config.enum_attribute(prost_path, attr); + } + + for (prost_path, attr) in &self.type_attributes { + config.type_attribute(prost_path, attr); + } + + for prost_path in &self.boxed { + config.boxed(prost_path); + } + + if let Some(ref paths) = self.btree_map { + config.btree_map(paths); + } + + if let Some(ref paths) = self.bytes { + config.bytes(paths); + } + + if self.compile_well_known_types { + config.compile_well_known_types(); + } + + for arg in &self.protoc_args { + config.protoc_arg(arg); + } + + if let Some(path) = &self.include_file { + config.include_file(path); + } + + // Note: We don't pass self.disable_comments to prost Config here + // because those are meant for service/method paths which are handled + // by the ServiceGenerator, not for message paths + + if !self.skip_debug.is_empty() { + config.skip_debug(self.skip_debug.clone()); + } + + if let Some(path) = &self.file_descriptor_set_path { + config.file_descriptor_set_path(path); + } + + if self.skip_protoc_run { + config.skip_protoc_run(); + } + + if self.build_client || self.build_server { + let service_generator = ServiceGenerator::new( + self.build_client, + self.build_server, + self.build_transport, + self.client_attributes, + self.server_attributes, + self.use_arc_self, + self.generate_default_stubs, + self.proto_path, + self.compile_well_known_types, + self.codec_path.clone(), + self.disable_comments, + out_dir, + ); + + config.service_generator(Box::new(service_generator)); + }; + + config.compile_fds(fds)?; + + Ok(()) + } +} diff --git a/tonic-prost-build/src/tests.rs b/tonic-prost-build/src/tests.rs new file mode 100644 index 000000000..230793e91 --- /dev/null +++ b/tonic-prost-build/src/tests.rs @@ -0,0 +1,281 @@ +use super::*; +use prost_build::{Comments, Method}; +use quote::quote; + +fn create_test_method(input_type: String, output_type: String) -> TonicBuildMethod { + TonicBuildMethod { + prost_method: Method { + name: "TestMethod".to_string(), + proto_name: "testMethod".to_string(), + comments: Comments { + leading: vec![], + trailing: vec![], + leading_detached: vec![], + }, + input_type: input_type.clone(), + output_type: output_type.clone(), + input_proto_type: input_type, + output_proto_type: output_type, + client_streaming: false, + server_streaming: false, + options: prost_types::MethodOptions::default(), + }, + codec_path: "tonic_prost::ProstCodec".to_string(), + } +} + +#[test] +fn test_request_response_name_google_types_not_compiled() { + // Test Google well-known types when compile_well_known_types is false + let test_cases = vec![ + (".google.protobuf.Empty", quote!(())), + (".google.protobuf.Any", quote!(::prost_types::Any)), + ( + ".google.protobuf.StringValue", + quote!(::prost::alloc::string::String), + ), + ( + ".google.protobuf.Timestamp", + quote!(::prost_types::Timestamp), + ), + (".google.protobuf.Duration", quote!(::prost_types::Duration)), + (".google.protobuf.Value", quote!(::prost_types::Value)), + ]; + + for (type_name, expected) in test_cases { + let method = create_test_method(type_name.to_string(), type_name.to_string()); + let (request, response) = method.request_response_name("super", false); + + assert_eq!( + request.to_string(), + expected.to_string(), + "Failed for input type: {}", + type_name + ); + assert_eq!( + response.to_string(), + expected.to_string(), + "Failed for output type: {}", + type_name + ); + } +} + +#[test] +fn test_request_response_name_google_types_compiled() { + // Test Google well-known types when compile_well_known_types is true + let test_cases = vec![ + ".google.protobuf.Empty", + ".google.protobuf.Any", + ".google.protobuf.StringValue", + ".google.protobuf.Timestamp", + ]; + + for type_name in test_cases { + let method = create_test_method(type_name.to_string(), type_name.to_string()); + let (request, response) = method.request_response_name("super", true); + + // When compile_well_known_types is true, it should use the normal path logic + let expected_path = format!( + "super :: google :: protobuf :: {}", + type_name.trim_start_matches(".google.protobuf.") + ); + + assert_eq!( + request.to_string(), + expected_path, + "Failed for input type: {}", + type_name + ); + assert_eq!( + response.to_string(), + expected_path, + "Failed for output type: {}", + type_name + ); + } +} + +#[test] +fn test_request_response_name_non_path_types() { + // Test types in NON_PATH_TYPE_ALLOWLIST + let method = create_test_method("()".to_string(), "()".to_string()); + let (request, response) = method.request_response_name("super", false); + + assert_eq!(request.to_string(), "()"); + assert_eq!(response.to_string(), "()"); +} + +#[test] +fn test_request_response_name_extern_types() { + // Test extern types that start with :: or crate:: + let test_cases = vec![ + "::my_crate::MyType", + "crate::module::MyType", + "::external::lib::Type", + ]; + + for type_name in test_cases { + let method = create_test_method(type_name.to_string(), type_name.to_string()); + let (request, response) = method.request_response_name("super", false); + + // The parsed TokenStream includes spaces between path segments + let expected = match type_name { + "::my_crate::MyType" => ":: my_crate :: MyType", + "crate::module::MyType" => "crate :: module :: MyType", + "::external::lib::Type" => ":: external :: lib :: Type", + _ => panic!("Unknown test case: {}", type_name), + }; + + assert_eq!( + request.to_string(), + expected, + "Failed for input type: {}", + type_name + ); + assert_eq!( + response.to_string(), + expected, + "Failed for output type: {}", + type_name + ); + } +} + +#[test] +fn test_request_response_name_regular_protobuf_types() { + // Test regular protobuf types (with dots) + let test_cases = vec![ + ("mypackage.MyMessage", "super :: mypackage :: MyMessage"), + ("com.example.User", "super :: com :: example :: User"), + (".mypackage.MyMessage", "super :: mypackage :: MyMessage"), // Leading dot + ( + "nested.package.Message", + "super :: nested :: package :: Message", + ), + ]; + + for (input, expected) in test_cases { + let method = create_test_method(input.to_string(), input.to_string()); + let (request, response) = method.request_response_name("super", false); + + assert_eq!( + request.to_string(), + expected, + "Failed for input type: {}", + input + ); + assert_eq!( + response.to_string(), + expected, + "Failed for output type: {}", + input + ); + } +} + +#[test] +fn test_request_response_name_different_proto_paths() { + // Test with different proto_path values + let method = create_test_method( + "mypackage.MyMessage".to_string(), + "mypackage.MyResponse".to_string(), + ); + + let test_paths = vec!["super", "crate::proto", "crate"]; + + for proto_path in test_paths { + let (request, response) = method.request_response_name(proto_path, false); + + // Handle the case where proto_path contains :: which gets spaced out + let expected_request = if proto_path.contains("::") { + format!( + "{} :: mypackage :: MyMessage", + proto_path.replace("::", " :: ") + ) + } else { + format!("{} :: mypackage :: MyMessage", proto_path) + }; + let expected_response = if proto_path.contains("::") { + format!( + "{} :: mypackage :: MyResponse", + proto_path.replace("::", " :: ") + ) + } else { + format!("{} :: mypackage :: MyResponse", proto_path) + }; + + assert_eq!( + request.to_string(), + expected_request, + "Failed for proto_path: {}", + proto_path + ); + assert_eq!( + response.to_string(), + expected_response, + "Failed for proto_path: {}", + proto_path + ); + } +} + +#[test] +fn test_request_response_name_mixed_types() { + // Test with different request and response types + let method = create_test_method( + ".google.protobuf.Empty".to_string(), + "mypackage.MyResponse".to_string(), + ); + let (request, response) = method.request_response_name("super", false); + + assert_eq!(request.to_string(), "()"); + assert_eq!(response.to_string(), "super :: mypackage :: MyResponse"); + + // Test with extern type as request and google type as response + let method = create_test_method( + "::external::Request".to_string(), + ".google.protobuf.Any".to_string(), + ); + let (request, response) = method.request_response_name("super", false); + + assert_eq!(request.to_string(), ":: external :: Request"); + assert_eq!(response.to_string(), ":: prost_types :: Any"); +} + +#[test] +fn test_is_google_type() { + assert!(is_google_type(".google.protobuf.Empty")); + assert!(is_google_type(".google.protobuf.Any")); + assert!(is_google_type(".google.protobuf.Timestamp")); + + assert!(!is_google_type("google.protobuf.Empty")); // Missing leading dot + assert!(!is_google_type(".google.api.Http")); // Not protobuf package + assert!(!is_google_type("mypackage.Message")); + assert!(!is_google_type("")); +} + +#[test] +fn test_non_path_type_allowlist() { + // Verify that NON_PATH_TYPE_ALLOWLIST contains expected values + assert!(NON_PATH_TYPE_ALLOWLIST.contains(&"()")); + assert_eq!(NON_PATH_TYPE_ALLOWLIST.len(), 1); +} + +#[test] +fn test_edge_cases() { + // Test empty string types - skip this test as empty strings cause parse errors + // This is an edge case that should be handled at a higher level + + // Test types with multiple dots + let method = create_test_method("a.b.c.d.Message".to_string(), "x.y.z.Response".to_string()); + let (request, response) = method.request_response_name("super", false); + assert_eq!(request.to_string(), "super :: a :: b :: c :: d :: Message"); + assert_eq!(response.to_string(), "super :: x :: y :: z :: Response"); + + // Test type that ends with () but has a package + let method = create_test_method("mypackage.()".to_string(), "mypackage.()".to_string()); + let (request, response) = method.request_response_name("super", false); + assert_eq!(request.to_string(), "mypackage . ()"); + assert_eq!(response.to_string(), "mypackage . ()"); +} diff --git a/tonic-prost/Cargo.toml b/tonic-prost/Cargo.toml new file mode 100644 index 000000000..61cf94b1c --- /dev/null +++ b/tonic-prost/Cargo.toml @@ -0,0 +1,30 @@ +[package] +name = "tonic-prost" +version = "0.14.0" +authors = ["Lucio Franco "] +edition = "2021" +license = "MIT" +repository = "https://github.com/hyperium/tonic" +homepage = "https://github.com/hyperium/tonic" +description = "Prost codec implementation for tonic" +readme = "README.md" +categories = ["network-programming", "asynchronous"] +keywords = ["rpc", "grpc", "prost", "protobuf", "tonic"] + +[dependencies] +tonic = { version = "0.14.0", path = "../tonic", default-features = false } +prost = "0.14" +bytes = "1" + +[dev-dependencies] +tokio = { version = "1", features = ["macros", "rt-multi-thread"] } +tokio-stream = "0.1" +http-body = "1" +http-body-util = "0.1" + +[package.metadata.cargo_check_external_types] +allowed_external_types = [ + "tonic::*", + "prost::*", + "prost" +] diff --git a/tonic-prost/README.md b/tonic-prost/README.md new file mode 100644 index 000000000..20933b409 --- /dev/null +++ b/tonic-prost/README.md @@ -0,0 +1,19 @@ +# tonic-prost + +Prost codec implementation for [tonic] gRPC framework. + +## Overview + +This crate provides the `ProstCodec` for encoding and decoding protobuf messages using the [prost] library. + +## Usage + +This crate is typically used through the main `tonic` crate with the `prost` feature enabled (which is enabled by default). + +```toml +[dependencies] +tonic = "0.14" +``` + +[tonic]: https://github.com/hyperium/tonic +[prost]: https://github.com/tokio-rs/prost \ No newline at end of file diff --git a/tonic/src/codec/prost.rs b/tonic-prost/src/codec.rs similarity index 94% rename from tonic/src/codec/prost.rs rename to tonic-prost/src/codec.rs index d676f41a6..bd22fad73 100644 --- a/tonic/src/codec/prost.rs +++ b/tonic-prost/src/codec.rs @@ -1,10 +1,9 @@ -use super::{BufferSettings, Codec, DecodeBuf, Decoder, Encoder}; -use crate::codec::EncodeBuf; -use crate::Status; use prost::Message; use std::marker::PhantomData; +use tonic::codec::{BufferSettings, Codec, DecodeBuf, Decoder, EncodeBuf, Encoder}; +use tonic::Status; -/// A [`Codec`] that implements `application/grpc+proto` via the prost library.. +/// A [`Codec`] that implements `application/grpc+proto` via the prost library. #[derive(Debug, Clone)] pub struct ProstCodec { _pd: PhantomData<(T, U)>, @@ -141,7 +140,7 @@ impl Decoder for ProstDecoder { } } -fn from_decode_error(error: prost::DecodeError) -> crate::Status { +fn from_decode_error(error: prost::DecodeError) -> Status { // Map Protobuf parse errors to an INTERNAL status code, as per // https://github.com/grpc/grpc/blob/master/doc/statuscodes.md Status::internal(error.to_string()) @@ -149,15 +148,13 @@ fn from_decode_error(error: prost::DecodeError) -> crate::Status { #[cfg(test)] mod tests { - use crate::codec::compression::SingleMessageCompressionOverride; - use crate::codec::{ - DecodeBuf, Decoder, EncodeBody, EncodeBuf, Encoder, Streaming, HEADER_SIZE, - }; - use crate::Status; + use super::*; use bytes::{Buf, BufMut, BytesMut}; use http_body::Body; use http_body_util::BodyExt as _; use std::pin::pin; + use tonic::codec::SingleMessageCompressionOverride; + use tonic::codec::{EncodeBody, Streaming, HEADER_SIZE}; const LEN: usize = 10000; // The maximum uncompressed size in bytes for a message. Set to 2MB. @@ -278,8 +275,6 @@ mod tests { #[cfg(not(target_family = "windows"))] #[tokio::test] async fn encode_too_big() { - use crate::codec::EncodeBody; - let encoder = MockEncoder::default(); let msg = vec![0u8; u32::MAX as usize + 1]; @@ -323,7 +318,7 @@ mod tests { Ok(()) } - fn buffer_settings(&self) -> crate::codec::BufferSettings { + fn buffer_settings(&self) -> BufferSettings { Default::default() } } @@ -341,19 +336,19 @@ mod tests { Ok(Some(out)) } - fn buffer_settings(&self) -> crate::codec::BufferSettings { + fn buffer_settings(&self) -> BufferSettings { Default::default() } } mod body { - use crate::Status; use bytes::Bytes; use http_body::{Body, Frame}; use std::{ pin::Pin, task::{Context, Poll}, }; + use tonic::Status; #[derive(Debug)] pub(super) struct MockBody { diff --git a/tonic-prost/src/lib.rs b/tonic-prost/src/lib.rs new file mode 100644 index 000000000..3bbbe07f9 --- /dev/null +++ b/tonic-prost/src/lib.rs @@ -0,0 +1,31 @@ +//! Prost codec implementation for tonic. +//! +//! This crate provides the [`ProstCodec`] for encoding and decoding protobuf +//! messages using the [`prost`] library. +//! +//! # Example +//! +//! ```rust,ignore +//! use tonic_prost::ProstCodec; +//! +//! let codec = ProstCodec::::default(); +//! ``` + +#![warn( + missing_docs, + missing_debug_implementations, + rust_2018_idioms, + unreachable_pub +)] +#![doc( + html_logo_url = "https://raw.githubusercontent.com/tokio-rs/website/master/public/img/icons/tonic.svg" +)] +#![doc(html_root_url = "https://docs.rs/tonic-prost/0.13.1")] +#![doc(issue_tracker_base_url = "https://github.com/hyperium/tonic/issues/")] + +mod codec; + +pub use codec::{ProstCodec, ProstDecoder, ProstEncoder}; + +// Re-export prost types that users might need +pub use prost; diff --git a/tonic-reflection/Cargo.toml b/tonic-reflection/Cargo.toml index 4d8e0f10d..a10856930 100644 --- a/tonic-reflection/Cargo.toml +++ b/tonic-reflection/Cargo.toml @@ -29,7 +29,8 @@ prost = "0.14" prost-types = {version = "0.14", optional = true} tokio = { version = "1.0", features = ["sync", "rt"], optional = true } tokio-stream = {version = "0.1", default-features = false, optional = true } -tonic = { version = "0.14.0", path = "../tonic", default-features = false, features = ["codegen", "prost"] } +tonic = { version = "0.14.0", path = "../tonic", default-features = false, features = ["codegen"] } +tonic-prost = { version = "0.14.0", path = "../tonic-prost", default-features = false } [dev-dependencies] tokio-stream = {version = "0.1", default-features = false, features = ["net"]} diff --git a/tonic-reflection/src/generated/grpc_reflection_v1.rs b/tonic-reflection/src/generated/grpc_reflection_v1.rs index cb56c7ae7..a78eaf20c 100644 --- a/tonic-reflection/src/generated/grpc_reflection_v1.rs +++ b/tonic-reflection/src/generated/grpc_reflection_v1.rs @@ -243,7 +243,7 @@ pub mod server_reflection_client { format!("Service was not ready: {}", e.into()), ) })?; - let codec = tonic::codec::ProstCodec::default(); + let codec = tonic_prost::ProstCodec::default(); let path = http::uri::PathAndQuery::from_static( "/grpc.reflection.v1.ServerReflection/ServerReflectionInfo", ); @@ -404,7 +404,7 @@ pub mod server_reflection_server { let inner = self.inner.clone(); let fut = async move { let method = ServerReflectionInfoSvc(inner); - let codec = tonic::codec::ProstCodec::default(); + let codec = tonic_prost::ProstCodec::default(); let mut grpc = tonic::server::Grpc::new(codec) .apply_compression_config( accept_compression_encodings, diff --git a/tonic-reflection/src/generated/grpc_reflection_v1alpha.rs b/tonic-reflection/src/generated/grpc_reflection_v1alpha.rs index cf2b3aa34..b2afc7f6a 100644 --- a/tonic-reflection/src/generated/grpc_reflection_v1alpha.rs +++ b/tonic-reflection/src/generated/grpc_reflection_v1alpha.rs @@ -243,7 +243,7 @@ pub mod server_reflection_client { format!("Service was not ready: {}", e.into()), ) })?; - let codec = tonic::codec::ProstCodec::default(); + let codec = tonic_prost::ProstCodec::default(); let path = http::uri::PathAndQuery::from_static( "/grpc.reflection.v1alpha.ServerReflection/ServerReflectionInfo", ); @@ -404,7 +404,7 @@ pub mod server_reflection_server { let inner = self.inner.clone(); let fut = async move { let method = ServerReflectionInfoSvc(inner); - let codec = tonic::codec::ProstCodec::default(); + let codec = tonic_prost::ProstCodec::default(); let mut grpc = tonic::server::Grpc::new(codec) .apply_compression_config( accept_compression_encodings, diff --git a/tonic/Cargo.toml b/tonic/Cargo.toml index 27768924c..102c2591b 100644 --- a/tonic/Cargo.toml +++ b/tonic/Cargo.toml @@ -24,8 +24,7 @@ codegen = ["dep:async-trait"] gzip = ["dep:flate2"] deflate = ["dep:flate2"] zstd = ["dep:zstd"] -default = ["router", "transport", "codegen", "prost"] -prost = ["dep:prost"] +default = ["router", "transport", "codegen"] _tls-any = ["dep:tokio-rustls", "dep:tokio", "tokio?/rt", "tokio?/macros"] # Internal. Please choose one of `tls-ring` or `tls-aws-lc` tls-ring = ["_tls-any", "tokio-rustls/ring"] tls-aws-lc = ["_tls-any", "tokio-rustls/aws-lc-rs"] @@ -68,8 +67,6 @@ tower-layer = "0.3" tower-service = "0.3" tokio-stream = {version = "0.1.16", default-features = false} -# prost -prost = {version = "0.14", default-features = false, features = ["std"], optional = true} # codegen async-trait = {version = "0.1.13", optional = true} diff --git a/tonic/src/client/grpc.rs b/tonic/src/client/grpc.rs index 981b72380..607515b29 100644 --- a/tonic/src/client/grpc.rs +++ b/tonic/src/client/grpc.rs @@ -1,5 +1,5 @@ -use crate::codec::compression::{CompressionEncoding, EnabledCompressionEncodings}; use crate::codec::EncodeBody; +use crate::codec::{CompressionEncoding, EnabledCompressionEncodings}; use crate::metadata::GRPC_CONTENT_TYPE; use crate::{ body::Body, diff --git a/tonic/src/codec/compression.rs b/tonic/src/codec/compression.rs index b07e380a8..f7c774d8a 100644 --- a/tonic/src/codec/compression.rs +++ b/tonic/src/codec/compression.rs @@ -297,6 +297,7 @@ pub(crate) fn decompress( Ok(()) } +/// Controls compression behavior for individual messages within a stream. #[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] pub enum SingleMessageCompressionOverride { /// Inherit whatever compression is already configured. If the stream is compressed this diff --git a/tonic/src/codec/mod.rs b/tonic/src/codec/mod.rs index d14a6ddf4..bd5e1fa82 100644 --- a/tonic/src/codec/mod.rs +++ b/tonic/src/codec/mod.rs @@ -7,9 +7,6 @@ mod buffer; pub(crate) mod compression; mod decode; mod encode; -#[cfg(feature = "prost")] -mod prost; - use crate::Status; use std::io; @@ -17,8 +14,11 @@ pub use self::buffer::{DecodeBuf, EncodeBuf}; pub use self::compression::{CompressionEncoding, EnabledCompressionEncodings}; pub use self::decode::Streaming; pub use self::encode::EncodeBody; -#[cfg(feature = "prost")] -pub use self::prost::ProstCodec; + +// Doc hidden since this is used in a test in another crate, we can expose this publically later +// if we need it. +#[doc(hidden)] +pub use self::compression::SingleMessageCompressionOverride; /// Unless overridden, this is the buffer size used for encoding requests. /// This is spent per-rpc, so you may wish to adjust it. The default is @@ -89,8 +89,10 @@ impl Default for BufferSettings { } } -// 5 bytes -const HEADER_SIZE: usize = +// Doc hidden because its used in tests in another crate but not part of the +// public api. +#[doc(hidden)] +pub const HEADER_SIZE: usize = // compression flag std::mem::size_of::() + // data length