Skip to content

Commit

Permalink
Feature/serverless zio (#2975)
Browse files Browse the repository at this point in the history
  • Loading branch information
TigranOhanyan authored Jun 26, 2023
1 parent b3795ae commit 66a98ca
Show file tree
Hide file tree
Showing 39 changed files with 560 additions and 22 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -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*
Expand Down
112 changes: 98 additions & 14 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -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 ++
Expand Down Expand Up @@ -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",
Expand All @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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))
}
Original file line number Diff line number Diff line change
Expand Up @@ -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)(
Expand All @@ -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]])(
Expand All @@ -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] =
Expand All @@ -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("/"),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 =>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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]

Expand Down
Original file line number Diff line number Diff line change
@@ -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))
}
Original file line number Diff line number Diff line change
@@ -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()
}
}
}
Original file line number Diff line number Diff line change
@@ -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
)

}
Loading

0 comments on commit 66a98ca

Please sign in to comment.