Skip to content

Commit

Permalink
Merge branch 'master' into zio-http-websocket
Browse files Browse the repository at this point in the history
  • Loading branch information
yabosedira committed Sep 16, 2023
2 parents 834a170 + e2a887b commit e922456
Show file tree
Hide file tree
Showing 13 changed files with 101 additions and 38 deletions.
2 changes: 1 addition & 1 deletion .scalafmt.conf
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
version = 3.7.13
version = 3.7.14
maxColumn = 140
runner.dialect = scala3
fileOverride {
Expand Down
1 change: 0 additions & 1 deletion .tool-versions

This file was deleted.

6 changes: 3 additions & 3 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import scala.concurrent.duration.DurationInt
import scala.sys.process.Process

val scala2_12 = "2.12.18"
val scala2_13 = "2.13.11"
val scala2_13 = "2.13.12"
val scala3 = "3.3.0"

val scala2Versions = List(scala2_12, scala2_13)
Expand Down Expand Up @@ -880,8 +880,8 @@ lazy val jsoniterScala: ProjectMatrix = (projectMatrix in file("json/jsoniter"))
.settings(
name := "tapir-jsoniter-scala",
libraryDependencies ++= Seq(
"com.github.plokhotnyuk.jsoniter-scala" %%% "jsoniter-scala-core" % "2.23.3",
"com.github.plokhotnyuk.jsoniter-scala" %%% "jsoniter-scala-macros" % "2.23.3" % Test,
"com.github.plokhotnyuk.jsoniter-scala" %%% "jsoniter-scala-core" % "2.23.4",
"com.github.plokhotnyuk.jsoniter-scala" %%% "jsoniter-scala-macros" % "2.23.4" % Test,
scalaTest.value % Test
)
)
Expand Down
11 changes: 11 additions & 0 deletions doc/endpoint/oneof.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,17 @@ There are two kind of one-of inputs/outputs:
* `oneOf` outputs where the arbitrary-output variants can represent different content using different outputs, and
* `oneOfBody` input/output where the body-only variants represent the same content, but with different content types

```eval_rst
.. note::
``oneOf`` and ``oneOfBody`` outputs are not related to ``oneOf:`` schemas when
`generating <https://tapir.softwaremill.com/en/latest/docs/openapi.html>`_ OpenAPI documentation.
Such schemas are generated for coproducts - e.g. ``sealed trait`` families - given an appropriate codec. See the
documentation on
`coproducts <https://tapir.softwaremill.com/en/latest/endpoint/schemas.html#sealed-traits-coproducts>`_ for details.
```

## `oneOf` outputs

Outputs with multiple variants can be specified using the `oneOf` output. Each variant is defined using a one-of
Expand Down
14 changes: 14 additions & 0 deletions doc/server/ziohttp.md
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,20 @@ The interpreter supports web sockets, with pipes of type `zio.stream.Stream[Thro
See [web sockets](../endpoint/websockets.md) for more details. It also supports auto-ping, auto-pong-on-ping, ignoring-pongs and handling
of fragmented frames.

## Error handling

By default, any endpoints interpreted with the `ZioHttpInterpreter` will use tapir's built-in failed effect handling,
which uses an interceptor. Errors can be sent in a custom format by [providing a custom `ErrorHandler`](errors.md).

If you'd prefer to use zio-http's error handling, you can disable tapir's exception interceptor by modifying the
[server options](options.md):

```scala mdoc:compile-only
import sttp.tapir.server.ziohttp.{ZioHttpInterpreter, ZioHttpServerOptions}

ZioHttpInterpreter(ZioHttpServerOptions.customiseInterceptors[Any].exceptionHandler(None).options)
```

## Configuration

The interpreter can be configured by providing an `ZioHttpServerOptions` value, see
Expand Down
18 changes: 9 additions & 9 deletions project/Versions.scala
Original file line number Diff line number Diff line change
Expand Up @@ -15,17 +15,17 @@ object Versions {
val akkaStreams = "2.6.20"
val pekkoHttp = "1.0.0"
val pekkoStreams = "1.0.1"
val swaggerUi = "5.4.2"
val swaggerUi = "5.6.1"
val upickle = "3.1.2"
val playJson = "2.9.4"
val playJson = "2.10.0"
val finatra = "22.12.0"
val catbird = "21.12.0"
val json4s = "4.0.6"
val nettyReactiveStreams = "2.0.9"
val sprayJson = "1.3.6"
val scalaCheck = "1.17.0"
val scalaTest = "3.2.16"
val scalaTestPlusScalaCheck = "3.2.16.0"
val scalaTest = "3.2.17"
val scalaTestPlusScalaCheck = "3.2.17.0"
val refined = "0.11.0"
val iron = "2.2.1"
val enumeratum = "1.7.3"
Expand All @@ -36,26 +36,26 @@ object Versions {
val zio = "2.0.16"
val zioInteropCats = "23.0.0.8"
val zioInteropReactiveStreams = "2.0.2"
val zioJson = "0.6.1"
val zioJson = "0.6.2"
val playClient = "2.1.11"
val playServer = "2.8.20"
val tethys = "0.26.0"
val vertx = "4.4.5"
val jsScalaJavaTime = "2.5.0"
val nativeScalaJavaTime = "2.4.0-M3"
val jwtScala = "9.4.3"
val jwtScala = "9.4.4"
val derevo = "0.13.0"
val newtype = "0.4.4"
val monixNewtype = "0.2.3"
val zioPrelude = "1.0.0-RC20"
val awsLambdaInterface = "2.4.0"
val awsLambdaInterface = "2.4.1"
val armeria = "1.25.2"
val scalaJava8Compat = "1.0.2"
val scalaCollectionCompat = "2.11.0"
val fs2 = "3.9.1"
val fs2 = "3.9.2"
val decline = "2.4.1"
val quicklens = "1.9.6"
val openTelemetry = "1.29.0"
val openTelemetry = "1.30.1"
val mockServer = "5.15.0"
val dogstatsdClient = "4.2.0"
val nettyAll = "4.1.97.Final"
Expand Down
2 changes: 1 addition & 1 deletion project/build.properties
Original file line number Diff line number Diff line change
@@ -1 +1 @@
sbt.version=1.9.4
sbt.version=1.9.5
6 changes: 3 additions & 3 deletions project/plugins.sbt
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
// https://github.com/sbt/sbt/issues/6997
ThisBuild / libraryDependencySchemes += "org.scala-lang.modules" %% "scala-xml" % VersionScheme.Always

val sbtSoftwareMillVersion = "2.0.13"
val sbtSoftwareMillVersion = "2.0.17"
addSbtPlugin("com.softwaremill.sbt-softwaremill" % "sbt-softwaremill-common" % sbtSoftwareMillVersion)
addSbtPlugin("com.softwaremill.sbt-softwaremill" % "sbt-softwaremill-publish" % sbtSoftwareMillVersion)
addSbtPlugin("com.softwaremill.sbt-softwaremill" % "sbt-softwaremill-browser-test-js" % sbtSoftwareMillVersion)
//addSbtPlugin("io.spray" % "sbt-boilerplate" % "0.6.1")
addSbtPlugin("com.typesafe" % "sbt-mima-plugin" % "1.1.3")
addSbtPlugin("com.typesafe.play" % "sbt-twirl" % "1.5.2")
addSbtPlugin("com.typesafe.play" % "sbt-twirl" % "1.6.0")
addSbtPlugin("org.scalameta" % "sbt-mdoc" % "2.3.7")
addSbtPlugin("com.eed3si9n" % "sbt-projectmatrix" % "0.9.1")
addSbtPlugin("org.jetbrains.scala" % "sbt-ide-settings" % "1.1.1")
Expand All @@ -16,7 +16,7 @@ addSbtPlugin("io.spray" % "sbt-revolver" % "0.10.0")
addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "2.1.1")
addSbtPlugin("io.gatling" % "gatling-sbt" % "4.5.0")
addSbtPlugin("com.eed3si9n" % "sbt-buildinfo" % "0.11.0")
addSbtPlugin("org.scala-native" % "sbt-scala-native" % "0.4.14")
addSbtPlugin("org.scala-native" % "sbt-scala-native" % "0.4.15")
addSbtPlugin("com.lightbend.akka.grpc" % "sbt-akka-grpc" % "2.1.4")

addDependencyTreePlugin
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ case class DefaultServerLog[F[_]](
def doLogAllDecodeFailures(f: (String, Option[Throwable]) => F[Unit]): DefaultServerLog[F] = copy(doLogAllDecodeFailures = f)
def doLogExceptions(f: (String, Throwable) => F[Unit]): DefaultServerLog[F] = copy(doLogExceptions = f)
def noLog(f: F[Unit]): DefaultServerLog[F] = copy(noLog = f)
def logWhenReceived(doLog: Boolean): DefaultServerLog[F] = copy(logWhenReceived = doLog)
def logWhenHandled(doLog: Boolean): DefaultServerLog[F] = copy(logWhenHandled = doLog)
def logAllDecodeFailures(doLog: Boolean): DefaultServerLog[F] = copy(logAllDecodeFailures = doLog)
def logLogicExceptions(doLog: Boolean): DefaultServerLog[F] = copy(logLogicExceptions = doLog)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ import cats.effect.IO
import cats.syntax.all._
import io.circe.generic.auto._
import org.scalatest.matchers.should.Matchers._
import sttp.capabilities.{Streams, WebSockets}
import sttp.capabilities.Streams
import sttp.capabilities.WebSockets
import sttp.client3._
import sttp.monad.MonadError
import sttp.tapir._
Expand All @@ -15,7 +16,8 @@ import sttp.tapir.server.interceptor.metrics.MetricsRequestInterceptor
import sttp.tapir.server.tests.ServerMetricsTest._
import sttp.tapir.tests.Test
import sttp.tapir.tests.data.Fruit
import sttp.ws.{WebSocket, WebSocketFrame}
import sttp.ws.WebSocket
import sttp.ws.WebSocketFrame

abstract class ServerWebSocketTests[F[_], S <: Streams[S], OPTIONS, ROUTE](
createServerTest: CreateServerTest[F, S with WebSockets, OPTIONS, ROUTE],
Expand Down Expand Up @@ -140,8 +142,27 @@ abstract class ServerWebSocketTests[F[_], S <: Streams[S], OPTIONS, ROUTE](
.get(baseUri.scheme("http"))
.send(backend)
.map(_.body shouldBe Left("Not a WS!"))
},
testServer(
endpoint.out(stringWs),
"ping-pong echo"
)((_: Unit) => pureResult(stringEcho.asRight[Unit])) { (backend, baseUri) =>
basicRequest
.response(asWebSocket { (ws: WebSocket[IO]) =>
for {
m1 <- ws.receive() // Auto ping
_ <- ws.send(WebSocketFrame.ping)
m2 <- ws.receive()
} yield List(m1, m2)
})
.get(baseUri.scheme("ws"))
.send(backend)
.map { response =>
response.body match {
case Right(List(_: WebSocketFrame.Ping, _: WebSocketFrame.Pong)) => succeed
case value => fail(s"$value was not equal to Right(List(_:WebSocketFrame.Ping, _:WebSocketFrame.Pong))")
}
}
}
)

// TODO: tests for ping/pong (control frames handling)
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,19 @@ package sttp.tapir.server.ziohttp

import sttp.capabilities.WebSockets
import sttp.capabilities.zio.ZioStreams
import sttp.model.{Method, Header => SttpHeader}
import sttp.model.Method
import sttp.model.{Header => SttpHeader}
import sttp.monad.MonadError
import sttp.tapir.server.interceptor.RequestResult
import sttp.tapir.server.interceptor.reject.RejectInterceptor
import sttp.tapir.server.interpreter.{FilterServerEndpoints, ServerInterpreter}
import sttp.tapir.server.interpreter.FilterServerEndpoints
import sttp.tapir.server.interpreter.ServerInterpreter
import sttp.tapir.server.model.ServerResponse
import sttp.tapir.ztapir._
import zio._
import zio.http.{Header => ZioHttpHeader, Headers => ZioHttpHeaders, _}
import zio.http.{Header => ZioHttpHeader}
import zio.http.{Headers => ZioHttpHeaders}
import zio.http._

trait ZioHttpInterpreter[R] {
def zioHttpServerOptions: ZioHttpServerOptions[R] = ZioHttpServerOptions.default
Expand Down Expand Up @@ -46,9 +50,7 @@ trait ZioHttpInterpreter[R] {
resp.body match {
case None => handleHttpResponse(resp, None)
case Some(Right(body)) => handleHttpResponse(resp, Some(body))
case Some(Left(body)) =>
println(body)
handleWebSocketResponse(body)
case Some(Left(body)) => handleWebSocketResponse(body)
}

case RequestResult.Failure(_) =>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,19 @@
package sttp.tapir.server.ziohttp
import sttp.capabilities.zio.ZioStreams
import sttp.capabilities.zio.ZioStreams.Pipe
import sttp.tapir.DecodeResult
import sttp.tapir.WebSocketBodyOutput
import sttp.tapir.model.WebSocketFrameDecodeFailure
import sttp.tapir.{DecodeResult, WebSocketBodyOutput}
import sttp.ws.{WebSocketFrame => SttpWebSocketFrame}
import zio.Chunk
import zio.Duration.fromScala
import zio.Schedule
import zio.ZIO
import zio.http.ChannelEvent.Read
import zio.http.{WebSocketChannelEvent, WebSocketFrame => ZioWebSocketFrame}
import zio.http.WebSocketChannelEvent
import zio.http.{WebSocketFrame => ZioWebSocketFrame}
import zio.stream
import zio.stream.ZStream
import zio.{Chunk, Schedule, ZIO, stream}

import scala.concurrent.duration.FiniteDuration

Expand All @@ -25,7 +30,7 @@ object ZioWebSockets {
sttpFrames = in.map(zWebSocketChannelEventToFrame).collectSome
concatenated = optionallyConcatenate(sttpFrames, o.concatenateFragmentedFrames)
ignoredPongs = optionallyIgnorePongs(concatenated, o.ignorePong)
autoPongs = optionallyAutoPong(ignoredPongs, pongs, o.autoPongOnPing)
autoPongs = optionallyAutoPongOnPing(ignoredPongs, pongs, o.autoPongOnPing)
autoPing = optionallyAutoPing(o.autoPing)
closeStream = stream.ZStream.from[SttpWebSocketFrame](SttpWebSocketFrame.close)
intermediateStream = autoPongs
Expand Down Expand Up @@ -92,15 +97,15 @@ object ZioWebSockets {
}
}

private def optionallyAutoPong(
private def optionallyAutoPongOnPing(
sttpFrames: ZStream[Any, Throwable, SttpWebSocketFrame],
pongs: zio.Queue[SttpWebSocketFrame],
autoPongOnPing: Boolean
): ZStream[Any, Throwable, SttpWebSocketFrame] = {
if (autoPongOnPing) {
sttpFrames.mapZIO {
case _: SttpWebSocketFrame.Ping if autoPongOnPing =>
pongs.offer(SttpWebSocketFrame.pong).as(Option.empty[SttpWebSocketFrame])
case SttpWebSocketFrame.Ping(payload) if autoPongOnPing =>
pongs.offer(SttpWebSocketFrame.Pong(payload)).as(Option.empty[SttpWebSocketFrame])
case f => ZIO.succeed(Some(f))
}.collectSome
} else sttpFrames
Expand All @@ -121,8 +126,15 @@ object ZioWebSockets {
case (None, f: SttpWebSocketFrame.Data[_]) if f.finalFragment => (None, Some(f))
case (Some(Left(acc)), f: SttpWebSocketFrame.Binary) if f.finalFragment => (None, Some(f.copy(payload = acc ++ f.payload)))
case (Some(Left(acc)), f: SttpWebSocketFrame.Binary) if !f.finalFragment => (Some(Left(acc ++ f.payload)), None)
case (Some(Right(acc)), f: SttpWebSocketFrame.Text) if f.finalFragment => (None, Some(f.copy(payload = acc + f.payload)))
case (Some(Right(acc)), f: SttpWebSocketFrame.Text) if !f.finalFragment => (Some(Right(acc + f.payload)), None)
case (Some(Right(acc)), f: SttpWebSocketFrame.Text) if f.finalFragment =>
println(s"final fragment: $f")
println(s"acc: $acc")
(None, Some(f.copy(payload = acc + f.payload)))
case (Some(Right(acc)), f: SttpWebSocketFrame.Text) if !f.finalFragment =>
println(s"final fragment: $f")
println(s"acc: $acc")
(Some(Right(acc + f.payload)), None)

case (acc, f) => throw new IllegalStateException(s"Cannot accumulate web socket frames. Accumulator: $acc, frame: $f.")
}
.collectSome
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,6 @@ import zio.stream.ZStream
import java.nio.charset.Charset
import java.time
import scala.concurrent.Future

class ZioHttpServerTest extends TestSuite {

// zio-http tests often fail with "Cause: java.io.IOException: parsing HTTP/1.1 status line, receiving [DEFAULT], parser state [STATUS_LINE]"
Expand Down Expand Up @@ -217,6 +216,10 @@ class ZioHttpServerTest extends TestSuite {
).tests() ++
new ServerStreamingTests(createServerTest, ZioStreams).tests() ++
new ZioHttpCompositionTest(createServerTest).tests() ++
new ServerWebSocketTests(createServerTest, ZioStreams) {
override def functionToPipe[A, B](f: A => B): ZioStreams.Pipe[A, B] = in => in.map(f)
override def emptyPipe[A, B]: ZioStreams.Pipe[A, B] = _ => ZStream.empty
}.tests() ++
additionalTests()
}
}
Expand Down

0 comments on commit e922456

Please sign in to comment.