Skip to content

Commit

Permalink
Fix enum serdes generated from codegen (#3085)
Browse files Browse the repository at this point in the history
  • Loading branch information
hughsimpson authored Aug 10, 2023
1 parent 05f9b2b commit e5bd96c
Show file tree
Hide file tree
Showing 6 changed files with 21 additions and 14 deletions.
3 changes: 2 additions & 1 deletion build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -1927,7 +1927,8 @@ lazy val openapiCodegenCore: ProjectMatrix = (projectMatrix in file("openapi-cod
"com.47deg" %% "scalacheck-toolbox-datetime" % "0.7.0" % Test,
scalaOrganization.value % "scala-reflect" % scalaVersion.value,
scalaOrganization.value % "scala-compiler" % scalaVersion.value % Test,
"com.beachape" %% "enumeratum" % "1.7.3" % Test
"com.beachape" %% "enumeratum" % "1.7.3" % Test,
"com.beachape" %% "enumeratum-circe" % "1.7.3" % Test
)
)
.dependsOn(core % Test, circeJson % Test)
Expand Down
4 changes: 3 additions & 1 deletion doc/generator/sbt-openapi-codegen.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,9 @@ val docs = TapirGeneratedEndpoints.generatedEndpoints.toOpenAPI("My Bookshop", "

Currently, the generated code depends on `"io.circe" %% "circe-generic"`. In the future probably we will make the encoder/decoder json lib configurable (PRs welcome).

String-like enums depend on `"com.beachape" %% "enumeratum"`. Other forms of OpenApi enum are not currently supported.
String-like enums in Scala 2 depend on both `"com.beachape" %% "enumeratum"` and `"com.beachape" %% "enumeratum-circe"`.
For Scala 3 we derive native enums, and depend instead on `"org.latestbit" %% "circe-tagged-adt-codec"`.
Other forms of OpenApi enum are not currently supported.

We currently miss a lot of OpenApi features like:
- tags
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,13 +37,13 @@ class ClassDefinitionGenerator {

// Uses enumeratum for scala 2, but generates scala 3 enums instead where it can
private[codegen] def generateEnum(name: String, obj: OpenapiSchemaEnum, targetScala3: Boolean): Seq[String] = if (targetScala3) {
s"""enum $name {
s"""enum $name derives org.latestbit.circe.adt.codec.JsonTaggedAdt.PureCodec {
| case ${obj.items.map(_.value).mkString(", ")}
|}""".stripMargin :: Nil
} else {
val members = obj.items.map { i => s"case object ${i.value} extends $name" }
s"""|sealed trait $name extends EnumEntry
|object $name extends Enum[$name] {
|object $name extends Enum[$name] with CirceEnum[$name] {
| val values = findValues
|${indent(2)(members.mkString("\n"))}
|}""".stripMargin :: Nil
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -89,8 +89,9 @@ object OpenapiSchemaType {
val nullable = false
}

// Can't support non-string enum types although that should apparently be legal (see https://json-schema.org/draft/2020-12/json-schema-validation.html#enum)
// Can't currently support non-string enum types although that should apparently be legal (see https://json-schema.org/draft/2020-12/json-schema-validation.html#enum)
case class OpenapiSchemaEnum(
`type`: String,
items: Seq[OpenapiSchemaConstantString],
nullable: Boolean
) extends OpenapiSchemaType
Expand Down Expand Up @@ -241,9 +242,11 @@ object OpenapiSchemaType {

implicit val OpenapiSchemaEnumDecoder: Decoder[OpenapiSchemaEnum] = { (c: HCursor) =>
for {
tpe <- c.downField("type").as[String]
_ <- Either.cond(tpe == "string", (), DecodingFailure("only string enums are supported", c.history))
items <- c.downField("enum").as[Seq[OpenapiSchemaConstantString]]
nb <- c.downField("nullable").as[Option[Boolean]]
} yield OpenapiSchemaEnum(items, nb.getOrElse(false))
} yield OpenapiSchemaEnum(tpe, items, nb.getOrElse(false))
}

implicit val OpenapiSchemaObjectDecoder: Decoder[OpenapiSchemaObject] = { (c: HCursor) =>
Expand Down Expand Up @@ -282,12 +285,12 @@ object OpenapiSchemaType {

implicit lazy val OpenapiSchemaTypeDecoder: Decoder[OpenapiSchemaType] =
List[Decoder[OpenapiSchemaType]](
Decoder[OpenapiSchemaEnum].widen,
Decoder[OpenapiSchemaSimpleType].widen,
Decoder[OpenapiSchemaMixedType].widen,
Decoder[OpenapiSchemaNot].widen,
Decoder[OpenapiSchemaObject].widen,
Decoder[OpenapiSchemaMap].widen,
Decoder[OpenapiSchemaArray].widen,
Decoder[OpenapiSchemaEnum].widen
Decoder[OpenapiSchemaArray].widen
).reduceLeft(_ or _)
}
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ class ClassDefinitionGeneratorSpec extends CompileCheckTestBase {
Some(
OpenapiComponent(
Map(
"Test" -> OpenapiSchemaEnum(Seq(OpenapiSchemaConstantString("paperback"), OpenapiSchemaConstantString("hardback")), false)
"Test" -> OpenapiSchemaEnum("string", Seq(OpenapiSchemaConstantString("paperback"), OpenapiSchemaConstantString("hardback")), false)
)
)
)
Expand Down Expand Up @@ -250,7 +250,7 @@ class ClassDefinitionGeneratorSpec extends CompileCheckTestBase {
Some(
OpenapiComponent(
Map(
"Test" -> OpenapiSchemaEnum(Seq(OpenapiSchemaConstantString("enum1"), OpenapiSchemaConstantString("enum2")), false)
"Test" -> OpenapiSchemaEnum("string", Seq(OpenapiSchemaConstantString("enum1"), OpenapiSchemaConstantString("enum2")), false)
)
)
)
Expand All @@ -259,7 +259,8 @@ class ClassDefinitionGeneratorSpec extends CompileCheckTestBase {
val gen = new ClassDefinitionGenerator()
val res = gen.classDefs(doc, true)
// can't just check whether this compiles, because our tests only run on scala 2.12 - so instead just eyeball it...
res shouldBe Some("""enum Test {
res shouldBe Some(
"""enum Test derives org.latestbit.circe.adt.codec.JsonTaggedAdt.PureCodec {
| case enum1, enum2
|}""".stripMargin)
}
Expand All @@ -273,7 +274,7 @@ class ClassDefinitionGeneratorSpec extends CompileCheckTestBase {
OpenapiComponent(
Map(
"MyObject" -> OpenapiSchemaObject(Map("text" -> OpenapiSchemaString(true)), Seq("text"), false),
"MyEnum" -> OpenapiSchemaEnum(Seq(OpenapiSchemaConstantString("enum1"), OpenapiSchemaConstantString("enum2")), false),
"MyEnum" -> OpenapiSchemaEnum("string", Seq(OpenapiSchemaConstantString("enum1"), OpenapiSchemaConstantString("enum2")), false),
"MyMapPrimitive" -> OpenapiSchemaMap(OpenapiSchemaString(false), false),
"MyMapObject" -> OpenapiSchemaMap(OpenapiSchemaRef("#/components/schemas/MyObject"), false),
"MyMapEnum" -> OpenapiSchemaMap(OpenapiSchemaRef("#/components/schemas/MyEnum"), false)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,7 @@ class ModelParserSpec extends AnyFlatSpec with Matchers with Checkers {

it should "parse enums" in {
val yaml =
"""
"""type: string
|enum:
|- paperback
|- hardback""".stripMargin
Expand All @@ -164,7 +164,7 @@ class ModelParserSpec extends AnyFlatSpec with Matchers with Checkers {
.flatMap(_.as[OpenapiSchemaType])

res shouldBe Right(
OpenapiSchemaEnum(Seq(OpenapiSchemaConstantString("paperback"), OpenapiSchemaConstantString("hardback")), false)
OpenapiSchemaEnum("string", Seq(OpenapiSchemaConstantString("paperback"), OpenapiSchemaConstantString("hardback")), false)
)
}
}

0 comments on commit e5bd96c

Please sign in to comment.