Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add helpers and example for using Http4sAdapter with F[_] #1206

Merged
merged 1 commit into from
Dec 14, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
}
}