Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
af64915
adjusted code from PoC, wip on runnable example
kubinio123 May 7, 2021
83824b3
tests wip
kubinio123 May 10, 2021
a2c129d
separate module for tests
kubinio123 May 11, 2021
e444466
some it tests
kubinio123 May 12, 2021
bba11db
basic tests with stub backend
kubinio123 May 12, 2021
5bcc9ac
file/multipart tests separated
kubinio123 May 13, 2021
ad8c83f
invoke sam local in sbt test
kubinio123 May 13, 2021
a2451dd
runnable example
kubinio123 May 14, 2021
64922f5
adjustments and docs
kubinio123 May 14, 2021
6d43016
runtime added
kubinio123 May 17, 2021
7aba8db
Merge branch 'master' into tapir-aws
kubinio123 May 17, 2021
d344f9f
merge & compile fix
kubinio123 May 17, 2021
fb68ef6
try to install sam on ci
kubinio123 May 17, 2021
4eb3cc5
try to install sam on ci (sudo privileges)
kubinio123 May 17, 2021
b6ae64e
try to install sam on ci (less output)
kubinio123 May 17, 2021
3755542
try to install sam on ci (pull java11 runtime for sam)
kubinio123 May 17, 2021
de40110
try to install sam on ci (different image)
kubinio123 May 17, 2021
b8e56bd
try to install sam on ci (different image 2)
kubinio123 May 17, 2021
708972f
revert commented ci stage
kubinio123 May 17, 2021
66f3c88
fix assembly on 2_12
kubinio123 May 17, 2021
7ccd267
lambda deployment
kubinio123 May 20, 2021
5fefe75
api gateway deployment
kubinio123 May 20, 2021
f574682
terraform encoders
kubinio123 May 24, 2021
cb805ef
differentiation of endpoints with same path by method
kubinio123 May 25, 2021
485cc45
fix duplicated path resources
kubinio123 May 25, 2021
38eb915
Merge branch 'tapir-aws-terraform' into tapir-aws
kubinio123 May 25, 2021
961c447
Merge branch 'master' into tapir-aws
kubinio123 May 25, 2021
ab49f54
clean up
kubinio123 May 25, 2021
5d9aa91
--update for sam install
kubinio123 May 25, 2021
61798fc
match fix
kubinio123 May 25, 2021
b63861a
update terraform resources to v2
kubinio123 May 26, 2021
794c18d
cr updates, docs
kubinio123 May 26, 2021
cf9b43d
cleanup
kubinio123 May 27, 2021
29c840c
runtime tests
kubinio123 May 27, 2021
38cdb0a
compile fix 2_12
kubinio123 May 31, 2021
5458707
test backend update
kubinio123 May 31, 2021
5a62a2f
remove unused type alias
kubinio123 May 31, 2021
bb23032
longer wait for sam to start up (when downloading image)
kubinio123 May 31, 2021
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
6 changes: 6 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,12 @@ jobs:
~/.ivy2/cache
~/.coursier
key: sbt-cache-${{ runner.os }}-${{ matrix.target-platform }}-${{ hashFiles('project/build.properties') }}
- name: Install sam cli
run: |
wget -q https://github.com/aws/aws-sam-cli/releases/latest/download/aws-sam-cli-linux-x86_64.zip
unzip -q aws-sam-cli-linux-x86_64.zip -d sam-installation
sudo ./sam-installation/install --update
sam --version
- name: Compile
run: sbt -v compile compileDocumentation
- name: Test
Expand Down
123 changes: 118 additions & 5 deletions build.sbt
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
import java.net.URL
import com.softwaremill.SbtSoftwareMillBrowserTestJS._
import com.softwaremill.UpdateVersionInDocs
import sbt.Reference.display
import sbt.internal.ProjectMatrix

import java.net.URL
import scala.concurrent.duration.DurationInt
import scala.sys.process.Process

val scala2_12 = "2.12.13"
val scala2_13 = "2.13.6"

