Документация Redoc для конечной точки тапира с запечатанной иерархией не отображается должным образом

Я пытаюсь определить конечную точку тапира, которая будет принимать две потенциально разные полезные нагрузки (в приведенном ниже фрагменте два разных способа определения Вещи). Я в целом следую инструкциям здесь: https://circe.github.io/circe/codecs/adt.html и определяю свою конечную точку:

      endpoint
    .post
    .in(jsonBody[ThingSpec].description("Specification of the thing"))
    .out(jsonBody[Thing].description("Thing!"))

ThingSpec— это запечатанный трейт, который расширяют оба класса, представляющие возможные полезные нагрузки:

      import io.circe.{Decoder, Encoder, derivation}
import io.circe.derivation.{deriveDecoder, deriveEncoder}
import sttp.tapir.Schema
import sttp.tapir.Schema.annotations.description
import sttp.tapir.generic.Configuration
import cats.syntax.functor._
import io.circe.syntax.EncoderOps

sealed trait ThingSpec {
  def kind: String
}

object ThingSpec {
  implicit val config: Configuration = Configuration.default.withSnakeCaseMemberNames
  implicit val thingConfigDecoder
    : Decoder[ThingSpec] = Decoder[ThingOneSpec].widen or Decoder[ThingTwoSpec].widen
  implicit val thingConfigEncoder: Encoder[ThingSpec] = {
    case one @ ThingOneSpec(_, _) => one.asJson
    case two @ ThingTwoSpec(_, _) => two.asJson
  }
  implicit val thingConfigSchema: Schema[ThingSpec] =
    Schema.oneOfUsingField[ThingSpec, String](_.kind, _.toString)(
      "one" -> ThingOneSpec.thingConfigSchema,
      "two" -> ThingTwoSpec.thingConfigSchema
    )
}

case class ThingOneSpec(
  name: String,
  age: Long               
) extends ThingSpec {
  def kind: String = "one"
}
object ThingOneSpec {
  implicit val config: Configuration = Configuration.default.withSnakeCaseMemberNames
  implicit val thingConfigEncoder: Encoder[ThingOneSpec] = deriveEncoder(
    derivation.renaming.snakeCase
  )
  implicit val thingConfigDecoder: Decoder[ThingOneSpec] = deriveDecoder(
    derivation.renaming.snakeCase
  )
  implicit val thingConfigSchema: Schema[ThingOneSpec] = Schema.derived
}

case class ThingTwoSpec(
  height: Long,
  weight: Long,
) extends ThingSpec {
  def kind: String = "two"
}
object ThingTwoSpec {
  implicit val config: Configuration = Configuration.default.withSnakeCaseMemberNames
  implicit val thingConfigEncoder: Encoder[ThingTwoSpec] = deriveEncoder(
    derivation.renaming.snakeCase
  )
  implicit val thingConfigDecoder: Decoder[ThingTwoSpec] = deriveDecoder(
    derivation.renaming.snakeCase
  )
  implicit val thingConfigSchema: Schema[ThingTwoSpec] = Schema.derived
}

Что, кажется, работает нормально, за исключением сгенерированных документов redoc. «Раздел тела запроса» редока, который, как я полагаю, создается из

.in(jsonBody[ThingSpec].description("Specification of the thing"))

содержит только сведения об объекте ThingOneSpec, ThingTwoSpec не упоминается. Пример раздела «полезная нагрузка» включает в себя и то, и другое.

Мой главный вопрос заключается в том, как получить раздел тела запроса документов, чтобы показать обе возможные полезные нагрузки.

Однако я знаю, что, возможно, я сделал это не лучшим образом (с точки зрения цирка/тапира). В идеале я бы не хотел включать явный дискриминатор (kind) в свойстве/классах, потому что я предпочел бы, чтобы он не был показан конечному пользователю в разделах «Полезная нагрузка» документации. Несмотря на чтение

Я не могу заставить это работать без явного дискриминатора.

1 ответ

Вы можете избавиться от дискриминатора, определив схему one-of вручную:

      implicit val thingConfigSchema: Schema[ThingSpec] =
  Schema(
    SchemaType.SCoproduct(List(ThingOneSpec.thingConfigSchema, ThingTwoSpec.thingConfigSchema), None) {
      case one: ThingOneSpec => Some(SchemaWithValue(ThingOneSpec.thingConfigSchema, one))
      case two: ThingTwoSpec => Some(SchemaWithValue(ThingTwoSpec.thingConfigSchema, two))
    },
    Some(Schema.SName(ThingSpec.getClass.getName))
  )

(Да, это излишне сложно писать; я посмотрю, может ли это быть сгенерировано макросом или иным образом.)

При рендеринге с помощью redoc я получаю переключатель «один из», поэтому я думаю, что это желаемый результат:

Другие вопросы по тегам