Документация 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
) в свойстве/классах, потому что я предпочел бы, чтобы он не был показан конечному пользователю в разделах «Полезная нагрузка» документации. Несмотря на чтение
- https://tapir.softwaremill.com/en/v0.17.7/endpoint/customtypes.html
- https://github.com/softwaremill/tapir/blob/master/examples/src/main/scala/sttp/tapir/examples/custom_types/SealedTraitWithDiscriminator.scala
- https://github.com/softwaremill/tapir/issues/315
Я не могу заставить это работать без явного дискриминатора.
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 я получаю переключатель «один из», поэтому я думаю, что это желаемый результат: