scala 使用Tapir和sealed traits生成正确的模式和文档

mmvthczy  于 5个月前  发布在  Scala
关注(0)|答案(1)|浏览(82)

我有以下端点:

val myEndpoint: PublicEndpoint[Unit, ErrorResponse, Entity, Any] = endpoint.get
    .in("test")
    .out(jsonBody[Entity])
    .errorOut(jsonBody[ErrorResponse])

字符串
其中EntityErrorResponse是:

sealed trait ErrorResponse
object ErrorResponse {
  final case class NotFound(id: String) extends ErrorResponse
  final case class UnknownError(error: String) extends ErrorResponse

  implicit val notFoundSchema: Schema[NotFound] = Schema.derived
  implicit val unknownErrorSchema: Schema[UnknownError] = Schema.derived
  implicit val errorResponseSchema: Schema[ErrorResponse] = Schema.derived
}


然后我将端点转换为akka路由:

val myEndpointRoute: Route = AkkaHttpServerInterpreter().toRoute(myEndpoint.serverLogic { _ =>
    val result: Either[ErrorResponse, Entity] = Right(Entity("some data of the entity"))
    Future.successful(result)
  })

  val swaggerEndpoint = SwaggerInterpreter().fromEndpoints[Future](List(myEndpoint), "Tapir Demo", "1.0")
  val swaggerRoutes: Route = AkkaHttpServerInterpreter().toRoute(swaggerEndpoint)


然后运行服务器:

Http()
    .newServerAt("localhost", 8080)
    .bind(myEndpointRoute ~ swaggerRoutes)
    .onComplete {
      case Success(_) =>
        println(s"Started on port 8080")
      case Failure(e) =>
        println("Failed to start ... ", e)
    }


