From f00c7b37df711d5fb8a33b41a408dd847b577aae Mon Sep 17 00:00:00 2001 From: Rich Dougherty Date: Thu, 30 Aug 2018 20:32:06 +1200 Subject: [PATCH] WIP Play routers and services --- .../scala/akka/grpc/gen/CodeGenerator.scala | 30 +++++++++ .../scala/akka/grpc/gen/javadsl/Service.scala | 4 ++ .../play/PlayJavaClientCodeGenerator.scala | 15 +---- .../play/PlayJavaServerCodeGenerator.scala | 35 ++++++++++- .../akka/grpc/gen/scaladsl/Service.scala | 5 +- .../play/PlayScalaClientCodeGenerator.scala | 30 +-------- .../play/PlayScalaServerCodeGenerator.scala | 34 +++++++++- .../PlayJava/AkkaGrpcClientModule.scala.txt | 2 +- .../AkkaGrpcServiceModule.scala.txt | 34 ++++++++++ .../templates/PlayJavaServer/Router.scala.txt | 25 ++++---- .../PlayScala/AkkaGrpcServiceModule.scala.txt | 29 +++++++++ .../templates/PlayScala/Router.scala.txt | 11 ++-- .../PlayScalaClientCodeGeneratorSpec.scala | 12 ++-- .../java/controllers/GreeterServiceImpl.java | 19 +++--- .../akka/grpc/gen/PlayJavaRouterSpec.scala | 4 +- .../controllers/GreeterServiceImpl.scala | 4 +- .../akka/grpc/gen/PlayScalaRouterSpec.scala | 3 +- .../scala/akka/grpc/internal/PlayRouter.scala | 25 ++------ .../AbstractAkkaGrpcServiceBindings.scala | 62 +++++++++++++++++++ 19 files changed, 278 insertions(+), 105 deletions(-) create mode 100644 codegen/src/main/twirl/templates/PlayJavaServer/AkkaGrpcServiceModule.scala.txt create mode 100644 codegen/src/main/twirl/templates/PlayScala/AkkaGrpcServiceModule.scala.txt create mode 100644 runtime/src/main/scala/akka/grpc/scaladsl/play/AbstractAkkaGrpcServiceBindings.scala diff --git a/codegen/src/main/scala/akka/grpc/gen/CodeGenerator.scala b/codegen/src/main/scala/akka/grpc/gen/CodeGenerator.scala index ab8bf9c8a..59c5dd9a7 100644 --- a/codegen/src/main/scala/akka/grpc/gen/CodeGenerator.scala +++ b/codegen/src/main/scala/akka/grpc/gen/CodeGenerator.scala @@ -4,10 +4,15 @@ package akka.grpc.gen +import akka.grpc.gen.javadsl.Service +import akka.grpc.gen.scaladsl.Service +import akka.grpc.gen.scaladsl.play.PlayScalaClientCodeGenerator import com.google.protobuf.compiler.PluginProtos.CodeGeneratorRequest import com.google.protobuf.compiler.PluginProtos.CodeGeneratorResponse import protocbridge.Artifact +import scala.annotation.tailrec + /** * Code generator trait that is not directly bound to scala-pb or protoc (other than the types). */ @@ -23,3 +28,28 @@ trait CodeGenerator { final def run(request: Array[Byte], logger: Logger): Array[Byte] = run(CodeGeneratorRequest.parseFrom(request), logger: Logger).toByteArray } + +private[gen] object CodeGenerator { + + /** Extract the longest common package prefix for a list of packages. */ + private[gen] def commonPackage(packages: Seq[String]): String = + packages.reduce(commonPackage(_, _)) + + /** Extract the longest common package prefix for two packages. */ + private[gen] def commonPackage(a: String, b: String): String = { + if (a == b) a else { + val aPackages = a.split('.') + val bPackages = b.split('.') + @tailrec + def countIdenticalPackage(pos: Int): Int = { + if (aPackages(pos) == bPackages(pos)) countIdenticalPackage(pos + 1) + else pos + } + + val prefixLength = countIdenticalPackage(0) + if (prefixLength == 0) "" // no common, use root package + else aPackages.take(prefixLength).mkString(".") + } + } + +} \ No newline at end of file diff --git a/codegen/src/main/scala/akka/grpc/gen/javadsl/Service.scala b/codegen/src/main/scala/akka/grpc/gen/javadsl/Service.scala index 66e1ac9a4..a331fe1eb 100644 --- a/codegen/src/main/scala/akka/grpc/gen/javadsl/Service.scala +++ b/codegen/src/main/scala/akka/grpc/gen/javadsl/Service.scala @@ -4,6 +4,7 @@ package akka.grpc.gen.javadsl +import akka.grpc.gen.CodeGenerator import com.google.protobuf.Descriptors.{ FileDescriptor, ServiceDescriptor } import scala.collection.JavaConverters._ @@ -22,4 +23,7 @@ object Service { fileDesc.getPackage + "." + serviceDescriptor.getName, serviceDescriptor.getMethods.asScala.map(method ⇒ Method(method)).to[immutable.Seq]) } + + private[javadsl] def commonPackage(allServices: Seq[Service]): String = + CodeGenerator.commonPackage(allServices.map(_.packageName)) } diff --git a/codegen/src/main/scala/akka/grpc/gen/javadsl/play/PlayJavaClientCodeGenerator.scala b/codegen/src/main/scala/akka/grpc/gen/javadsl/play/PlayJavaClientCodeGenerator.scala index f61976d79..c3a3a2e03 100644 --- a/codegen/src/main/scala/akka/grpc/gen/javadsl/play/PlayJavaClientCodeGenerator.scala +++ b/codegen/src/main/scala/akka/grpc/gen/javadsl/play/PlayJavaClientCodeGenerator.scala @@ -4,15 +4,12 @@ package akka.grpc.gen.javadsl.play -import akka.grpc.gen.Logger import akka.grpc.gen.javadsl.{ JavaCodeGenerator, Service } import akka.grpc.gen.scaladsl.play.PlayScalaClientCodeGenerator +import akka.grpc.gen.{ CodeGenerator, Logger } import com.google.protobuf.compiler.PluginProtos.CodeGeneratorResponse import templates.PlayJava.txt.{ AkkaGrpcClientModule, ClientProvider } -import scala.annotation.tailrec -import akka.grpc.gen.scaladsl.play.PlayScalaClientCodeGenerator - object PlayJavaClientCodeGenerator extends PlayJavaClientCodeGenerator trait PlayJavaClientCodeGenerator extends JavaCodeGenerator { @@ -43,13 +40,5 @@ trait PlayJavaClientCodeGenerator extends JavaCodeGenerator { } private[play] def packageForSharedModuleFile(allServices: Seq[Service]): String = - // single service or all services in single package - use that - if (allServices.forall(_.packageName == allServices.head.packageName)) allServices.head.packageName - else { - // try to find longest common prefix - allServices.tail.foldLeft(allServices.head.packageName)((packageName, service) => - if (packageName == service.packageName) packageName - else PlayScalaClientCodeGenerator.commonPackage(packageName, service.packageName)) - } - + CodeGenerator.commonPackage(allServices.map(_.packageName)) } diff --git a/codegen/src/main/scala/akka/grpc/gen/javadsl/play/PlayJavaServerCodeGenerator.scala b/codegen/src/main/scala/akka/grpc/gen/javadsl/play/PlayJavaServerCodeGenerator.scala index 43b29f75d..cef07b03b 100644 --- a/codegen/src/main/scala/akka/grpc/gen/javadsl/play/PlayJavaServerCodeGenerator.scala +++ b/codegen/src/main/scala/akka/grpc/gen/javadsl/play/PlayJavaServerCodeGenerator.scala @@ -7,11 +7,14 @@ package akka.grpc.gen.javadsl.play import akka.grpc.gen.Logger import akka.grpc.gen.javadsl.{ JavaCodeGenerator, Service } import com.google.protobuf.compiler.PluginProtos.CodeGeneratorResponse -import templates.PlayJavaServer.txt.Router +import templates.PlayJavaServer.txt.{ AkkaGrpcServiceModule, Router } object PlayJavaServerCodeGenerator extends PlayJavaServerCodeGenerator trait PlayJavaServerCodeGenerator extends JavaCodeGenerator { + + val ServiceModuleName = "AkkaGrpcServiceModule" + override def name: String = "akka-grpc-play-server-java" override def perServiceContent: Set[(Logger, Service) ⇒ CodeGeneratorResponse.File] = @@ -20,8 +23,36 @@ trait PlayJavaServerCodeGenerator extends JavaCodeGenerator { private val generateRouter: (Logger, Service) => CodeGeneratorResponse.File = (logger, service) => { val b = CodeGeneratorResponse.File.newBuilder() b.setContent(Router(service).body) - b.setName(s"${service.packageDir}/Abstract${service.name}Router.java") + b.setName(s"${service.packageDir}/${service.name}Router.java") b.build } + // FIXME: This code is duplicated for the Scala codegen too + override def staticContent(logger: Logger, allServices: Seq[Service]): Set[CodeGeneratorResponse.File] = { + if (allServices.nonEmpty) { + val packageName = Service.commonPackage(allServices) + val b = CodeGeneratorResponse.File.newBuilder() + b.setContent(AkkaGrpcServiceModule(packageName, allServices).body) + b.setName(s"${packageName.replace('.', '/')}/${ServiceModuleName}.java") + val set = Set(b.build) + + logger.info(s"Generated '$packageName.$ServiceModuleName'. \n" + + s"Add 'play.modules.enabled += $packageName.$ServiceModuleName' to your configuration to bind services. " + + s"The following services will be bound: \n" + + allServices.map { service => + val serviceClass = service.packageName + "." + service.name + s""" * @Named("impl") $serviceClass -> ${serviceClass}Impl\n""" + + s""" - You will need to create the implementation class '${serviceClass}Impl'.\n""" + + s""" - To use a different implementation class, set 'akka.grpc.service."$serviceClass".class' to a new classname.\n""" + + s""" - To disable binding an implementation class, set configuration 'akka.grpc.service."$serviceClass".enabled = false'.""" + }.mkString("\n") + "\n" + + "Add the following to your routes file to support all services:\n" + + allServices.map { service => + val serviceClass = service.packageName + "." + service.name + s"-> / ${serviceClass}Router" + }.mkString("\n")) + + set + } else Set.empty + } } diff --git a/codegen/src/main/scala/akka/grpc/gen/scaladsl/Service.scala b/codegen/src/main/scala/akka/grpc/gen/scaladsl/Service.scala index 57bd4991d..425b822b2 100644 --- a/codegen/src/main/scala/akka/grpc/gen/scaladsl/Service.scala +++ b/codegen/src/main/scala/akka/grpc/gen/scaladsl/Service.scala @@ -4,8 +4,8 @@ package akka.grpc.gen.scaladsl +import akka.grpc.gen.CodeGenerator import scala.collection.immutable - import scala.collection.JavaConverters._ import com.google.protobuf.Descriptors._ import scalapb.compiler.{ DescriptorPimps, GeneratorParams } @@ -30,4 +30,7 @@ object Service { fileDesc.getPackage + "." + serviceDescriptor.getName, serviceDescriptor.getMethods.asScala.map(method ⇒ Method(method)).to[immutable.Seq]) } + + private[scaladsl] def commonPackage(allServices: Seq[Service]): String = + CodeGenerator.commonPackage(allServices.map(_.packageName)) } diff --git a/codegen/src/main/scala/akka/grpc/gen/scaladsl/play/PlayScalaClientCodeGenerator.scala b/codegen/src/main/scala/akka/grpc/gen/scaladsl/play/PlayScalaClientCodeGenerator.scala index 87f16172f..bda50fbf7 100644 --- a/codegen/src/main/scala/akka/grpc/gen/scaladsl/play/PlayScalaClientCodeGenerator.scala +++ b/codegen/src/main/scala/akka/grpc/gen/scaladsl/play/PlayScalaClientCodeGenerator.scala @@ -4,8 +4,8 @@ package akka.grpc.gen.scaladsl.play -import akka.grpc.gen.Logger import akka.grpc.gen.scaladsl.{ ScalaCodeGenerator, Service } +import akka.grpc.gen.{ CodeGenerator, Logger } import com.google.protobuf.compiler.PluginProtos.CodeGeneratorResponse import templates.PlayScala.txt.{ AkkaGrpcClientModule, ClientProvider } @@ -30,7 +30,7 @@ trait PlayScalaClientCodeGenerator extends ScalaCodeGenerator { override def staticContent(logger: Logger, allServices: Seq[Service]): Set[CodeGeneratorResponse.File] = { if (allServices.nonEmpty) { - val packageName = packageForSharedModuleFile(allServices) + val packageName = Service.commonPackage(allServices) val b = CodeGeneratorResponse.File.newBuilder() b.setContent(AkkaGrpcClientModule(packageName, allServices).body) b.setName(s"${packageName.replace('.', '/')}/${ClientModuleName}.scala") @@ -42,30 +42,4 @@ trait PlayScalaClientCodeGenerator extends ScalaCodeGenerator { } else Set.empty } - def packageForSharedModuleFile(allServices: Seq[Service]): String = - // single service or all services in single package - use that - if (allServices.forall(_.packageName == allServices.head.packageName)) allServices.head.packageName - else { - // try to find longest common prefix - allServices.tail.foldLeft(allServices.head.packageName)((packageName, service) => - if (packageName == service.packageName) packageName - else commonPackage(packageName, service.packageName)) - } - - /** extract the longest common package prefix for two classes */ - def commonPackage(a: String, b: String): String = { - val aPackages = a.split('.') - val bPackages = b.split('.') - @tailrec - def countIdenticalPackage(pos: Int): Int = { - if (aPackages(pos) == bPackages(pos)) countIdenticalPackage(pos + 1) - else pos - } - - val prefixLength = countIdenticalPackage(0) - if (prefixLength == 0) "" // no common, use root package - else aPackages.take(prefixLength).mkString(".") - - } - } diff --git a/codegen/src/main/scala/akka/grpc/gen/scaladsl/play/PlayScalaServerCodeGenerator.scala b/codegen/src/main/scala/akka/grpc/gen/scaladsl/play/PlayScalaServerCodeGenerator.scala index 1069ff281..3a239ea5c 100644 --- a/codegen/src/main/scala/akka/grpc/gen/scaladsl/play/PlayScalaServerCodeGenerator.scala +++ b/codegen/src/main/scala/akka/grpc/gen/scaladsl/play/PlayScalaServerCodeGenerator.scala @@ -7,11 +7,14 @@ package akka.grpc.gen.scaladsl.play import akka.grpc.gen.Logger import akka.grpc.gen.scaladsl.{ ScalaCodeGenerator, Service } import com.google.protobuf.compiler.PluginProtos.CodeGeneratorResponse -import templates.PlayScala.txt.Router +import templates.PlayScala.txt.{ AkkaGrpcClientModule, AkkaGrpcServiceModule, Router } object PlayScalaServerCodeGenerator extends PlayScalaServerCodeGenerator trait PlayScalaServerCodeGenerator extends ScalaCodeGenerator { + + val ServiceModuleName = "AkkaGrpcServiceModule" + override def name: String = "akka-grpc-play-server-scala" override def perServiceContent = super.perServiceContent + generateRouter @@ -22,4 +25,33 @@ trait PlayScalaServerCodeGenerator extends ScalaCodeGenerator { b.setName(s"${service.packageDir}/Abstract${service.name}Router.scala") b.build } + + override def staticContent(logger: Logger, allServices: Seq[Service]): Set[CodeGeneratorResponse.File] = { + if (allServices.nonEmpty) { + val packageName = Service.commonPackage(allServices) + val b = CodeGeneratorResponse.File.newBuilder() + b.setContent(AkkaGrpcServiceModule(packageName, allServices).body) + b.setName(s"${packageName.replace('.', '/')}/${ServiceModuleName}.scala") + val set = Set(b.build) + + logger.info(s"Generated '$packageName.$ServiceModuleName'. \n" + + s"Add 'play.modules.enabled += $packageName.$ServiceModuleName' to your configuration to bind services. " + + s"The following services will be bound: \n" + + allServices.map { service => + val serviceClass = service.packageName + "." + service.name + s""" * @Named("impl") $serviceClass -> ${serviceClass}Impl\n""" + + s""" - You will need to create the implementation class '${serviceClass}Impl'.\n""" + + s""" - To use a different implementation class, set 'akka.grpc.service."$serviceClass".class' to a new classname.\n""" + + s""" - To disable binding an implementation class, set configuration 'akka.grpc.service."$serviceClass".enabled = false'.""" + }.mkString("\n") + "\n" + + "Add the following to your routes file to support all services:\n" + + allServices.map { service => + val serviceClass = service.packageName + "." + service.name + s"-> / ${serviceClass}Router" + }.mkString("\n")) + + set + } else Set.empty + } + } diff --git a/codegen/src/main/twirl/templates/PlayJava/AkkaGrpcClientModule.scala.txt b/codegen/src/main/twirl/templates/PlayJava/AkkaGrpcClientModule.scala.txt index e0a452009..61151a74b 100644 --- a/codegen/src/main/twirl/templates/PlayJava/AkkaGrpcClientModule.scala.txt +++ b/codegen/src/main/twirl/templates/PlayJava/AkkaGrpcClientModule.scala.txt @@ -26,7 +26,7 @@ public class AkkaGrpcClientModule extends Module { return seq( @services.map { service => bind(@{service.name}Client.class).toProvider(@{service.name}ClientProvider.class) - }.mkString(",") + }.mkString(", ") ); } } diff --git a/codegen/src/main/twirl/templates/PlayJavaServer/AkkaGrpcServiceModule.scala.txt b/codegen/src/main/twirl/templates/PlayJavaServer/AkkaGrpcServiceModule.scala.txt new file mode 100644 index 000000000..a399ae914 --- /dev/null +++ b/codegen/src/main/twirl/templates/PlayJavaServer/AkkaGrpcServiceModule.scala.txt @@ -0,0 +1,34 @@ +@* + * Copyright (C) 2018 Lightbend Inc. + *@ + +@(packageName: String, services: Seq[akka.grpc.gen.javadsl.Service]) + +@akka.grpc.gen.Constants.DoNotEditComment +package @{packageName}; + +import play.api.Configuration; +import play.api.Environment; +import play.api.inject.Binding; +import play.api.inject.Module; +import scala.collection.Seq; +@services.map { service => +import @{service.packageName}.*; +} + +import akka.grpc.scaladsl.play.AbstractAkkaGrpcServiceModule; + +/** + * Add this generated AkkaGrpcServiceModule to play.modules.enabled + * in your application.conf to have the available gRPC services injectable + */ +public class AkkaGrpcServiceModule extends AbstractAkkaGrpcServiceModule { + @@Override + public Seq> services() { + return classSeq( + @services.map { service => + @{service.name}.class + }.mkString(",") + ); + } +} diff --git a/codegen/src/main/twirl/templates/PlayJavaServer/Router.scala.txt b/codegen/src/main/twirl/templates/PlayJavaServer/Router.scala.txt index f63317d28..5da71710a 100644 --- a/codegen/src/main/twirl/templates/PlayJavaServer/Router.scala.txt +++ b/codegen/src/main/twirl/templates/PlayJavaServer/Router.scala.txt @@ -8,6 +8,8 @@ package @service.packageName; import java.util.concurrent.CompletionStage; +import javax.inject.Inject; +import javax.inject.Named; import akka.japi.Function; import scala.concurrent.Future; @@ -17,25 +19,20 @@ import akka.http.scaladsl.model.HttpResponse; import akka.stream.Materializer; import akka.grpc.internal.PlayRouter; +import akka.grpc.internal.PlayRouterHelper; /** * Abstract base class for implementing @{service.name} in Java and using as a play Router */ -public abstract class Abstract@{service.name}Router extends PlayRouter implements @{service.name} { - - public Abstract@{service.name}Router(Materializer mat) { - super(mat, @{service.name}.name); - } - - /** - * INTERNAL API - */ - final public scala.Function1> createHandler(String prefix, Materializer mat) { - return akka.grpc.internal.PlayRouterHelper.handlerFor( - @{service.name}HandlerFactory.create(this, prefix, mat) - ); +public class @{service.name}Router extends PlayRouter { + + @@Inject + public @{service.name}Router(@@Named("impl") @{service.name} service, Materializer mat) { + super( + '/' + @{service.name}.name, + PlayRouterHelper.handlerFor(@{service.name}HandlerFactory.create(service, @{service.name}.name, mat)) + ); } - } diff --git a/codegen/src/main/twirl/templates/PlayScala/AkkaGrpcServiceModule.scala.txt b/codegen/src/main/twirl/templates/PlayScala/AkkaGrpcServiceModule.scala.txt new file mode 100644 index 000000000..3b7262938 --- /dev/null +++ b/codegen/src/main/twirl/templates/PlayScala/AkkaGrpcServiceModule.scala.txt @@ -0,0 +1,29 @@ +@* + * Copyright (C) 2018 Lightbend Inc. + *@ + +@(packageName: String, services: Seq[akka.grpc.gen.scaladsl.Service]) + +@akka.grpc.gen.Constants.DoNotEditComment +package @{packageName} + +import akka.grpc.scaladsl.play.AbstractAkkaGrpcServiceModule +import play.api.inject.Binding +import play.api.{Configuration, Environment} +@services.map { service => +import @{service.packageName}._ +} + +/** + * Add this generated AkkaGrpcServiceModule to play.modules.enabled + * in your application.conf to have the available gRPC services injectable + */ +class AkkaGrpcServiceModule extends AbstractAkkaGrpcServiceModule { + override protected def services: Seq[Class[_]] = { + Seq( + @services.map { service => + classOf[@{service.name}], + } + ) + } +} \ No newline at end of file diff --git a/codegen/src/main/twirl/templates/PlayScala/Router.scala.txt b/codegen/src/main/twirl/templates/PlayScala/Router.scala.txt index c7059f895..a8211d36a 100644 --- a/codegen/src/main/twirl/templates/PlayScala/Router.scala.txt +++ b/codegen/src/main/twirl/templates/PlayScala/Router.scala.txt @@ -7,6 +7,7 @@ @akka.grpc.gen.Constants.DoNotEditComment package @service.packageName +import javax.inject.{ Inject, Named } import scala.concurrent.Future import akka.http.scaladsl.model.{ HttpRequest, HttpResponse } @@ -15,11 +16,7 @@ import akka.stream.Materializer import akka.grpc.internal.PlayRouter /** - * Abstract base class for implementing @{service.name} and using as a play Router + * A class for routing gRPC requests to service @{service.name}. */ -abstract class Abstract@{service.name}Router(mat: Materializer) extends PlayRouter(mat, @{service.name}.name) with @{service.name} { - - final override def createHandler(serviceName: String, mat: Materializer): HttpRequest => Future[HttpResponse] = - @{service.name}Handler(this, serviceName)(mat) - -} +class @{service.name}Router @@Inject() (@@Named("impl") service: @{service.name})(implicit mat: Materializer) + extends PlayRouter('/' + @{service.name}.name, @{service.name}Handler(service, @{service.name}.name)(mat)) diff --git a/codegen/src/test/scala/akka/grpc/gen/scaladsl/play/PlayScalaClientCodeGeneratorSpec.scala b/codegen/src/test/scala/akka/grpc/gen/scaladsl/play/PlayScalaClientCodeGeneratorSpec.scala index c9e7e1ca4..685bf502e 100644 --- a/codegen/src/test/scala/akka/grpc/gen/scaladsl/play/PlayScalaClientCodeGeneratorSpec.scala +++ b/codegen/src/test/scala/akka/grpc/gen/scaladsl/play/PlayScalaClientCodeGeneratorSpec.scala @@ -12,20 +12,20 @@ class PlayScalaClientCodeGeneratorSpec extends WordSpec with Matchers { "The PlayScalaClientCodeGenerator" must { "choose the single package name" in { - PlayScalaClientCodeGenerator - .packageForSharedModuleFile(Seq(Service("a.b", "MyService", "???", Nil))) should ===("a.b") + Service + .commonPackage(Seq(Service("a.b", "MyService", "???", Nil))) should ===("a.b") } "choose the longest common package name" in { - PlayScalaClientCodeGenerator - .packageForSharedModuleFile(Seq( + Service + .commonPackage(Seq( Service("a.b.c", "MyService", "???", Nil), Service("a.b.e", "OtherService", "???", Nil))) should ===("a.b") } "choose the root package if no common packages" in { - PlayScalaClientCodeGenerator - .packageForSharedModuleFile(Seq( + Service + .commonPackage(Seq( Service("a.b.c", "MyService", "???", Nil), Service("c.d.e", "OtherService", "???", Nil))) should ===("") } diff --git a/play-interop-test-java/src/main/java/controllers/GreeterServiceImpl.java b/play-interop-test-java/src/main/java/controllers/GreeterServiceImpl.java index 2fd18f3c6..a5884201b 100644 --- a/play-interop-test-java/src/main/java/controllers/GreeterServiceImpl.java +++ b/play-interop-test-java/src/main/java/controllers/GreeterServiceImpl.java @@ -3,27 +3,32 @@ */ // #service-impl -package controllers; +package controllers; // TODO: Move into 'services' package? -import akka.stream.Materializer; -import com.google.inject.Inject; -import example.myapp.helloworld.grpc.AbstractGreeterServiceRouter; +import example.myapp.helloworld.grpc.GreeterService; import example.myapp.helloworld.grpc.HelloReply; import example.myapp.helloworld.grpc.HelloRequest; +import javax.inject.Inject; import javax.inject.Singleton; import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletionStage; /** User implementation, with support for dependency injection etc */ @Singleton -public class GreeterServiceImpl extends AbstractGreeterServiceRouter { +public class GreeterServiceImpl implements GreeterService { @Inject - public GreeterServiceImpl(Materializer mat) { - super(mat); + public GreeterServiceImpl() { } + + // FIXME: We don't actually need a Materializer here for this example, but should we include it anyway? + // @Inject + // public GreeterServiceImpl(Materializer mat) { + // super(mat); + // } + @Override public CompletionStage sayHello(HelloRequest in) { String message = String.format("Hello, %s!", in.getName()); diff --git a/play-interop-test-java/src/test/scala/akka/grpc/gen/PlayJavaRouterSpec.scala b/play-interop-test-java/src/test/scala/akka/grpc/gen/PlayJavaRouterSpec.scala index 21ef41695..b547862d5 100644 --- a/play-interop-test-java/src/test/scala/akka/grpc/gen/PlayJavaRouterSpec.scala +++ b/play-interop-test-java/src/test/scala/akka/grpc/gen/PlayJavaRouterSpec.scala @@ -16,7 +16,7 @@ import akka.stream.scaladsl.{ Sink, Source } import akka.stream.{ ActorMaterializer, Materializer } import akka.util.ByteString import controllers.GreeterServiceImpl -import example.myapp.helloworld.grpc.{ GreeterService, HelloReply, HelloRequest } +import example.myapp.helloworld.grpc.{ GreeterService, GreeterServiceRouter, HelloReply, HelloRequest } import org.scalatest.concurrent.ScalaFutures import org.scalatest.{ BeforeAndAfterAll, Matchers, WordSpec } import play.api.inject.SimpleInjector @@ -50,7 +50,7 @@ class PlayJavaRouterSpec extends WordSpec with Matchers with BeforeAndAfterAll w implicit def fromSourceMarshaller[T](implicit serializer: ProtobufSerializer[T], mat: Materializer, codec: Codec): ToResponseMarshaller[Source[T, NotUsed]] = Marshaller.opaque((response: Source[T, NotUsed]) ⇒ GrpcMarshalling.marshalStream(response)(serializer, mat, codec)) - val router = new GreeterServiceImpl(mat) + val router = new GreeterServiceRouter(new GreeterServiceImpl(), mat) "The generated Play (Java) Router" should { diff --git a/play-interop-test-scala/src/main/scala/controllers/GreeterServiceImpl.scala b/play-interop-test-scala/src/main/scala/controllers/GreeterServiceImpl.scala index 3c6d97479..4a5e394bb 100644 --- a/play-interop-test-scala/src/main/scala/controllers/GreeterServiceImpl.scala +++ b/play-interop-test-scala/src/main/scala/controllers/GreeterServiceImpl.scala @@ -6,14 +6,14 @@ package controllers import akka.stream.Materializer -import example.myapp.helloworld.grpc.helloworld.{ AbstractGreeterServiceRouter, HelloReply, HelloRequest } +import example.myapp.helloworld.grpc.helloworld.{ GreeterService, HelloReply, HelloRequest } import javax.inject.{ Inject, Singleton } import scala.concurrent.Future /** User implementation, with support for dependency injection etc */ @Singleton -class GreeterServiceImpl @Inject() (implicit mat: Materializer) extends AbstractGreeterServiceRouter(mat) { +class GreeterServiceImpl @Inject() (implicit mat: Materializer /* param not needed in this example */ ) extends GreeterService { override def sayHello(in: HelloRequest): Future[HelloReply] = Future.successful(HelloReply(s"Hello, ${in.name}!")) diff --git a/play-interop-test-scala/src/test/scala/akka/grpc/gen/PlayScalaRouterSpec.scala b/play-interop-test-scala/src/test/scala/akka/grpc/gen/PlayScalaRouterSpec.scala index 3bf8eca19..112740140 100644 --- a/play-interop-test-scala/src/test/scala/akka/grpc/gen/PlayScalaRouterSpec.scala +++ b/play-interop-test-scala/src/test/scala/akka/grpc/gen/PlayScalaRouterSpec.scala @@ -15,6 +15,7 @@ import play.api.mvc.request.{ RemoteConnection, RequestFactory, RequestTarget } import controllers.GreeterServiceImpl import example.myapp.helloworld.grpc.helloworld._ import GreeterServiceMarshallers._ +import akka.grpc.internal.PlayRouter import akka.grpc.{ Grpc, ProtobufSerializer } import akka.http.scaladsl.model.HttpEntity.Chunk import akka.stream.scaladsl.{ Sink, Source } @@ -29,7 +30,7 @@ class PlayScalaRouterSpec extends WordSpec with Matchers with BeforeAndAfterAll implicit val ec = sys.dispatcher implicit val patience = PatienceConfig(timeout = 3.seconds, interval = 15.milliseconds) - val router = new GreeterServiceImpl + val router = new GreeterServiceRouter(new GreeterServiceImpl) "The generated Play Router" should { diff --git a/runtime/src/main/scala/akka/grpc/internal/PlayRouter.scala b/runtime/src/main/scala/akka/grpc/internal/PlayRouter.scala index fbe525cb0..ec23fc360 100644 --- a/runtime/src/main/scala/akka/grpc/internal/PlayRouter.scala +++ b/runtime/src/main/scala/akka/grpc/internal/PlayRouter.scala @@ -4,23 +4,17 @@ package akka.grpc.internal -import java.util.Optional import java.util.concurrent.CompletionStage import akka.annotation.InternalApi -import akka.dispatch.{ Dispatchers, ExecutionContexts } +import akka.dispatch.ExecutionContexts import akka.http.scaladsl.model.{ HttpRequest, HttpResponse } -import akka.stream.Materializer -import play.api.inject.Injector -import play.api.mvc.Handler import play.api.mvc.akkahttp.AkkaHttpHandler import play.api.routing.Router import play.api.routing.Router.Routes -import play.mvc.Http -import scala.concurrent.Future import scala.compat.java8.FutureConverters._ -import scala.compat.java8.OptionConverters._ +import scala.concurrent.Future /** * INTERNAL API @@ -38,19 +32,10 @@ import scala.compat.java8.OptionConverters._ * * INTERNAL API */ -@InternalApi abstract class PlayRouter(mat: Materializer, serviceName: String) extends play.api.routing.Router { - - private val prefix = s"/$serviceName" - - /** - * INTERNAL API - */ - @InternalApi - protected def createHandler(serviceName: String, mat: Materializer): HttpRequest => Future[HttpResponse] +@InternalApi abstract class PlayRouter(prefix: String, underlyingHandler: HttpRequest => Future[HttpResponse]) extends play.api.routing.Router { private val handler = new AkkaHttpHandler { - val handler = createHandler(serviceName, mat) - override def apply(request: HttpRequest): Future[HttpResponse] = handler(request) + override def apply(request: HttpRequest): Future[HttpResponse] = underlyingHandler(request) } // Scala API @@ -65,7 +50,7 @@ import scala.compat.java8.OptionConverters._ * so therefore not supported. */ final override def withPrefix(prefix: String): Router = - if (prefix == "/") this + if (prefix == "/") this // Prefixing with / is the identity operation, which is allowed else throw new UnsupportedOperationException("Prefixing gRPC services is not widely supported by clients, " + s"strongly discouraged by the specification and therefore not supported. " + diff --git a/runtime/src/main/scala/akka/grpc/scaladsl/play/AbstractAkkaGrpcServiceBindings.scala b/runtime/src/main/scala/akka/grpc/scaladsl/play/AbstractAkkaGrpcServiceBindings.scala new file mode 100644 index 000000000..2035f9927 --- /dev/null +++ b/runtime/src/main/scala/akka/grpc/scaladsl/play/AbstractAkkaGrpcServiceBindings.scala @@ -0,0 +1,62 @@ +package akka.grpc.scaladsl.play + +import play.api.inject.Binding +import play.api.{ Configuration, Environment, Logger } + +import scala.annotation.varargs +import scala.reflect.ClassTag + +/** + * Helper for generating Play service bindings. + */ +abstract class AbstractAkkaGrpcServiceModule extends play.api.inject.Module { + + override def bindings(environment: Environment, configuration: Configuration): Seq[Binding[_]] = + services.flatMap(bindingsForService(_, environment, configuration)) + + protected def services: Seq[Class[_]] + + @varargs // Helper for Java subclasses + final protected def classSeq(classes: Class[_]*): Seq[Class[_]] = classes + + protected def bindingsForService[T](serviceClass: Class[T], environment: Environment, configuration: Configuration): Seq[Binding[_]] = { + val logger = Logger(classOf[AbstractAkkaGrpcServiceModule]) + + val baseConfigPath = s"""akka.grpc.service."${serviceClass.getName}"""" + val enabledConfig = s"$baseConfigPath.enabled" + val classNameConfig = s"$baseConfigPath.class" + + if (!configuration.has(enabledConfig) || configuration.get[Boolean](enabledConfig)) { + // We support disabling service loading, since some generated services might not actually be needed + + logger.info(s"Service ${serviceClass.getName} not bound to an implementation clas because setting '$enabledConfig' is false") + Seq.empty + } else { + // Service loading is enabled + + // Get the service implementation class name and a bit of diagnostic info about how it was loaded + val (implClassName, logReason): (String, String) = if (configuration.has(classNameConfig)) { + (configuration.get[String](classNameConfig), s"implementation class name read from configuration at $classNameConfig") + } else { + (serviceClass.getName + "Impl", s"no configuration value at $classNameConfig, so using default implementation class name") + } + logger.debug(s"Binding service interface ${serviceClass.getName} to implementation class ${implClassName}: $logReason") + + val implClass: Class[_ <: T] = try Class.forName(implClassName, false, environment.classLoader).asInstanceOf[Class[_ <: T]] catch { + case origException: ClassNotFoundException => + throw new ClassNotFoundException( + s"Failed to load implementation class $implClassName needed to bind service ${serviceClass.getName}. " + + s"To disable binding for this service, set config value '$enabledConfig' to false. To override the implementation " + + s"class name, set config value '$classNameConfig' to the full class name to use instead: $logReason", + origException) + } + + if (!serviceClass.isAssignableFrom(implClass)) { + throw new ClassCastException(s"Implementation class $implClassName does not implement service ${serviceClass.getName}") + } + + Seq(bind(serviceClass).qualifiedWith("impl").to(implClass)) + } + } + +}