Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

codegen: Improve enum support #3861

Merged
merged 15 commits into from
Jun 26, 2024
Merged
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
add tests for inline enums, make them pass
hughsimpson committed Jun 24, 2024
commit a526b76114e830f0a3053324e99de412b84545cd
Original file line number Diff line number Diff line change
@@ -250,7 +250,7 @@ class EndpointGenerator {
(params ++ rqBody).mkString("\n") -> maybeEnumDefns.foldLeft(Option.empty[String]) {
case (acc, None) => acc
case (None, Some(nxt)) => Some(nxt.mkString("\n"))
case (Some(acc), Some(nxt)) => Some(acc + "\n" + nxt)
case (Some(acc), Some(nxt)) => Some(acc + "\n" + nxt.mkString("\n"))
}
}

@@ -339,15 +339,6 @@ class EndpointGenerator {
s"multipartBody[$t]"
case x => bail(s"$contentType only supports schema ref or binary. Found $x")
}
case "application/octet-stream" =>
schema match {
case _: OpenapiSchemaBinary =>
"multipartBody"
case schemaRef: OpenapiSchemaRef =>
val (t, _) = mapSchemaSimpleTypeToType(schemaRef, multipartForm = true)
s"multipartBody[$t]"
case x => bail(s"$contentType only supports schema ref or binary. Found $x")
}

case x => bail(s"Not all content types supported! Found $x")
}
Original file line number Diff line number Diff line change
@@ -195,6 +195,11 @@ object SchemaGenerator {
schemaForObject(s"$name${k.capitalize}Item", `type`)
case (k, OpenapiSchemaField(OpenapiSchemaMap(`type`: OpenapiSchemaObject, _), _)) =>
schemaForObject(s"$name${k.capitalize}Item", `type`)
case (k, OpenapiSchemaField(_: OpenapiSchemaEnum, _)) => schemaForEnum(s"$name${k.capitalize}")
case (k, OpenapiSchemaField(OpenapiSchemaArray(_: OpenapiSchemaEnum, _), _)) =>
schemaForEnum(s"$name${k.capitalize}Item")
case (k, OpenapiSchemaField(OpenapiSchemaMap(_: OpenapiSchemaEnum, _), _)) =>
schemaForEnum(s"$name${k.capitalize}Item")
} match {
case Nil => ""
case s => s.mkString("", "\n", "\n")
@@ -208,6 +213,9 @@ object SchemaGenerator {
}
subs.fold("")("\n" + _)
}
private def schemaForEnum(name: String): String =
s"""implicit lazy val ${BasicGenerator.uncapitalise(name)}TapirSchema: sttp.tapir.Schema[$name] = sttp.tapir.Schema.derived"""