Expand Down Expand Up @@ -112,6 +115,11 @@ lazy val allAggregates = core.projectRefs ++
playServer.projectRefs ++
vertxServer.projectRefs ++
zioServer.projectRefs ++
awsLambda.projectRefs ++
awsLambdaTests.projectRefs ++
awsSam.projectRefs ++
awsTerraform.projectRefs ++
awsExamples.projectRefs ++
http4sClient.projectRefs ++
sttpClient.projectRefs ++
playClient.projectRefs ++
Expand Down Expand Up @@ -263,7 +271,7 @@ lazy val tests: ProjectMatrix = (projectMatrix in file("tests"))
"com.beachape" %%% "enumeratum-circe" % Versions.enumeratum,
"com.softwaremill.common" %%% "tagging" % "2.3.0",
scalaTest.value,
"com.softwaremill.macwire" %% "macros" % "2.3.7" % "provided",
"com.softwaremill.macwire" %% "macros" % "2.3.7",
Comment thread
adamw marked this conversation as resolved.
"org.typelevel" %%% "cats-effect" % Versions.catsEffect
),
libraryDependencies ++= loggerDependencies
Expand All @@ -283,6 +291,7 @@ lazy val cats: ProjectMatrix = (projectMatrix in file("integrations/cats"))
name := "tapir-cats",
libraryDependencies ++= Seq(
"org.typelevel" %%% "cats-core" % "2.6.1",
"org.typelevel" %%% "cats-effect" % Versions.catsEffect,
scalaTest.value % Test,
scalaCheck.value % Test,
scalaTestPlusScalaCheck.value % Test,
Expand Down Expand Up @@ -397,8 +406,8 @@ lazy val circeJson: ProjectMatrix = (projectMatrix in file("json/circe"))
libraryDependencies ++= Seq(
"io.circe" %%% "circe-core" % Versions.circe,
"io.circe" %%% "circe-parser" % Versions.circe,
scalaTest.value % Test,
"io.circe" %%% "circe-generic" % Versions.circe % Test
"io.circe" %%% "circe-generic" % Versions.circe,
scalaTest.value % Test
)
)
.jvmPlatform(scalaVersions = allScalaVersions)
Expand Down Expand Up @@ -775,7 +784,7 @@ lazy val http4sServer: ProjectMatrix = (projectMatrix in file("server/http4s-ser
)
)
.jvmPlatform(scalaVersions = allScalaVersions)
.dependsOn(core, serverTests % Test)
.dependsOn(core, cats, serverTests % Test)
Comment thread
adamw marked this conversation as resolved.

