Skip to content

Commit

Permalink
Provide more examples of tapir interop (#2375)
Browse files Browse the repository at this point in the history
  • Loading branch information
calvinlfer authored Aug 26, 2024
1 parent 193714f commit 8df7696
Show file tree
Hide file tree
Showing 8 changed files with 191 additions and 18 deletions.
28 changes: 15 additions & 13 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -536,19 +536,21 @@ lazy val examples = project
crossScalaVersions := Seq(scala213),
libraryDependencySchemes += "org.scala-lang.modules" %% "scala-java8-compat" % "always",
libraryDependencies ++= Seq(
"org.typelevel" %% "cats-mtl" % catsMtlVersion,
"org.http4s" %% "http4s-ember-server" % http4sVersion,
"org.http4s" %% "http4s-dsl" % http4sVersion,
"com.softwaremill.sttp.client3" %% "zio" % sttpVersion,
"io.circe" %% "circe-generic" % circeVersion,
"dev.zio" %% "zio-http" % zioHttpVersion,
"org.playframework" %% "play-pekko-http-server" % playVersion,
"com.typesafe.akka" %% "akka-actor-typed" % akkaVersion,
"com.softwaremill.sttp.tapir" %% "tapir-jsoniter-scala" % tapirVersion,
"com.softwaremill.sttp.tapir" %% "tapir-json-circe" % tapirVersion,
"com.softwaremill.sttp.tapir" %% "tapir-json-play" % tapirVersion,
"com.softwaremill.sttp.tapir" %% "tapir-jsoniter-scala" % tapirVersion,
"com.github.plokhotnyuk.jsoniter-scala" %% "jsoniter-scala-macros" % jsoniterVersion % Provided
"org.typelevel" %% "cats-mtl" % catsMtlVersion,
"org.http4s" %% "http4s-ember-server" % http4sVersion,
"org.http4s" %% "http4s-dsl" % http4sVersion,
"com.softwaremill.sttp.client3" %% "zio" % sttpVersion,
"io.circe" %% "circe-generic" % circeVersion,
"dev.zio" %% "zio-http" % zioHttpVersion,
"org.playframework" %% "play-pekko-http-server" % playVersion,
"com.typesafe.akka" %% "akka-actor-typed" % akkaVersion,
"com.softwaremill.sttp.tapir" %% "tapir-jsoniter-scala" % tapirVersion,
"com.softwaremill.sttp.tapir" %% "tapir-json-circe" % tapirVersion,
"com.softwaremill.sttp.tapir" %% "tapir-json-play" % tapirVersion,
"com.softwaremill.sttp.tapir" %% "tapir-jsoniter-scala" % tapirVersion,
"com.softwaremill.sttp.tapir" %% "tapir-swagger-ui-bundle" % tapirVersion,
"com.softwaremill.sttp.tapir" %% "tapir-zio-http-server" % tapirVersion,
"com.github.plokhotnyuk.jsoniter-scala" %% "jsoniter-scala-macros" % jsoniterVersion % Provided
)
)
.dependsOn(
Expand Down
62 changes: 62 additions & 0 deletions examples/src/main/scala/example/calibantotapir/MainApp.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package example.calibantotapir

import example.calibantotapir.graphql._
import zio._
import zio.http._
import caliban._
import caliban.interop.tapir._
import sttp.tapir.json.jsoniter._
import sttp.capabilities.zio.ZioStreams
import sttp.tapir.server.ziohttp._
import sttp.tapir.server.ServerEndpoint
import sttp.tapir.swagger.bundle.SwaggerInterpreter
import example.calibantotapir.tapir.SampleRestEndpoint

object MainApp extends ZIOAppDefault {
val graphQLToTapir: ZIO[Queries & Mutations, CalibanError, List[ServerEndpoint[ZioStreams, Task]]] =
for {
queries <- ZIO.service[Queries]
mutations <- ZIO.service[Mutations]
graphql = graphQL(RootResolver(queries, mutations))
interpreter <- graphql.interpreter
endpoints =
HttpInterpreter(interpreter)
.serverEndpoints[Any, ZioStreams](ZioStreams)
.map(_.prependSecurityIn("graphql"))
} yield endpoints

def documented(allEndpoints: List[ServerEndpoint[ZioStreams, Task]]) = {
val doc = SwaggerInterpreter().fromServerEndpoints(allEndpoints, "playground", "1.0")
doc ++ allEndpoints
}

// Redirect / to /docs
val redirectRootToDocs =
Routes(
Method.GET / "" ->
handler(Response.redirect(URL(path = Path.root / "docs"), isPermanent = true))
)

val serve: ZIO[Queries & Mutations & Server, CalibanError, Nothing] =
for {
graphqlEndpoints <- graphQLToTapir
restEndpoint = SampleRestEndpoint.endpoint
all = documented(restEndpoint :: graphqlEndpoints)
routes = ZioHttpInterpreter().toHttp[Any](all)
server <- ZIO.service[Server]
_ <- Server.install(redirectRootToDocs ++ routes)
port <- server.port
_ <- ZIO.logInfo(s"Server started on port $port")
result <- ZIO.never
} yield result

override val run =
serve
.provide(
BookRepository.layer,
Queries.layer,
Mutations.layer,
Server.live,
ZLayer.succeed(Server.Config.default)
)
}
11 changes: 11 additions & 0 deletions examples/src/main/scala/example/calibantotapir/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# Caliban to Tapir

This example shows how to convert a Caliban GraphQL datatypes and turn them into Tapir endpoints.
This is useful when you have both REST APIs and GraphQL endpoints and you want to use Tapir to expose a unified API.

The example also shows how to mount the GraphQL endpoint to `POST` and `GET` `/graphql` as well as provide `/example` for a sample endpoint returning JSON.
All of the endpoints are documented in Swagger. Visiting `localhost:8080/` or `/docs` will show the Swagger UI.

This example interprets the Tapir endpoints using ZIO HTTP.

Note: If you are only using GraphQL endpoints, then you are much better of using the quick adapter instead of this. This example is provided for when you want to use both GraphQL and REST endpoints and have them managed by Tapir to gain the benefits of documentation.
69 changes: 69 additions & 0 deletions examples/src/main/scala/example/calibantotapir/graphql/Book.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
package example.calibantotapir.graphql

import caliban.schema.ArgBuilder
import caliban.schema.ArgBuilder.auto._
import caliban.schema.Schema
import caliban.schema.Schema.auto._
import zio._

final case class Book(title: String, year: Int)
object Book {
implicit val bookSchema: Schema[Any, Book] = Schema.gen
}

final case class BookSearchArgs(year: Option[Int], limit: Option[Int])
object BookSearchArgs {
implicit val bookSearchArgsSchema: ArgBuilder[BookSearchArgs] = ArgBuilder.gen
}

class BookRepository(books: Ref[List[Book]]) {
def add(book: Book): UIO[Unit] = books.update(books => book :: books)

def delete(title: String): UIO[Unit] = books.update(_.filterNot(_.title == title))

def list(year: Option[Int], limit: Option[Int]): UIO[List[Book]] = {
val lim = limit.getOrElse(Int.MaxValue)
val yearMatch = (incomingYear: Int) => year.map(_ == incomingYear).getOrElse(true)
books.get.map(_.filter(book => yearMatch(book.year)).take(lim))
}
}
object BookRepository {
val books = List(
Book("The Sorrows of Young Werther", 1774),
Book("Iliad", -8000),
Book("Nad Niemnem", 1888),
Book("The Colour of Magic", 1983),
Book("The Art of Computer Programming", 1968),
Book("Pharaoh", 1897),
Book("Lords and Ladies", 1992)
)

val layer: ULayer[BookRepository] = ZLayer(Ref.make[List[Book]](books).map(new BookRepository(_)))
}

final case class Queries(
books: BookSearchArgs => UIO[List[Book]]
)
object Queries {
implicit val queriesSchema: Schema[Any, Queries] = Schema.gen

val layer: URLayer[BookRepository, Queries] = ZLayer {
for {
repo <- ZIO.service[BookRepository]
} yield Queries(args => repo.list(args.year, args.limit))
}
}

final case class Mutations(
addBook: Book => UIO[Unit],
deleteBook: String => UIO[Unit]
)
object Mutations {
implicit val mutationsSchema: Schema[Any, Mutations] = Schema.gen

val layer: URLayer[BookRepository, Mutations] = ZLayer {
for {
repo <- ZIO.service[BookRepository]
} yield Mutations(repo.add, repo.delete)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package example.calibantotapir.tapir

import sttp.tapir.ztapir._
import sttp.tapir.json.jsoniter._
import zio._
import sttp.capabilities.zio.ZioStreams
import io.circe.JsonObject
import zio.constraintless.Sample
import com.github.plokhotnyuk.jsoniter_scala.core.JsonValueCodec
import com.github.plokhotnyuk.jsoniter_scala.macros.JsonCodecMaker
import sttp.tapir.Schema

object SampleRestEndpoint {
// NOTE: You can also add the graphiql endpoint in a similar fashion
val endpoint: ZServerEndpoint[Any, ZioStreams] =
infallibleEndpoint.get
.in("example")
.out(jsonBody[SampleResponse])
.serverLogic(_ => ZIO.right(SampleResponse("Hello", 1, 2)))

final case class SampleResponse(data: String, x: Int, y: Int)
object SampleResponse {
implicit val sampleResponseCodec: JsonValueCodec[SampleResponse] = JsonCodecMaker.make
implicit val sampleResponseSchema: Schema[SampleResponse] = Schema.derived
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package example.tapir
package example.tapirtocaliban

import io.circe.generic.auto._
import sttp.tapir._
Expand Down Expand Up @@ -60,15 +60,15 @@ object Endpoints {

def bookAddLogic(book: Book, token: String): IO[String, Unit] =
if (token != "secret") {
ZIO.fail("Unauthorized access!!!11")
ZIO.fail("Unauthorized access!!!")
} else {
books = book :: books
ZIO.unit
}

def bookDeleteLogic(title: String, token: String): IO[String, Unit] =
if (token != "secret") {
ZIO.fail("Unauthorized access!!!11")
ZIO.fail("Unauthorized access!!!")
} else {
books = books.filterNot(_.title == title)
ZIO.unit
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package example.tapir
package example.tapirtocaliban

import example.tapir.Endpoints._
import example.tapirtocaliban.Endpoints._
import caliban.interop.tapir._
import caliban.{ GraphQL, Http4sAdapter }
import caliban.schema.Schema.auto._
Expand Down
3 changes: 3 additions & 0 deletions examples/src/main/scala/example/tapirtocaliban/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Tapir to Caliban

This example shows how you can take Tapir endpoints and turn them into Caliban GraphQL datatypes.

0 comments on commit 8df7696

Please sign in to comment.