Skip to content

Commit

Permalink
Merge pull request #1830 from ikhoon/armeria
Browse files Browse the repository at this point in the history
Add Armeria server interpreters
  • Loading branch information
adamw authored Feb 18, 2022
2 parents 89cdf06 + fd23df4 commit 81d9f3b
Show file tree
Hide file tree
Showing 43 changed files with 2,011 additions and 6 deletions.
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.zioInteropReactiveStreams
)
)
.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
4 changes: 4 additions & 0 deletions core/src/main/scala-2/sttp/tapir/macros/SchemaMacros.scala
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ import sttp.tapir.generic.internal.{OneOfMacro, SchemaMagnoliaDerivation, Schema
import sttp.tapir.internal.{ModifySchemaMacro, SchemaEnumerationMacro}

trait SchemaMacros[T] {

/** Modifies nested schemas for case classes and case class families (sealed traits / enums), accessible with `path`, using the given
* `modification` function. To traverse collections, use `.each`.
*/
def modify[U](path: T => U)(modification: Schema[U] => Schema[U]): Schema[T] = macro ModifySchemaMacro.generateModify[T, U]
}

Expand Down
4 changes: 4 additions & 0 deletions core/src/main/scala-3/sttp/tapir/macros/SchemaMacros.scala
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ import magnolia1._
import scala.quoted.*

trait SchemaMacros[T] { this: Schema[T] =>

/** Modifies nested schemas for case classes and case class families (sealed traits / enums), accessible with `path`, using the given
* `modification` function. To traverse collections, use `.each`.
*/
inline def modify[U](inline path: T => U)(inline modification: Schema[U] => Schema[U]): Schema[T] = ${
SchemaMacros.modifyImpl[T, U]('this)('path)('modification)
}
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()
}
4 changes: 4 additions & 0 deletions project/Versions.scala
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ object Versions {
val zio1Json = "0.2.0-M3"
val zio = "2.0.0-RC2"
val zioInteropCats = "3.3.0-RC2"
val zioInteropReactiveStreams = "2.0.0-RC3"
val zioJson = "0.3.0-RC3"
val playClient = "2.1.7"
val playServer = "2.8.13"
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"
}
Loading

0 comments on commit 81d9f3b

Please sign in to comment.