-
Notifications
You must be signed in to change notification settings - Fork 422
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #1830 from ikhoon/armeria
Add Armeria server interpreters
- Loading branch information
Showing
43 changed files
with
2,011 additions
and
6 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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. |
36 changes: 36 additions & 0 deletions
36
examples/src/main/scala/sttp/tapir/examples/HelloWorldArmeriaServer.scala
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.