diff --git a/build.sbt b/build.sbt index f4af1d05..5b307928 100644 --- a/build.sbt +++ b/build.sbt @@ -161,14 +161,27 @@ lazy val e2e = (projectMatrix in file("e2e")) val output = (Compile / sourceManaged).value / "fs2-grpc" / "disable-trailers" } + val renderContextAsImplicit = new { + val args = Seq( + "fs2_grpc:service_suffix=Fs2GrpcRenderContextAsImplicit", + "fs2_grpc:render_context_as_implicit" + ) ++ (if (tlIsScala3.value) Seq("scala3_sources") else Nil) + + val output = (Compile / sourceManaged).value / "fs2-grpc" / "render-context-as-implicit" + } + Seq( scalapb.gen() -> (Compile / sourceManaged).value / "scalapb", genModule(codegenFullName + "$") -> (Compile / sourceManaged).value / "fs2-grpc", - (genModule(codegenFullName + "$"), disableTrailers.args) -> disableTrailers.output + (genModule(codegenFullName + "$"), disableTrailers.args) -> disableTrailers.output, + (genModule(codegenFullName + "$"), renderContextAsImplicit.args) -> renderContextAsImplicit.output ) }, buildInfoPackage := "fs2.grpc.e2e.buildinfo", - buildInfoKeys := Seq[BuildInfoKey]("sourceManaged" -> (Compile / sourceManaged).value / "fs2-grpc"), + buildInfoKeys := Seq[BuildInfoKey]( + "sourceManaged" -> (Compile / sourceManaged).value / "fs2-grpc", + "scalaVersion" -> scalaVersion.value + ), githubWorkflowArtifactUpload := false, scalacOptions := { if (tlIsScala3.value) { diff --git a/codegen/src/main/scala/fs2/grpc/codegen/Fs2AbstractServicePrinter.scala b/codegen/src/main/scala/fs2/grpc/codegen/Fs2AbstractServicePrinter.scala index 8c07b7ab..e81f8f90 100644 --- a/codegen/src/main/scala/fs2/grpc/codegen/Fs2AbstractServicePrinter.scala +++ b/codegen/src/main/scala/fs2/grpc/codegen/Fs2AbstractServicePrinter.scala @@ -32,6 +32,8 @@ abstract class Fs2AbstractServicePrinter extends Fs2ServicePrinter { val service: ServiceDescriptor val serviceSuffix: String val di: DescriptorImplicits + protected[this] val renderContextAsImplicit: Boolean = false + protected[this] val scala3Sources: Boolean = false import di._ @@ -55,6 +57,18 @@ abstract class Fs2AbstractServicePrinter extends Fs2ServicePrinter { basicClientCall } + protected[this] def renderCtxParameter(): String = { + if (renderContextAsImplicit) { + if (scala3Sources) { + s")(using ctx: $Ctx" + } else { + s")(implicit ctx: $Ctx" + } + } else { + s", ctx: $Ctx" + } + } + private[this] def serviceMethodImplementation(method: MethodDescriptor): PrinterEndo = { p => val inType = method.inputType.scalaType val outType = method.outputType.scalaType @@ -79,6 +93,15 @@ abstract class Fs2AbstractServicePrinter extends Fs2ServicePrinter { val handler = s"$Fs2ServerCallHandler[G](dispatcher, serverOptions).${handleMethod(method)}[$inType, $outType]" val serviceCall = s"serviceImpl.${method.name}" + val invoke = if (renderContextAsImplicit) { + if (scala3Sources) { + s"$serviceCall(r)(using m)" + } else { + s"$serviceCall(r)(m)" + } + } else { + s"$serviceCall(r, m)" + } p.addStringMargin { s"""|.addMethod( @@ -87,7 +110,7 @@ abstract class Fs2AbstractServicePrinter extends Fs2ServicePrinter { | serviceAspect.${visitMethod(method)}[$inType, $outType]( | ${ServiceCallContext}(m, $descriptor), | r, - | (r, m) => $serviceCall(r, m) + | (r, m) => $invoke | ) | } |)""" diff --git a/codegen/src/main/scala/fs2/grpc/codegen/Fs2CodeGenerator.scala b/codegen/src/main/scala/fs2/grpc/codegen/Fs2CodeGenerator.scala index 20107417..5d46065a 100644 --- a/codegen/src/main/scala/fs2/grpc/codegen/Fs2CodeGenerator.scala +++ b/codegen/src/main/scala/fs2/grpc/codegen/Fs2CodeGenerator.scala @@ -33,9 +33,13 @@ import scala.jdk.CollectionConverters.* sealed trait Fs2Params { def serviceSuffix: String def disableTrailers: Boolean + def renderContextAsImplicit: Boolean + def scala3Sources: Boolean def withServiceSuffix(serviceSuffix: String): Fs2Params def withDisableTrailers(value: Boolean): Fs2Params + def withRenderContextAsImplicit(value: Boolean): Fs2Params + def withScala3Sources(value: Boolean): Fs2Params } object Fs2Params { @@ -43,18 +47,28 @@ object Fs2Params { def default: Fs2Params = Impl( serviceSuffix = "Fs2Grpc", - disableTrailers = false + disableTrailers = false, + renderContextAsImplicit = false, + scala3Sources = false ) private final case class Impl( serviceSuffix: String, - disableTrailers: Boolean + disableTrailers: Boolean, + renderContextAsImplicit: Boolean, + scala3Sources: Boolean ) extends Fs2Params { def withServiceSuffix(serviceSuffix: String): Fs2Params = copy(serviceSuffix = serviceSuffix) def withDisableTrailers(value: Boolean): Fs2Params = copy(disableTrailers = value) + + def withRenderContextAsImplicit(value: Boolean): Fs2Params = + copy(renderContextAsImplicit = value) + + def withScala3Sources(value: Boolean): Fs2Params = + copy(scala3Sources = value) } } @@ -92,7 +106,13 @@ object Fs2CodeGenerator extends CodeGenApp { service, fs2params.serviceSuffix + "Trailers", di, - new Fs2GrpcExhaustiveTrailersServicePrinter(_, fs2params.serviceSuffix + "Trailers", di) + new Fs2GrpcExhaustiveTrailersServicePrinter( + _, + fs2params.serviceSuffix + "Trailers", + fs2params.renderContextAsImplicit, + fs2params.scala3Sources, + di + ) ) ) @@ -102,7 +122,13 @@ object Fs2CodeGenerator extends CodeGenApp { service, fs2params.serviceSuffix, di, - new Fs2GrpcServicePrinter(_, fs2params.serviceSuffix, di) + new Fs2GrpcServicePrinter( + _, + fs2params.serviceSuffix, + fs2params.renderContextAsImplicit, + fs2params.scala3Sources, + di + ) ) trailers :+ general @@ -114,9 +140,11 @@ 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.default)) { + initial = Fs2Params.default.withScala3Sources(params.scala3Sources) + suffix <- unparsed.map(_.split("=", 2).toList).foldLeft[Either[String, Fs2Params]](Right(initial)) { case (Right(params), ServiceSuffix :: suffix :: Nil) => Right(params.withServiceSuffix(suffix)) case (Right(params), DisableTrailers :: Nil) => Right(params.withDisableTrailers(true)) + case (Right(params), RenderContextAsImplicit :: Nil) => Right(params.withRenderContextAsImplicit(true)) case (Right(_), ServiceSuffixPluginKey :: _ :: Nil) => Left(s"The '$ServiceSuffixPluginKey' is replaced with '$ServiceSuffix'") case (Right(_), xs) => Left(s"Unrecognized parameter: $xs") @@ -152,4 +180,5 @@ object Fs2CodeGenerator extends CodeGenApp { private[codegen] val ServiceSuffixPluginKey: String = "serviceSuffix" private[codegen] val ServiceSuffix: String = "fs2_grpc:service_suffix" private[codegen] val DisableTrailers: String = "fs2_grpc:disable_trailers" + private[codegen] val RenderContextAsImplicit: String = "fs2_grpc:render_context_as_implicit" } diff --git a/codegen/src/main/scala/fs2/grpc/codegen/Fs2GrpcExhaustiveTrailersServicePrinter.scala b/codegen/src/main/scala/fs2/grpc/codegen/Fs2GrpcExhaustiveTrailersServicePrinter.scala index 67616c26..ee6f6e95 100644 --- a/codegen/src/main/scala/fs2/grpc/codegen/Fs2GrpcExhaustiveTrailersServicePrinter.scala +++ b/codegen/src/main/scala/fs2/grpc/codegen/Fs2GrpcExhaustiveTrailersServicePrinter.scala @@ -27,6 +27,8 @@ import scalapb.compiler.{DescriptorImplicits, StreamType} class Fs2GrpcExhaustiveTrailersServicePrinter( val service: ServiceDescriptor, val serviceSuffix: String, + override val renderContextAsImplicit: Boolean, + override val scala3Sources: Boolean, val di: DescriptorImplicits ) extends Fs2AbstractServicePrinter { import fs2.grpc.codegen.Fs2AbstractServicePrinter.constants._ @@ -36,13 +38,13 @@ class Fs2GrpcExhaustiveTrailersServicePrinter( val scalaInType = method.inputType.scalaType val scalaOutType = method.outputType.scalaType - val ctx = s"ctx: $Ctx" + val ctx = renderCtxParameter() s"def ${method.name}" + (method.streamType match { - case StreamType.Unary => s"(request: $scalaInType, $ctx): F[($scalaOutType, $Metadata)]" - case StreamType.ClientStreaming => s"(request: $Stream[F, $scalaInType], $ctx): F[($scalaOutType, $Metadata)]" - case StreamType.ServerStreaming => s"(request: $scalaInType, $ctx): $Stream[F, $scalaOutType]" - case StreamType.Bidirectional => s"(request: $Stream[F, $scalaInType], $ctx): $Stream[F, $scalaOutType]" + case StreamType.Unary => s"(request: $scalaInType$ctx): F[($scalaOutType, $Metadata)]" + case StreamType.ClientStreaming => s"(request: $Stream[F, $scalaInType]$ctx): F[($scalaOutType, $Metadata)]" + case StreamType.ServerStreaming => s"(request: $scalaInType$ctx): $Stream[F, $scalaOutType]" + case StreamType.Bidirectional => s"(request: $Stream[F, $scalaInType]$ctx): $Stream[F, $scalaOutType]" }) } diff --git a/codegen/src/main/scala/fs2/grpc/codegen/Fs2GrpcServicePrinter.scala b/codegen/src/main/scala/fs2/grpc/codegen/Fs2GrpcServicePrinter.scala index 31061b1c..72a835b2 100644 --- a/codegen/src/main/scala/fs2/grpc/codegen/Fs2GrpcServicePrinter.scala +++ b/codegen/src/main/scala/fs2/grpc/codegen/Fs2GrpcServicePrinter.scala @@ -24,8 +24,13 @@ package fs2.grpc.codegen import com.google.protobuf.Descriptors.{MethodDescriptor, ServiceDescriptor} import scalapb.compiler.{DescriptorImplicits, StreamType} -class Fs2GrpcServicePrinter(val service: ServiceDescriptor, val serviceSuffix: String, val di: DescriptorImplicits) - extends Fs2AbstractServicePrinter { +class Fs2GrpcServicePrinter( + val service: ServiceDescriptor, + val serviceSuffix: String, + override val renderContextAsImplicit: Boolean, + override val scala3Sources: Boolean, + val di: DescriptorImplicits +) extends Fs2AbstractServicePrinter { import fs2.grpc.codegen.Fs2AbstractServicePrinter.constants._ import di._ @@ -33,13 +38,13 @@ class Fs2GrpcServicePrinter(val service: ServiceDescriptor, val serviceSuffix: S val scalaInType = method.inputType.scalaType val scalaOutType = method.outputType.scalaType - val ctx = s"ctx: $Ctx" + val ctx = renderCtxParameter() s"def ${method.name}" + (method.streamType match { - case StreamType.Unary => s"(request: $scalaInType, $ctx): F[$scalaOutType]" - case StreamType.ClientStreaming => s"(request: $Stream[F, $scalaInType], $ctx): F[$scalaOutType]" - case StreamType.ServerStreaming => s"(request: $scalaInType, $ctx): $Stream[F, $scalaOutType]" - case StreamType.Bidirectional => s"(request: $Stream[F, $scalaInType], $ctx): $Stream[F, $scalaOutType]" + case StreamType.Unary => s"(request: $scalaInType$ctx): F[$scalaOutType]" + case StreamType.ClientStreaming => s"(request: $Stream[F, $scalaInType]$ctx): F[$scalaOutType]" + case StreamType.ServerStreaming => s"(request: $scalaInType$ctx): $Stream[F, $scalaOutType]" + case StreamType.Bidirectional => s"(request: $Stream[F, $scalaInType]$ctx): $Stream[F, $scalaOutType]" }) } diff --git a/e2e/src/test/resources/TestServiceFs2GrpcRenderContextAsImplicit.scala.txt b/e2e/src/test/resources/TestServiceFs2GrpcRenderContextAsImplicit.scala.txt new file mode 100644 index 00000000..19cad83d --- /dev/null +++ b/e2e/src/test/resources/TestServiceFs2GrpcRenderContextAsImplicit.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 TestServiceFs2GrpcRenderContextAsImplicit[F[_], A] { + /** Unary RPC: no streaming in either direction + */ + def noStreaming(request: hello.world.TestMessage)(implicit 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])(implicit ctx: A): F[hello.world.TestMessage] + /** Server streaming RPC: client sends one request, server streams responses + */ + def serverStreaming(request: hello.world.TestMessage)(implicit 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])(implicit ctx: A): _root_.fs2.Stream[F, hello.world.TestMessage] +} + +object TestServiceFs2GrpcRenderContextAsImplicit extends _root_.fs2.grpc.GeneratedCompanion[TestServiceFs2GrpcRenderContextAsImplicit] { + + 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 + ): TestServiceFs2GrpcRenderContextAsImplicit[F, A] = new TestServiceFs2GrpcRenderContextAsImplicit[F, A] { + def noStreaming(request: hello.world.TestMessage)(implicit 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])(implicit 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)(implicit 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])(implicit 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: TestServiceFs2GrpcRenderContextAsImplicit[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/resources/TestServiceFs2GrpcRenderContextAsImplicitScala3.scala.txt b/e2e/src/test/resources/TestServiceFs2GrpcRenderContextAsImplicitScala3.scala.txt new file mode 100644 index 00000000..98afe457 --- /dev/null +++ b/e2e/src/test/resources/TestServiceFs2GrpcRenderContextAsImplicitScala3.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 TestServiceFs2GrpcRenderContextAsImplicit[F[_], A] { + /** Unary RPC: no streaming in either direction + */ + def noStreaming(request: hello.world.TestMessage)(using 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])(using ctx: A): F[hello.world.TestMessage] + /** Server streaming RPC: client sends one request, server streams responses + */ + def serverStreaming(request: hello.world.TestMessage)(using 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])(using ctx: A): _root_.fs2.Stream[F, hello.world.TestMessage] +} + +object TestServiceFs2GrpcRenderContextAsImplicit extends _root_.fs2.grpc.GeneratedCompanion[TestServiceFs2GrpcRenderContextAsImplicit] { + + 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 + ): TestServiceFs2GrpcRenderContextAsImplicit[F, A] = new TestServiceFs2GrpcRenderContextAsImplicit[F, A] { + def noStreaming(request: hello.world.TestMessage)(using 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])(using 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)(using 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])(using 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: TestServiceFs2GrpcRenderContextAsImplicit[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)(using 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)(using 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)(using 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)(using m) + ) + } + ) + .build() + } + +} diff --git a/e2e/src/test/resources/TestServiceFs2GrpcRenderContextAsImplicitTrailers.scala.txt b/e2e/src/test/resources/TestServiceFs2GrpcRenderContextAsImplicitTrailers.scala.txt new file mode 100644 index 00000000..4587e3ad --- /dev/null +++ b/e2e/src/test/resources/TestServiceFs2GrpcRenderContextAsImplicitTrailers.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 TestServiceFs2GrpcRenderContextAsImplicitTrailers[F[_], A] { + /** Unary RPC: no streaming in either direction + */ + def noStreaming(request: hello.world.TestMessage)(implicit ctx: A): F[(hello.world.TestMessage, _root_.io.grpc.Metadata)] + /** Client streaming RPC: client streams, server returns a single response + */ + def clientStreaming(request: _root_.fs2.Stream[F, hello.world.TestMessage])(implicit ctx: A): F[(hello.world.TestMessage, _root_.io.grpc.Metadata)] + /** Server streaming RPC: client sends one request, server streams responses + */ + def serverStreaming(request: hello.world.TestMessage)(implicit 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])(implicit ctx: A): _root_.fs2.Stream[F, hello.world.TestMessage] +} + +object TestServiceFs2GrpcRenderContextAsImplicitTrailers extends _root_.fs2.grpc.GeneratedCompanion[TestServiceFs2GrpcRenderContextAsImplicitTrailers] { + + 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 + ): TestServiceFs2GrpcRenderContextAsImplicitTrailers[F, A] = new TestServiceFs2GrpcRenderContextAsImplicitTrailers[F, A] { + def noStreaming(request: hello.world.TestMessage)(implicit ctx: A): F[(hello.world.TestMessage, _root_.io.grpc.Metadata)] = + clientAspect.visitUnaryToUnaryCallTrailers[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(_.unaryToUnaryCallTrailers(req, m)) + ) + def clientStreaming(request: _root_.fs2.Stream[F, hello.world.TestMessage])(implicit ctx: A): F[(hello.world.TestMessage, _root_.io.grpc.Metadata)] = + clientAspect.visitStreamingToUnaryCallTrailers[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(_.streamingToUnaryCallTrailers(req, m)) + ) + def serverStreaming(request: hello.world.TestMessage)(implicit 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])(implicit 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: TestServiceFs2GrpcRenderContextAsImplicitTrailers[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).unaryToUnaryCallTrailers[hello.world.TestMessage, hello.world.TestMessage]{ (r, m) => + serviceAspect.visitUnaryToUnaryCallTrailers[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).streamingToUnaryCallTrailers[hello.world.TestMessage, hello.world.TestMessage]{ (r, m) => + serviceAspect.visitStreamingToUnaryCallTrailers[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/resources/TestServiceFs2GrpcRenderContextAsImplicitTrailersScala3.scala.txt b/e2e/src/test/resources/TestServiceFs2GrpcRenderContextAsImplicitTrailersScala3.scala.txt new file mode 100644 index 00000000..a4a132d0 --- /dev/null +++ b/e2e/src/test/resources/TestServiceFs2GrpcRenderContextAsImplicitTrailersScala3.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 TestServiceFs2GrpcRenderContextAsImplicitTrailers[F[_], A] { + /** Unary RPC: no streaming in either direction + */ + def noStreaming(request: hello.world.TestMessage)(using ctx: A): F[(hello.world.TestMessage, _root_.io.grpc.Metadata)] + /** Client streaming RPC: client streams, server returns a single response + */ + def clientStreaming(request: _root_.fs2.Stream[F, hello.world.TestMessage])(using ctx: A): F[(hello.world.TestMessage, _root_.io.grpc.Metadata)] + /** Server streaming RPC: client sends one request, server streams responses + */ + def serverStreaming(request: hello.world.TestMessage)(using 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])(using ctx: A): _root_.fs2.Stream[F, hello.world.TestMessage] +} + +object TestServiceFs2GrpcRenderContextAsImplicitTrailers extends _root_.fs2.grpc.GeneratedCompanion[TestServiceFs2GrpcRenderContextAsImplicitTrailers] { + + 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 + ): TestServiceFs2GrpcRenderContextAsImplicitTrailers[F, A] = new TestServiceFs2GrpcRenderContextAsImplicitTrailers[F, A] { + def noStreaming(request: hello.world.TestMessage)(using ctx: A): F[(hello.world.TestMessage, _root_.io.grpc.Metadata)] = + clientAspect.visitUnaryToUnaryCallTrailers[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(_.unaryToUnaryCallTrailers(req, m)) + ) + def clientStreaming(request: _root_.fs2.Stream[F, hello.world.TestMessage])(using ctx: A): F[(hello.world.TestMessage, _root_.io.grpc.Metadata)] = + clientAspect.visitStreamingToUnaryCallTrailers[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(_.streamingToUnaryCallTrailers(req, m)) + ) + def serverStreaming(request: hello.world.TestMessage)(using 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])(using 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: TestServiceFs2GrpcRenderContextAsImplicitTrailers[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).unaryToUnaryCallTrailers[hello.world.TestMessage, hello.world.TestMessage]{ (r, m) => + serviceAspect.visitUnaryToUnaryCallTrailers[hello.world.TestMessage, hello.world.TestMessage]( + _root_.fs2.grpc.server.ServiceCallContext(m, hello.world.TestServiceGrpc.METHOD_NO_STREAMING), + r, + (r, m) => serviceImpl.noStreaming(r)(using m) + ) + } + ) + .addMethod( + hello.world.TestServiceGrpc.METHOD_CLIENT_STREAMING, + _root_.fs2.grpc.server.Fs2ServerCallHandler[G](dispatcher, serverOptions).streamingToUnaryCallTrailers[hello.world.TestMessage, hello.world.TestMessage]{ (r, m) => + serviceAspect.visitStreamingToUnaryCallTrailers[hello.world.TestMessage, hello.world.TestMessage]( + _root_.fs2.grpc.server.ServiceCallContext(m, hello.world.TestServiceGrpc.METHOD_CLIENT_STREAMING), + r, + (r, m) => serviceImpl.clientStreaming(r)(using 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)(using 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)(using m) + ) + } + ) + .build() + } + +} diff --git a/e2e/src/test/scala/fs2/grpc/e2e/Fs2CodeGeneratorRenderContextAsImplicitSpec.scala b/e2e/src/test/scala/fs2/grpc/e2e/Fs2CodeGeneratorRenderContextAsImplicitSpec.scala new file mode 100644 index 00000000..7a125410 --- /dev/null +++ b/e2e/src/test/scala/fs2/grpc/e2e/Fs2CodeGeneratorRenderContextAsImplicitSpec.scala @@ -0,0 +1,65 @@ +/* + * 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, scalaVersion} +import hello.world._ + +import scala.io.Source + +class Fs2CodeGeneratorRenderContextAsImplicitSpec extends munit.FunSuite { + + val sourcesGenerated = new File(sourceManaged.getAbsolutePath, "render-context-as-implicit/hello/world") + val suffix = if (scalaVersion.startsWith("3")) "Scala3" else "" + + test("code generator outputs correct service file") { + val fileName = "TestServiceFs2GrpcRenderContextAsImplicit" + val testFileName = s"$fileName.scala" + val referenceName = s"$fileName$suffix.scala" + val reference = Source.fromResource(s"$referenceName.txt").getLines().mkString("\n") + val generated = Source.fromFile(new File(sourcesGenerated, testFileName)).getLines().mkString("\n") + + assertEquals(generated, reference) + } + + test("code generator outputs correct service file for trailers") { + val fileName = s"TestServiceFs2GrpcRenderContextAsImplicitTrailers" + val testFileName = s"$fileName.scala" + val referenceName = s"$fileName$suffix.scala" + val reference = Source.fromResource(s"$referenceName.txt").getLines().mkString("\n") + val generated = Source.fromFile(new File(sourcesGenerated, testFileName)).getLines().mkString("\n") + + assertEquals(generated, reference) + } + + test("implicit of companion resolves") { + implicitly[GeneratedCompanion[TestServiceFs2GrpcRenderContextAsImplicit]] + } + + test("implicit of companion resolves trailers") { + implicitly[GeneratedCompanion[TestServiceFs2GrpcRenderContextAsImplicitTrailers]] + } + +} diff --git a/plugin/src/main/scala/fs2/grpc/codegen/Fs2GrpcPlugin.scala b/plugin/src/main/scala/fs2/grpc/codegen/Fs2GrpcPlugin.scala index aa96b653..9886b234 100644 --- a/plugin/src/main/scala/fs2/grpc/codegen/Fs2GrpcPlugin.scala +++ b/plugin/src/main/scala/fs2/grpc/codegen/Fs2GrpcPlugin.scala @@ -81,6 +81,22 @@ object Fs2GrpcPlugin extends AutoPlugin { case object Fs2GrpcDisableTrailers extends CodeGeneratorOption { override def toString: String = "fs2_grpc:disable_trailers" } + + /** Renders context in the implicit position. + * + * Scala 2: + * {{{ + * def noStreaming(request: Req)(implicit ctx: A): F[Resp] + * }}} + * + * Scala 3, when `Scala3Sources` code generator is used: + * {{{ + * def noStreaming(request: Req)(using ctx: A): F[Resp] + * }}} + */ + case object Fs2GrpcRenderContextAsImplicit extends CodeGeneratorOption { + override def toString: String = "fs2_grpc:render_context_as_implicit" + } } val scalapbCodeGeneratorOptions = diff --git a/plugin/src/sbt-test/fs2-grpc-sbt/code-generator-option-render-context-as-implicit/build.sbt b/plugin/src/sbt-test/fs2-grpc-sbt/code-generator-option-render-context-as-implicit/build.sbt new file mode 100644 index 00000000..ad965310 --- /dev/null +++ b/plugin/src/sbt-test/fs2-grpc-sbt/code-generator-option-render-context-as-implicit/build.sbt @@ -0,0 +1,8 @@ +lazy val root = project + .in(file(".")) + .enablePlugins(Fs2Grpc) + .settings( + scalaVersion := "3.7.3", + scalapbCodeGeneratorOptions += CodeGeneratorOption.Scala3Sources, + scalapbCodeGeneratorOptions += CodeGeneratorOption.Fs2GrpcRenderContextAsImplicit + ) diff --git a/plugin/src/sbt-test/fs2-grpc-sbt/code-generator-option-render-context-as-implicit/project/plugins.sbt b/plugin/src/sbt-test/fs2-grpc-sbt/code-generator-option-render-context-as-implicit/project/plugins.sbt new file mode 100644 index 00000000..884dc7cb --- /dev/null +++ b/plugin/src/sbt-test/fs2-grpc-sbt/code-generator-option-render-context-as-implicit/project/plugins.sbt @@ -0,0 +1 @@ +addSbtPlugin("org.typelevel" % "sbt-fs2-grpc" % System.getProperty("plugin.version")) diff --git a/plugin/src/sbt-test/fs2-grpc-sbt/code-generator-option-render-context-as-implicit/src/main/protobuf/test_service.proto b/plugin/src/sbt-test/fs2-grpc-sbt/code-generator-option-render-context-as-implicit/src/main/protobuf/test_service.proto new file mode 100644 index 00000000..28d77618 --- /dev/null +++ b/plugin/src/sbt-test/fs2-grpc-sbt/code-generator-option-render-context-as-implicit/src/main/protobuf/test_service.proto @@ -0,0 +1,18 @@ +syntax = "proto3"; + +package hello.world; + +// TestService: Example gRPC service used in e2e tests +// It demonstrates all four RPC shapes. +service TestService { + // Unary RPC: no streaming in either direction + rpc noStreaming (TestMessage) returns (TestMessage); + // Client streaming RPC: client streams, server returns a single response + rpc clientStreaming (stream TestMessage) returns (TestMessage); + // Server streaming RPC: client sends one request, server streams responses + rpc serverStreaming (TestMessage) returns (stream TestMessage); + // Bidirectional streaming RPC: both client and server stream + rpc bothStreaming (stream TestMessage) returns (stream TestMessage); +} + +message TestMessage {} diff --git a/plugin/src/sbt-test/fs2-grpc-sbt/code-generator-option-render-context-as-implicit/test b/plugin/src/sbt-test/fs2-grpc-sbt/code-generator-option-render-context-as-implicit/test new file mode 100644 index 00000000..8517342d --- /dev/null +++ b/plugin/src/sbt-test/fs2-grpc-sbt/code-generator-option-render-context-as-implicit/test @@ -0,0 +1,3 @@ +> compile +$ exec bash -c 'grep -q "(using ctx: A)" target/scala-3.7.3/src_managed/main/fs2-grpc/hello/world/test_service/TestServiceFs2Grpc.scala || exit 1' +$ exec bash -c 'grep -q "(using ctx: A)" target/scala-3.7.3/src_managed/main/fs2-grpc/hello/world/test_service/TestServiceFs2GrpcTrailers.scala || exit 1'