private def genADTSchema(name: String, schema: OpenapiSchemaOneOf, fullModelPath: Option[String]): String = {
val schemaImpl = schema match {
case OpenapiSchemaOneOf(_, None) => "sttp.tapir.Schema.derived"
Original file line number Diff line number Diff line change
@@ -11,6 +11,38 @@ object TapirGeneratedEndpoints {
import sttp.tapir.generated.TapirGeneratedEndpointsJsonSerdes._
import TapirGeneratedEndpointsSchemas._

def makeQueryCodecForEnum[T <: enumeratum.EnumEntry](enumName: String, T: enumeratum.Enum[T]): sttp.tapir.Codec[List[String], T, sttp.tapir.CodecFormat.TextPlain] =
sttp.tapir.Codec.listHead[String, String, sttp.tapir.CodecFormat.TextPlain]
.mapDecode(s =>
// Case-insensitive mapping
scala.util.Try(T.upperCaseNameValuesToMap(s.toUpperCase))
.fold(
_ =>
sttp.tapir.DecodeResult.Error(
s,
new NoSuchElementException(
s"Could not find value $s for enum ${enumName}, available values: ${T.values.mkString(", ")}"
)
),
sttp.tapir.DecodeResult.Value(_)
)
)(_.entryName)
def makeQuerySeqCodecForEnum[T <: enumeratum.EnumEntry](enumName: String, T: enumeratum.Enum[T]): sttp.tapir.Codec[List[String], List[T], sttp.tapir.CodecFormat.TextPlain] =
sttp.tapir.Codec.list[String, String, sttp.tapir.CodecFormat.TextPlain]
.mapDecode(values =>
// Case-insensitive mapping
scala.util.Try(values.map(s => T.upperCaseNameValuesToMap(s.toUpperCase)))
.fold(
_ =>
sttp.tapir.DecodeResult.Error(
values.mkString(","),
new NoSuchElementException(
s"Could not find all values $values for enum ${enumName}, available values: ${T.values.mkString(", ")}"
)
),
sttp.tapir.DecodeResult.Value(_)
)
)(_.map(_.entryName))
sealed trait ADTWithoutDiscriminator
sealed trait ADTWithDiscriminator
sealed trait ADTWithDiscriminatorNoMapping
@@ -31,6 +63,19 @@ object TapirGeneratedEndpoints {
e: Option[AnEnum] = None,
absent: Option[String] = None
) extends ADTWithoutDiscriminator
case class ObjectWithInlineEnum (
id: java.util.UUID,
inlineEnum: ObjectWithInlineEnumInlineEnum
)

sealed trait ObjectWithInlineEnumInlineEnum extends enumeratum.EnumEntry
object ObjectWithInlineEnumInlineEnum extends enumeratum.Enum[ObjectWithInlineEnumInlineEnum] with enumeratum.CirceEnum[ObjectWithInlineEnumInlineEnum] {
val values = findValues
case object foo1 extends ObjectWithInlineEnumInlineEnum
case object foo2 extends ObjectWithInlineEnumInlineEnum
case object foo3 extends ObjectWithInlineEnumInlineEnum
case object foo4 extends ObjectWithInlineEnumInlineEnum
}
case class SubtypeWithoutD2 (
a: Seq[String],
absent: Option[String] = None
@@ -54,15 +99,44 @@ object TapirGeneratedEndpoints {
.in(("adt" / "test"))
.in(jsonBody[ADTWithoutDiscriminator])
.out(jsonBody[ADTWithoutDiscriminator].description("successful operation"))

lazy val postAdtTest =
endpoint
.post
.in(("adt" / "test"))
.in(jsonBody[ADTWithDiscriminatorNoMapping])
.out(jsonBody[ADTWithDiscriminator].description("successful operation"))


lazy val generatedEndpoints = List(putAdtTest, postAdtTest)

lazy val postInlineEnumTest =
endpoint
.post
.in(("inline" / "enum" / "test"))
.in(query[PostInlineEnumTestQueryEnum]("query-enum").description("An enum, inline, in a query string"))
.in(query[PostInlineEnumTestQuerySeqEnum]("query-seq-enum").description("A sequence of enums, inline, in a query string"))
.in(jsonBody[ObjectWithInlineEnum])
.out(statusCode(sttp.model.StatusCode(204)).description("No Content"))

sealed trait PostInlineEnumTestQueryEnum extends enumeratum.EnumEntry
object PostInlineEnumTestQueryEnum extends enumeratum.Enum[PostInlineEnumTestQueryEnum] with enumeratum.CirceEnum[PostInlineEnumTestQueryEnum] {
val values = findValues
case object bar1 extends PostInlineEnumTestQueryEnum
case object bar2 extends PostInlineEnumTestQueryEnum
case object bar3 extends PostInlineEnumTestQueryEnum
implicit val postInlineEnumTestQueryEnumQueryCodec: sttp.tapir.Codec[List[String], PostInlineEnumTestQueryEnum, sttp.tapir.CodecFormat.TextPlain] =
makeQueryCodecForEnum("PostInlineEnumTestQueryEnum", PostInlineEnumTestQueryEnum)
}

sealed trait PostInlineEnumTestQuerySeqEnum extends enumeratum.EnumEntry
object PostInlineEnumTestQuerySeqEnum extends enumeratum.Enum[PostInlineEnumTestQuerySeqEnum] with enumeratum.CirceEnum[PostInlineEnumTestQuerySeqEnum] {
val values = findValues
case object baz1 extends PostInlineEnumTestQuerySeqEnum
case object baz2 extends PostInlineEnumTestQuerySeqEnum
case object baz3 extends PostInlineEnumTestQuerySeqEnum
implicit val postInlineEnumTestQuerySeqEnumQueryCodec: sttp.tapir.Codec[List[String], PostInlineEnumTestQuerySeqEnum, sttp.tapir.CodecFormat.TextPlain] =
makeQueryCodecForEnum("PostInlineEnumTestQuerySeqEnum", PostInlineEnumTestQuerySeqEnum)
}


lazy val generatedEndpoints = List(putAdtTest, postAdtTest, postInlineEnumTest)

}
Original file line number Diff line number Diff line change
@@ -35,6 +35,8 @@ object TapirGeneratedEndpointsJsonSerdes {
}
implicit lazy val subtypeWithoutD3JsonDecoder: io.circe.Decoder[SubtypeWithoutD3] = io.circe.generic.semiauto.deriveDecoder[SubtypeWithoutD3]
implicit lazy val subtypeWithoutD3JsonEncoder: io.circe.Encoder[SubtypeWithoutD3] = io.circe.generic.semiauto.deriveEncoder[SubtypeWithoutD3]
implicit lazy val objectWithInlineEnumJsonDecoder: io.circe.Decoder[ObjectWithInlineEnum] = io.circe.generic.semiauto.deriveDecoder[ObjectWithInlineEnum]
implicit lazy val objectWithInlineEnumJsonEncoder: io.circe.Encoder[ObjectWithInlineEnum] = io.circe.generic.semiauto.deriveEncoder[ObjectWithInlineEnum]
implicit lazy val subtypeWithoutD2JsonDecoder: io.circe.Decoder[SubtypeWithoutD2] = io.circe.generic.semiauto.deriveDecoder[SubtypeWithoutD2]
implicit lazy val subtypeWithoutD2JsonEncoder: io.circe.Encoder[SubtypeWithoutD2] = io.circe.generic.semiauto.deriveEncoder[SubtypeWithoutD2]
implicit lazy val subtypeWithD2JsonDecoder: io.circe.Decoder[SubtypeWithD2] = io.circe.generic.semiauto.deriveDecoder[SubtypeWithD2]
Original file line number Diff line number Diff line change
@@ -4,6 +4,8 @@ object TapirGeneratedEndpointsSchemas {
import sttp.tapir.generated.TapirGeneratedEndpoints._
import sttp.tapir.generic.auto._
implicit lazy val anEnumTapirSchema: sttp.tapir.Schema[AnEnum] = sttp.tapir.Schema.derived
implicit lazy val objectWithInlineEnumInlineEnumTapirSchema: sttp.tapir.Schema[ObjectWithInlineEnumInlineEnum] = sttp.tapir.Schema.derived
implicit lazy val objectWithInlineEnumTapirSchema: sttp.tapir.Schema[ObjectWithInlineEnum] = sttp.tapir.Schema.derived
implicit lazy val subtypeWithD1TapirSchema: sttp.tapir.Schema[SubtypeWithD1] = sttp.tapir.Schema.derived
implicit lazy val subtypeWithD2TapirSchema: sttp.tapir.Schema[SubtypeWithD2] = sttp.tapir.Schema.derived
implicit lazy val subtypeWithoutD1TapirSchema: sttp.tapir.Schema[SubtypeWithoutD1] = sttp.tapir.Schema.derived
Original file line number Diff line number Diff line change
@@ -5,7 +5,7 @@ info:
description: File for testing json roundtripping of oneOf defns in scala 2.x with circe
version: 1.0.20-SNAPSHOT
title: OneOf Json test for scala 2
tags: []
tags: [ ]
paths:
'/adt/test':
post:
@@ -38,6 +38,40 @@ paths:
application/json:
schema:
$ref: '#/components/schemas/ADTWithoutDiscriminator'
'/inline/enum/test':
post:
parameters:
- name: query-enum
in: query
description: An enum, inline, in a query string
required: true
schema:
type: string
enum:
- bar1
- bar2
- bar3
- name: query-seq-enum
in: query
description: A sequence of enums, inline, in a query string
required: true
schema:
type: string
enum:
- baz1
- baz2
- baz3
default: baz2
responses:
'204':
description: No Content
requestBody:
required: true
description: Check inline enums
content:
application/json:
schema:
$ref: '#/components/schemas/ObjectWithInlineEnum'

components:
schemas:
@@ -135,4 +169,21 @@ components:
enum:
- Foo
- Bar
- Baz
- Baz
ObjectWithInlineEnum:
title: ObjectWithInlineEnum
required:
- id
- inlineEnum
type: object
properties:
id:
type: string
format: uuid
inlineEnum:
type: string
enum:
- foo1
- foo2
- foo3
- foo4