Skip to content

Commit

Permalink
Add helpers and example for using Http4sAdapter with F[_] (ghostdogpr…
Browse files Browse the repository at this point in the history
  • Loading branch information
ghostdogpr authored Dec 14, 2021
1 parent c4e0345 commit 0d0bc2b
Show file tree
Hide file tree
Showing 2 changed files with 147 additions and 2 deletions.
100 changes: 98 additions & 2 deletions adapters/http4s/src/main/scala/caliban/Http4sAdapter.scala
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,20 @@ package caliban

import caliban.execution.QueryExecution
import caliban.interop.cats.CatsInterop
import caliban.interop.tapir.TapirAdapter.zioMonadError
import caliban.interop.tapir.TapirAdapter.{ zioMonadError, CalibanPipe, ZioWebSockets }
import caliban.interop.tapir.{ RequestInterceptor, TapirAdapter, WebSocketHooks }
import cats.data.Kleisli
import cats.effect.Async
import cats.effect.std.Dispatcher
import cats.~>
import org.http4s._
import org.http4s.server.websocket.WebSocketBuilder2
import sttp.capabilities.WebSockets
import sttp.capabilities.fs2.Fs2Streams
import sttp.tapir.Endpoint
import sttp.tapir.json.circe._
import sttp.tapir.server.ServerEndpoint
import sttp.tapir.server.http4s.Http4sServerInterpreter
import sttp.tapir.server.http4s.ztapir.ZHttp4sServerInterpreter
import zio._
import zio.blocking.Blocking
Expand Down Expand Up @@ -38,6 +43,24 @@ object Http4sAdapter {
ZHttp4sServerInterpreter().from(endpoints).toRoutes
}

def makeHttpServiceF[F[_]: Async, R, E](
interpreter: GraphQLInterpreter[R, E],
skipValidation: Boolean = false,
enableIntrospection: Boolean = true,
queryExecution: QueryExecution = QueryExecution.Parallel,
requestInterceptor: RequestInterceptor[R] = RequestInterceptor.empty
)(implicit runtime: Runtime[R]): HttpRoutes[F] = {
val endpoints = TapirAdapter.makeHttpService[R, E](
interpreter,
skipValidation,
enableIntrospection,
queryExecution,
requestInterceptor
)
val endpointsF = endpoints.map(convertHttpEndpointToF[F, R, E])
Http4sServerInterpreter().toRoutes(endpointsF)
}

def makeHttpUploadService[R <: Has[_] with Random, E](
interpreter: GraphQLInterpreter[R, E],
skipValidation: Boolean = false,
Expand All @@ -55,6 +78,24 @@ object Http4sAdapter {
ZHttp4sServerInterpreter().from(endpoint).toRoutes
}

def makeHttpUploadServiceF[F[_]: Async, R <: Has[_] with Random, E](
interpreter: GraphQLInterpreter[R, E],
skipValidation: Boolean = false,
enableIntrospection: Boolean = true,
queryExecution: QueryExecution = QueryExecution.Parallel,
requestInterceptor: RequestInterceptor[R] = RequestInterceptor.empty
)(implicit runtime: Runtime[R]): HttpRoutes[F] = {
val endpoint = TapirAdapter.makeHttpUploadService[R, E](
interpreter,
skipValidation,
enableIntrospection,
queryExecution,
requestInterceptor
)
val endpointF = convertHttpEndpointToF[F, R, E](endpoint)
Http4sServerInterpreter().toRoutes(endpointF)
}

def makeWebSocketService[R, R1 <: R, E](
builder: WebSocketBuilder2[RIO[R with Clock with Blocking, *]],
interpreter: GraphQLInterpreter[R1, E],
Expand All @@ -79,6 +120,29 @@ object Http4sAdapter {
.toRoutes(builder.asInstanceOf[WebSocketBuilder2[RIO[R1 with Clock with Blocking, *]]])
}

def makeWebSocketServiceF[F[_]: Async: Dispatcher, R, E](
builder: WebSocketBuilder2[F],
interpreter: GraphQLInterpreter[R, E],
skipValidation: Boolean = false,
enableIntrospection: Boolean = true,
keepAliveTime: Option[Duration] = None,
queryExecution: QueryExecution = QueryExecution.Parallel,
requestInterceptor: RequestInterceptor[R] = RequestInterceptor.empty,
webSocketHooks: WebSocketHooks[R, E] = WebSocketHooks.empty
)(implicit runtime: Runtime[R]): HttpRoutes[F] = {
val endpoint = TapirAdapter.makeWebSocketService[R, E](
interpreter,
skipValidation,
enableIntrospection,
keepAliveTime,
queryExecution,
requestInterceptor,
webSocketHooks
)
val endpointF = convertWebSocketEndpointToF[F, R, E](endpoint)
Http4sServerInterpreter().toWebSocketRoutes(endpointF)(builder)
}

/**
* Utility function to create an http4s middleware that can extracts something from each request
* and provide a layer to eliminate the ZIO environment
Expand Down Expand Up @@ -131,7 +195,7 @@ object Http4sAdapter {
* If you wish to use `Http4sServerInterpreter` with cats-effect IO instead of `ZHttp4sServerInterpreter`,
* you can use this function to convert the tapir endpoints to their cats-effect counterpart.
*/
def convertHttpEndpointToF[E, R, F[_]: Async](
def convertHttpEndpointToF[F[_]: Async, R, E](
endpoint: ServerEndpoint[Any, RIO[R, *]]
)(implicit runtime: Runtime[R]): ServerEndpoint[Any, F] =
ServerEndpoint[endpoint.A, endpoint.U, endpoint.I, endpoint.E, endpoint.O, Any, F](
Expand All @@ -140,4 +204,36 @@ object Http4sAdapter {
_ => u => req => CatsInterop.toEffect(endpoint.logic(zioMonadError)(u)(req))
)

/**
* If you wish to use `Http4sServerInterpreter` with cats-effect IO instead of `ZHttp4sServerInterpreter`,
* you can use this function to convert the tapir endpoints to their cats-effect counterpart.
*/
def convertWebSocketEndpointToF[F[_]: Async: Dispatcher, R, E](
endpoint: ServerEndpoint[ZioWebSockets, RIO[R, *]]
)(implicit runtime: Runtime[R]): ServerEndpoint[Fs2Streams[F] with WebSockets, F] = {
type Fs2Pipe = fs2.Pipe[F, GraphQLWSInput, GraphQLWSOutput]

val e = endpoint
.asInstanceOf[
ServerEndpoint.Full[endpoint.A, endpoint.U, endpoint.I, endpoint.E, CalibanPipe, ZioWebSockets, RIO[R, *]]
]

ServerEndpoint[endpoint.A, endpoint.U, endpoint.I, endpoint.E, Fs2Pipe, Fs2Streams[F] with WebSockets, F](
e.endpoint.asInstanceOf[Endpoint[endpoint.A, endpoint.I, endpoint.E, Fs2Pipe, Any]],
_ => a => CatsInterop.toEffect(e.securityLogic(zioMonadError)(a)),
_ =>
u =>
req =>
CatsInterop.toEffect(
e.logic(zioMonadError)(u)(req)
.map(_.map { zioPipe =>
import zio.stream.interop.fs2z._
fs2InputStream =>
zioPipe(fs2InputStream.translate(CatsInterop.fromEffectK[F, Any]).toZStream()).toFs2Stream
.translate(CatsInterop.toEffectK)
})
)
)
}

}
49 changes: 49 additions & 0 deletions examples/src/main/scala/example/http4s/ExampleAppF.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package example.http4s

import caliban.interop.cats.implicits._
import caliban.{ CalibanError, Http4sAdapter }
import cats.data.Kleisli
import cats.effect.std.Dispatcher
import cats.effect.{ ExitCode, IO, IOApp }
import example.ExampleData.sampleCharacters
import example.ExampleService.ExampleService
import example.{ ExampleApi, ExampleService }
import org.http4s.StaticFile
import org.http4s.blaze.server.BlazeServerBuilder
import org.http4s.implicits._
import org.http4s.server.Router
import org.http4s.server.middleware.CORS
import zio.Runtime
import zio.clock.Clock
import zio.console.Console
import zio.internal.Platform

object ExampleAppF extends IOApp {

type MyEnv = Console with Clock with ExampleService

implicit val zioRuntime: Runtime[MyEnv] =
Runtime.unsafeFromLayer(ExampleService.make(sampleCharacters) ++ Console.live ++ Clock.live, Platform.default)

override def run(args: List[String]): IO[ExitCode] =
Dispatcher[IO].use { implicit dispatcher =>
for {
interpreter <- ExampleApi.api.interpreterAsync[IO]
_ <- BlazeServerBuilder[IO]
.bindHttp(8088, "localhost")
.withHttpWebSocketApp(wsBuilder =>
Router[IO](
"/api/graphql" ->
CORS.policy(Http4sAdapter.makeHttpServiceF[IO, MyEnv, CalibanError](interpreter)),
"/ws/graphql" ->
CORS.policy(Http4sAdapter.makeWebSocketServiceF[IO, MyEnv, CalibanError](wsBuilder, interpreter)),
"/graphiql" ->
Kleisli.liftF(StaticFile.fromResource("/graphiql.html", None))
).orNotFound
)
.serve
.compile
.drain
} yield ExitCode.Success
}
}

0 comments on commit 0d0bc2b

Please sign in to comment.