Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/serverless zio #2975

Merged
merged 6 commits into from
Jun 26, 2023
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
108 changes: 96 additions & 12 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")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
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)")

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

}
}),
Tests.Cleanup(() => {
sam.destroy()
val exit = sam.exitValue()
log.info(s"stopped sam local (exit code: $exit")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
log.info(s"stopped sam local (exit code: $exit")
log.info(s"stopped sam local (exit code: $exit)")

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

})
)
},
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,11 +1571,11 @@ 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()
Expand All @@ -1509,7 +1593,7 @@ lazy val awsLambdaTests: ProjectMatrix = (projectMatrix in file("serverless/aws/
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] {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

question: This change isn't needed, right? Or maybe I'm missing something?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That is required, because sttp.tapir.serverless.aws.ziolambda.AwsZioServerInterpreter depends on sttp.tapir.serverless.aws.lambda.AwsServerInterpreter and for these two the closest common package is sttp.tapir.serverless.aws. That is why it should be private to [aws] to make the code compile.


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