From 56b9ef2ed882ed5e54a4366eee71e75d5f313a01 Mon Sep 17 00:00:00 2001 From: Renato Cavalcanti Date: Wed, 22 Sep 2021 14:03:30 +0200 Subject: [PATCH] feat(scalasdk): generate action sources for Scala SDK --- .../akkasls/codegen/ModelBuilder.scala | 12 +- .../java/ActionServiceSourceGenerator.scala | 34 +- .../impl/ActionServiceSourceGenerator.scala | 315 ++++++++++++++++++ .../scalasdk/impl/SourceGenerator.scala | 4 + .../ActionServiceSourceGeneratorSuite.scala | 203 +++++++++++ samples/scala-fibonacci-action/build.sbt | 46 +++ .../scala-fibonacci-action/docker-compose.yml | 18 + .../project/build.properties | 1 + .../project/plugins.sbt | 2 + .../com/example/fibonacci/fibonacci.proto | 35 ++ .../build.sbt | 2 +- .../project/plugins.sbt | 3 +- 12 files changed, 650 insertions(+), 25 deletions(-) create mode 100644 codegen/scala-gen/src/main/scala/com/akkaserverless/codegen/scalasdk/impl/ActionServiceSourceGenerator.scala create mode 100644 codegen/scala-gen/src/test/scala/com/akkaserverless/codegen/scalasdk/ActionServiceSourceGeneratorSuite.scala create mode 100644 samples/scala-fibonacci-action/build.sbt create mode 100644 samples/scala-fibonacci-action/docker-compose.yml create mode 100644 samples/scala-fibonacci-action/project/build.properties create mode 100644 samples/scala-fibonacci-action/project/plugins.sbt create mode 100644 samples/scala-fibonacci-action/src/main/proto/com/example/fibonacci/fibonacci.proto diff --git a/codegen/core/src/main/scala/com/lightbend/akkasls/codegen/ModelBuilder.scala b/codegen/core/src/main/scala/com/lightbend/akkasls/codegen/ModelBuilder.scala index 603c4aaa0b..6086771947 100644 --- a/codegen/core/src/main/scala/com/lightbend/akkasls/codegen/ModelBuilder.scala +++ b/codegen/core/src/main/scala/com/lightbend/akkasls/codegen/ModelBuilder.scala @@ -189,7 +189,7 @@ object ModelBuilder { val className = if (fqn.name.contains("Action")) fqn.name + "Impl" else fqn.name + "Action" - val interfaceName = "Abstract" + baseClassName + val abstractActionName = "Abstract" + baseClassName val handlerName = baseClassName + "Handler" val providerName = baseClassName + "Provider" @@ -252,7 +252,15 @@ object ModelBuilder { streamedInput: Boolean, streamedOutput: Boolean, inFromTopic: Boolean, - outToTopic: Boolean) + outToTopic: Boolean) { + + def isUnary: Boolean = !streamedInput && !streamedOutput + def isStreamIn: Boolean = streamedInput && !streamedOutput + def isStreamOut: Boolean = !streamedInput && streamedOutput + def isStreamInOut: Boolean = streamedInput && streamedOutput + def hasStream: Boolean = isStreamIn || isStreamOut || isStreamInOut + + } object Command { def from(method: Descriptors.MethodDescriptor)(implicit fqnExtractor: FullyQualifiedNameExtractor): Command = { diff --git a/codegen/java-gen/src/main/scala/com/lightbend/akkasls/codegen/java/ActionServiceSourceGenerator.scala b/codegen/java-gen/src/main/scala/com/lightbend/akkasls/codegen/java/ActionServiceSourceGenerator.scala index ad784db7bf..db4a4323a5 100644 --- a/codegen/java-gen/src/main/scala/com/lightbend/akkasls/codegen/java/ActionServiceSourceGenerator.scala +++ b/codegen/java-gen/src/main/scala/com/lightbend/akkasls/codegen/java/ActionServiceSourceGenerator.scala @@ -48,7 +48,7 @@ object ActionServiceSourceGenerator { sourceDirectory.resolve(packagePath.resolve(service.className + ".java")) val interfaceSourcePath = - generatedSourceDirectory.resolve(packagePath.resolve(service.interfaceName + ".java")) + generatedSourceDirectory.resolve(packagePath.resolve(service.abstractActionName + ".java")) interfaceSourcePath.getParent.toFile.mkdirs() Files.write(interfaceSourcePath, abstractActionSource(service).getBytes(Charsets.UTF_8)) @@ -73,14 +73,8 @@ object ActionServiceSourceGenerator { List(implSourcePath, interfaceSourcePath, providerSourcePath, handlerSourcePath) } - private def isUnary(cmd: ModelBuilder.Command): Boolean = !cmd.streamedInput && !cmd.streamedOutput - private def isStreamIn(cmd: ModelBuilder.Command): Boolean = cmd.streamedInput && !cmd.streamedOutput - private def isStreamOut(cmd: ModelBuilder.Command): Boolean = !cmd.streamedInput && cmd.streamedOutput - private def isStreamInOut(cmd: ModelBuilder.Command): Boolean = cmd.streamedInput && cmd.streamedOutput - private def hasStream(cmd: ModelBuilder.Command): Boolean = isStreamIn(cmd) || isStreamOut(cmd) || isStreamInOut(cmd) - private def streamImports(commands: Iterable[ModelBuilder.Command]): Seq[String] = { - if (commands.exists(c => hasStream(c))) + if (commands.exists(_.hasStream)) "akka.NotUsed" :: "akka.stream.javadsl.Source" :: Nil else Nil @@ -102,7 +96,7 @@ object ActionServiceSourceGenerator { val inputTypeFullName = cmd.inputType.fullName val outputType = cmd.outputType.fullName - if (isUnary(cmd)) { + if (cmd.isUnary) { val jsonTopicHint = { // note: the somewhat funky indenting is on purpose to lf+indent only if comment present if (cmd.inFromTopic && cmd.inputType.fullQualifiedName == "com.google.protobuf.Any") @@ -119,14 +113,14 @@ object ActionServiceSourceGenerator { |public Effect<$outputType> ${lowerFirst(methodName)}($inputTypeFullName $input) { | ${jsonTopicHint}throw new RuntimeException("The command handler for `$methodName` is not implemented, yet"); |}""".stripMargin - } else if (isStreamOut(cmd)) { + } else if (cmd.isStreamOut) { s""" |/** Handler for "$methodName". */ |@Override |public Source, NotUsed> ${lowerFirst(methodName)}($inputTypeFullName $input) { | throw new RuntimeException("The command handler for `$methodName` is not implemented, yet"); |}""".stripMargin - } else if (isStreamIn(cmd)) { + } else if (cmd.isStreamIn) { s""" |/** Handler for "$methodName". */ |@Override @@ -150,7 +144,7 @@ object ActionServiceSourceGenerator { |$imports | |/** An action. */ - |public class $className extends ${service.interfaceName} { + |public class $className extends ${service.abstractActionName} { | | public $className(ActionCreationContext creationContext) {} | @@ -173,15 +167,15 @@ object ActionServiceSourceGenerator { val inputTypeFullName = cmd.inputType.fullName val outputType = cmd.outputType.fullName - if (isUnary(cmd)) { + if (cmd.isUnary) { s"""|/** Handler for "$methodName". */ |public abstract Effect<$outputType> ${lowerFirst(methodName)}($inputTypeFullName $input);""".stripMargin - } else if (isStreamOut(cmd)) { + } else if (cmd.isStreamOut) { s""" |/** Handler for "$methodName". */ |public abstract Source, NotUsed> ${lowerFirst( methodName)}($inputTypeFullName $input);""".stripMargin - } else if (isStreamIn(cmd)) { + } else if (cmd.isStreamIn) { s""" |/** Handler for "$methodName". */ |public abstract Effect<$outputType> ${lowerFirst( @@ -201,7 +195,7 @@ object ActionServiceSourceGenerator { |$imports | |/** An action. */ - |public abstract class ${service.interfaceName} extends Action { + |public abstract class ${service.abstractActionName} extends Action { | | ${Format.indent(methods, 2)} |}""".stripMargin @@ -212,7 +206,7 @@ object ActionServiceSourceGenerator { val className = service.className val packageName = service.fqn.parent.javaPackage - val unaryCases = service.commands.filter(isUnary).map { cmd => + val unaryCases = service.commands.filter(_.isUnary).map { cmd => val methodName = cmd.name val inputTypeFullName = cmd.inputType.fullName @@ -222,7 +216,7 @@ object ActionServiceSourceGenerator { |""".stripMargin } - val streamOutCases = service.commands.filter(isStreamOut).map { cmd => + val streamOutCases = service.commands.filter(_.isStreamOut).map { cmd => val methodName = cmd.name val inputTypeFullName = cmd.inputType.fullName @@ -232,7 +226,7 @@ object ActionServiceSourceGenerator { |""".stripMargin } - val streamInCases = service.commands.filter(isStreamIn).map { cmd => + val streamInCases = service.commands.filter(_.isStreamIn).map { cmd => val methodName = cmd.name val inputTypeFullName = cmd.inputType.fullName @@ -242,7 +236,7 @@ object ActionServiceSourceGenerator { |""".stripMargin } - val streamInOutCases = service.commands.filter(isStreamInOut).map { cmd => + val streamInOutCases = service.commands.filter(_.isStreamInOut).map { cmd => val methodName = cmd.name val inputTypeFullName = cmd.inputType.fullName diff --git a/codegen/scala-gen/src/main/scala/com/akkaserverless/codegen/scalasdk/impl/ActionServiceSourceGenerator.scala b/codegen/scala-gen/src/main/scala/com/akkaserverless/codegen/scalasdk/impl/ActionServiceSourceGenerator.scala new file mode 100644 index 0000000000..2a3bd715bf --- /dev/null +++ b/codegen/scala-gen/src/main/scala/com/akkaserverless/codegen/scalasdk/impl/ActionServiceSourceGenerator.scala @@ -0,0 +1,315 @@ +/* + * Copyright 2021 Lightbend Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.akkaserverless.codegen.scalasdk.impl + +import com.akkaserverless.codegen.scalasdk.File +import com.lightbend.akkasls.codegen.Format +import com.lightbend.akkasls.codegen.ModelBuilder + +/** + * Responsible for generating Scala sourced for Actions + */ +object ActionServiceSourceGenerator { + + import com.lightbend.akkasls.codegen.SourceGeneratorUtils._ + + /** + * Generate Scala sources the user view source file. + */ + def generateUnmanaged(service: ModelBuilder.ActionService): Iterable[File] = { + val generatedSources = Seq.newBuilder[File] + + val packageName = service.fqn.parent.scalaPackage + val packagePath = packageAsPath(packageName) + + generatedSources += File(s"$packagePath/${service.className}.scala", actionSource(service)) + + generatedSources.result() + } + + /** + * Generate Scala sources for provider, handler, abstract baseclass for a view. + */ + def generateManaged(service: ModelBuilder.ActionService): Iterable[File] = { + val generatedSources = Seq.newBuilder[File] + + val packageName = service.fqn.parent.scalaPackage + val packagePath = packageAsPath(packageName) + + generatedSources += File(s"$packagePath/${service.abstractActionName}.scala", abstractAction(service)) + generatedSources += File(s"$packagePath/${service.handlerName}.scala", actionHandler(service)) + generatedSources += File(s"$packagePath/${service.providerName}.scala", actionProvider(service)) + + generatedSources.result() + } + + private def streamImports(commands: Iterable[ModelBuilder.Command]): Seq[String] = { + if (commands.exists(_.hasStream)) + "akka.NotUsed" :: "akka.stream.scaladsl.Source" :: Nil + else + Nil + } + + private[codegen] def actionSource(service: ModelBuilder.ActionService): String = { + + val className = service.className + + implicit val imports = generateImports( + service.commandTypes, + service.fqn.parent.scalaPackage, + otherImports = Seq( + "com.akkaserverless.scalasdk.action.Action", + "com.akkaserverless.scalasdk.action.ActionCreationContext") ++ streamImports(service.commands), + semi = false) + + val methods = service.commands.map { cmd => + val methodName = cmd.name + val input = lowerFirst(cmd.inputType.name) + val inputType = typeName(cmd.inputType) + val outputType = typeName(cmd.outputType) + + if (cmd.isUnary) { + val jsonTopicHint = { + // note: the somewhat funky indenting is on purpose to lf+indent only if comment present + if (cmd.inFromTopic && cmd.inputType.fullQualifiedName == "com.google.protobuf.Any") + """|// JSON input from a topic can be decoded using JsonSupport.decodeJson(classOf[MyClass], any) + | """.stripMargin + else if (cmd.outToTopic && cmd.outputType.fullQualifiedName == "com.google.protobuf.Any") + """|// JSON output to emit to a topic can be encoded using JsonSupport.encodeJson(myPojo) + | """.stripMargin + else "" + } + + s"""|/** Handler for "$methodName". */ + |override def ${lowerFirst(methodName)}($input: $inputType): Action.Effect[$outputType] = { + | ${jsonTopicHint}throw new RuntimeException("The command handler for `$methodName` is not implemented, yet") + |}""".stripMargin + } else if (cmd.isStreamOut) { + s""" + |/** Handler for "$methodName". */ + |override def ${lowerFirst(methodName)}($input: $inputType): Source[Action.Effect[$outputType], NotUsed] = { + | throw new RuntimeException("The command handler for `$methodName` is not implemented, yet") + |}""".stripMargin + } else if (cmd.isStreamIn) { + s""" + |/** Handler for "$methodName". */ + |override def ${lowerFirst(methodName)}(${input}Src: Source[$inputType, NotUsed]): Action.Effect[$outputType] = { + | throw new RuntimeException("The command handler for `$methodName` is not implemented, yet") + |}""".stripMargin + } else { + s""" + |/** Handler for "$methodName". */ + |override def ${lowerFirst(methodName)}(${input}Src: Source[$inputType, NotUsed]): Source[Action.Effect[$outputType], NotUsed] = { + | throw new RuntimeException("The command handler for `$methodName` is not implemented, yet") + |}""".stripMargin + } + } + + s"""|package ${service.fqn.parent.scalaPackage} + | + |$imports + | + |/** An action. */ + |class $className(creationContext: ActionCreationContext) extends ${service.abstractActionName} { + | + | ${Format.indent(methods, 2)} + |} + |""".stripMargin + } + + private[codegen] def abstractAction(service: ModelBuilder.ActionService): String = { + + implicit val imports = generateImports( + service.commandTypes, + service.fqn.parent.scalaPackage, + otherImports = Seq("com.akkaserverless.scalasdk.action.Action") ++ streamImports(service.commands), + semi = false) + + val methods = service.commands.map { cmd => + val methodName = cmd.name + val input = lowerFirst(cmd.inputType.name) + val inputType = typeName(cmd.inputType) + val outputType = typeName(cmd.outputType) + + if (cmd.isUnary) { + s"""|/** Handler for "$methodName". */ + |def ${lowerFirst(methodName)}($input: $inputType): Action.Effect[$outputType]""".stripMargin + } else if (cmd.isStreamOut) { + s""" + |/** Handler for "$methodName". */ + |def ${lowerFirst( + methodName)}($input: $inputType): Source[Action.Effect[$outputType], NotUsed]""".stripMargin + } else if (cmd.isStreamIn) { + s""" + |/** Handler for "$methodName". */ + |def ${lowerFirst( + methodName)}(${input}Src: Source[$inputType, NotUsed]): Action.Effect[$outputType]""".stripMargin + } else { + s""" + |/** Handler for "$methodName". */ + |def ${lowerFirst( + methodName)}(${input}Src: Source[$inputType, NotUsed]): Source[Action.Effect[$outputType], NotUsed]""".stripMargin + } + } + + s"""|package ${service.fqn.parent.scalaPackage} + | + |$imports + | + |/** An action. */ + |abstract class ${service.abstractActionName} extends Action { + | + | ${Format.indent(methods, 2)} + |} + |""".stripMargin + } + + private[codegen] def actionHandler(service: ModelBuilder.ActionService): String = { + implicit val imports = generateImports( + commandTypes(service.commands), + service.fqn.parent.scalaPackage, + otherImports = Seq( + "com.akkaserverless.javasdk.impl.action.ActionHandler.HandlerNotFound", + "com.akkaserverless.scalasdk.impl.action.ActionHandler", + "com.akkaserverless.scalasdk.action.Action", + "com.akkaserverless.scalasdk.action.MessageEnvelope", + "akka.NotUsed", + "akka.stream.scaladsl.Source"), + semi = false) + + val unaryCases = service.commands.filter(_.isUnary).map { cmd => + val methodName = cmd.name + val inputType = typeName(cmd.inputType) + + s"""|case "$methodName" => + | action.${lowerFirst(methodName)}(message.payload.asInstanceOf[$inputType]) + |""".stripMargin + } + + val streamOutCases = service.commands.filter(_.isStreamOut).map { cmd => + val methodName = cmd.name + val inputType = typeName(cmd.inputType) + + s"""|case "$methodName" => + | action.${lowerFirst(methodName)}(message.payload.asInstanceOf[$inputType]) + |""".stripMargin + } + + val streamInCases = service.commands.filter(_.isStreamIn).map { cmd => + val methodName = cmd.name + val inputType = typeName(cmd.inputType) + + s"""|case "$methodName" => + | action.${lowerFirst(methodName)}(stream.map(el => el.payload.asInstanceOf[$inputType])) + |""".stripMargin + } + + val streamInOutCases = service.commands.filter(_.isStreamInOut).map { cmd => + val methodName = cmd.name + val inputType = typeName(cmd.inputType) + + s"""|case "$methodName" => + | action.${lowerFirst(methodName)}(stream.map(el => el.payload.asInstanceOf[$inputType])) + |""".stripMargin + } + + s"""|package ${service.fqn.parent.scalaPackage} + | + |$imports + | + |/** A Action handler */ + |class ${service.handlerName}(actionBehavior: ${service.className}) extends ActionHandler[${service.className}](actionBehavior) { + | + | override def handleUnary(commandName: String, message: MessageEnvelope[Any]): Action.Effect[_] = { + | commandName match { + | ${Format.indent(unaryCases, 6)} + | case _ => + | throw new HandlerNotFound(commandName) + | } + | } + | + | override def handleStreamedOut(commandName: String, message: MessageEnvelope[Any]): Source[Action.Effect[_], NotUsed] = { + | commandName match { + | ${Format.indent(streamOutCases, 6)} + | case _ => + | throw new HandlerNotFound(commandName) + | } + | } + | + | override def handleStreamedIn(commandName: String, stream: Source[MessageEnvelope[Any], NotUsed]): Action.Effect[_] = { + | commandName match { + | ${Format.indent(streamInCases, 6)} + | case _ => + | throw new HandlerNotFound(commandName) + | } + | } + | + | override def handleStreamed(commandName: String, stream: Source[MessageEnvelope[Any], NotUsed]): Source[Action.Effect[_], NotUsed] = { + | commandName match { + | ${Format.indent(streamInOutCases, 6)} + | case _ => + | throw new HandlerNotFound(commandName) + | } + | } + |} + |""".stripMargin + } + + private[codegen] def actionProvider(service: ModelBuilder.ActionService): String = { + val imports = generateImports( + commandTypes(service.commands), + service.fqn.parent.scalaPackage, + otherImports = Seq( + "com.akkaserverless.scalasdk.action.ActionProvider", + "com.akkaserverless.scalasdk.action.ActionCreationContext", + "com.akkaserverless.scalasdk.action.ActionOptions", + "com.google.protobuf.Descriptors", + "scala.collection.immutable"), + semi = false) + + s"""|package ${service.fqn.parent.scalaPackage} + | + |$imports + | + |object ${service.providerName} { + | def apply(actionFactory: ActionCreationContext => ${service.className}): ${service.providerName} = + | new ${service.providerName}(actionFactory, ActionOptions.defaults) + | + | def apply(actionFactory: ActionCreationContext => ${service.className}, options: ActionOptions): ${service.providerName} = + | new ${service.providerName}(actionFactory, options) + |} + | + |class ${service.providerName} private(actionFactory: ActionCreationContext => ${service.className}, + | options: ActionOptions) + | extends ActionProvider[${service.className}] { + | + | override final def serviceDescriptor: Descriptors.ServiceDescriptor = + | ${service.fqn.parent.name}Proto.javaDescriptor.findServiceByName("${service.fqn.protoName}") + | + | override final def newHandler(context: ActionCreationContext): ${service.handlerName} = + | new ${service.handlerName}(actionFactory(context)) + | + | override final def additionalDescriptors: immutable.Seq[Descriptors.FileDescriptor] = + | ${service.fqn.parent.name}Proto.javaDescriptor :: + | Nil + | + | def withOptions(options: ActionOptions): ${service.providerName} = + | new ${service.providerName}(actionFactory, options) + |} + |""".stripMargin + } +} diff --git a/codegen/scala-gen/src/main/scala/com/akkaserverless/codegen/scalasdk/impl/SourceGenerator.scala b/codegen/scala-gen/src/main/scala/com/akkaserverless/codegen/scalasdk/impl/SourceGenerator.scala index 3bb3467fee..dea1941c2b 100644 --- a/codegen/scala-gen/src/main/scala/com/akkaserverless/codegen/scalasdk/impl/SourceGenerator.scala +++ b/codegen/scala-gen/src/main/scala/com/akkaserverless/codegen/scalasdk/impl/SourceGenerator.scala @@ -41,6 +41,8 @@ object SourceGenerator { } case service: ModelBuilder.ViewService => ViewServiceSourceGenerator.generateManaged(service) + case service: ModelBuilder.ActionService => + ActionServiceSourceGenerator.generateManaged(service) case _ => Nil // FIXME } .map(_.prepend(managedComment)) @@ -72,6 +74,8 @@ object SourceGenerator { } case service: ModelBuilder.ViewService => ViewServiceSourceGenerator.generateUnmanaged(service) + case service: ModelBuilder.ActionService => + ActionServiceSourceGenerator.generateUnmanaged(service) case _ => Nil // FIXME } .map(_.prepend(unmanagedComment)) diff --git a/codegen/scala-gen/src/test/scala/com/akkaserverless/codegen/scalasdk/ActionServiceSourceGeneratorSuite.scala b/codegen/scala-gen/src/test/scala/com/akkaserverless/codegen/scalasdk/ActionServiceSourceGeneratorSuite.scala new file mode 100644 index 0000000000..a8fb5bdd4e --- /dev/null +++ b/codegen/scala-gen/src/test/scala/com/akkaserverless/codegen/scalasdk/ActionServiceSourceGeneratorSuite.scala @@ -0,0 +1,203 @@ +/* + * Copyright 2021 Lightbend Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.akkaserverless.codegen.scalasdk + +import com.akkaserverless.codegen.scalasdk.impl.ActionServiceSourceGenerator +import com.akkaserverless.codegen.scalasdk.impl.ViewServiceSourceGenerator +import com.lightbend.akkasls.codegen.TestData + +class ActionServiceSourceGeneratorSuite extends munit.FunSuite { + + private val testData = TestData() + + test("action source") { + val service = testData.simpleActionService() + val generatedSrc = + ActionServiceSourceGenerator.actionSource(service) + assertNoDiff( + generatedSrc, + """|package com.example.service + | + |import akka.NotUsed + |import akka.stream.scaladsl.Source + |import com.akkaserverless.scalasdk.action.Action + |import com.akkaserverless.scalasdk.action.ActionCreationContext + |import com.external.Empty + | + |/** An action. */ + |class MyServiceAction(creationContext: ActionCreationContext) extends AbstractMyServiceAction { + | + | /** Handler for "SimpleMethod". */ + | override def simpleMethod(myRequest: MyRequest): Action.Effect[Empty] = { + | throw new RuntimeException("The command handler for `SimpleMethod` is not implemented, yet") + | } + | + | /** Handler for "StreamedOutputMethod". */ + | override def streamedOutputMethod(myRequest: MyRequest): Source[Action.Effect[Empty], NotUsed] = { + | throw new RuntimeException("The command handler for `StreamedOutputMethod` is not implemented, yet") + | } + | + | /** Handler for "StreamedInputMethod". */ + | override def streamedInputMethod(myRequestSrc: Source[MyRequest, NotUsed]): Action.Effect[Empty] = { + | throw new RuntimeException("The command handler for `StreamedInputMethod` is not implemented, yet") + | } + | + | /** Handler for "FullStreamedMethod". */ + | override def fullStreamedMethod(myRequestSrc: Source[MyRequest, NotUsed]): Source[Action.Effect[Empty], NotUsed] = { + | throw new RuntimeException("The command handler for `FullStreamedMethod` is not implemented, yet") + | } + |} + |""".stripMargin) + } + + test("abstract source") { + val service = testData.simpleActionService() + + val generatedSrc = + ActionServiceSourceGenerator.abstractAction(service) + assertNoDiff( + generatedSrc, + """|package com.example.service + | + |import akka.NotUsed + |import akka.stream.scaladsl.Source + |import com.akkaserverless.scalasdk.action.Action + |import com.external.Empty + | + |/** An action. */ + |abstract class AbstractMyServiceAction extends Action { + | + | /** Handler for "SimpleMethod". */ + | def simpleMethod(myRequest: MyRequest): Action.Effect[Empty] + | + | /** Handler for "StreamedOutputMethod". */ + | def streamedOutputMethod(myRequest: MyRequest): Source[Action.Effect[Empty], NotUsed] + | + | /** Handler for "StreamedInputMethod". */ + | def streamedInputMethod(myRequestSrc: Source[MyRequest, NotUsed]): Action.Effect[Empty] + | + | /** Handler for "FullStreamedMethod". */ + | def fullStreamedMethod(myRequestSrc: Source[MyRequest, NotUsed]): Source[Action.Effect[Empty], NotUsed] + |} + |""".stripMargin) + } + + test("handler source") { + val service = testData.simpleActionService() + + val generatedSrc = + ActionServiceSourceGenerator.actionHandler(service) + + assertNoDiff( + generatedSrc, + """|package com.example.service + | + |import akka.NotUsed + |import akka.stream.scaladsl.Source + |import com.akkaserverless.javasdk.impl.action.ActionHandler.HandlerNotFound + |import com.akkaserverless.scalasdk.action.Action + |import com.akkaserverless.scalasdk.action.MessageEnvelope + |import com.akkaserverless.scalasdk.impl.action.ActionHandler + |import com.external.Empty + | + |/** A Action handler */ + |class MyServiceActionHandler(actionBehavior: MyServiceAction) extends ActionHandler[MyServiceAction](actionBehavior) { + | + | override def handleUnary(commandName: String, message: MessageEnvelope[Any]): Action.Effect[_] = { + | commandName match { + | case "SimpleMethod" => + | action.simpleMethod(message.payload.asInstanceOf[MyRequest]) + | case _ => + | throw new HandlerNotFound(commandName) + | } + | } + | + | override def handleStreamedOut(commandName: String, message: MessageEnvelope[Any]): Source[Action.Effect[_], NotUsed] = { + | commandName match { + | case "StreamedOutputMethod" => + | action.streamedOutputMethod(message.payload.asInstanceOf[MyRequest]) + | case _ => + | throw new HandlerNotFound(commandName) + | } + | } + | + | override def handleStreamedIn(commandName: String, stream: Source[MessageEnvelope[Any], NotUsed]): Action.Effect[_] = { + | commandName match { + | case "StreamedInputMethod" => + | action.streamedInputMethod(stream.map(el => el.payload.asInstanceOf[MyRequest])) + | case _ => + | throw new HandlerNotFound(commandName) + | } + | } + | + | override def handleStreamed(commandName: String, stream: Source[MessageEnvelope[Any], NotUsed]): Source[Action.Effect[_], NotUsed] = { + | commandName match { + | case "FullStreamedMethod" => + | action.fullStreamedMethod(stream.map(el => el.payload.asInstanceOf[MyRequest])) + | case _ => + | throw new HandlerNotFound(commandName) + | } + | } + |} + |""".stripMargin) + } + + test("provider source") { + val service = testData.simpleActionService() + + val generatedSrc = + ActionServiceSourceGenerator.actionProvider(service) + + assertNoDiff( + generatedSrc, + """|package com.example.service + | + |import com.akkaserverless.scalasdk.action.ActionCreationContext + |import com.akkaserverless.scalasdk.action.ActionOptions + |import com.akkaserverless.scalasdk.action.ActionProvider + |import com.external.Empty + |import com.google.protobuf.Descriptors + |import scala.collection.immutable + | + |object MyServiceActionProvider { + | def apply(actionFactory: ActionCreationContext => MyServiceAction): MyServiceActionProvider = + | new MyServiceActionProvider(actionFactory, ActionOptions.defaults) + | + | def apply(actionFactory: ActionCreationContext => MyServiceAction, options: ActionOptions): MyServiceActionProvider = + | new MyServiceActionProvider(actionFactory, options) + |} + | + |class MyServiceActionProvider private(actionFactory: ActionCreationContext => MyServiceAction, + | options: ActionOptions) + | extends ActionProvider[MyServiceAction] { + | + | override final def serviceDescriptor: Descriptors.ServiceDescriptor = + | MyServiceProto.javaDescriptor.findServiceByName("MyService") + | + | override final def newHandler(context: ActionCreationContext): MyServiceActionHandler = + | new MyServiceActionHandler(actionFactory(context)) + | + | override final def additionalDescriptors: immutable.Seq[Descriptors.FileDescriptor] = + | MyServiceProto.javaDescriptor :: + | Nil + | + | def withOptions(options: ActionOptions): MyServiceActionProvider = + | new MyServiceActionProvider(actionFactory, options) + |} + |""".stripMargin) + } +} diff --git a/samples/scala-fibonacci-action/build.sbt b/samples/scala-fibonacci-action/build.sbt new file mode 100644 index 0000000000..28f5a91928 --- /dev/null +++ b/samples/scala-fibonacci-action/build.sbt @@ -0,0 +1,46 @@ +import com.akkaserverless.sbt.AkkaserverlessPlugin.autoImport.generateUnmanaged + +name := "fibonacci-action" + +organization := "com.akkaseverless.samples" +organizationHomepage := Some(url("https://akkaserverless.com")) +licenses := Seq( + ("CC0", url("https://creativecommons.org/publicdomain/zero/1.0")) +) + +scalaVersion := "2.13.6" + +enablePlugins(AkkaGrpcPlugin) +enablePlugins(AkkaserverlessPlugin) + +Compile / scalacOptions ++= Seq( + "-target:11", + "-deprecation", + "-feature", + "-unchecked", + "-Xlog-reflective-calls", + "-Xlint" +) +Compile / javacOptions ++= Seq("-Xlint:unchecked", "-Xlint:deprecation") + +Test / parallelExecution := false +Test / testOptions += Tests.Argument("-oDF") +Test / logBuffered := false + +run / fork := false +Global / cancelable := false // ctrl-c + +Compile / compile := { + // Make sure 'generateUnmanaged' is executed on each compile, to generate scaffolding code for + // newly-introduced concepts. + // After initial generation they are to be maintained manually and will not be overwritten. + (Compile / generateUnmanaged).value + (Compile / compile).value +} + +// FIXME sdk dependency should be included via sbt-akkaserverless +val AkkaServerlessSdkVersion = System.getProperty("akkaserverless-sdk.version", "0.7.2") + +libraryDependencies ++= Seq( + "com.akkaserverless" %% "akkaserverless-scala-sdk" % AkkaServerlessSdkVersion +) diff --git a/samples/scala-fibonacci-action/docker-compose.yml b/samples/scala-fibonacci-action/docker-compose.yml new file mode 100644 index 0000000000..44256ad968 --- /dev/null +++ b/samples/scala-fibonacci-action/docker-compose.yml @@ -0,0 +1,18 @@ +version: "3" +services: + akka-serverless-proxy: + image: gcr.io/akkaserverless-public/akkaserverless-proxy:0.7.0-beta.19 + command: -Dconfig.resource=dev-mode.conf -Dakkaserverless.proxy.eventing.support=google-pubsub-emulator + ports: + - "9000:9000" + extra_hosts: + - "host.docker.internal:host-gateway" + environment: + USER_FUNCTION_HOST: ${USER_FUNCTION_HOST:-host.docker.internal} + USER_FUNCTION_PORT: ${USER_FUNCTION_PORT:-8080} + PUBSUB_EMULATOR_HOST: gcloud-pubsub-emulator + gcloud-pubsub-emulator: + image: gcr.io/google.com/cloudsdktool/cloud-sdk:341.0.0 + command: gcloud beta emulators pubsub start --project=test --host-port=0.0.0.0:8085 + ports: + - 8085:8085 diff --git a/samples/scala-fibonacci-action/project/build.properties b/samples/scala-fibonacci-action/project/build.properties new file mode 100644 index 0000000000..dbae93bcfd --- /dev/null +++ b/samples/scala-fibonacci-action/project/build.properties @@ -0,0 +1 @@ +sbt.version=1.4.9 diff --git a/samples/scala-fibonacci-action/project/plugins.sbt b/samples/scala-fibonacci-action/project/plugins.sbt new file mode 100644 index 0000000000..fccd908eaf --- /dev/null +++ b/samples/scala-fibonacci-action/project/plugins.sbt @@ -0,0 +1,2 @@ +addSbtPlugin("com.akkaserverless" % "sbt-akkaserverless" % System.getProperty("akkaserverless-sdk.version", "0.7.2")) +addSbtPlugin("com.lightbend.akka.grpc" % "sbt-akka-grpc" % "2.1.0") // FIXME should be included via sbt-akkaserverless diff --git a/samples/scala-fibonacci-action/src/main/proto/com/example/fibonacci/fibonacci.proto b/samples/scala-fibonacci-action/src/main/proto/com/example/fibonacci/fibonacci.proto new file mode 100644 index 0000000000..ce741fa39a --- /dev/null +++ b/samples/scala-fibonacci-action/src/main/proto/com/example/fibonacci/fibonacci.proto @@ -0,0 +1,35 @@ +// Copyright 2021 Lightbend Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// tag::actions[] +syntax = "proto3"; +package com.example.fibonacci; // <1> + +import "akkaserverless/annotations.proto"; // <2> + +option java_outer_classname = "FibonacciApi"; // <3> + +message Number { + int64 value = 1; +} + +service Fibonacci { + option (akkaserverless.service) = { + type : SERVICE_TYPE_ACTION // <4> + }; + + rpc NextNumber(Number) returns (Number) {} + +} +// end::actions[] \ No newline at end of file diff --git a/samples/scala-value-entity-customer-registry/build.sbt b/samples/scala-value-entity-customer-registry/build.sbt index fdf191b5e0..4481e5ecaa 100644 --- a/samples/scala-value-entity-customer-registry/build.sbt +++ b/samples/scala-value-entity-customer-registry/build.sbt @@ -39,7 +39,7 @@ Compile / compile := { } // FIXME sdk dependency should be included via sbt-akkaserverless -val AkkaServerlessSdkVersion = System.getProperty("akkaserverless-sdk.version", "0.7.1") +val AkkaServerlessSdkVersion = System.getProperty("akkaserverless-sdk.version", "0.7.2") libraryDependencies ++= Seq( "com.akkaserverless" %% "akkaserverless-scala-sdk" % AkkaServerlessSdkVersion diff --git a/samples/scala-value-entity-customer-registry/project/plugins.sbt b/samples/scala-value-entity-customer-registry/project/plugins.sbt index 161e2bd078..fccd908eaf 100644 --- a/samples/scala-value-entity-customer-registry/project/plugins.sbt +++ b/samples/scala-value-entity-customer-registry/project/plugins.sbt @@ -1,3 +1,2 @@ -addSbtPlugin("com.akkaserverless" % "sbt-akkaserverless" % System.getProperty("akkaserverless-sdk.version", "0.7.1")) +addSbtPlugin("com.akkaserverless" % "sbt-akkaserverless" % System.getProperty("akkaserverless-sdk.version", "0.7.2")) addSbtPlugin("com.lightbend.akka.grpc" % "sbt-akka-grpc" % "2.1.0") // FIXME should be included via sbt-akkaserverless -