diff --git a/build.sbt b/build.sbt index 4fca21f6..209db598 100644 --- a/build.sbt +++ b/build.sbt @@ -18,7 +18,7 @@ inThisBuild( List( githubWorkflowBuildSbtStepPreamble := Seq(), scalaVersion := Scala3, - tlBaseVersion := "2.11", + tlBaseVersion := "3.0", startYear := Some(2018), licenses := Seq(("MIT", url("https://github.com/typelevel/fs2-grpc/blob/master/LICENSE"))), organizationName := "Gary Coady / Fs2 Grpc Developers", @@ -146,10 +146,18 @@ lazy val e2e = (projectMatrix in file("e2e")) ceMunit % Test, "io.grpc" % "grpc-inprocess" % versions.grpc % Test ), - Compile / PB.targets := Seq( - scalapb.gen() -> (Compile / sourceManaged).value / "scalapb", - genModule(codegenFullName + "$") -> (Compile / sourceManaged).value / "fs2-grpc" - ), + Compile / PB.targets := { + val disableTrailers = new { + val args = Seq("serviceSuffix=Fs2GrpcDisableTrailers", "fs2_grpc:disable_trailers") + val output = (Compile / sourceManaged).value / "fs2-grpc" / "disable-trailers" + } + + Seq( + scalapb.gen() -> (Compile / sourceManaged).value / "scalapb", + genModule(codegenFullName + "$") -> (Compile / sourceManaged).value / "fs2-grpc", + (genModule(codegenFullName + "$"), disableTrailers.args) -> disableTrailers.output + ) + }, buildInfoPackage := "fs2.grpc.e2e.buildinfo", buildInfoKeys := Seq[BuildInfoKey]("sourceManaged" -> (Compile / sourceManaged).value / "fs2-grpc"), githubWorkflowArtifactUpload := false, diff --git a/codegen/src/main/scala/fs2/grpc/codegen/Fs2CodeGenerator.scala b/codegen/src/main/scala/fs2/grpc/codegen/Fs2CodeGenerator.scala index 44601485..d914b261 100644 --- a/codegen/src/main/scala/fs2/grpc/codegen/Fs2CodeGenerator.scala +++ b/codegen/src/main/scala/fs2/grpc/codegen/Fs2CodeGenerator.scala @@ -30,7 +30,33 @@ import scalapb.options.Scalapb import scala.jdk.CollectionConverters.* -final case class Fs2Params(serviceSuffix: String = "Fs2Grpc") +sealed trait Fs2Params { + def serviceSuffix: String + def disableTrailers: Boolean + + def withServiceSuffix(serviceSuffix: String): Fs2Params + def withDisableTrailers(value: Boolean): Fs2Params +} + +object Fs2Params { + + def default: Fs2Params = + Impl( + serviceSuffix = "Fs2Grpc", + disableTrailers = false + ) + + private final case class Impl( + serviceSuffix: String, + disableTrailers: Boolean + ) extends Fs2Params { + def withServiceSuffix(serviceSuffix: String): Fs2Params = + copy(serviceSuffix = serviceSuffix) + + def withDisableTrailers(value: Boolean): Fs2Params = + copy(disableTrailers = value) + } +} object Fs2CodeGenerator extends CodeGenApp { @@ -56,20 +82,30 @@ object Fs2CodeGenerator extends CodeGenApp { di: DescriptorImplicits ): Seq[PluginProtos.CodeGeneratorResponse.File] = { file.getServices.asScala.flatMap { service => - generateServiceFile( - file, - service, - fs2params.serviceSuffix + "Trailers", - di, - new Fs2GrpcExhaustiveTrailersServicePrinter(_, fs2params.serviceSuffix + "Trailers", di) - ) :: + val trailers = + if (fs2params.disableTrailers) + Nil + else + List( + generateServiceFile( + file, + service, + fs2params.serviceSuffix + "Trailers", + di, + new Fs2GrpcExhaustiveTrailersServicePrinter(_, fs2params.serviceSuffix + "Trailers", di) + ) + ) + + val general = generateServiceFile( file, service, fs2params.serviceSuffix, di, new Fs2GrpcServicePrinter(_, fs2params.serviceSuffix, di) - ) :: Nil + ) + + trailers :+ general }.toSeq } @@ -78,8 +114,9 @@ object Fs2CodeGenerator extends CodeGenApp { paramsAndUnparsed <- GeneratorParams.fromStringCollectUnrecognized(params) params = paramsAndUnparsed._1 unparsed = paramsAndUnparsed._2 - suffix <- unparsed.map(_.split("=", 2).toList).foldLeft[Either[String, Fs2Params]](Right(Fs2Params())) { - case (Right(params), ServiceSuffix :: suffix :: Nil) => Right(params.copy(serviceSuffix = suffix)) + suffix <- unparsed.map(_.split("=", 2).toList).foldLeft[Either[String, Fs2Params]](Right(Fs2Params.default)) { + case (Right(params), ServiceSuffix :: suffix :: Nil) => Right(params.withServiceSuffix(suffix)) + case (Right(params), DisableTrailers :: Nil) => Right(params.withDisableTrailers(true)) case (Right(_), xs) => Left(s"Unrecognized parameter: $xs") case (Left(e), _) => Left(e) } @@ -111,4 +148,5 @@ object Fs2CodeGenerator extends CodeGenApp { } private[codegen] val ServiceSuffix: String = "serviceSuffix" + private[codegen] val DisableTrailers: String = "fs2_grpc:disable_trailers" } diff --git a/e2e/src/test/resources/TestServiceFs2GrpcDisableTrailers.scala.txt b/e2e/src/test/resources/TestServiceFs2GrpcDisableTrailers.scala.txt new file mode 100644 index 00000000..782b7994 --- /dev/null +++ b/e2e/src/test/resources/TestServiceFs2GrpcDisableTrailers.scala.txt @@ -0,0 +1,110 @@ +package hello.world + +import _root_.cats.syntax.all._ + +/** TestService: Example gRPC service used in e2e tests + * It demonstrates all four RPC shapes. + */ +trait TestServiceFs2GrpcDisableTrailers[F[_], A] { + /** Unary RPC: no streaming in either direction + */ + def noStreaming(request: hello.world.TestMessage, ctx: A): F[hello.world.TestMessage] + /** Client streaming RPC: client streams, server returns a single response + */ + def clientStreaming(request: _root_.fs2.Stream[F, hello.world.TestMessage], ctx: A): F[hello.world.TestMessage] + /** Server streaming RPC: client sends one request, server streams responses + */ + def serverStreaming(request: hello.world.TestMessage, ctx: A): _root_.fs2.Stream[F, hello.world.TestMessage] + /** Bidirectional streaming RPC: both client and server stream + */ + def bothStreaming(request: _root_.fs2.Stream[F, hello.world.TestMessage], ctx: A): _root_.fs2.Stream[F, hello.world.TestMessage] +} + +object TestServiceFs2GrpcDisableTrailers extends _root_.fs2.grpc.GeneratedCompanion[TestServiceFs2GrpcDisableTrailers] { + + def serviceDescriptor: _root_.io.grpc.ServiceDescriptor = hello.world.TestServiceGrpc.SERVICE + + def mkClientFull[F[_], G[_]: _root_.cats.effect.Async, A]( + dispatcher: _root_.cats.effect.std.Dispatcher[G], + channel: _root_.io.grpc.Channel, + clientAspect: _root_.fs2.grpc.client.ClientAspect[F, G, A], + clientOptions: _root_.fs2.grpc.client.ClientOptions + ): TestServiceFs2GrpcDisableTrailers[F, A] = new TestServiceFs2GrpcDisableTrailers[F, A] { + def noStreaming(request: hello.world.TestMessage, ctx: A): F[hello.world.TestMessage] = + clientAspect.visitUnaryToUnaryCall[hello.world.TestMessage, hello.world.TestMessage]( + _root_.fs2.grpc.client.ClientCallContext(ctx, hello.world.TestServiceGrpc.METHOD_NO_STREAMING), + request, + (req, m) => _root_.fs2.grpc.client.Fs2ClientCall[G](channel, hello.world.TestServiceGrpc.METHOD_NO_STREAMING, dispatcher, clientOptions).flatMap(_.unaryToUnaryCall(req, m)) + ) + def clientStreaming(request: _root_.fs2.Stream[F, hello.world.TestMessage], ctx: A): F[hello.world.TestMessage] = + clientAspect.visitStreamingToUnaryCall[hello.world.TestMessage, hello.world.TestMessage]( + _root_.fs2.grpc.client.ClientCallContext(ctx, hello.world.TestServiceGrpc.METHOD_CLIENT_STREAMING), + request, + (req, m) => _root_.fs2.grpc.client.Fs2ClientCall[G](channel, hello.world.TestServiceGrpc.METHOD_CLIENT_STREAMING, dispatcher, clientOptions).flatMap(_.streamingToUnaryCall(req, m)) + ) + def serverStreaming(request: hello.world.TestMessage, ctx: A): _root_.fs2.Stream[F, hello.world.TestMessage] = + clientAspect.visitUnaryToStreamingCall[hello.world.TestMessage, hello.world.TestMessage]( + _root_.fs2.grpc.client.ClientCallContext(ctx, hello.world.TestServiceGrpc.METHOD_SERVER_STREAMING), + request, + (req, m) => _root_.fs2.Stream.eval(_root_.fs2.grpc.client.Fs2ClientCall[G](channel, hello.world.TestServiceGrpc.METHOD_SERVER_STREAMING, dispatcher, clientOptions)).flatMap(_.unaryToStreamingCall(req, m)) + ) + def bothStreaming(request: _root_.fs2.Stream[F, hello.world.TestMessage], ctx: A): _root_.fs2.Stream[F, hello.world.TestMessage] = + clientAspect.visitStreamingToStreamingCall[hello.world.TestMessage, hello.world.TestMessage]( + _root_.fs2.grpc.client.ClientCallContext(ctx, hello.world.TestServiceGrpc.METHOD_BOTH_STREAMING), + request, + (req, m) => _root_.fs2.Stream.eval(_root_.fs2.grpc.client.Fs2ClientCall[G](channel, hello.world.TestServiceGrpc.METHOD_BOTH_STREAMING, dispatcher, clientOptions)).flatMap(_.streamingToStreamingCall(req, m)) + ) + } + + protected def serviceBindingFull[F[_], G[_]: _root_.cats.effect.Async, A]( + dispatcher: _root_.cats.effect.std.Dispatcher[G], + serviceImpl: TestServiceFs2GrpcDisableTrailers[F, A], + serviceAspect: _root_.fs2.grpc.server.ServiceAspect[F, G, A], + serverOptions: _root_.fs2.grpc.server.ServerOptions + ) = { + _root_.io.grpc.ServerServiceDefinition + .builder(hello.world.TestServiceGrpc.SERVICE) + .addMethod( + hello.world.TestServiceGrpc.METHOD_NO_STREAMING, + _root_.fs2.grpc.server.Fs2ServerCallHandler[G](dispatcher, serverOptions).unaryToUnaryCall[hello.world.TestMessage, hello.world.TestMessage]{ (r, m) => + serviceAspect.visitUnaryToUnaryCall[hello.world.TestMessage, hello.world.TestMessage]( + _root_.fs2.grpc.server.ServiceCallContext(m, hello.world.TestServiceGrpc.METHOD_NO_STREAMING), + r, + (r, m) => serviceImpl.noStreaming(r, m) + ) + } + ) + .addMethod( + hello.world.TestServiceGrpc.METHOD_CLIENT_STREAMING, + _root_.fs2.grpc.server.Fs2ServerCallHandler[G](dispatcher, serverOptions).streamingToUnaryCall[hello.world.TestMessage, hello.world.TestMessage]{ (r, m) => + serviceAspect.visitStreamingToUnaryCall[hello.world.TestMessage, hello.world.TestMessage]( + _root_.fs2.grpc.server.ServiceCallContext(m, hello.world.TestServiceGrpc.METHOD_CLIENT_STREAMING), + r, + (r, m) => serviceImpl.clientStreaming(r, m) + ) + } + ) + .addMethod( + hello.world.TestServiceGrpc.METHOD_SERVER_STREAMING, + _root_.fs2.grpc.server.Fs2ServerCallHandler[G](dispatcher, serverOptions).unaryToStreamingCall[hello.world.TestMessage, hello.world.TestMessage]{ (r, m) => + serviceAspect.visitUnaryToStreamingCall[hello.world.TestMessage, hello.world.TestMessage]( + _root_.fs2.grpc.server.ServiceCallContext(m, hello.world.TestServiceGrpc.METHOD_SERVER_STREAMING), + r, + (r, m) => serviceImpl.serverStreaming(r, m) + ) + } + ) + .addMethod( + hello.world.TestServiceGrpc.METHOD_BOTH_STREAMING, + _root_.fs2.grpc.server.Fs2ServerCallHandler[G](dispatcher, serverOptions).streamingToStreamingCall[hello.world.TestMessage, hello.world.TestMessage]{ (r, m) => + serviceAspect.visitStreamingToStreamingCall[hello.world.TestMessage, hello.world.TestMessage]( + _root_.fs2.grpc.server.ServiceCallContext(m, hello.world.TestServiceGrpc.METHOD_BOTH_STREAMING), + r, + (r, m) => serviceImpl.bothStreaming(r, m) + ) + } + ) + .build() + } + +} diff --git a/e2e/src/test/scala/fs2/grpc/e2e/Fs2CodeGeneratorDisableTrailersSpec.scala b/e2e/src/test/scala/fs2/grpc/e2e/Fs2CodeGeneratorDisableTrailersSpec.scala new file mode 100644 index 00000000..a392cd5a --- /dev/null +++ b/e2e/src/test/scala/fs2/grpc/e2e/Fs2CodeGeneratorDisableTrailersSpec.scala @@ -0,0 +1,52 @@ +/* + * Copyright (c) 2018 Gary Coady / Fs2 Grpc Developers + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +package fs2.grpc.e2e + +import java.io.File + +import fs2.grpc.GeneratedCompanion +import fs2.grpc.e2e.buildinfo.BuildInfo.sourceManaged +import hello.world._ + +import scala.io.Source + +class Fs2CodeGeneratorDisableTrailersSpec extends munit.FunSuite { + + val sourcesGenerated = new File(sourceManaged.getAbsolutePath, "disable-trailers/hello/world") + + test("code generator outputs correct service file") { + val testFileName = "TestServiceFs2GrpcDisableTrailers.scala" + val reference = Source.fromResource(s"$testFileName.txt").getLines().mkString("\n") + val generated = Source.fromFile(new File(sourcesGenerated, testFileName)).getLines().mkString("\n") + + assertEquals(generated, reference) + } + + test("code generator outputs: do not generate trailers") { + assertEquals(sourcesGenerated.list().length, 1) + } + + test("implicit of companion resolves") { + implicitly[GeneratedCompanion[TestServiceFs2GrpcDisableTrailers]] + } + +} diff --git a/plugin/src/main/scala/fs2/grpc/codegen/Fs2GrpcPlugin.scala b/plugin/src/main/scala/fs2/grpc/codegen/Fs2GrpcPlugin.scala index 4cd7478e..7906e095 100644 --- a/plugin/src/main/scala/fs2/grpc/codegen/Fs2GrpcPlugin.scala +++ b/plugin/src/main/scala/fs2/grpc/codegen/Fs2GrpcPlugin.scala @@ -70,6 +70,10 @@ object Fs2GrpcPlugin extends AutoPlugin { case object Scala3Sources extends CodeGeneratorOption { override def toString: String = "scala3_sources" } + // Disable generation of the trailers + case object Fs2GrpcDisableTrailers extends CodeGeneratorOption { + override def toString: String = "fs2_grpc:disable_trailers" + } } val scalapbCodeGeneratorOptions =