diff --git a/build.sbt b/build.sbt index 5708b4e83e..872ef11653 100644 --- a/build.sbt +++ b/build.sbt @@ -121,6 +121,7 @@ lazy val allAggregates = core.projectRefs ++ swaggerUiPlay.projectRefs ++ redocPlay.projectRefs ++ swaggerUiVertx.projectRefs ++ + swaggerUiZioHttp.projectRefs ++ serverTests.projectRefs ++ akkaHttpServer.projectRefs ++ http4sServer.projectRefs ++ @@ -785,6 +786,18 @@ lazy val swaggerUiVertx: ProjectMatrix = (projectMatrix in file("docs/swagger-ui ) .jvmPlatform(scalaVersions = scala2And3Versions) +lazy val swaggerUiZioHttp: ProjectMatrix = (projectMatrix in file("docs/swagger-ui-zio-http")) + .settings(commonJvmSettings) + .settings( + name := "tapir-swagger-ui-zio-http", + libraryDependencies ++= Seq( + "io.d11" %% "zhttp" % "1.0.0.0-RC17", + "org.webjars" % "swagger-ui" % Versions.swaggerUi, + scalaTest.value % Test + ) + ) + .jvmPlatform(scalaVersions = scala2And3Versions) + // server lazy val serverTests: ProjectMatrix = (projectMatrix in file("server/tests")) @@ -1188,6 +1201,7 @@ lazy val examples: ProjectMatrix = (projectMatrix in file("examples")) circeJson, swaggerUiAkka, swaggerUiHttp4s, + swaggerUiZioHttp, zioHttp4sServer, zioHttp, sttpStubServer, @@ -1224,6 +1238,7 @@ lazy val playground: ProjectMatrix = (projectMatrix in file("playground")) circeJson, swaggerUiAkka, swaggerUiHttp4s, + swaggerUiZioHttp, refined, cats, zioHttp4sServer, diff --git a/doc/docs/openapi.md b/doc/docs/openapi.md index 0a20dd681f..b2313de615 100644 --- a/doc/docs/openapi.md +++ b/doc/docs/openapi.md @@ -184,6 +184,8 @@ For Play, use `SwaggerPlay` or `RedocPlay` classes. For Vert.x, use `SwaggerVertx` class. +For zio-http, use `SwaggerZioHttp` class. + ### Using with sbt-assembly The `tapir-swagger-ui-*` modules rely on a file in the `META-INF` directory tree, to determine the version of the Swagger UI. diff --git a/doc/examples.md b/doc/examples.md index d64cbb7e58..6c4ae00b04 100644 --- a/doc/examples.md +++ b/doc/examples.md @@ -20,6 +20,7 @@ The [`examples`](https://github.com/softwaremill/tapir/tree/master/examples/src/ * [ZIO example, using http4s](https://github.com/softwaremill/tapir/blob/master/examples/src/main/scala/sttp/tapir/examples/ZioExampleHttp4sServer.scala) * [ZIO example with environment, using http4s](https://github.com/softwaremill/tapir/blob/master/examples/src/main/scala/sttp/tapir/examples/ZioEnvExampleHttp4sServer.scala) * [ZIO partial server logic example, using http4s](https://github.com/softwaremill/tapir/blob/master/examples/src/main/scala/sttp/tapir/examples/ZioPartialServerLogicHttp4s.scala) +* [ZIO example using ZIO http](https://github.com/softwaremill/tapir/blob/master/examples/src/main/scala/sttp/tapir/examples/ZioExampleZioHttpServer.scala) * [Streaming body, using akka-http](https://github.com/softwaremill/tapir/blob/master/examples/src/main/scala/sttp/tapir/examples/StreamingAkkaServer.scala) * [Streaming body, using http4s + fs2](https://github.com/softwaremill/tapir/blob/master/examples/src/main/scala/sttp/tapir/examples/StreamingHttp4sFs2Server.scala) * [Web sockets server, using akka-http](https://github.com/softwaremill/tapir/blob/master/examples/src/main/scala/sttp/tapir/examples/WebSocketAkkaServer.scala) diff --git a/docs/swagger-ui-zio-http/src/main/scala/sttp/tapir/swagger/ziohttp/SwaggerZioHttp.scala b/docs/swagger-ui-zio-http/src/main/scala/sttp/tapir/swagger/ziohttp/SwaggerZioHttp.scala new file mode 100644 index 0000000000..987decec05 --- /dev/null +++ b/docs/swagger-ui-zio-http/src/main/scala/sttp/tapir/swagger/ziohttp/SwaggerZioHttp.scala @@ -0,0 +1,40 @@ +package sttp.tapir.swagger.ziohttp + +import zhttp.http._ +import zio.Chunk +import zio.blocking.Blocking +import zio.stream.ZStream + +import java.util.Properties + +class SwaggerZioHttp( + yaml: String, + contextPath: String = "docs", + yamlName: String = "docs.yaml" +) { + private val resourcePathPrefix = { + val swaggerVersion: String = { + val p = new Properties() + val pomProperties = getClass.getResourceAsStream("/META-INF/maven/org.webjars/swagger-ui/pom.properties") + try p.load(pomProperties) + finally pomProperties.close() + p.getProperty("version") + } + s"META-INF/resources/webjars/swagger-ui/$swaggerVersion" + } + + def route: Http[Blocking, Throwable, Request, Response[Blocking, Throwable]] = { + Http.collect[Request] { + case Method.GET -> Root / `contextPath` => + val location = s"/$contextPath/index.html?url=/$contextPath/$yamlName" + Response.http(Status.MOVED_PERMANENTLY, List(Header.custom("Location", location))) + case Method.GET -> Root / `contextPath` / `yamlName` => + val body = HttpData.CompleteData(Chunk.fromArray(yaml.getBytes(HTTP_CHARSET))) + Response.http[Blocking, Throwable](Status.OK, List(Header.custom("content-type", "text/yaml")), body) + case Method.GET -> Root / `contextPath` / swaggerResource => + val staticResource = this.getClass.getClassLoader.getResourceAsStream(s"$resourcePathPrefix/$swaggerResource") + val content = HttpData.fromStream(ZStream.fromInputStream(staticResource)) + Response.http(content = content) + } + } +} diff --git a/examples/src/main/scala/sttp/tapir/examples/ZioExampleZioHttpServer.scala b/examples/src/main/scala/sttp/tapir/examples/ZioExampleZioHttpServer.scala new file mode 100644 index 0000000000..a885a37cf9 --- /dev/null +++ b/examples/src/main/scala/sttp/tapir/examples/ZioExampleZioHttpServer.scala @@ -0,0 +1,47 @@ +package sttp.tapir.examples + +import io.circe.generic.auto._ +import sttp.tapir.generic.auto._ +import sttp.tapir.json.circe._ +import sttp.tapir.server.ziohttp.ZioHttpInterpreter +import sttp.tapir.swagger.ziohttp.SwaggerZioHttp +import sttp.tapir.ztapir._ +import zhttp.http.HttpApp +import zhttp.service.Server +import zio.{App, ExitCode, IO, UIO, URIO, ZIO} + +object ZioExampleZioHttpServer extends App { + case class Pet(species: String, url: String) + + // Sample endpoint, with the logic implemented directly using .toRoutes + val petEndpoint: ZEndpoint[Int, String, Pet] = + endpoint.get.in("pet" / path[Int]("petId")).errorOut(stringBody).out(jsonBody[Pet]) + + val petRoutes: HttpApp[Any, Throwable] = + ZioHttpInterpreter().toHttp(petEndpoint)(petId => + if (petId == 35) ZIO.succeed(Right(Pet("Tapirus terrestris", "https://en.wikipedia.org/wiki/Tapir"))) + else ZIO.succeed(Left("Unknown pet id")) + ) + + // Same as above, but combining endpoint description with server logic: + val petServerEndpoint: ZServerEndpoint[Any, Int, String, Pet] = petEndpoint.zServerLogic { petId => + if (petId == 35) { + UIO(Pet("Tapirus terrestris", "https://en.wikipedia.org/wiki/Tapir")) + } else { + IO.fail("Unknown pet id") + } + } + val petServerRoutes: HttpApp[Any, Throwable] = ZioHttpInterpreter().toHttp(List(petServerEndpoint)) + + // + + val yaml: String = { + import sttp.tapir.docs.openapi.OpenAPIDocsInterpreter + import sttp.tapir.openapi.circe.yaml._ + OpenAPIDocsInterpreter().toOpenAPI(petEndpoint, "Our pets", "1.0").toYaml + } + + // Starting the server + override def run(args: List[String]): URIO[zio.ZEnv, ExitCode] = + Server.start(8080, petRoutes <> new SwaggerZioHttp(yaml).route).exitCode +}