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

Add Armeria server interpreters #1830

Merged
merged 25 commits into from
Feb 18, 2022
Merged
Show file tree
Hide file tree
Changes from 15 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
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
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ interpreted as:
* [Finatra](https://tapir.softwaremill.com/en/latest/server/finatra.html) `FinatraRoute`
* [Play](https://tapir.softwaremill.com/en/latest/server/play.html) `Route`
* [ZIO Http](https://tapir.softwaremill.com/en/latest/server/ziohttp.html) `Http`
* [Armeria](https://tapir.softwaremill.com/en/latest/server/armeria.html) `HttpServiceWithRoutes`
* [aws](https://tapir.softwaremill.com/en/latest/server/aws.html) through Lambda/SAM/Terraform
* a client, which is a function from input parameters to output parameters.
Currently supported:
Expand Down
45 changes: 45 additions & 0 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,9 @@ lazy val allAggregates = core.projectRefs ++
redocBundle.projectRefs ++
serverTests.projectRefs ++
akkaHttpServer.projectRefs ++
armeriaServer.projectRefs ++
armeriaServerCats.projectRefs ++
armeriaServerZio.projectRefs ++
http4sServer.projectRefs ++
sttpStubServer.projectRefs ++
sttpMockServer.projectRefs ++
Expand Down Expand Up @@ -816,6 +819,44 @@ lazy val akkaHttpServer: ProjectMatrix = (projectMatrix in file("server/akka-htt
.jvmPlatform(scalaVersions = scala2Versions)
.dependsOn(core, serverTests % Test)

lazy val armeriaServer: ProjectMatrix = (projectMatrix in file("server/armeria-server"))
.settings(commonJvmSettings)
.settings(
name := "tapir-armeria-server",
libraryDependencies ++= Seq(
"com.linecorp.armeria" % "armeria" % Versions.armeria,
"org.scala-lang.modules" %% "scala-java8-compat" % Versions.scalaJava8Compat,
"com.softwaremill.sttp.shared" %% "armeria" % Versions.sttpShared
)
)
.jvmPlatform(scalaVersions = scala2And3Versions)
.dependsOn(core, serverTests % Test)

lazy val armeriaServerCats: ProjectMatrix =
(projectMatrix in file("server/armeria-server/cats"))
.settings(commonJvmSettings)
.settings(
name := "tapir-armeria-server-cats",
libraryDependencies ++= Seq(
"com.softwaremill.sttp.shared" %% "fs2" % Versions.sttpShared,
"co.fs2" %% "fs2-reactive-streams" % Versions.fs2
)
)
.jvmPlatform(scalaVersions = scala2And3Versions)
.dependsOn(armeriaServer % "compile->compile;test->test", cats, serverTests % Test)

lazy val armeriaServerZio: ProjectMatrix =
(projectMatrix in file("server/armeria-server/zio"))
.settings(commonJvmSettings)
.settings(
name := "tapir-armeria-server-zio",
libraryDependencies ++= Seq(
"dev.zio" %% "zio-interop-reactivestreams" % Versions.zio1InteropReactiveStreams
)
)
.jvmPlatform(scalaVersions = scala2And3Versions)
.dependsOn(armeriaServer % "compile->compile;test->test", zio, serverTests % Test)

lazy val http4sServer: ProjectMatrix = (projectMatrix in file("server/http4s-server"))
.settings(commonJvmSettings)
.settings(
Expand Down Expand Up @@ -1250,6 +1291,7 @@ lazy val examples: ProjectMatrix = (projectMatrix in file("examples"))
.jvmPlatform(scalaVersions = examplesScalaVersions)
.dependsOn(
akkaHttpServer,
armeriaServer,
http4sServer,
http4sClient,
sttpClient,
Expand Down Expand Up @@ -1321,6 +1363,9 @@ lazy val documentation: ProjectMatrix = (projectMatrix in file("generated-doc"))
.dependsOn(
core % "compile->test",
akkaHttpServer,
armeriaServer,
armeriaServerCats,
armeriaServerZio,
circeJson,
enumeratum,
finatraServer,
Expand Down
2 changes: 2 additions & 0 deletions doc/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ input and output parameters. An endpoint specification can be interpreted as:
* [Finatra](server/finatra.md) `http.Controller`
* [Play](server/play.md) `Route`
* [ZIO Http](server/ziohttp.md) `Http`
* [Armeria](server/armeria.md) `HttpServiceWithRoutes`
* [aws](server/aws.md) through Lambda/SAM/Terraform
* a client, which is a function from input parameters to output parameters.
Currently supported:
Expand Down Expand Up @@ -149,6 +150,7 @@ Development and maintenance of sttp tapir is sponsored by [SoftwareMill](https:/
server/play
server/vertx
server/ziohttp
server/armeria
server/aws
server/options
server/interceptors
Expand Down
201 changes: 201 additions & 0 deletions doc/server/armeria.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,201 @@
# Running as an Armeria server

Endpoints can be mounted as `TapirService[S, F]` on top of [Armeria](https://armeria.dev)'s `HttpServiceWithRoutes`.

Armeria interpreter can be used with different effect systems (cats-effect, ZIO) as well as Scala's standard `Future`.

## Scala's standard `Future`

Add the following dependency
```scala
"com.softwaremill.sttp.tapir" %% "tapir-armeria-server" % "@VERSION@"
```

and import the object:

```scala mdoc:compile-only
import sttp.tapir.server.armeria.ArmeriaFutureServerInterpreter
```
to use this interpreter with `Future`.

The `toService` method require a single, or a list of `ServerEndpoint`s, which can be created by adding
[server logic](logic.md) to an endpoint.

```scala mdoc:compile-only
import sttp.tapir._
import sttp.tapir.server.armeria.ArmeriaFutureServerInterpreter
import scala.concurrent.Future
import com.linecorp.armeria.server.Server

object Main {
// JVM entry point that starts the HTTP server
def main(args: Array[String]): Unit = {
val tapirEndpoint: PublicEndpoint[(String, Int), Unit, String, Any] = ??? // your definition here
def logic(s: String, i: Int): Future[Either[Unit, String]] = ??? // your logic here
val tapirService = ArmeriaFutureServerInterpreter().toService(tapirEndpoint.serverLogic((logic _).tupled))
val server = Server
.builder()
.service(tapirService) // your endpoint is bound to the server
.build()
server.start().join()
}
}
```

This interpreter also supports streaming using Armeria Streams which is fully compatible with Reactive Streams:

```scala mdoc:compile-only

import sttp.capabilities.armeria.ArmeriaStreams
import sttp.tapir._
import sttp.tapir.server.armeria.ArmeriaFutureServerInterpreter
import scala.concurrent.Future
import com.linecorp.armeria.common.HttpData
import com.linecorp.armeria.common.stream.StreamMessage
import org.reactivestreams.Publisher

val streamingResponse: PublicEndpoint[Int, Unit, Publisher[HttpData], ArmeriaStreams] =
endpoint
.in("stream")
.in(query[Int]("key"))
.out(streamTextBody(ArmeriaStreams)(CodecFormat.TextPlain()))

def streamLogic(foo: Int): Future[Publisher[HttpData]] = {
Future.successful(StreamMessage.of(HttpData.ofUtf8("hello"), HttpData.ofUtf8("world")))
}

val tapirService = ArmeriaFutureServerInterpreter().toService(streamingResponse.serverLogicSuccess(streamLogic))
```

## Configuration

Every endpoint can be configured by providing an instance of `ArmeriaFutureEndpointOptions`, see [server options](options.md) for details.
Note that Armeria automatically injects an `ExecutionContext` on top of Armeria's `EventLoop` to invoke the logic.

## Cats Effect

Add the following dependency
```scala
"com.softwaremill.sttp.tapir" %% "tapir-armeria-server-cats" % "@VERSION@"
```
to use this interpreter with Cats Effect typeclasses.

Then import the object:
```scala mdoc:compile-only
import sttp.tapir.server.armeria.cats.ArmeriaCatsServerInterpreter
```

This object contains the `toService(e: ServerEndpoint[Fs2Streams[F], F])` method which returns a `TapirService[Fs2Streams[F], F]`.
An HTTP server can then be started as in the following example:

```scala mdoc:compile-only
import sttp.tapir._
import sttp.tapir.server.armeria.cats.ArmeriaCatsServerInterpreter
import cats.effect._
import cats.effect.std.Dispatcher
import com.linecorp.armeria.server.Server

object Main extends IOApp {
override def run(args: List[String]): IO[ExitCode] = {
val tapirEndpoint: PublicEndpoint[String, Unit, String, Any] = ???
def logic(req: String): IO[Either[Unit, String]] = ???

Dispatcher[IO]
.flatMap { dispatcher =>
Resource
.make(
IO.async_[Server] { cb =>
val tapirService = ArmeriaCatsServerInterpreter[IO](dispatcher).toService(tapirEndpoint.serverLogic(logic))

val server = Server
.builder()
.service(tapirService)
.build()
server.start().handle[Unit] {
case (_, null) => cb(Right(server))
case (_, cause) => cb(Left(cause))
}
}
)({ server =>
IO.fromCompletableFuture(IO(server.closeAsync())).void
})
}
.use(_ => IO.never)
}
}
```

This interpreter also supports streaming using FS2 streams:

```scala mdoc:compile-only
import sttp.capabilities.fs2.Fs2Streams
import sttp.tapir._
import sttp.tapir.server.armeria.cats.ArmeriaCatsServerInterpreter
import cats.effect._
import cats.effect.std.Dispatcher
import fs2._

val streamingResponse: Endpoint[Unit, Int, Unit, Stream[IO, Byte], Fs2Streams[IO]] =
endpoint
.in("stream")
.in(query[Int]("times"))
.out(streamTextBody(Fs2Streams[IO])(CodecFormat.TextPlain()))

def streamLogic(times: Int): IO[Stream[IO, Byte]] = {
IO.pure(Stream.chunk(Chunk.array("Hello world!".getBytes)).repeatN(times))
}

def dispatcher: Dispatcher[IO] = ???

val tapirService = ArmeriaCatsServerInterpreter(dispatcher).toService(streamingResponse.serverLogicSuccess(streamLogic))
```

## ZIO

Add the following dependency

```scala
"com.softwaremill.sttp.tapir" %% "tapir-armeria-server-zio" % "@VERSION@"
```

to use this interpreter with ZIO.

Then import the object:
```scala mdoc:compile-only
import sttp.tapir.server.armeria.zio.ArmeriaZioServerInterpreter
```

This object contains `toService(e: ServerEndpoint[ZioStreams, RIO[R, *]])` method which returns a `TapirService[ZioStreams, RIO[R, *]]`.
An HTTP server can then be started as in the following example:

```scala mdoc:compile-only
import sttp.tapir._
import sttp.tapir.server.armeria.zio.ArmeriaZioServerInterpreter
import sttp.tapir.ztapir._
import zio._
import com.linecorp.armeria.server.Server

object Main extends zio.App {
override def run(args: List[String]): URIO[ZEnv, ExitCode] = {
implicit val runtime = Runtime.default

val tapirEndpoint: PublicEndpoint[String, Unit, String, Any] = ???
def logic(key: String): UIO[String] = ???

ZManaged
.make(ZIO.fromCompletableFuture {
val tapirService = ArmeriaZioServerInterpreter().toService(tapirEndpoint.zServerLogic(logic))

val server = Server
.builder()
.service(tapirService)
.build()
server.start().thenApply[Server](_ => server)
}) { server =>
ZIO.fromCompletableFuture(server.closeAsync()).orDie
}.useForever.as(ExitCode.success).orDie
}
}
```

This interpreter supports streaming using ZStreams.
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package sttp.tapir.examples

import com.linecorp.armeria.server.Server
import scala.concurrent.Future
import sttp.client3.{HttpURLConnectionBackend, Identity, SttpBackend, UriContext, asStringAlways, basicRequest}
import sttp.capabilities.armeria.ArmeriaStreams
import sttp.tapir.server.armeria.{ArmeriaFutureServerInterpreter, TapirService}
import sttp.tapir.{PublicEndpoint, endpoint, query, stringBody}

object HelloWorldArmeriaServer extends App {

// the endpoint: single fixed path input ("hello"), single query parameter
// corresponds to: GET /hello?name=...
val helloWorld: PublicEndpoint[String, Unit, String, Any] =
endpoint.get.in("hello").in(query[String]("name")).out(stringBody)

// converting an endpoint to a TapirService (providing server-side logic); extension method comes from imported packages
val helloWorldService: TapirService[ArmeriaStreams, Future] =
ArmeriaFutureServerInterpreter().toService(helloWorld.serverLogicSuccess(name => Future.successful(s"Hello, $name!")))

// starting the server
val server: Server = Server
.builder()
.http(8080)
.service(helloWorldService)
.build()

server.start().join()
// testing
val backend: SttpBackend[Identity, Any] = HttpURLConnectionBackend()
val result: String = basicRequest.response(asStringAlways).get(uri"http://localhost:8080/hello?name=Frodo").send(backend).body
println("Got result: " + result)

assert(result == "Hello, Frodo!")
server.stop().join()
}
6 changes: 5 additions & 1 deletion project/Versions.scala
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ object Versions {
val circeYaml = "0.14.1"
val sttp = "3.4.1"
val sttpModel = "1.4.23"
val sttpShared = "1.3.1"
val sttpShared = "1.3.2"
val akkaHttp = "10.2.8"
val akkaStreams = "2.6.18"
val swaggerUi = "4.5.0"
Expand All @@ -23,6 +23,7 @@ object Versions {
val zio1 = "1.0.13"
val zio1InteropCats = "3.2.9.1"
val zio1Json = "0.2.0-M3"
val zio1InteropReactiveStreams = "1.3.9"
val zio = "2.0.0-RC1"
val zioInteropCats = "3.3.0-RC1"
val zioJson = "0.3.0-RC3"
Expand All @@ -35,4 +36,7 @@ object Versions {
val derevo = "0.13.0"
val newtype = "0.4.4"
val awsLambdaInterface = "2.1.0"
val armeria = "1.14.0"
val scalaJava8Compat = "1.0.2"
val fs2 = "3.2.4"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package sttp.tapir.server.armeria.cats

import cats.effect.Async
import cats.effect.std.Dispatcher
import sttp.capabilities.fs2.Fs2Streams
import sttp.tapir.server.ServerEndpoint
import sttp.tapir.server.armeria.TapirService

trait ArmeriaCatsServerInterpreter[F[_]] {

implicit def fa: Async[F]

def armeriaServerOptions: ArmeriaCatsServerOptions[F]

def toService(serverEndpoint: ServerEndpoint[Fs2Streams[F], F]): TapirService[Fs2Streams[F], F] =
toService(List(serverEndpoint))

def toService(serverEndpoints: List[ServerEndpoint[Fs2Streams[F], F]]): TapirService[Fs2Streams[F], F] =
TapirCatsService(serverEndpoints, armeriaServerOptions)
}

object ArmeriaCatsServerInterpreter {
def apply[F[_]](dispatcher: Dispatcher[F])(implicit _fa: Async[F]): ArmeriaCatsServerInterpreter[F] = {
new ArmeriaCatsServerInterpreter[F] {
override implicit def fa: Async[F] = _fa
override def armeriaServerOptions: ArmeriaCatsServerOptions[F] = ArmeriaCatsServerOptions.default[F](dispatcher)(fa)
}
}

def apply[F[_]](serverOptions: ArmeriaCatsServerOptions[F])(implicit _fa: Async[F]): ArmeriaCatsServerInterpreter[F] = {
new ArmeriaCatsServerInterpreter[F] {
override implicit def fa: Async[F] = _fa
override def armeriaServerOptions: ArmeriaCatsServerOptions[F] = serverOptions
}
}
}
Loading