我遇到的问题是,当浏览ErrorResponse的模式时,我看到的是对象的层次结构(#0#1),而不是像NotFoundUnknownError这样的子类型。


的数据
我应该如何定义ErrorResponse的模式?
PS:dependencies:

"com.softwaremill.sttp.tapir" %% "tapir-core" % "1.9.6",
  "com.softwaremill.sttp.tapir" %% "tapir-json-circe" % "1.9.6"
  "com.softwaremill.sttp.tapir" %% "tapir-swagger-ui-bundle"  % "1.9.6",
  "com.softwaremill.sttp.tapir" %% "tapir-akka-http-server" % "1.9.6"

nxagd54h

nxagd54h1#

这就是swagger UIOpenAPI Spec 3.1.0中渲染oneOf的方式,OpenAPI Spec 3.1.0tapir使用的默认版本。如果您将其更改为OpenAPI Spec 3.0.3或任何3.x.x,您将获得预期的结果。这里您有一个重现您的案例的POC(我使用pekko-http而不是akka-http,但它是相同的)

*build.sbt

lazy val root = (project in file("."))
  .settings(
    name := "tapir-swagger-ui-poc",
    libraryDependencies ++= Seq(
      "ch.qos.logback"                 % "logback-classic"         % "1.4.14",
      "com.typesafe.scala-logging"    %% "scala-logging"           % "3.9.5",
      "org.apache.pekko"              %% "pekko-actor-typed"       % "1.0.2",
      "com.softwaremill.sttp.tapir"   %% "tapir-pekko-http-server" % "1.9.6",
      "com.softwaremill.sttp.tapir"   %% "tapir-sttp-stub-server"  % "1.9.6",
      "com.softwaremill.sttp.tapir"   %% "tapir-openapi-docs"      % "1.9.6",
      "com.softwaremill.sttp.tapir"   %% "tapir-json-circe"        % "1.9.6",
      "com.softwaremill.sttp.tapir"   %% "tapir-swagger-ui-bundle" % "1.9.6",
      "com.softwaremill.sttp.apispec" %% "openapi-circe-yaml"      % "0.7.3"
    ),
    run / fork := true
  )

字符串

*Main.scala

import com.typesafe.scalalogging.Logger
import io.circe.generic.auto._
import org.apache.pekko.actor.typed.ActorSystem
import org.apache.pekko.actor.typed.scaladsl.Behaviors
import org.apache.pekko.http.scaladsl.Http
import org.apache.pekko.http.scaladsl.server.Route
import sttp.tapir._
import sttp.tapir.json.circe._
import sttp.tapir.server.ServerEndpoint
import sttp.tapir.server.pekkohttp.PekkoHttpServerInterpreter
import sttp.tapir.swagger.SwaggerUIOptions
import sttp.tapir.swagger.bundle.SwaggerInterpreter

import scala.concurrent.{ExecutionContextExecutor, Future}

object Main {

  sealed trait ErrorResponse

  object ErrorResponse {
    final case class NotFound(id: String) extends ErrorResponse

    final case class UnknownError(error: String) extends ErrorResponse

    implicit val notFoundSchema: Schema[NotFound] = Schema.derived
    implicit val unknownErrorSchema: Schema[UnknownError] = Schema.derived
    implicit val errorResponseSchema: Schema[ErrorResponse] = Schema.derived
  }

  private val myEndpoint = endpoint.get
    .in("test")
    .out(stringBody)
    .errorOut(jsonBody[ErrorResponse])

  def main(args: Array[String]): Unit = {

    val logger = Logger(getClass)

    implicit val system: ActorSystem[Nothing] =
      ActorSystem(Behaviors.empty, "money-maniacs-http")

    implicit val executionContext: ExecutionContextExecutor =
      system.executionContext

    /**
     * swagger UI endpoints for OpenAPI Spec 3.0.3
     */
    val swaggerEndpoints_3_0_3: List[ServerEndpoint[Any, Future]] =
      SwaggerInterpreter(
        customiseDocsModel = openAPI => openAPI.openapi("3.0.3"), // set OpenAPI spec version
        swaggerUIOptions = SwaggerUIOptions.default.pathPrefix(List("docs-3.0.3")) // set path prefix for docs
      )
      .fromEndpoints[Future](
        List(myEndpoint), // list of endpoints 
        "My App", 
        "1.0"
      )

    /**
     * swagger UI endpoints for OpenAPI Spec 3.1.0
     */
    val swaggerEndpoints_3_1_0: List[ServerEndpoint[Any, Future]] =
      SwaggerInterpreter(
        swaggerUIOptions = SwaggerUIOptions.default.pathPrefix(List("docs-3.1.0")) // set path prefix for docs
      )
      .fromEndpoints[Future](
        List(myEndpoint), // list of endpoints
        "My App", 
        "1.0"
      )

    val routes: Route =
      PekkoHttpServerInterpreter()
        .toRoute(swaggerEndpoints_3_0_3 ::: swaggerEndpoints_3_1_0)

    val interface = "0.0.0.0"
    val port = 9000

    Http()
      .newServerAt(interface = interface, port = port)
      .bind(routes)
      .foreach { _ =>
        logger.info(s"Server started at $interface:$port")
        logger.info(s"Press enter to stop the server")
      }
  }

}


使用sbt run启动服务器后,可以检查端点是否符合OpenAPI Spec 3.1.03.0.3

open http://localhost:9000/docs-3.0.3

open http://localhost:9000/docs-3.1.0

Swagger中报告了一个similar issue。建议添加与对象同名的属性title
对于tapir 1.9.6(您正在使用的版本,也是目前发布的最新版本),您可以将定义的模式更改为以下内容,

import scala.reflect.runtime.universe.typeOf

implicit val notFoundSchema: Schema[NotFound] =
  Schema.derived.title(typeOf[NotFound].typeSymbol.name.toString)
implicit val unknownErrorSchema: Schema[UnknownError] =
  Schema.derived.title(typeOf[UnknownError].typeSymbol.name.toString)
implicit val errorResponseSchema: Schema[ErrorResponse] =
  Schema.derived.title(typeOf[UnknownError].typeSymbol.name.toString)


这样做,将产生您正在寻找的结果(带有数字的哈希仍然存在,但现在它也添加了您想要的描述)


相关问题