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

Provide more examples of tapir interop #2375

Merged
merged 3 commits into from
Aug 26, 2024
Merged
Show file tree
Hide file tree
Changes from 2 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
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 `/graphiql` for the GraphiQL UI.
calvinlfer marked this conversation as resolved.
Show resolved Hide resolved
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.