From 66a98ca37a972dfa5eb55e13ebb3db360c9b7949 Mon Sep 17 00:00:00 2001 From: Tigran Ohanyan <32593716+TigranOhanyan@users.noreply.github.com> Date: Mon, 26 Jun 2023 11:04:43 +0400 Subject: [PATCH] Feature/serverless zio (#2975) --- .gitignore | 2 + build.sbt | 112 ++++++++++++++-- .../aws/lambda/tests/IOLambdaHandlerV2.scala | 0 .../aws/lambda/tests/LambdaSamTemplate.scala | 5 +- .../serverless/aws/lambda/tests/package.scala | 0 .../tests/AwsLambdaCreateServerStubTest.scala | 8 +- .../tests/AwsLambdaSamLocalHttpTest.scala | 2 +- .../lambda/tests/AwsLambdaStubHttpTest.scala | 0 .../AwsCatsEffectServerInterpreter.scala | 0 .../lambda/AwsCatsEffectServerOptions.scala | 0 .../serverless/aws/lambda/LambdaHandler.scala | 0 .../runtime/AwsLambdaRuntimeInvocation.scala | 0 .../aws/lambda/js/AwsJsRouteHandler.scala | 0 .../aws/lambda/runtime/AwsLambdaRuntime.scala | 0 .../AwsLambdaRuntimeInvocationTest.scala | 0 .../aws/lambda/AwsBodyListener.scala | 0 .../lambda/AwsFutureServerInterpreter.scala | 0 .../aws/lambda/AwsFutureServerOptions.scala | 0 .../aws/lambda/AwsRequestBody.scala | 0 .../aws/lambda/AwsServerInterpreter.scala | 2 +- .../aws/lambda/AwsServerOptions.scala | 0 .../aws/lambda/AwsServerRequest.scala | 0 .../tapir/serverless/aws/lambda/model.scala | 0 .../tapir/serverless/aws/lambda/package.scala | 0 .../aws/lambda/AwsToResponseBody.scala | 0 .../aws/lambda/js/AwsJsRequest.scala | 0 .../aws/lambda/js/AwsJsResponse.scala | 0 .../serverless/aws/lambda/js/package.scala | 0 .../aws/lambda/AwsToResponseBody.scala | 0 .../serverless/aws/lambda}/MapperTest.scala | 0 .../ziolambda/tests/LambdaSamTemplate.scala | 23 ++++ .../tests/ZioLambdaHandlerImpl.scala | 22 ++++ .../aws/ziolambda/tests/package.scala | 52 ++++++++ .../tests/AwsLambdaCreateServerStubTest.scala | 120 ++++++++++++++++++ .../tests/AwsLambdaSamLocalHttpTest.scala | 71 +++++++++++ .../tests/AwsLambdaStubHttpTest.scala | 38 ++++++ .../ziolambda/AwsZioServerInterpreter.scala | 23 ++++ .../aws/ziolambda/AwsZioServerOptions.scala | 22 ++++ .../aws/ziolambda/ZioLambdaHandler.scala | 80 ++++++++++++ 39 files changed, 560 insertions(+), 22 deletions(-) rename serverless/aws/{lambda-tests => lambda-cats-effect-tests}/src/main/scala/sttp/tapir/serverless/aws/lambda/tests/IOLambdaHandlerV2.scala (100%) rename serverless/aws/{lambda-tests => lambda-cats-effect-tests}/src/main/scala/sttp/tapir/serverless/aws/lambda/tests/LambdaSamTemplate.scala (69%) rename serverless/aws/{lambda-tests => lambda-cats-effect-tests}/src/main/scala/sttp/tapir/serverless/aws/lambda/tests/package.scala (100%) rename serverless/aws/{lambda-tests => lambda-cats-effect-tests}/src/test/scala/sttp/tapir/serverless/aws/lambda/tests/AwsLambdaCreateServerStubTest.scala (96%) rename serverless/aws/{lambda-tests => lambda-cats-effect-tests}/src/test/scala/sttp/tapir/serverless/aws/lambda/tests/AwsLambdaSamLocalHttpTest.scala (97%) rename serverless/aws/{lambda-tests => lambda-cats-effect-tests}/src/test/scala/sttp/tapir/serverless/aws/lambda/tests/AwsLambdaStubHttpTest.scala (100%) rename serverless/aws/{lambda => lambda-cats-effect}/src/main/scala/sttp/tapir/serverless/aws/lambda/AwsCatsEffectServerInterpreter.scala (100%) rename serverless/aws/{lambda => lambda-cats-effect}/src/main/scala/sttp/tapir/serverless/aws/lambda/AwsCatsEffectServerOptions.scala (100%) rename serverless/aws/{lambda => lambda-cats-effect}/src/main/scala/sttp/tapir/serverless/aws/lambda/LambdaHandler.scala (100%) rename serverless/aws/{lambda => lambda-cats-effect}/src/main/scala/sttp/tapir/serverless/aws/lambda/runtime/AwsLambdaRuntimeInvocation.scala (100%) rename serverless/aws/{lambda => lambda-cats-effect}/src/main/scalajs/sttp/tapir/serverless/aws/lambda/js/AwsJsRouteHandler.scala (100%) rename serverless/aws/{lambda => lambda-cats-effect}/src/main/scalajvm/sttp/tapir/serverless/aws/lambda/runtime/AwsLambdaRuntime.scala (100%) rename serverless/aws/{lambda => lambda-cats-effect}/src/test/scalajvm/sttp/tapir/serverless/aws/lambda/runtime/AwsLambdaRuntimeInvocationTest.scala (100%) rename serverless/aws/{lambda => lambda-core}/src/main/scala/sttp/tapir/serverless/aws/lambda/AwsBodyListener.scala (100%) rename serverless/aws/{lambda => lambda-core}/src/main/scala/sttp/tapir/serverless/aws/lambda/AwsFutureServerInterpreter.scala (100%) rename serverless/aws/{lambda => lambda-core}/src/main/scala/sttp/tapir/serverless/aws/lambda/AwsFutureServerOptions.scala (100%) rename serverless/aws/{lambda => lambda-core}/src/main/scala/sttp/tapir/serverless/aws/lambda/AwsRequestBody.scala (100%) rename serverless/aws/{lambda => lambda-core}/src/main/scala/sttp/tapir/serverless/aws/lambda/AwsServerInterpreter.scala (96%) rename serverless/aws/{lambda => lambda-core}/src/main/scala/sttp/tapir/serverless/aws/lambda/AwsServerOptions.scala (100%) rename serverless/aws/{lambda => lambda-core}/src/main/scala/sttp/tapir/serverless/aws/lambda/AwsServerRequest.scala (100%) rename serverless/aws/{lambda => lambda-core}/src/main/scala/sttp/tapir/serverless/aws/lambda/model.scala (100%) rename serverless/aws/{lambda => lambda-core}/src/main/scala/sttp/tapir/serverless/aws/lambda/package.scala (100%) rename serverless/aws/{lambda => lambda-core}/src/main/scalajs/sttp/tapir/serverless/aws/lambda/AwsToResponseBody.scala (100%) rename serverless/aws/{lambda => lambda-core}/src/main/scalajs/sttp/tapir/serverless/aws/lambda/js/AwsJsRequest.scala (100%) rename serverless/aws/{lambda => lambda-core}/src/main/scalajs/sttp/tapir/serverless/aws/lambda/js/AwsJsResponse.scala (100%) rename serverless/aws/{lambda => lambda-core}/src/main/scalajs/sttp/tapir/serverless/aws/lambda/js/package.scala (100%) rename serverless/aws/{lambda => lambda-core}/src/main/scalajvm/sttp/tapir/serverless/aws/lambda/AwsToResponseBody.scala (100%) rename serverless/aws/{lambda-tests/src/test/scala/sttp/tapir/serverless/aws/lambda/tests => lambda-core/src/test/scalajvm/sttp/tapir/serverless/aws/lambda}/MapperTest.scala (100%) create mode 100644 serverless/aws/lambda-zio-tests/src/main/scala/sttp/tapir/serverless/aws/ziolambda/tests/LambdaSamTemplate.scala create mode 100644 serverless/aws/lambda-zio-tests/src/main/scala/sttp/tapir/serverless/aws/ziolambda/tests/ZioLambdaHandlerImpl.scala create mode 100644 serverless/aws/lambda-zio-tests/src/main/scala/sttp/tapir/serverless/aws/ziolambda/tests/package.scala create mode 100644 serverless/aws/lambda-zio-tests/src/test/scala/sttp/tapir/serverless/aws/ziolambda/tests/AwsLambdaCreateServerStubTest.scala create mode 100644 serverless/aws/lambda-zio-tests/src/test/scala/sttp/tapir/serverless/aws/ziolambda/tests/AwsLambdaSamLocalHttpTest.scala create mode 100644 serverless/aws/lambda-zio-tests/src/test/scala/sttp/tapir/serverless/aws/ziolambda/tests/AwsLambdaStubHttpTest.scala create mode 100644 serverless/aws/lambda-zio/src/main/scala/sttp/tapir/serverless/aws/ziolambda/AwsZioServerInterpreter.scala create mode 100644 serverless/aws/lambda-zio/src/main/scala/sttp/tapir/serverless/aws/ziolambda/AwsZioServerOptions.scala create mode 100644 serverless/aws/lambda-zio/src/main/scala/sttp/tapir/serverless/aws/ziolambda/ZioLambdaHandler.scala diff --git a/.gitignore b/.gitignore index a64ca08514..ba11b7ac0a 100644 --- a/.gitignore +++ b/.gitignore @@ -17,6 +17,8 @@ project/plugins/project/ .metals metals.sbt template.yaml +aws-lambda-cats-effect-template.yaml +aws-lambda-zio-template.yaml .bsp .idea* diff --git a/build.sbt b/build.sbt index 4529c63867..46936387cf 100644 --- a/build.sbt +++ b/build.sbt @@ -219,8 +219,11 @@ lazy val rawAllAggregates = core.projectRefs ++ nettyServerZio.projectRefs ++ zio1HttpServer.projectRefs ++ zioHttpServer.projectRefs ++ - awsLambda.projectRefs ++ - awsLambdaTests.projectRefs ++ + awsLambdaCore.projectRefs ++ + awsLambdaCatsEffect.projectRefs ++ + awsLambdaCatsEffectTests.projectRefs ++ + awsLambdaZio.projectRefs ++ + awsLambdaZioTests.projectRefs ++ awsSam.projectRefs ++ awsTerraform.projectRefs ++ awsExamples.projectRefs ++ @@ -1442,7 +1445,88 @@ lazy val zioHttpServer: ProjectMatrix = (projectMatrix in file("server/zio-http- // serverless -lazy val awsLambda: ProjectMatrix = (projectMatrix in file("serverless/aws/lambda")) +lazy val awsLambdaCore: ProjectMatrix = (projectMatrix in file("serverless/aws/lambda-core")) + .settings(commonJvmSettings) + .settings( + name := "tapir-aws-lambda-core", + libraryDependencies ++= loggerDependencies + ) + .jvmPlatform(scalaVersions = scala2And3Versions) + .jsPlatform(scalaVersions = scala2Versions) + .dependsOn(serverCore, circeJson, tests % "test") + +lazy val awsLambdaZio: ProjectMatrix = (projectMatrix in file("serverless/aws/lambda-zio")) + .settings(commonJvmSettings) + .settings( + name := "tapir-aws-lambda-zio", + libraryDependencies ++= loggerDependencies, + libraryDependencies ++= Seq( + "com.amazonaws" % "aws-lambda-java-runtime-interface-client" % Versions.awsLambdaInterface + ) + ) + .jvmPlatform(scalaVersions = scala2And3Versions) + .dependsOn(serverCore, awsLambdaCore, zio, zioHttpServer, circeJson, tests % "test") + +// integration tests for lambda interpreter +// it's a separate project since it needs a fat jar with lambda code which cannot be build from tests sources +// runs sam local cmd line tool to start AWS Api Gateway with lambda proxy +lazy val awsLambdaZioTests: ProjectMatrix = (projectMatrix in file("serverless/aws/lambda-zio-tests")) + .settings(commonJvmSettings) + .settings( + name := "tapir-aws-lambda-zio-tests", + assembly / assemblyJarName := "tapir-aws-lambda-zio-tests.jar", + assembly / test := {}, // no tests before building jar + assembly / assemblyMergeStrategy := { + case PathList("META-INF", "io.netty.versions.properties") => MergeStrategy.first + case PathList(ps @ _*) if ps.last contains "FlowAdapters" => MergeStrategy.first + case PathList(ps @ _*) if ps.last == "module-info.class" => MergeStrategy.first + case _ @("scala/annotation/nowarn.class" | "scala/annotation/nowarn$.class") => MergeStrategy.first + case PathList("deriving.conf") => MergeStrategy.concat + case x => (assembly / assemblyMergeStrategy).value(x) + }, + Test / test := { + if (scalaVersion.value == scala2_13) { // only one test can run concurrently, as it starts a local sam instance + (Test / test) + .dependsOn( + Def.sequential( + (Compile / runMain).toTask(" sttp.tapir.serverless.aws.ziolambda.tests.LambdaSamTemplate"), + assembly + ) + ) + .value + } + }, + Test / testOptions ++= { + val log = sLog.value + // process uses template.yaml which is generated by `LambdaSamTemplate` called above + lazy val sam = Process("sam local start-api -p 3002 -t aws-lambda-zio-template.yaml --warm-containers EAGER").run() + Seq( + Tests.Setup(() => { + val samReady = PollingUtils.poll(60.seconds, 1.second) { + sam.isAlive() && PollingUtils.urlConnectionAvailable(new URL(s"http://127.0.0.1:3002/health")) + } + if (!samReady) { + sam.destroy() + val exit = sam.exitValue() + log.error(s"failed to start sam local within 60 seconds (exit code: $exit)") + } + }), + Tests.Cleanup(() => { + sam.destroy() + val exit = sam.exitValue() + log.info(s"stopped sam local (exit code: $exit)") + }) + ) + }, + Test / parallelExecution := false + ) + .jvmPlatform(scalaVersions = scala2Versions) + .dependsOn(core, zio, circeJson, awsLambdaZio, awsSam, sttpStubServer, serverTests) + .settings( + libraryDependencies ++= Seq("dev.zio" %% "zio-interop-cats" % Versions.zioInteropCats) + ) + +lazy val awsLambdaCatsEffect: ProjectMatrix = (projectMatrix in file("serverless/aws/lambda-cats-effect")) .settings(commonJvmSettings) .settings( name := "tapir-aws-lambda", @@ -1454,16 +1538,16 @@ lazy val awsLambda: ProjectMatrix = (projectMatrix in file("serverless/aws/lambd ) .jvmPlatform(scalaVersions = scala2And3Versions) .jsPlatform(scalaVersions = scala2Versions) - .dependsOn(serverCore, cats, catsEffect, circeJson, tests % "test") + .dependsOn(serverCore, awsLambdaCore, cats, catsEffect, circeJson, tests % "test") // integration tests for lambda interpreter // it's a separate project since it needs a fat jar with lambda code which cannot be build from tests sources // runs sam local cmd line tool to start AWS Api Gateway with lambda proxy -lazy val awsLambdaTests: ProjectMatrix = (projectMatrix in file("serverless/aws/lambda-tests")) +lazy val awsLambdaCatsEffectTests: ProjectMatrix = (projectMatrix in file("serverless/aws/lambda-cats-effect-tests")) .settings(commonJvmSettings) .settings( - name := "tapir-aws-lambda-tests", - assembly / assemblyJarName := "tapir-aws-lambda-tests.jar", + name := "tapir-aws-lambda-cats-effect-tests", + assembly / assemblyJarName := "tapir-aws-lambda-cats-effect-tests.jar", assembly / test := {}, // no tests before building jar assembly / assemblyMergeStrategy := { case PathList("META-INF", "io.netty.versions.properties") => MergeStrategy.first @@ -1487,29 +1571,29 @@ lazy val awsLambdaTests: ProjectMatrix = (projectMatrix in file("serverless/aws/ Test / testOptions ++= { val log = sLog.value // process uses template.yaml which is generated by `LambdaSamTemplate` called above - lazy val sam = Process("sam local start-api --warm-containers EAGER").run() + lazy val sam = Process("sam local start-api -p 3001 -t aws-lambda-cats-effect-template.yaml --warm-containers EAGER").run() Seq( Tests.Setup(() => { val samReady = PollingUtils.poll(60.seconds, 1.second) { - sam.isAlive() && PollingUtils.urlConnectionAvailable(new URL(s"http://127.0.0.1:3000/health")) + sam.isAlive() && PollingUtils.urlConnectionAvailable(new URL(s"http://127.0.0.1:3001/health")) } if (!samReady) { sam.destroy() val exit = sam.exitValue() - log.error(s"failed to start sam local within 60 seconds (exit code: $exit") + log.error(s"failed to start sam local within 60 seconds (exit code: $exit)") } }), Tests.Cleanup(() => { sam.destroy() val exit = sam.exitValue() - log.info(s"stopped sam local (exit code: $exit") + log.info(s"stopped sam local (exit code: $exit)") }) ) }, Test / parallelExecution := false ) .jvmPlatform(scalaVersions = scala2Versions) - .dependsOn(core, cats, circeJson, awsLambda, awsSam, sttpStubServer, serverTests) + .dependsOn(core, awsLambdaCatsEffect, cats, circeJson, awsSam, sttpStubServer, serverTests) // integration tests for aws cdk interpreter // it's a separate project since it needs a fat jar with lambda code which cannot be build from tests sources @@ -1609,7 +1693,7 @@ lazy val awsCdk: ProjectMatrix = (projectMatrix in file("serverless/aws/cdk")) ) ) .jvmPlatform(scalaVersions = scala2And3Versions) - .dependsOn(core, tests % Test, awsLambda) + .dependsOn(core, tests % Test, awsLambdaCore, awsLambdaCatsEffect) lazy val awsTerraform: ProjectMatrix = (projectMatrix in file("serverless/aws/terraform")) .settings(commonJvmSettings) @@ -1654,7 +1738,7 @@ lazy val awsExamples: ProjectMatrix = (projectMatrix in file("serverless/aws/exa scalaJSLinkerConfig ~= { _.withModuleKind(ModuleKind.CommonJSModule) } ) ) - .dependsOn(awsLambda) + .dependsOn(awsLambdaCore, awsLambdaCatsEffect) lazy val awsExamples2_12 = awsExamples.jvm(scala2_12).dependsOn(awsSam.jvm(scala2_12), awsTerraform.jvm(scala2_12), awsCdk.jvm(scala2_12)) lazy val awsExamples2_13 = awsExamples.jvm(scala2_13).dependsOn(awsSam.jvm(scala2_13), awsTerraform.jvm(scala2_13), awsCdk.jvm(scala2_13)) diff --git a/serverless/aws/lambda-tests/src/main/scala/sttp/tapir/serverless/aws/lambda/tests/IOLambdaHandlerV2.scala b/serverless/aws/lambda-cats-effect-tests/src/main/scala/sttp/tapir/serverless/aws/lambda/tests/IOLambdaHandlerV2.scala similarity index 100% rename from serverless/aws/lambda-tests/src/main/scala/sttp/tapir/serverless/aws/lambda/tests/IOLambdaHandlerV2.scala rename to serverless/aws/lambda-cats-effect-tests/src/main/scala/sttp/tapir/serverless/aws/lambda/tests/IOLambdaHandlerV2.scala diff --git a/serverless/aws/lambda-tests/src/main/scala/sttp/tapir/serverless/aws/lambda/tests/LambdaSamTemplate.scala b/serverless/aws/lambda-cats-effect-tests/src/main/scala/sttp/tapir/serverless/aws/lambda/tests/LambdaSamTemplate.scala similarity index 69% rename from serverless/aws/lambda-tests/src/main/scala/sttp/tapir/serverless/aws/lambda/tests/LambdaSamTemplate.scala rename to serverless/aws/lambda-cats-effect-tests/src/main/scala/sttp/tapir/serverless/aws/lambda/tests/LambdaSamTemplate.scala index 5af04df4b2..7c2f37c1e8 100644 --- a/serverless/aws/lambda-tests/src/main/scala/sttp/tapir/serverless/aws/lambda/tests/LambdaSamTemplate.scala +++ b/serverless/aws/lambda-cats-effect-tests/src/main/scala/sttp/tapir/serverless/aws/lambda/tests/LambdaSamTemplate.scala @@ -7,7 +7,8 @@ import java.nio.file.{Files, Paths} object LambdaSamTemplate extends App { - val jarPath = Paths.get("serverless/aws/lambda-tests/target/jvm-2.13/tapir-aws-lambda-tests.jar").toAbsolutePath.toString + val jarPath = + Paths.get("serverless/aws/lambda-cats-effect-tests/target/jvm-2.13/tapir-aws-lambda-cats-effect-tests.jar").toAbsolutePath.toString val samOptions: AwsSamOptions = AwsSamOptions( "Tests", @@ -19,5 +20,5 @@ object LambdaSamTemplate extends App { memorySize = 1024 ) val yaml = AwsSamInterpreter(samOptions).toSamTemplate(allEndpoints.map(_.endpoint).toList).toYaml - Files.write(Paths.get("template.yaml"), yaml.getBytes(UTF_8)) + Files.write(Paths.get("aws-lambda-cats-effect-template.yaml"), yaml.getBytes(UTF_8)) } diff --git a/serverless/aws/lambda-tests/src/main/scala/sttp/tapir/serverless/aws/lambda/tests/package.scala b/serverless/aws/lambda-cats-effect-tests/src/main/scala/sttp/tapir/serverless/aws/lambda/tests/package.scala similarity index 100% rename from serverless/aws/lambda-tests/src/main/scala/sttp/tapir/serverless/aws/lambda/tests/package.scala rename to serverless/aws/lambda-cats-effect-tests/src/main/scala/sttp/tapir/serverless/aws/lambda/tests/package.scala diff --git a/serverless/aws/lambda-tests/src/test/scala/sttp/tapir/serverless/aws/lambda/tests/AwsLambdaCreateServerStubTest.scala b/serverless/aws/lambda-cats-effect-tests/src/test/scala/sttp/tapir/serverless/aws/lambda/tests/AwsLambdaCreateServerStubTest.scala similarity index 96% rename from serverless/aws/lambda-tests/src/test/scala/sttp/tapir/serverless/aws/lambda/tests/AwsLambdaCreateServerStubTest.scala rename to serverless/aws/lambda-cats-effect-tests/src/test/scala/sttp/tapir/serverless/aws/lambda/tests/AwsLambdaCreateServerStubTest.scala index f901fe6188..a66edc907b 100644 --- a/serverless/aws/lambda-tests/src/test/scala/sttp/tapir/serverless/aws/lambda/tests/AwsLambdaCreateServerStubTest.scala +++ b/serverless/aws/lambda-cats-effect-tests/src/test/scala/sttp/tapir/serverless/aws/lambda/tests/AwsLambdaCreateServerStubTest.scala @@ -28,7 +28,7 @@ class AwsLambdaCreateServerStubTest extends CreateServerTest[IO, Any, AwsServerO val se: ServerEndpoint[Any, IO] = e.serverLogic(fn) val route: Route[IO] = AwsCatsEffectServerInterpreter(serverOptions).toRoute(se) val name = e.showDetail + (if (testNameSuffix == "") "" else " " + testNameSuffix) - Test(name)(runTest(stubBackend(route), uri"http://localhost:3000").unsafeToFuture()) + Test(name)(runTest(stubBackend(route), uri"http://localhost:3001").unsafeToFuture()) } override def testServerLogic(e: ServerEndpoint[Any, IO], testNameSuffix: String, interceptors: Interceptors = identity)( @@ -38,7 +38,7 @@ class AwsLambdaCreateServerStubTest extends CreateServerTest[IO, Any, AwsServerO .copy(encodeResponseBody = false) val route: Route[IO] = AwsCatsEffectServerInterpreter(serverOptions).toRoute(e) val name = e.showDetail + (if (testNameSuffix == "") "" else " " + testNameSuffix) - Test(name)(runTest(stubBackend(route), uri"http://localhost:3000").unsafeToFuture()) + Test(name)(runTest(stubBackend(route), uri"http://localhost:3001").unsafeToFuture()) } override def testServer(name: String, rs: => NonEmptyList[Route[IO]])( @@ -51,7 +51,7 @@ class AwsLambdaCreateServerStubTest extends CreateServerTest[IO, Any, AwsServerO } IO.pure(responses.find(_.code != StatusCode.NotFound).getOrElse(Response("", StatusCode.NotFound))) } - Test(name)(runTest(backend, uri"http://localhost:3000").unsafeToFuture()) + Test(name)(runTest(backend, uri"http://localhost:3001").unsafeToFuture()) } private def stubBackend(route: Route[IO]): SttpBackend[IO, Fs2Streams[IO] with WebSockets] = @@ -71,7 +71,7 @@ object AwsLambdaCreateServerStubTest { }, headers = request.headers.map(h => h.name -> h.value).toMap, requestContext = AwsRequestContext( - domainName = Some("localhost:3000"), + domainName = Some("localhost:3001"), http = AwsHttp( request.method.method, request.uri.path.mkString("/"), diff --git a/serverless/aws/lambda-tests/src/test/scala/sttp/tapir/serverless/aws/lambda/tests/AwsLambdaSamLocalHttpTest.scala b/serverless/aws/lambda-cats-effect-tests/src/test/scala/sttp/tapir/serverless/aws/lambda/tests/AwsLambdaSamLocalHttpTest.scala similarity index 97% rename from serverless/aws/lambda-tests/src/test/scala/sttp/tapir/serverless/aws/lambda/tests/AwsLambdaSamLocalHttpTest.scala rename to serverless/aws/lambda-cats-effect-tests/src/test/scala/sttp/tapir/serverless/aws/lambda/tests/AwsLambdaSamLocalHttpTest.scala index 24e51b2bcb..78af95762a 100644 --- a/serverless/aws/lambda-tests/src/test/scala/sttp/tapir/serverless/aws/lambda/tests/AwsLambdaSamLocalHttpTest.scala +++ b/serverless/aws/lambda-cats-effect-tests/src/test/scala/sttp/tapir/serverless/aws/lambda/tests/AwsLambdaSamLocalHttpTest.scala @@ -18,7 +18,7 @@ import sttp.tapir.server.tests.backendResource */ class AwsLambdaSamLocalHttpTest extends AnyFunSuite { - private val baseUri: Uri = uri"http://localhost:3000" + private val baseUri: Uri = uri"http://localhost:3001" testServer(in_path_path_out_string_endpoint) { backend => basicRequest.get(uri"$baseUri/fruit/orange/amount/20").send(backend).map { req => diff --git a/serverless/aws/lambda-tests/src/test/scala/sttp/tapir/serverless/aws/lambda/tests/AwsLambdaStubHttpTest.scala b/serverless/aws/lambda-cats-effect-tests/src/test/scala/sttp/tapir/serverless/aws/lambda/tests/AwsLambdaStubHttpTest.scala similarity index 100% rename from serverless/aws/lambda-tests/src/test/scala/sttp/tapir/serverless/aws/lambda/tests/AwsLambdaStubHttpTest.scala rename to serverless/aws/lambda-cats-effect-tests/src/test/scala/sttp/tapir/serverless/aws/lambda/tests/AwsLambdaStubHttpTest.scala diff --git a/serverless/aws/lambda/src/main/scala/sttp/tapir/serverless/aws/lambda/AwsCatsEffectServerInterpreter.scala b/serverless/aws/lambda-cats-effect/src/main/scala/sttp/tapir/serverless/aws/lambda/AwsCatsEffectServerInterpreter.scala similarity index 100% rename from serverless/aws/lambda/src/main/scala/sttp/tapir/serverless/aws/lambda/AwsCatsEffectServerInterpreter.scala rename to serverless/aws/lambda-cats-effect/src/main/scala/sttp/tapir/serverless/aws/lambda/AwsCatsEffectServerInterpreter.scala diff --git a/serverless/aws/lambda/src/main/scala/sttp/tapir/serverless/aws/lambda/AwsCatsEffectServerOptions.scala b/serverless/aws/lambda-cats-effect/src/main/scala/sttp/tapir/serverless/aws/lambda/AwsCatsEffectServerOptions.scala similarity index 100% rename from serverless/aws/lambda/src/main/scala/sttp/tapir/serverless/aws/lambda/AwsCatsEffectServerOptions.scala rename to serverless/aws/lambda-cats-effect/src/main/scala/sttp/tapir/serverless/aws/lambda/AwsCatsEffectServerOptions.scala diff --git a/serverless/aws/lambda/src/main/scala/sttp/tapir/serverless/aws/lambda/LambdaHandler.scala b/serverless/aws/lambda-cats-effect/src/main/scala/sttp/tapir/serverless/aws/lambda/LambdaHandler.scala similarity index 100% rename from serverless/aws/lambda/src/main/scala/sttp/tapir/serverless/aws/lambda/LambdaHandler.scala rename to serverless/aws/lambda-cats-effect/src/main/scala/sttp/tapir/serverless/aws/lambda/LambdaHandler.scala diff --git a/serverless/aws/lambda/src/main/scala/sttp/tapir/serverless/aws/lambda/runtime/AwsLambdaRuntimeInvocation.scala b/serverless/aws/lambda-cats-effect/src/main/scala/sttp/tapir/serverless/aws/lambda/runtime/AwsLambdaRuntimeInvocation.scala similarity index 100% rename from serverless/aws/lambda/src/main/scala/sttp/tapir/serverless/aws/lambda/runtime/AwsLambdaRuntimeInvocation.scala rename to serverless/aws/lambda-cats-effect/src/main/scala/sttp/tapir/serverless/aws/lambda/runtime/AwsLambdaRuntimeInvocation.scala diff --git a/serverless/aws/lambda/src/main/scalajs/sttp/tapir/serverless/aws/lambda/js/AwsJsRouteHandler.scala b/serverless/aws/lambda-cats-effect/src/main/scalajs/sttp/tapir/serverless/aws/lambda/js/AwsJsRouteHandler.scala similarity index 100% rename from serverless/aws/lambda/src/main/scalajs/sttp/tapir/serverless/aws/lambda/js/AwsJsRouteHandler.scala rename to serverless/aws/lambda-cats-effect/src/main/scalajs/sttp/tapir/serverless/aws/lambda/js/AwsJsRouteHandler.scala diff --git a/serverless/aws/lambda/src/main/scalajvm/sttp/tapir/serverless/aws/lambda/runtime/AwsLambdaRuntime.scala b/serverless/aws/lambda-cats-effect/src/main/scalajvm/sttp/tapir/serverless/aws/lambda/runtime/AwsLambdaRuntime.scala similarity index 100% rename from serverless/aws/lambda/src/main/scalajvm/sttp/tapir/serverless/aws/lambda/runtime/AwsLambdaRuntime.scala rename to serverless/aws/lambda-cats-effect/src/main/scalajvm/sttp/tapir/serverless/aws/lambda/runtime/AwsLambdaRuntime.scala diff --git a/serverless/aws/lambda/src/test/scalajvm/sttp/tapir/serverless/aws/lambda/runtime/AwsLambdaRuntimeInvocationTest.scala b/serverless/aws/lambda-cats-effect/src/test/scalajvm/sttp/tapir/serverless/aws/lambda/runtime/AwsLambdaRuntimeInvocationTest.scala similarity index 100% rename from serverless/aws/lambda/src/test/scalajvm/sttp/tapir/serverless/aws/lambda/runtime/AwsLambdaRuntimeInvocationTest.scala rename to serverless/aws/lambda-cats-effect/src/test/scalajvm/sttp/tapir/serverless/aws/lambda/runtime/AwsLambdaRuntimeInvocationTest.scala diff --git a/serverless/aws/lambda/src/main/scala/sttp/tapir/serverless/aws/lambda/AwsBodyListener.scala b/serverless/aws/lambda-core/src/main/scala/sttp/tapir/serverless/aws/lambda/AwsBodyListener.scala similarity index 100% rename from serverless/aws/lambda/src/main/scala/sttp/tapir/serverless/aws/lambda/AwsBodyListener.scala rename to serverless/aws/lambda-core/src/main/scala/sttp/tapir/serverless/aws/lambda/AwsBodyListener.scala diff --git a/serverless/aws/lambda/src/main/scala/sttp/tapir/serverless/aws/lambda/AwsFutureServerInterpreter.scala b/serverless/aws/lambda-core/src/main/scala/sttp/tapir/serverless/aws/lambda/AwsFutureServerInterpreter.scala similarity index 100% rename from serverless/aws/lambda/src/main/scala/sttp/tapir/serverless/aws/lambda/AwsFutureServerInterpreter.scala rename to serverless/aws/lambda-core/src/main/scala/sttp/tapir/serverless/aws/lambda/AwsFutureServerInterpreter.scala diff --git a/serverless/aws/lambda/src/main/scala/sttp/tapir/serverless/aws/lambda/AwsFutureServerOptions.scala b/serverless/aws/lambda-core/src/main/scala/sttp/tapir/serverless/aws/lambda/AwsFutureServerOptions.scala similarity index 100% rename from serverless/aws/lambda/src/main/scala/sttp/tapir/serverless/aws/lambda/AwsFutureServerOptions.scala rename to serverless/aws/lambda-core/src/main/scala/sttp/tapir/serverless/aws/lambda/AwsFutureServerOptions.scala diff --git a/serverless/aws/lambda/src/main/scala/sttp/tapir/serverless/aws/lambda/AwsRequestBody.scala b/serverless/aws/lambda-core/src/main/scala/sttp/tapir/serverless/aws/lambda/AwsRequestBody.scala similarity index 100% rename from serverless/aws/lambda/src/main/scala/sttp/tapir/serverless/aws/lambda/AwsRequestBody.scala rename to serverless/aws/lambda-core/src/main/scala/sttp/tapir/serverless/aws/lambda/AwsRequestBody.scala diff --git a/serverless/aws/lambda/src/main/scala/sttp/tapir/serverless/aws/lambda/AwsServerInterpreter.scala b/serverless/aws/lambda-core/src/main/scala/sttp/tapir/serverless/aws/lambda/AwsServerInterpreter.scala similarity index 96% rename from serverless/aws/lambda/src/main/scala/sttp/tapir/serverless/aws/lambda/AwsServerInterpreter.scala rename to serverless/aws/lambda-core/src/main/scala/sttp/tapir/serverless/aws/lambda/AwsServerInterpreter.scala index 1e676d0894..ea38e82034 100644 --- a/serverless/aws/lambda/src/main/scala/sttp/tapir/serverless/aws/lambda/AwsServerInterpreter.scala +++ b/serverless/aws/lambda-core/src/main/scala/sttp/tapir/serverless/aws/lambda/AwsServerInterpreter.scala @@ -9,7 +9,7 @@ import sttp.tapir.server.interceptor.RequestResult import sttp.tapir.server.interceptor.reject.RejectInterceptor import sttp.tapir.server.interpreter.{BodyListener, FilterServerEndpoints, ServerInterpreter} -private[lambda] abstract class AwsServerInterpreter[F[_]: MonadError] { +private[aws] abstract class AwsServerInterpreter[F[_]: MonadError] { def awsServerOptions: AwsServerOptions[F] diff --git a/serverless/aws/lambda/src/main/scala/sttp/tapir/serverless/aws/lambda/AwsServerOptions.scala b/serverless/aws/lambda-core/src/main/scala/sttp/tapir/serverless/aws/lambda/AwsServerOptions.scala similarity index 100% rename from serverless/aws/lambda/src/main/scala/sttp/tapir/serverless/aws/lambda/AwsServerOptions.scala rename to serverless/aws/lambda-core/src/main/scala/sttp/tapir/serverless/aws/lambda/AwsServerOptions.scala diff --git a/serverless/aws/lambda/src/main/scala/sttp/tapir/serverless/aws/lambda/AwsServerRequest.scala b/serverless/aws/lambda-core/src/main/scala/sttp/tapir/serverless/aws/lambda/AwsServerRequest.scala similarity index 100% rename from serverless/aws/lambda/src/main/scala/sttp/tapir/serverless/aws/lambda/AwsServerRequest.scala rename to serverless/aws/lambda-core/src/main/scala/sttp/tapir/serverless/aws/lambda/AwsServerRequest.scala diff --git a/serverless/aws/lambda/src/main/scala/sttp/tapir/serverless/aws/lambda/model.scala b/serverless/aws/lambda-core/src/main/scala/sttp/tapir/serverless/aws/lambda/model.scala similarity index 100% rename from serverless/aws/lambda/src/main/scala/sttp/tapir/serverless/aws/lambda/model.scala rename to serverless/aws/lambda-core/src/main/scala/sttp/tapir/serverless/aws/lambda/model.scala diff --git a/serverless/aws/lambda/src/main/scala/sttp/tapir/serverless/aws/lambda/package.scala b/serverless/aws/lambda-core/src/main/scala/sttp/tapir/serverless/aws/lambda/package.scala similarity index 100% rename from serverless/aws/lambda/src/main/scala/sttp/tapir/serverless/aws/lambda/package.scala rename to serverless/aws/lambda-core/src/main/scala/sttp/tapir/serverless/aws/lambda/package.scala diff --git a/serverless/aws/lambda/src/main/scalajs/sttp/tapir/serverless/aws/lambda/AwsToResponseBody.scala b/serverless/aws/lambda-core/src/main/scalajs/sttp/tapir/serverless/aws/lambda/AwsToResponseBody.scala similarity index 100% rename from serverless/aws/lambda/src/main/scalajs/sttp/tapir/serverless/aws/lambda/AwsToResponseBody.scala rename to serverless/aws/lambda-core/src/main/scalajs/sttp/tapir/serverless/aws/lambda/AwsToResponseBody.scala diff --git a/serverless/aws/lambda/src/main/scalajs/sttp/tapir/serverless/aws/lambda/js/AwsJsRequest.scala b/serverless/aws/lambda-core/src/main/scalajs/sttp/tapir/serverless/aws/lambda/js/AwsJsRequest.scala similarity index 100% rename from serverless/aws/lambda/src/main/scalajs/sttp/tapir/serverless/aws/lambda/js/AwsJsRequest.scala rename to serverless/aws/lambda-core/src/main/scalajs/sttp/tapir/serverless/aws/lambda/js/AwsJsRequest.scala diff --git a/serverless/aws/lambda/src/main/scalajs/sttp/tapir/serverless/aws/lambda/js/AwsJsResponse.scala b/serverless/aws/lambda-core/src/main/scalajs/sttp/tapir/serverless/aws/lambda/js/AwsJsResponse.scala similarity index 100% rename from serverless/aws/lambda/src/main/scalajs/sttp/tapir/serverless/aws/lambda/js/AwsJsResponse.scala rename to serverless/aws/lambda-core/src/main/scalajs/sttp/tapir/serverless/aws/lambda/js/AwsJsResponse.scala diff --git a/serverless/aws/lambda/src/main/scalajs/sttp/tapir/serverless/aws/lambda/js/package.scala b/serverless/aws/lambda-core/src/main/scalajs/sttp/tapir/serverless/aws/lambda/js/package.scala similarity index 100% rename from serverless/aws/lambda/src/main/scalajs/sttp/tapir/serverless/aws/lambda/js/package.scala rename to serverless/aws/lambda-core/src/main/scalajs/sttp/tapir/serverless/aws/lambda/js/package.scala diff --git a/serverless/aws/lambda/src/main/scalajvm/sttp/tapir/serverless/aws/lambda/AwsToResponseBody.scala b/serverless/aws/lambda-core/src/main/scalajvm/sttp/tapir/serverless/aws/lambda/AwsToResponseBody.scala similarity index 100% rename from serverless/aws/lambda/src/main/scalajvm/sttp/tapir/serverless/aws/lambda/AwsToResponseBody.scala rename to serverless/aws/lambda-core/src/main/scalajvm/sttp/tapir/serverless/aws/lambda/AwsToResponseBody.scala diff --git a/serverless/aws/lambda-tests/src/test/scala/sttp/tapir/serverless/aws/lambda/tests/MapperTest.scala b/serverless/aws/lambda-core/src/test/scalajvm/sttp/tapir/serverless/aws/lambda/MapperTest.scala similarity index 100% rename from serverless/aws/lambda-tests/src/test/scala/sttp/tapir/serverless/aws/lambda/tests/MapperTest.scala rename to serverless/aws/lambda-core/src/test/scalajvm/sttp/tapir/serverless/aws/lambda/MapperTest.scala diff --git a/serverless/aws/lambda-zio-tests/src/main/scala/sttp/tapir/serverless/aws/ziolambda/tests/LambdaSamTemplate.scala b/serverless/aws/lambda-zio-tests/src/main/scala/sttp/tapir/serverless/aws/ziolambda/tests/LambdaSamTemplate.scala new file mode 100644 index 0000000000..76ea008bee --- /dev/null +++ b/serverless/aws/lambda-zio-tests/src/main/scala/sttp/tapir/serverless/aws/ziolambda/tests/LambdaSamTemplate.scala @@ -0,0 +1,23 @@ +package sttp.tapir.serverless.aws.ziolambda.tests + +import sttp.tapir.serverless.aws.sam.{AwsSamInterpreter, AwsSamOptions, CodeSource} + +import java.nio.charset.StandardCharsets.UTF_8 +import java.nio.file.{Files, Paths} + +object LambdaSamTemplate extends App { + + val jarPath = Paths.get("serverless/aws/lambda-zio-tests/target/jvm-2.13/tapir-aws-lambda-zio-tests.jar").toAbsolutePath.toString + + val samOptions: AwsSamOptions = AwsSamOptions( + "Tests", + source = CodeSource( + "java11", + jarPath, + "sttp.tapir.serverless.aws.ziolambda.tests.ZioLambdaHandlerImpl::handleRequest" + ), + memorySize = 1024 + ) + val yaml = AwsSamInterpreter(samOptions).toSamTemplate(allEndpoints.map(_.endpoint).toList).toYaml + Files.write(Paths.get("aws-lambda-zio-template.yaml"), yaml.getBytes(UTF_8)) +} diff --git a/serverless/aws/lambda-zio-tests/src/main/scala/sttp/tapir/serverless/aws/ziolambda/tests/ZioLambdaHandlerImpl.scala b/serverless/aws/lambda-zio-tests/src/main/scala/sttp/tapir/serverless/aws/ziolambda/tests/ZioLambdaHandlerImpl.scala new file mode 100644 index 0000000000..7894e1075a --- /dev/null +++ b/serverless/aws/lambda-zio-tests/src/main/scala/sttp/tapir/serverless/aws/ziolambda/tests/ZioLambdaHandlerImpl.scala @@ -0,0 +1,22 @@ +package sttp.tapir.serverless.aws.ziolambda.tests + +import com.amazonaws.services.lambda.runtime.{Context, RequestStreamHandler} +import io.circe.generic.auto._ +import sttp.tapir.serverless.aws.lambda.AwsRequest +import sttp.tapir.serverless.aws.ziolambda.ZioLambdaHandler +import sttp.tapir.ztapir.RIOMonadError +import zio.{Runtime, Unsafe} + +import java.io.{InputStream, OutputStream} + +class ZioLambdaHandlerImpl extends RequestStreamHandler { + private implicit val m = new RIOMonadError[Any] + private val handler = ZioLambdaHandler.default[Any](allEndpoints.toList) + + override def handleRequest(input: InputStream, output: OutputStream, context: Context): Unit = { + val runtime = Runtime.default + Unsafe.unsafe { implicit unsafe => + runtime.unsafe.run(handler.process[AwsRequest](input, output)).getOrThrowFiberFailure() + } + } +} diff --git a/serverless/aws/lambda-zio-tests/src/main/scala/sttp/tapir/serverless/aws/ziolambda/tests/package.scala b/serverless/aws/lambda-zio-tests/src/main/scala/sttp/tapir/serverless/aws/ziolambda/tests/package.scala new file mode 100644 index 0000000000..135160f8b4 --- /dev/null +++ b/serverless/aws/lambda-zio-tests/src/main/scala/sttp/tapir/serverless/aws/ziolambda/tests/package.scala @@ -0,0 +1,52 @@ +package sttp.tapir.serverless.aws.ziolambda + +import sttp.tapir.{endpoint, stringToPath} +import sttp.tapir.tests.Basic._ +import sttp.tapir.tests.Mapping.in_4query_out_4header_extended +import sttp.tapir.tests.TestUtil.inputStreamToByteArray +import sttp.tapir.ztapir.ZTapir +import zio.ZIO + +import java.io.{ByteArrayInputStream, InputStream} + +package object tests extends ZTapir { + + type ZioEndpoint = ZServerEndpoint[Any, Any] + + // this endpoint is used to wait until sam local starts up before running actual tests + val health_endpoint: ZioEndpoint = + endpoint.get.in("health").zServerLogic(_ => ZIO.unit) + + val in_path_path_out_string_endpoint: ZioEndpoint = + in_path_path_out_string.zServerLogic { case (fruit: String, amount: Int) => + ZIO.attempt(s"$fruit $amount").mapError(_ => ()) + } + + val in_string_out_string_endpoint: ZioEndpoint = + in_string_out_string.in("string").zServerLogic(v => ZIO.attempt(v).mapError(_ => ())) + + val in_json_out_json_endpoint: ZioEndpoint = + in_json_out_json.in("json").zServerLogic(v => ZIO.attempt(v).mapError(_ => ())) + + val in_headers_out_headers_endpoint: ZioEndpoint = + in_headers_out_headers.zServerLogic(v => ZIO.attempt(v).mapError(_ => ())) + + val in_input_stream_out_input_stream_endpoint: ZioEndpoint = + in_input_stream_out_input_stream.in("is").zServerLogic { is => + ZIO.attempt(new ByteArrayInputStream(inputStreamToByteArray(is)): InputStream).orDie + } + + val in_4query_out_4header_extended_endpoint: ZioEndpoint = + in_4query_out_4header_extended.in("echo" / "query").zServerLogic(v => ZIO.attempt(v).mapError(_ => ())) + + val allEndpoints: Set[ZioEndpoint] = Set( + health_endpoint, + in_path_path_out_string_endpoint, + in_string_out_string_endpoint, + in_json_out_json_endpoint, + in_headers_out_headers_endpoint, + in_input_stream_out_input_stream_endpoint, + in_4query_out_4header_extended_endpoint + ) + +} diff --git a/serverless/aws/lambda-zio-tests/src/test/scala/sttp/tapir/serverless/aws/ziolambda/tests/AwsLambdaCreateServerStubTest.scala b/serverless/aws/lambda-zio-tests/src/test/scala/sttp/tapir/serverless/aws/ziolambda/tests/AwsLambdaCreateServerStubTest.scala new file mode 100644 index 0000000000..80693513bc --- /dev/null +++ b/serverless/aws/lambda-zio-tests/src/test/scala/sttp/tapir/serverless/aws/ziolambda/tests/AwsLambdaCreateServerStubTest.scala @@ -0,0 +1,120 @@ +package sttp.tapir.serverless.aws.ziolambda.tests + +import _root_.zio.{Runtime, Task, Unsafe} +import cats.data.NonEmptyList +import cats.effect.IO +import cats.effect.unsafe.implicits.global +import org.scalatest.Assertion +import sttp.capabilities.WebSockets +import sttp.capabilities.fs2.Fs2Streams +import sttp.client3 +import sttp.client3.impl.cats.CatsMonadError +import sttp.client3.testing.SttpBackendStub +import sttp.client3.{ByteArrayBody, ByteBufferBody, InputStreamBody, NoBody, Request, Response, StringBody, SttpBackend, _} +import sttp.model.{Header, StatusCode, Uri} +import sttp.tapir.PublicEndpoint +import sttp.tapir.server.ServerEndpoint +import sttp.tapir.server.tests.CreateServerTest +import sttp.tapir.serverless.aws.lambda._ +import sttp.tapir.serverless.aws.ziolambda.{AwsZioServerInterpreter, AwsZioServerOptions} +import sttp.tapir.serverless.aws.ziolambda.tests.AwsLambdaCreateServerStubTest.{awsToSttpResponse, sttpToAwsRequest} +import sttp.tapir.tests.Test +import sttp.tapir.ztapir.RIOMonadError + +class AwsLambdaCreateServerStubTest extends CreateServerTest[Task, Any, AwsServerOptions[Task], Route[Task]] { + + private implicit val m: RIOMonadError[Any] = new RIOMonadError[Any] + + override def testServer[I, E, O](e: PublicEndpoint[I, E, O, Any], testNameSuffix: String, interceptors: Interceptors = identity)( + fn: I => Task[Either[E, O]] + )(runTest: (SttpBackend[IO, Fs2Streams[IO] with WebSockets], Uri) => IO[Assertion]): Test = { + val serverOptions: AwsServerOptions[Task] = interceptors(AwsZioServerOptions.customiseInterceptors[Any]).options + .copy(encodeResponseBody = false) + val se: ServerEndpoint[Any, Task] = e.serverLogic(fn) + val interpreter = AwsZioServerInterpreter(serverOptions) + val route: Route[Task] = interpreter.toRoute(se) + val name = e.showDetail + (if (testNameSuffix == "") "" else " " + testNameSuffix) + + Test(name)(runTest(stubBackend(transformMonad(route)), uri"http://localhost:3002").unsafeToFuture()) + } + + override def testServerLogic(e: ServerEndpoint[Any, Task], testNameSuffix: String, interceptors: Interceptors = identity)( + runTest: (SttpBackend[IO, Fs2Streams[IO] with WebSockets], Uri) => IO[Assertion] + ): Test = { + val serverOptions: AwsServerOptions[Task] = interceptors(AwsZioServerOptions.customiseInterceptors[Any]).options + .copy(encodeResponseBody = false) + val interpreter = AwsZioServerInterpreter(serverOptions) + val route: Route[Task] = interpreter.toRoute(e) + val name = e.showDetail + (if (testNameSuffix == "") "" else " " + testNameSuffix) + Test(name)(runTest(stubBackend(transformMonad(route)), uri"http://localhost:3002").unsafeToFuture()) + } + + override def testServer(name: String, rs: => NonEmptyList[Route[Task]])( + runTest: (SttpBackend[IO, Fs2Streams[IO] with WebSockets], Uri) => IO[Assertion] + ): Test = { + val backend = SttpBackendStub[IO, Fs2Streams[IO] with WebSockets](AwsLambdaCreateServerStubTest.catsMonadIO).whenAnyRequest + .thenRespondF { request => + val responses: NonEmptyList[Response[String]] = rs.map { route => + transformMonad(route)(sttpToAwsRequest(request)).map(awsToSttpResponse).unsafeRunSync() + } + IO.pure(responses.find(_.code != StatusCode.NotFound).getOrElse(Response("", StatusCode.NotFound))) + } + Test(name)(runTest(backend, uri"http://localhost:3002").unsafeToFuture()) + } + + private def stubBackend(route: Route[IO]): SttpBackend[IO, Fs2Streams[IO] with WebSockets] = + SttpBackendStub[IO, Fs2Streams[IO] with WebSockets](AwsLambdaCreateServerStubTest.catsMonadIO).whenAnyRequest.thenRespondF { request => + route(sttpToAwsRequest(request)).map(awsToSttpResponse) + } + + private def transformMonad(route: Route[Task]): Route[IO] = r => + Unsafe.unsafe { implicit unsafe => + IO.fromFuture(IO(Runtime.default.unsafe.runToFuture(route(r)))) + } + +} + +object AwsLambdaCreateServerStubTest { + implicit val catsMonadIO: CatsMonadError[IO] = new CatsMonadError[IO] + + def sttpToAwsRequest(request: Request[_, _]): AwsRequest = { + AwsRequest( + rawPath = request.uri.pathSegments.toString, + rawQueryString = request.uri.params.toMultiSeq.foldLeft("") { case (q, (name, values)) => + s"${if (q == "") "" else s"$q&"}${if (values.isEmpty) name else values.map(v => s"$name=$v").mkString("&")}" + }, + headers = request.headers.map(h => h.name -> h.value).toMap, + requestContext = AwsRequestContext( + domainName = Some("localhost:3002"), + http = AwsHttp( + request.method.method, + request.uri.path.mkString("/"), + "http", + "127.0.0.1", + "Internet Explorer" + ) + ), + Some(request.body match { + case NoBody => "" + case StringBody(b, _, _) => new String(b) + case ByteArrayBody(b, _) => new String(b) + case ByteBufferBody(b, _) => new String(b.array()) + case InputStreamBody(b, _) => new String(b.readAllBytes()) + case _ => throw new UnsupportedOperationException + }), + isBase64Encoded = false + ) + } + + def awsToSttpResponse(response: AwsResponse): Response[String] = + client3.Response( + new String(response.body), + new StatusCode(response.statusCode), + "", + response.headers + .map { case (n, v) => v.split(",").map(Header(n, _)) } + .flatten + .toSeq + .asInstanceOf[scala.collection.immutable.Seq[Header]] + ) +} diff --git a/serverless/aws/lambda-zio-tests/src/test/scala/sttp/tapir/serverless/aws/ziolambda/tests/AwsLambdaSamLocalHttpTest.scala b/serverless/aws/lambda-zio-tests/src/test/scala/sttp/tapir/serverless/aws/ziolambda/tests/AwsLambdaSamLocalHttpTest.scala new file mode 100644 index 0000000000..a383df44cc --- /dev/null +++ b/serverless/aws/lambda-zio-tests/src/test/scala/sttp/tapir/serverless/aws/ziolambda/tests/AwsLambdaSamLocalHttpTest.scala @@ -0,0 +1,71 @@ +package sttp.tapir.serverless.aws.ziolambda.tests + +import cats.effect.IO +import cats.effect.unsafe.implicits.global +import org.scalatest.Assertions +import org.scalatest.compatible.Assertion +import org.scalatest.funsuite.AnyFunSuite +import org.scalatest.matchers.should.Matchers._ +import sttp.capabilities.WebSockets +import sttp.capabilities.fs2.Fs2Streams +import sttp.client3._ +import sttp.model.{Header, Uri} +import sttp.tapir.Endpoint +import sttp.tapir.server.tests.backendResource + +/** Requires running sam-local process with template generated by `LambdaSamTemplate`, it's automated in sbt test task but requires sam cli + * installed. + */ +class AwsLambdaSamLocalHttpTest extends AnyFunSuite { + + private val baseUri: Uri = uri"http://localhost:3002" + + testServer(in_path_path_out_string_endpoint.endpoint) { backend => + basicRequest.get(uri"$baseUri/fruit/orange/amount/20").send(backend).map { req => + req.body + .map(_ shouldBe "orange 20") + .getOrElse(Assertions.fail()) + } + } + + testServer(in_string_out_string_endpoint.endpoint) { backend => + basicRequest.post(uri"$baseUri/api/echo/string").body("Sweet").send(backend).map { req => + req.body.map(_ shouldBe "Sweet").getOrElse(Assertions.fail()) + } + } + + testServer(in_json_out_json_endpoint.endpoint) { backend => + basicRequest + .post(uri"$baseUri/api/echo/json") + .body("""{"fruit":"orange","amount":11}""") + .send(backend) + .map { req => + req.body + .map(_ shouldBe """{"fruit":"orange","amount":11}""") + .getOrElse(Assertions.fail()) + } + } + + testServer(in_headers_out_headers_endpoint.endpoint) { backend => + basicRequest + .get(uri"$baseUri/api/echo/headers") + .headers(Header.unsafeApply("X-Fruit", "apple"), Header.unsafeApply("Y-Fruit", "Orange")) + .send(backend) + .map(_.headers should contain allOf (Header.unsafeApply("X-Fruit", "apple"), Header.unsafeApply("Y-Fruit", "Orange"))) + } + + testServer(in_input_stream_out_input_stream_endpoint.endpoint) { backend => + basicRequest.post(uri"$baseUri/api/echo/is").body("mango").send(backend).map(_.body shouldBe Right("mango")) + } + + testServer(in_4query_out_4header_extended_endpoint.endpoint) { backend => + basicRequest + .get(uri"$baseUri/echo/query?a=1&b=2&x=3&y=4") + .send(backend) + .map(_.headers.map(h => h.name.toLowerCase -> h.value).toSet should contain allOf ("a" -> "1", "b" -> "2", "x" -> "3", "y" -> "4")) + } + + private def testServer(t: Endpoint[?, ?, ?, ?, ?], suffix: String = "")( + f: SttpBackend[IO, Fs2Streams[IO] with WebSockets] => IO[Assertion] + ): Unit = test(s"${t.showDetail} $suffix")(backendResource.use(f(_)).unsafeRunSync()) +} diff --git a/serverless/aws/lambda-zio-tests/src/test/scala/sttp/tapir/serverless/aws/ziolambda/tests/AwsLambdaStubHttpTest.scala b/serverless/aws/lambda-zio-tests/src/test/scala/sttp/tapir/serverless/aws/ziolambda/tests/AwsLambdaStubHttpTest.scala new file mode 100644 index 0000000000..32c154ed55 --- /dev/null +++ b/serverless/aws/lambda-zio-tests/src/test/scala/sttp/tapir/serverless/aws/ziolambda/tests/AwsLambdaStubHttpTest.scala @@ -0,0 +1,38 @@ +package sttp.tapir.serverless.aws.ziolambda.tests + +import cats.data.NonEmptyList +import cats.effect.{IO, Resource} +import sttp.tapir.server.ServerEndpoint +import sttp.tapir.server.tests.{ServerBasicTests, ServerMetricsTest, TestServerInterpreter} +import sttp.tapir.serverless.aws.ziolambda.{AwsZioServerInterpreter, AwsZioServerOptions} +import sttp.tapir.serverless.aws.lambda.{AwsServerOptions, Route} +import sttp.tapir.tests.{Port, Test, TestSuite} +import sttp.tapir.ztapir.RIOMonadError +import zio.Task + +class AwsLambdaStubHttpTest extends TestSuite { + override def tests: Resource[IO, List[Test]] = Resource.eval( + IO.pure { + import AwsLambdaStubHttpTest.m + + val createTestServer = new AwsLambdaCreateServerStubTest + new ServerBasicTests(createTestServer, AwsLambdaStubHttpTest.testServerInterpreter).tests() ++ + new ServerMetricsTest(createTestServer).tests() + } + ) +} + +object AwsLambdaStubHttpTest { + implicit val m: RIOMonadError[Any] = new RIOMonadError[Any] + + private val testServerInterpreter = new TestServerInterpreter[Task, Any, AwsServerOptions[Task], Route[Task]] { + + override def route(es: List[ServerEndpoint[Any, Task]], interceptors: Interceptors): Route[Task] = { + val serverOptions: AwsServerOptions[Task] = + interceptors(AwsZioServerOptions.customiseInterceptors[Any]).options.copy(encodeResponseBody = false) + AwsZioServerInterpreter(serverOptions).toRoute(es) + } + + override def server(routes: NonEmptyList[Route[Task]]): Resource[IO, Port] = ??? + } +} diff --git a/serverless/aws/lambda-zio/src/main/scala/sttp/tapir/serverless/aws/ziolambda/AwsZioServerInterpreter.scala b/serverless/aws/lambda-zio/src/main/scala/sttp/tapir/serverless/aws/ziolambda/AwsZioServerInterpreter.scala new file mode 100644 index 0000000000..9e34187dde --- /dev/null +++ b/serverless/aws/lambda-zio/src/main/scala/sttp/tapir/serverless/aws/ziolambda/AwsZioServerInterpreter.scala @@ -0,0 +1,23 @@ +package sttp.tapir.serverless.aws.ziolambda + +import sttp.tapir.serverless.aws.lambda.{AwsServerInterpreter, AwsServerOptions} +import sttp.tapir.ztapir.RIOMonadError +import zio.RIO + +abstract class AwsZioServerInterpreter[R: RIOMonadError] extends AwsServerInterpreter[RIO[R, *]] + +object AwsZioServerInterpreter { + + def apply[R: RIOMonadError](serverOptions: AwsServerOptions[RIO[R, *]]): AwsZioServerInterpreter[R] = + new AwsZioServerInterpreter[R] { + override def awsServerOptions: AwsServerOptions[RIO[R, *]] = serverOptions + + } + + def apply[R: RIOMonadError](): AwsZioServerInterpreter[R] = + new AwsZioServerInterpreter[R] { + override def awsServerOptions: AwsServerOptions[RIO[R, *]] = AwsZioServerOptions.default[R] + + } + +} diff --git a/serverless/aws/lambda-zio/src/main/scala/sttp/tapir/serverless/aws/ziolambda/AwsZioServerOptions.scala b/serverless/aws/lambda-zio/src/main/scala/sttp/tapir/serverless/aws/ziolambda/AwsZioServerOptions.scala new file mode 100644 index 0000000000..6d672af40d --- /dev/null +++ b/serverless/aws/lambda-zio/src/main/scala/sttp/tapir/serverless/aws/ziolambda/AwsZioServerOptions.scala @@ -0,0 +1,22 @@ +package sttp.tapir.serverless.aws.ziolambda + +import sttp.tapir.server.interceptor.CustomiseInterceptors +import sttp.tapir.serverless.aws.lambda.AwsServerOptions +import zio.RIO + +object AwsZioServerOptions { + def customiseInterceptors[R]: CustomiseInterceptors[RIO[R, *], AwsServerOptions[RIO[R, *]]] = + CustomiseInterceptors( + createOptions = + (ci: CustomiseInterceptors[RIO[R, *], AwsServerOptions[RIO[R, *]]]) => AwsServerOptions(encodeResponseBody = true, ci.interceptors) + ) + + def default[R]: AwsServerOptions[RIO[R, *]] = customiseInterceptors.options + + def noEncoding[R]: AwsServerOptions[RIO[R, *]] = + this.default[R].copy(encodeResponseBody = false) + + def noEncoding[R](options: AwsServerOptions[RIO[R, *]]): AwsServerOptions[RIO[R, *]] = + options.copy(encodeResponseBody = false) + +} diff --git a/serverless/aws/lambda-zio/src/main/scala/sttp/tapir/serverless/aws/ziolambda/ZioLambdaHandler.scala b/serverless/aws/lambda-zio/src/main/scala/sttp/tapir/serverless/aws/ziolambda/ZioLambdaHandler.scala new file mode 100644 index 0000000000..37d5bc7efb --- /dev/null +++ b/serverless/aws/lambda-zio/src/main/scala/sttp/tapir/serverless/aws/ziolambda/ZioLambdaHandler.scala @@ -0,0 +1,80 @@ +package sttp.tapir.serverless.aws.ziolambda + +import io.circe._ +import io.circe.generic.auto._ +import io.circe.parser.decode +import io.circe.syntax.EncoderOps +import sttp.tapir.server.ziohttp.ZioHttpServerOptions +import sttp.tapir.serverless.aws.lambda.{AwsRequest, AwsRequestV1, AwsResponse, AwsServerOptions} +import sttp.tapir.ztapir._ +import zio.{RIO, Task, ZIO} + +import java.io.{BufferedWriter, InputStream, OutputStream, OutputStreamWriter} +import java.nio.charset.StandardCharsets + +/** [[ZioLambdaHandler]] is an entry point for handling requests sent to AWS Lambda application which exposes Tapir endpoints. + * + * @tparam Env + * The Environment type of the handler . + * @tparam options + * Server options of type AwsServerOptions. + */ +abstract class ZioLambdaHandler[Env: RIOMonadError](options: AwsServerOptions[RIO[Env, *]]) { + + protected def getAllEndpoints: List[ZServerEndpoint[Env, Any]] + + def process[R: Decoder](input: InputStream, output: OutputStream): RIO[Env, Unit] = { + + val server: AwsZioServerInterpreter[Env] = + AwsZioServerInterpreter[Env](options) + + for { + allBytes <- ZIO.attempt(input.readAllBytes()) + str = new String(allBytes, StandardCharsets.UTF_8) + decoded = decode[R](str) + response <- decoded match { + case Left(e) => ZIO.succeed(AwsResponse.badRequest(s"Invalid AWS request: ${e.getMessage}")) + case Right(r: AwsRequestV1) => server.toRoute(getAllEndpoints)(r.toV2) + case Right(r: AwsRequest) => server.toRoute(getAllEndpoints)(r) + case Right(r) => + val message = s"Request of type ${r.getClass.getCanonicalName} is not supported" + ZIO.fail(new IllegalArgumentException(message)) + } + _ <- writerResource(response, output) + } yield () + } + + private def writerResource(response: AwsResponse, output: OutputStream): Task[Unit] = { + val acquire = ZIO.attempt(new BufferedWriter(new OutputStreamWriter(output, StandardCharsets.UTF_8))) + val release = (writer: BufferedWriter) => + ZIO.attempt { + writer.flush() + writer.close() + }.orDie + val use = (writer: BufferedWriter) => ZIO.attempt(writer.write(Printer.noSpaces.print(response.asJson))) + ZIO.acquireReleaseWith(acquire)(release)(use) + } +} + +object ZioLambdaHandler { + + def apply[Env: RIOMonadError](endpoints: List[ZServerEndpoint[Env, Any]], options: AwsServerOptions[RIO[Env, *]]): ZioLambdaHandler[Env] = + new ZioLambdaHandler[Env](options) { + override protected def getAllEndpoints: List[ZServerEndpoint[Env, Any]] = endpoints + } + + def default[Env: RIOMonadError](endpoints: List[ZServerEndpoint[Env, Any]]): ZioLambdaHandler[Env] = { + val serverLogger = + ZioHttpServerOptions.defaultServerLog[Env] + + val options = + AwsZioServerOptions.noEncoding[Env]( + AwsZioServerOptions + .customiseInterceptors[Env] + .serverLog(serverLogger) + .options + ) + + ZioLambdaHandler(endpoints, options) + } +}