diff --git a/build.sbt b/build.sbt index 2c88df4e8..339575590 100644 --- a/build.sbt +++ b/build.sbt @@ -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( diff --git a/examples/src/main/scala/example/calibantotapir/MainApp.scala b/examples/src/main/scala/example/calibantotapir/MainApp.scala new file mode 100644 index 000000000..2175faa7a --- /dev/null +++ b/examples/src/main/scala/example/calibantotapir/MainApp.scala @@ -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) + ) +} diff --git a/examples/src/main/scala/example/calibantotapir/README.md b/examples/src/main/scala/example/calibantotapir/README.md new file mode 100644 index 000000000..e4e4e1297 --- /dev/null +++ b/examples/src/main/scala/example/calibantotapir/README.md @@ -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. \ No newline at end of file diff --git a/examples/src/main/scala/example/calibantotapir/graphql/Book.scala b/examples/src/main/scala/example/calibantotapir/graphql/Book.scala new file mode 100644 index 000000000..e73c3f4fc --- /dev/null +++ b/examples/src/main/scala/example/calibantotapir/graphql/Book.scala @@ -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) + } +} diff --git a/examples/src/main/scala/example/calibantotapir/tapir/SampleRestEndpoint.scala b/examples/src/main/scala/example/calibantotapir/tapir/SampleRestEndpoint.scala new file mode 100644 index 000000000..6d9548bbb --- /dev/null +++ b/examples/src/main/scala/example/calibantotapir/tapir/SampleRestEndpoint.scala @@ -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 + } +} diff --git a/examples/src/main/scala/example/tapir/Endpoints.scala b/examples/src/main/scala/example/tapirtocaliban/Endpoints.scala similarity index 95% rename from examples/src/main/scala/example/tapir/Endpoints.scala rename to examples/src/main/scala/example/tapirtocaliban/Endpoints.scala index 29178bd39..36a9db3ea 100644 --- a/examples/src/main/scala/example/tapir/Endpoints.scala +++ b/examples/src/main/scala/example/tapirtocaliban/Endpoints.scala @@ -1,4 +1,4 @@ -package example.tapir +package example.tapirtocaliban import io.circe.generic.auto._ import sttp.tapir._ @@ -60,7 +60,7 @@ 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 @@ -68,7 +68,7 @@ object Endpoints { 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 diff --git a/examples/src/main/scala/example/tapir/ExampleApp.scala b/examples/src/main/scala/example/tapirtocaliban/ExampleApp.scala similarity index 97% rename from examples/src/main/scala/example/tapir/ExampleApp.scala rename to examples/src/main/scala/example/tapirtocaliban/ExampleApp.scala index ff33a4ed0..3973cf848 100644 --- a/examples/src/main/scala/example/tapir/ExampleApp.scala +++ b/examples/src/main/scala/example/tapirtocaliban/ExampleApp.scala @@ -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._ diff --git a/examples/src/main/scala/example/tapirtocaliban/README.md b/examples/src/main/scala/example/tapirtocaliban/README.md new file mode 100644 index 000000000..5427df180 --- /dev/null +++ b/examples/src/main/scala/example/tapirtocaliban/README.md @@ -0,0 +1,3 @@ +# Tapir to Caliban + +This example shows how you can take Tapir endpoints and turn them into Caliban GraphQL datatypes. \ No newline at end of file