lazy val sttpStubServer: ProjectMatrix = (projectMatrix in file("server/sttp-stub-server"))
.settings(commonJvmSettings)
Expand Down Expand Up @@ -874,6 +883,110 @@ lazy val zioServer: ProjectMatrix = (projectMatrix in file("server/zio-http4s-se
.jvmPlatform(scalaVersions = allScalaVersions)
.dependsOn(zio, http4sServer, serverTests % Test)

// serverless

lazy val awsLambda: ProjectMatrix = (projectMatrix in file("serverless/aws/lambda"))
.settings(commonJvmSettings)
.settings(
name := "tapir-aws-lambda",
libraryDependencies ++= loggerDependencies,
libraryDependencies ++= Seq(
"com.softwaremill.sttp.client3" %% "http4s-ce2-backend" % Versions.sttp,
"org.http4s" %% "http4s-blaze-client" % Versions.http4s
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

probably a follow-up task: I think we are always using this with Java 11+? If so, we might be able to use the HttpClient backend, which would reduce the dependencies of this project (to just cats-effect and regular java)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I wrote it down

)
)
.jvmPlatform(scalaVersions = allScalaVersions)
.dependsOn(core, cats, circeJson, awsSam, sttpStubServer % "test", tests % "test", serverTests)

// 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"))
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I'd add a comment detailing what the test is doing and why is it done this way

.settings(commonJvmSettings)
.settings(
name := "tapir-aws-lambda-tests",
libraryDependencies += "com.amazonaws" % "aws-lambda-java-runtime-interface-client" % Versions.awsLambdaInterface,
assembly / assemblyJarName := "tapir-aws-lambda-tests.jar",
assembly / test := {}, // no tests before building jar
assembly / assemblyMergeStrategy := {
case PathList("META-INF", "io.netty.versions.properties") => MergeStrategy.first
case _ @("scala/annotation/nowarn.class" | "scala/annotation/nowarn$.class") => MergeStrategy.first
case x => (assembly / assemblyMergeStrategy).value(x)
},
Test / test := (Test / test)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

if you'd like to run it using 2.13 only, I think you could do sth like:

Test / test := if (scala.version == scala2_13) {...} else () 

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

did you manage to run the tests on 2.13 only or is this solved some other way?

.dependsOn((Compile / runMain).toTask(" sttp.tapir.serverless.aws.lambda.tests.LambdaSamTemplate"))
.dependsOn(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 --warm-containers EAGER").run()
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

we might add a comment that this uses the template.yaml which is generated by the application above - if I understand correctly :)

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"))
}
if (!samReady) {
sam.destroy()
val exit = sam.exitValue()
log.error(s"failed to start sam local within 30 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 = allScalaVersions)
.dependsOn(core, cats, circeJson, awsLambda, awsSam, tests)

lazy val awsSam: ProjectMatrix = (projectMatrix in file("serverless/aws/sam"))
.settings(commonJvmSettings)
.settings(
name := "tapir-aws-sam",
libraryDependencies ++= Seq(
"io.circe" %% "circe-yaml" % Versions.circeYaml,
"io.circe" %% "circe-generic" % Versions.circe
)
)
.jvmPlatform(scalaVersions = allScalaVersions)
.dependsOn(core, tests % Test)

lazy val awsTerraform: ProjectMatrix = (projectMatrix in file("serverless/aws/terraform"))
.settings(commonJvmSettings)
.settings(
name := "tapir-aws-terraform",
libraryDependencies ++= Seq(
"io.circe" %% "circe-yaml" % Versions.circeYaml,
"io.circe" %% "circe-generic" % Versions.circe,
"io.circe" %% "circe-literal" % Versions.circe,
"org.typelevel" %% "jawn-parser" % "1.0.0"
)
)
.jvmPlatform(scalaVersions = allScalaVersions)
.dependsOn(core, tests % Test)

lazy val awsExamples: ProjectMatrix = (projectMatrix in file("serverless/aws/examples"))
.settings(commonJvmSettings)
.settings(
libraryDependencies += "com.amazonaws" % "aws-lambda-java-runtime-interface-client" % Versions.awsLambdaInterface
)
.settings(
name := "tapir-aws-examples",
assembly / assemblyJarName := "tapir-aws-examples.jar",
assembly / assemblyMergeStrategy := {
case PathList("META-INF", "io.netty.versions.properties") => MergeStrategy.first
case _ @("scala/annotation/nowarn.class" | "scala/annotation/nowarn$.class") => MergeStrategy.first
case x => (assembly / assemblyMergeStrategy).value(x)
}
)
.jvmPlatform(scalaVersions = allScalaVersions)
.dependsOn(awsLambda, awsSam, awsTerraform)

// client

lazy val clientTests: ProjectMatrix = (projectMatrix in file("client/tests"))
Expand Down
1 change: 1 addition & 0 deletions doc/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,7 @@ Development and maintenance of sttp tapir is sponsored by [SoftwareMill](https:/
:caption: Server interpreters

server/akkahttp
server/aws
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

it's a bit ironic that's a "serverless" interpreter is listed under "server interpreters", but I guess that's the proper place ;)

server/http4s
server/finatra
server/play
Expand Down
66 changes: 66 additions & 0 deletions doc/server/aws.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
# Running behind AWS API Gateway
Comment thread
adamw marked this conversation as resolved.

[AWS API Gateway](https://docs.aws.amazon.com/apigateway/latest/developerguide/welcome.html) provides a proxy
integration
with [AWS Lambda](https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-develop-integrations-lambda.html)
which allows you to implement API routes using Lambda functions. On the other hand tools
like [AWS SAM](https://aws.amazon.com/serverless/sam/) and [Terraform](https://www.terraform.io/) provides a
configuration mechanism for binding AWS Api Gateway routes to Lambda functions and automating cloud deployments.

This concept of serverless API has been adapted to Tapir in form of three components.

The first one is `AwsServerInterpreter` which routes AWS API Gateway requests to responses just as any other server
interpreter does. It should be used in your lambda function code.

```scala
"com.softwaremill.sttp.tapir" %% "tapir-aws-lambda" % "@VERSION@"
```

The remaining two are `AwsSamInterpreter` which interprets Tapir `Endpoints` into AWS SAM template file
and `AwsTerraformInterpreter` which interprets `Endpoints` into terraform configuration file. One of them should be used
to configure your API Gateway.

```scala
"com.softwaremill.sttp.tapir" %% "tapir-aws-sam" % "@VERSION@"
"com.softwaremill.sttp.tapir" %% "tapir-aws-terraform" % "@VERSION@"
```

## Examples

In
our [GitHub repository](https://github.com/softwaremill/tapir/tree/master/serverless/aws/examples/src/main/scala/sttp/tapir/serverless/aws/examples)
you'll find a `LambdaApiExample` handler which uses `AwsServerInterpreter` to route a hello endpoint along
with `SamTemplateExample` and `TerraformConfigExample` which interpret endpoints to SAM/Terraform configuration. Go
ahead and clone tapir project and select `project awsExamples` from sbt shell.

Make sure you
have [AWS command line tools installed](https://docs.aws.amazon.com/cli/latest/userguide/install-cliv2.html).

### SAM

To try it out using SAM template you don't need an AWS account.

* install [AWS SAM command line tool](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/serverless-sam-cli-command-reference.html)
* run `assembly` task and `runMain sttp.tapir.serverless.aws.examples.SamTemplateExample`
* open a terminal and in tapir root directory run `sam local start-api --warm-containers EAGER`

That will create `template.yaml` and start up AWS Api Gateway locally. Hello endpoint will be available
under `curl http://127.0.0.1:3000/api/hello`. First invocation will take a while but subsequent ones will be faster
since the created container will be reused.

### Terraform

To run the example using terraform you will need an AWS account, and an S3 bucket.

* install [Terraform](https://learn.hashicorp.com/tutorials/terraform/install-cli)
* run `assembly` task
* open a terminal in `tapir/serverless/aws/examples/target/jvm-2.13` directory. That's where the fat jar is saved. You
need to upload it into your s3 bucket. Using command line
tools: `aws s3 cp tapir-aws-examples.jar s3://{your-bucket}/{your-key}`.
* Run `runMain sttp.tapir.serverless.aws.examples.TerraformConfigExample {your-aws-region} {your-bucket} {your-key}`
* open terminal in tapir root directory, run `terraform init` and `terraform apply`

That will create `api_gateway.tf.json` configuration and deploy Api Gateway and lambda function to AWS. Terraform will
output the url of the created API Gateway which you can call followed by `/api/hello` path.

To destroy all the created resources run `terraform destroy`.
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
package sttp.tapir.server.http4s
package sttp.tapir.integ.cats

import cats.effect.Sync
import sttp.monad.MonadError

private[http4s] class CatsMonadError[F[_]](implicit F: Sync[F]) extends MonadError[F] {
class CatsMonadError[F[_]](implicit F: Sync[F]) extends MonadError[F] {
override def unit[T](t: T): F[T] = F.pure(t)
override def map[T, T2](fa: F[T])(f: T => T2): F[T2] = F.map(fa)(f)
override def flatMap[T, T2](fa: F[T])(f: T => F[T2]): F[T2] = F.flatMap(fa)(f)
Expand All @@ -13,4 +13,4 @@ private[http4s] class CatsMonadError[F[_]](implicit F: Sync[F]) extends MonadErr
override def suspend[T](t: => F[T]): F[T] = F.suspend(t)
override def flatten[T](ffa: F[F[T]]): F[T] = F.flatten(ffa)
override def ensure[T](f: F[T], e: => F[Unit]): F[T] = F.guarantee(f)(e)
}
}
1 change: 1 addition & 0 deletions project/Versions.scala
Original file line number Diff line number Diff line change
Expand Up @@ -31,4 +31,5 @@ object Versions {
val jwtScala = "5.0.0"
val derevo = "0.12.5"
val newtype = "0.4.4"
val awsLambdaInterface = "1.0.0"
}
1 change: 1 addition & 0 deletions project/plugins.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,4 @@ addSbtPlugin("com.eed3si9n" % "sbt-projectmatrix" % "0.8.0")
addSbtPlugin("org.jetbrains.scala" % "sbt-ide-settings" % "1.1.1")
addSbtPlugin("org.scala-js" % "sbt-scalajs" % "1.5.1")
addSbtPlugin("io.spray" % "sbt-revolver" % "0.9.1")
addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "0.15.0")
Original file line number Diff line number Diff line change
Expand Up @@ -9,21 +9,15 @@ import cats.implicits._
import org.scalatest.EitherValues
import org.scalatest.matchers.should.Matchers._
import sttp.capabilities.akka.AkkaStreams
import akka.http.scaladsl.server.Route
import sttp.capabilities.{WebSockets, akka}
import sttp.client3._
import sttp.client3.akkahttp.AkkaHttpBackend
import sttp.model.sse.ServerSentEvent
import sttp.monad.FutureMonad
import sttp.monad.syntax._
import sttp.tapir._
import sttp.tapir.server.tests.{
CreateServerTest,
ServerAuthenticationTests,
ServerBasicTests,
ServerMetricsTest,
ServerStreamingTests,
ServerWebSocketTests,
backendResource
}
import sttp.tapir.server.tests.{DefaultCreateServerTest, ServerAuthenticationTests, ServerBasicTests, ServerFileMultipartTests, ServerMetricsTest, ServerStreamingTests, ServerWebSocketTests, backendResource}
import sttp.tapir.tests.{Test, TestSuite}

import java.util.UUID
Expand All @@ -43,7 +37,7 @@ class AkkaHttpServerTest extends TestSuite with EitherValues {
implicit val m: FutureMonad = new FutureMonad()(actorSystem.dispatcher)

val interpreter = new AkkaHttpTestServerInterpreter()(actorSystem)
val createServerTest = new CreateServerTest(interpreter)
val createServerTest = new DefaultCreateServerTest(backend, interpreter)

def additionalTests(): List[Test] = List(
Test("endpoint nested in a path directive") {
Expand Down Expand Up @@ -86,13 +80,14 @@ class AkkaHttpServerTest extends TestSuite with EitherValues {
}
)

new ServerBasicTests(backend, createServerTest, interpreter).tests() ++
new ServerStreamingTests(backend, createServerTest, AkkaStreams).tests() ++
new ServerWebSocketTests(backend, createServerTest, AkkaStreams) {
new ServerBasicTests(createServerTest, interpreter).tests() ++
new ServerFileMultipartTests(createServerTest).tests() ++
new ServerWebSocketTests(createServerTest, AkkaStreams) {
override def functionToPipe[A, B](f: A => B): streams.Pipe[A, B] = Flow.fromFunction(f)
}.tests() ++
new ServerAuthenticationTests(backend, createServerTest).tests() ++
new ServerMetricsTest(backend, createServerTest).tests() ++
new ServerStreamingTests(createServerTest, AkkaStreams).tests() ++
new ServerAuthenticationTests(createServerTest).tests() ++
new ServerMetricsTest(createServerTest).tests() ++
additionalTests()
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,18 @@ package sttp.tapir.server.finatra.cats

import cats.effect.{IO, Resource}
import sttp.client3.impl.cats.CatsMonadAsyncError
import sttp.tapir.server.tests.{CreateServerTest, ServerAuthenticationTests, ServerBasicTests, backendResource}
import sttp.tapir.server.tests.{DefaultCreateServerTest, ServerAuthenticationTests, ServerBasicTests, ServerFileMultipartTests, backendResource}
import sttp.tapir.tests.{Test, TestSuite}

class FinatraServerCatsTests extends TestSuite {
override def tests: Resource[IO, List[Test]] = backendResource.map { backend =>
implicit val m: CatsMonadAsyncError[IO] = new CatsMonadAsyncError[IO]()

val interpreter = new FinatraCatsTestServerInterpreter()
val createServerTest = new CreateServerTest(interpreter)
val createTestServer = new DefaultCreateServerTest(backend, interpreter)

new ServerBasicTests(backend, createServerTest, interpreter).tests() ++
new ServerAuthenticationTests(backend, createServerTest).tests()
new ServerBasicTests(createTestServer, interpreter).tests() ++
new ServerFileMultipartTests(createTestServer).tests() ++
new ServerAuthenticationTests(createTestServer).tests()
}
}
Loading