diff --git a/build.sbt b/build.sbt index 091db72913..6e96438f96 100644 --- a/build.sbt +++ b/build.sbt @@ -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) diff --git a/doc/generator/sbt-openapi-codegen.md b/doc/generator/sbt-openapi-codegen.md index 4156a2b591..42ba97c918 100644 --- a/doc/generator/sbt-openapi-codegen.md +++ b/doc/generator/sbt-openapi-codegen.md @@ -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 diff --git a/openapi-codegen/core/src/main/scala/sttp/tapir/codegen/ClassDefinitionGenerator.scala b/openapi-codegen/core/src/main/scala/sttp/tapir/codegen/ClassDefinitionGenerator.scala index 59cc8a569d..405d839460 100644 --- a/openapi-codegen/core/src/main/scala/sttp/tapir/codegen/ClassDefinitionGenerator.scala +++ b/openapi-codegen/core/src/main/scala/sttp/tapir/codegen/ClassDefinitionGenerator.scala @@ -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 diff --git a/openapi-codegen/core/src/main/scala/sttp/tapir/codegen/openapi/models/OpenapiSchemaType.scala b/openapi-codegen/core/src/main/scala/sttp/tapir/codegen/openapi/models/OpenapiSchemaType.scala index 2ddd7acf78..c5057ff6c0 100644 --- a/openapi-codegen/core/src/main/scala/sttp/tapir/codegen/openapi/models/OpenapiSchemaType.scala +++ b/openapi-codegen/core/src/main/scala/sttp/tapir/codegen/openapi/models/OpenapiSchemaType.scala @@ -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 @@ -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) => @@ -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 _) } diff --git a/openapi-codegen/core/src/test/scala/sttp/tapir/codegen/ClassDefinitionGeneratorSpec.scala b/openapi-codegen/core/src/test/scala/sttp/tapir/codegen/ClassDefinitionGeneratorSpec.scala index 5e2d88200f..af664bb813 100644 --- a/openapi-codegen/core/src/test/scala/sttp/tapir/codegen/ClassDefinitionGeneratorSpec.scala +++ b/openapi-codegen/core/src/test/scala/sttp/tapir/codegen/ClassDefinitionGeneratorSpec.scala @@ -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) ) ) ) @@ -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) ) ) ) @@ -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) } @@ -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) diff --git a/openapi-codegen/core/src/test/scala/sttp/tapir/codegen/models/ModelParserSpec.scala b/openapi-codegen/core/src/test/scala/sttp/tapir/codegen/models/ModelParserSpec.scala index da796f0c1f..1489ad50bb 100644 --- a/openapi-codegen/core/src/test/scala/sttp/tapir/codegen/models/ModelParserSpec.scala +++ b/openapi-codegen/core/src/test/scala/sttp/tapir/codegen/models/ModelParserSpec.scala @@ -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 @@ -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) ) } }