Skip to content

Commit

Permalink
Update zio-http to RC4 & Play to 3.0.0 (#1968)
Browse files Browse the repository at this point in the history
* Update zio-http to RC3

* Skip mima checks for zio-http and use Handler.fromResource

* Provide WS config from server request

* Upgrade play to v3

* fmt

* Fix CI and refactor WS options

* Patch WS server options in a separate method

* Fix Scala 2.12 CI

* Update QuickAdapter to zio-http RC4

* Fix CI

* QuickAdapter header handling improvements

* Update tapir version

* Return handlers in ZHttpAdapter methods

* Fix tests and examples

* Use caliban-quick in Federation example
  • Loading branch information
kyri-petrou authored Dec 7, 2023
1 parent ccb9716 commit f50b6d2
Show file tree
Hide file tree
Showing 19 changed files with 192 additions and 209 deletions.
10 changes: 5 additions & 5 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ jobs:
- checkout
- restore_cache:
key: sbtcache
- run: sbt ++2.12.18 core/test http4s/test akkaHttp/test pekkoHttp/test play/test zioHttp/test quickAdapter/test examples/compile catsInterop/test tools/test codegenSbt/test clientJVM/test monixInterop/compile tapirInterop/test federation/test reporting/test tracing/test
- run: sbt ++2.12.18 core/test http4s/test akkaHttp/test pekkoHttp/test zioHttp/test quickAdapter/test catsInterop/test tools/test codegenSbt/test clientJVM/test monixInterop/compile tapirInterop/test federation/test reporting/test tracing/test
- save_cache:
key: sbtcache
paths:
Expand Down Expand Up @@ -98,7 +98,7 @@ jobs:
- checkout
- restore_cache:
key: sbtcache
- run: sbt ++3.3.1 core/test catsInterop/test benchmarks/compile monixInterop/compile clientJVM/test clientJS/compile zioHttp/test quickAdapter/test tapirInterop/test pekkoHttp/test http4s/test federation/test tools/test reporting/test tracing/test
- run: sbt ++3.3.1 core/test catsInterop/test benchmarks/compile monixInterop/compile clientJVM/test clientJS/compile zioHttp/test quickAdapter/test tapirInterop/test pekkoHttp/test http4s/test play/test federation/test tools/test reporting/test tracing/test
- save_cache:
key: sbtcache
paths:
Expand All @@ -113,7 +113,7 @@ jobs:
- checkout
- restore_cache:
key: sbtcache
- run: sbt ++3.3.1 core/test catsInterop/test benchmarks/compile monixInterop/compile clientJVM/test clientJS/compile zioHttp/test quickAdapter/test tapirInterop/test pekkoHttp/test http4s/test federation/test tools/test reporting/test tracing/test
- run: sbt ++3.3.1 core/test catsInterop/test benchmarks/compile monixInterop/compile clientJVM/test clientJS/compile zioHttp/test quickAdapter/test tapirInterop/test pekkoHttp/test http4s/test play/test federation/test tools/test reporting/test tracing/test
- save_cache:
key: sbtcache
paths:
Expand Down Expand Up @@ -177,8 +177,8 @@ jobs:
- checkout
- restore_cache:
key: sbtcache
- run: sbt ++2.13.12 http4s/mimaReportBinaryIssues akkaHttp/mimaReportBinaryIssues pekkoHttp/mimaReportBinaryIssues play/mimaReportBinaryIssues zioHttp/mimaReportBinaryIssues catsInterop/mimaReportBinaryIssues monixInterop/mimaReportBinaryIssues clientJVM/mimaReportBinaryIssues clientJS/mimaReportBinaryIssues clientLaminextJS/mimaReportBinaryIssues federation/mimaReportBinaryIssues reporting/mimaReportBinaryIssues tracing/mimaReportBinaryIssues tools/mimaReportBinaryIssues core/mimaReportBinaryIssues tapirInterop/mimaReportBinaryIssues # quickAdapter/mimaReportBinaryIssues
- run: sbt ++3.3.1 catsInterop/mimaReportBinaryIssues monixInterop/mimaReportBinaryIssues clientJVM/mimaReportBinaryIssues clientJS/mimaReportBinaryIssues clientLaminextJS/mimaReportBinaryIssues zioHttp/mimaReportBinaryIssues http4s/mimaReportBinaryIssues federation/mimaReportBinaryIssues reporting/mimaReportBinaryIssues tracing/mimaReportBinaryIssues tools/mimaReportBinaryIssues core/mimaReportBinaryIssues tapirInterop/mimaReportBinaryIssues pekkoHttp/mimaReportBinaryIssues # quickAdapter/mimaReportBinaryIssues
- run: sbt ++2.13.12 http4s/mimaReportBinaryIssues akkaHttp/mimaReportBinaryIssues pekkoHttp/mimaReportBinaryIssues catsInterop/mimaReportBinaryIssues monixInterop/mimaReportBinaryIssues clientJVM/mimaReportBinaryIssues clientJS/mimaReportBinaryIssues clientLaminextJS/mimaReportBinaryIssues federation/mimaReportBinaryIssues reporting/mimaReportBinaryIssues tracing/mimaReportBinaryIssues tools/mimaReportBinaryIssues core/mimaReportBinaryIssues tapirInterop/mimaReportBinaryIssues # quickAdapter/mimaReportBinaryIssues zioHttp/mimaReportBinaryIssues play/mimaReportBinaryIssues
- run: sbt ++3.3.1 catsInterop/mimaReportBinaryIssues monixInterop/mimaReportBinaryIssues clientJVM/mimaReportBinaryIssues clientJS/mimaReportBinaryIssues clientLaminextJS/mimaReportBinaryIssues http4s/mimaReportBinaryIssues federation/mimaReportBinaryIssues reporting/mimaReportBinaryIssues tracing/mimaReportBinaryIssues tools/mimaReportBinaryIssues core/mimaReportBinaryIssues tapirInterop/mimaReportBinaryIssues pekkoHttp/mimaReportBinaryIssues # quickAdapter/mimaReportBinaryIssues zioHttp/mimaReportBinaryIssues play/mimaReportBinaryIssues
- save_cache:
key: sbtcache
paths:
Expand Down
34 changes: 17 additions & 17 deletions adapters/play/src/main/scala/caliban/PlayAdapter.scala
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
package caliban

import akka.stream.scaladsl.{ Flow, Sink, Source }
import akka.stream.{ Materializer, OverflowStrategy }
import akka.util.ByteString
import caliban.PlayAdapter.convertHttpStreamingEndpoint
import caliban.interop.tapir.TapirAdapter._
import caliban.interop.tapir.{ HttpInterpreter, HttpUploadInterpreter, StreamConstructor, WebSocketInterpreter }
import org.apache.pekko.stream.scaladsl.{ Flow, Sink, Source }
import org.apache.pekko.stream.{ Materializer, OverflowStrategy }
import org.apache.pekko.util.ByteString
import play.api.routing.Router.Routes
import sttp.capabilities.{ akka, WebSockets }
import sttp.capabilities.akka.AkkaStreams
import sttp.capabilities.akka.AkkaStreams.Pipe
import sttp.model.{ MediaType, StatusCode }
import sttp.capabilities.WebSockets
import sttp.capabilities.pekko.PekkoStreams
import sttp.capabilities.pekko.PekkoStreams.Pipe
import sttp.model.StatusCode
import sttp.tapir.Codec.JsonCodec
import sttp.tapir.PublicEndpoint
import sttp.tapir.model.ServerRequest
Expand All @@ -30,7 +30,7 @@ class PlayAdapter private (private val options: Option[PlayServerOptions]) {
)(implicit runtime: Runtime[R], materializer: Materializer): Routes =
playInterpreter.toRoutes(
interpreter
.serverEndpoints[R, AkkaStreams](AkkaStreams)
.serverEndpoints[R, PekkoStreams](PekkoStreams)
.map(convertHttpStreamingEndpoint[R, (GraphQLRequest, ServerRequest)])
)

Expand All @@ -40,7 +40,7 @@ class PlayAdapter private (private val options: Option[PlayServerOptions]) {
requestCodec: JsonCodec[GraphQLRequest],
mapCodec: JsonCodec[Map[String, Seq[String]]]
): Routes =
playInterpreter.toRoutes(convertHttpStreamingEndpoint(interpreter.serverEndpoint[R, AkkaStreams](AkkaStreams)))
playInterpreter.toRoutes(convertHttpStreamingEndpoint(interpreter.serverEndpoint[R, PekkoStreams](PekkoStreams)))

def makeWebSocketService[R, E](
interpreter: WebSocketInterpreter[R, E]
Expand All @@ -66,7 +66,7 @@ class PlayAdapter private (private val options: Option[PlayServerOptions]) {
private implicit def streamConstructor(implicit
runtime: Runtime[Any],
mat: Materializer
): StreamConstructor[AkkaStreams.BinaryStream] =
): StreamConstructor[PekkoStreams.BinaryStream] =
new StreamConstructor[Source[ByteString, Any]] {
override def apply(stream: ZStream[Any, Throwable, Byte]): Source[ByteString, Any] =
Unsafe.unsafe(implicit u =>
Expand Down Expand Up @@ -111,18 +111,18 @@ object PlayAdapter extends PlayAdapter(None) {
Unit,
Input,
TapirResponse,
CalibanResponse[AkkaStreams.BinaryStream],
AkkaStreams,
CalibanResponse[PekkoStreams.BinaryStream],
PekkoStreams,
RIO[R, *]
]
)(implicit runtime: Runtime[R], mat: Materializer): ServerEndpoint[AkkaStreams, Future] =
)(implicit runtime: Runtime[R], mat: Materializer): ServerEndpoint[PekkoStreams, Future] =
ServerEndpoint[
Unit,
Unit,
Input,
TapirResponse,
CalibanResponse[akka.AkkaStreams.BinaryStream],
AkkaStreams,
CalibanResponse[PekkoStreams.BinaryStream],
PekkoStreams,
Future
](
endpoint.endpoint,
Expand Down Expand Up @@ -151,14 +151,14 @@ object PlayAdapter extends PlayAdapter(None) {
)(implicit
runtime: Runtime[R],
materializer: Materializer
): ServerEndpoint[AkkaStreams with WebSockets, Future] =
): ServerEndpoint[PekkoStreams with WebSockets, Future] =
ServerEndpoint[
Unit,
Unit,
(ServerRequest, String),
StatusCode,
(String, AkkaPipe),
AkkaStreams with WebSockets,
PekkoStreams with WebSockets,
Future
](
endpoint.endpoint
Expand Down
10 changes: 5 additions & 5 deletions adapters/play/src/test/scala/caliban/PlayAdapterSpec.scala
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
package caliban

import akka.actor.ActorSystem
import akka.stream.Materializer
import caliban.interop.tapir.TestData.sampleCharacters
import caliban.interop.tapir.{
FakeAuthorizationInterceptor,
Expand All @@ -13,18 +11,20 @@ import caliban.interop.tapir.{
WebSocketInterpreter
}
import caliban.uploads.Uploads
import org.apache.pekko.actor.ActorSystem
import org.apache.pekko.stream.Materializer
import play.api.Mode
import play.api.routing._
import play.api.routing.sird._
import play.core.server.{ AkkaHttpServer, ServerConfig }
import play.core.server.{ PekkoHttpServer, ServerConfig }
import sttp.client3.UriContext
import zio._
import zio.test.{ Live, ZIOSpecDefault }

import scala.language.postfixOps

object PlayAdapterSpec extends ZIOSpecDefault {
import sttp.tapir.json.circe._
import sttp.tapir.json.play._

private val envLayer = TestService.make(sampleCharacters) ++ Uploads.empty

Expand All @@ -51,7 +51,7 @@ object PlayAdapterSpec extends ZIOSpecDefault {
}
_ <- ZIO
.attempt(
AkkaHttpServer.fromRouterWithComponents(
PekkoHttpServer.fromRouterWithComponents(
ServerConfig(
mode = Mode.Dev,
port = Some(8088),
Expand Down
25 changes: 12 additions & 13 deletions adapters/quick/src/main/scala/caliban/QuickAdapter.scala
Original file line number Diff line number Diff line change
Expand Up @@ -7,29 +7,28 @@ import zio.http._
final class QuickAdapter[-R, E] private (requestHandler: QuickRequestHandler[R, E]) {

/**
* Converts this adapter to a [[zio.http.RequestHandler]] which can be used to create zio-http [[zio.http.App]]s
* Converts this adapter to a [[zio.http.RequestHandler]] which can be used to create zio-http `HttpApp`
*/
val handler: RequestHandler[R, Nothing] =
Handler.fromFunctionZIO[Request](requestHandler.handleRequest)

/**
* Converts this adapter to an [[zio.http.App]] serving the GraphQL API at the specified path.
* Converts this adapter to an `HttpApp` serving the GraphQL API at the specified path.
*
* @param apiPath The path where the GraphQL API will be served.
* @param graphiqlPath The path where the GraphiQL UI will be served. If None, GraphiQL will not be served.
*/
def toApp(apiPath: Path, graphiqlPath: Option[Path] = None): App[R] = {
val apiApp = Http.collectHandler[Request] {
case (Method.GET | Method.POST) -> path if path == apiPath => handler
}
graphiqlPath match {
case None => apiApp
case Some(uiPath) =>
val uiHandler = GraphiQLHandler.handler(apiPath.toString(), uiPath.toString)
apiApp ++ Http.collectHandler[Request] {
case Method.GET -> path if path == uiPath => uiHandler
}
def toApp(apiPath: Path, graphiqlPath: Option[Path] = None): HttpApp[R] = {
val apiRoutes = List(
RoutePattern(Method.POST, apiPath) -> handler,
RoutePattern(Method.GET, apiPath) -> handler
)
val graphiqlRoute = graphiqlPath.fold(List.empty[Route[R, Nothing]]) { uiPath =>
val uiHandler = GraphiQLHandler.handler(apiPath.toString(), uiPath.toString)
List(RoutePattern(Method.GET, uiPath) -> uiHandler)
}

Routes.fromIterable(apiRoutes ::: graphiqlRoute).toHttpApp
}

/**
Expand Down
50 changes: 25 additions & 25 deletions adapters/quick/src/main/scala/caliban/QuickRequestHandler.scala
Original file line number Diff line number Diff line change
Expand Up @@ -39,43 +39,42 @@ final private class QuickRequestHandler[-R, E](interpreter: GraphQLInterpreter[R
val queryParams = httpReq.url.queryParams

def extractFields(key: String): Either[Response, Option[Map[String, InputValue]]] =
try Right(
queryParams
.get(key)
.flatMap(_.headOption)
.map(readFromString[InputValue.ObjectValue](_).fields)
)
try Right(queryParams.get(key).map(readFromString[InputValue.ObjectValue](_).fields))
catch { case NonFatal(_) => Left(badRequest(s"Invalid $key query param")) }

def fromQueryParams: Either[Response, GraphQLRequest] =
for {
vars <- extractFields("variables")
exts <- extractFields("extensions")
} yield GraphQLRequest(
query = queryParams.get("query").flatMap(_.headOption),
operationName = queryParams.get("operationName").flatMap(_.headOption),
query = queryParams.get("query"),
operationName = queryParams.get("operationName"),
variables = vars,
extensions = exts
)

def isApplicationGql =
httpReq.headers.get("content-type").fold(false)(_.startsWith("application/graphql"))
def isGqlJson =
httpReq.header(Header.ContentType).exists { h =>
h.mediaType.subType.equalsIgnoreCase("graphql") &&
h.mediaType.mainType.equalsIgnoreCase("application")
}

def decodeApplicationGql() =
httpReq.body.asString.mapBoth(_ => BodyDecodeErrorResponse, b => GraphQLRequest(Some(b)))

def decodeJson() =
httpReq.body.asArray
.flatMap(arr => ZIO.attempt(readFromArray[GraphQLRequest](arr)))
.orElseFail(BodyDecodeErrorResponse)

val resp = {
if (httpReq.method == Method.GET || queryParams.get("query").isDefined)
ZIO.fromEither(fromQueryParams)
else {
val req =
if (isApplicationGql)
httpReq.body.asString.mapBoth(_ => BodyDecodeErrorResponse, b => GraphQLRequest(Some(b)))
else
httpReq.body.asArray
.flatMap(arr => ZIO.attempt(readFromArray[GraphQLRequest](arr)))
.orElseFail(BodyDecodeErrorResponse)

val req = if (isGqlJson) decodeApplicationGql() else decodeJson()
httpReq.headers
.get(GraphQLRequest.`apollo-federation-include-trace`)
.collect { case GraphQLRequest.ftv1 => req.map(_.withFederatedTracing) }
.collect { case s if s.equalsIgnoreCase(GraphQLRequest.ftv1) => req.map(_.withFederatedTracing) }
.getOrElse(req)
}
}
Expand All @@ -95,15 +94,16 @@ final private class QuickRequestHandler[-R, E](interpreter: GraphQLInterpreter[R

private def transformResponse(httpReq: Request, resp: GraphQLResponse[E])(implicit trace: Trace): Response = {
def acceptsGqlJson: Boolean =
httpReq.header(Header.Accept).fold(false) { h =>
h.mimeTypes.exists(mt =>
mt.mediaType.subType == "graphql-response+json" && mt.mediaType.mainType == "application"
)
httpReq.header(Header.Accept).exists { h =>
h.mimeTypes.exists { mt =>
mt.mediaType.subType.equalsIgnoreCase("graphql-response+json") &&
mt.mediaType.mainType.equalsIgnoreCase("application")
}
}

val cacheDirective = HttpUtils.computeCacheDirective(resp.extensions)

(resp match {
resp match {
case resp @ GraphQLResponse(StreamValue(stream), _, _, _) =>
Response(
Status.Ok,
Expand All @@ -125,7 +125,7 @@ final private class QuickRequestHandler[-R, E](interpreter: GraphQLInterpreter[R
headers = responseHeaders(ContentTypeJson, cacheDirective),
body = encodeSingleResponse(resp, keepDataOnErrors = true, hasCacheDirective = cacheDirective.isDefined)
)
}).withServerTime
}
}

private def encodeSingleResponse(
Expand Down
13 changes: 12 additions & 1 deletion adapters/quick/src/main/scala/caliban/quick/package.scala
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
package caliban

import caliban.Configurator.ExecutionConfiguration
import zio.http.{ HttpApp, Path, RequestHandler, Response }
import zio.{ RIO, Trace, ZIO }
import zio.http.{ RequestHandler, Response }

package object quick {

Expand All @@ -21,6 +21,17 @@ package object quick {
): RIO[R, Nothing] =
gql.interpreter.flatMap(QuickAdapter(_).runServer(port, apiPath, graphiqlPath))

/**
* Creates zio-http `HttpApp` from the GraphQL API
*
* @param apiPath The route to serve the API on, e.g., `/api/graphql`
* @param graphiqlPath Optionally define a route to serve the GraphiQL UI on, e.g., `/graphiql`
*/
def toApp(apiPath: String, graphiqlPath: Option[String] = None)(implicit
trace: Trace
): RIO[R, HttpApp[R]] =
gql.interpreter.map(QuickAdapter(_).toApp(Path.decode(apiPath), graphiqlPath.map(Path.decode)))

/**
* Creates a zio-http handler for the GraphQL API
*
Expand Down
11 changes: 4 additions & 7 deletions adapters/quick/src/test/scala/caliban/QuickAdapterSpec.scala
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ package caliban

import caliban.interop.tapir.TestData.sampleCharacters
import caliban.interop.tapir.{ TapirAdapterSpec, TestApi, TestService }
import caliban.quick._
import caliban.uploads.Uploads
import sttp.client3.UriContext
import zio._
Expand All @@ -12,23 +11,21 @@ import zio.test.{ Live, ZIOSpecDefault }
import scala.language.postfixOps

object QuickAdapterSpec extends ZIOSpecDefault {
import caliban.quick._
import sttp.tapir.json.jsoniter._

private val envLayer = TestService.make(sampleCharacters) ++ Uploads.empty

private val auth = HttpAppMiddleware.intercept { case (req, resp) =>
private val auth = Middleware.intercept { case (req, resp) =>
if (req.headers.get("X-Invalid").nonEmpty)
Response(Status.Unauthorized, body = Body.fromString("You are unauthorized!"))
else resp
}

private val apiLayer = envLayer >>> ZLayer.fromZIO {
for {
handler <- TestApi.api.handler.map(_ @@ auth)
_ <-
Server
.serve(Http.collectHandler { case _ -> Root / "api" / "graphql" => handler })
.forkScoped
app <- TestApi.api.toApp("/api/graphql").map(_ @@ auth)
_ <- Server.serve(app).forkScoped
_ <- Live.live(Clock.sleep(3 seconds))
service <- ZIO.service[TestService]
} yield service
Expand Down
Loading

0 comments on commit f50b6d2

Please sign in to comment.