Skip to content

Commit

Permalink
[gen] map common abstract fields in trait (oneOf enum) to have (un)al…
Browse files Browse the repository at this point in the history
…iasing translation of types (#3089)
  • Loading branch information
hochgi authored Sep 5, 2024
1 parent 200e816 commit 1d092b9
Show file tree
Hide file tree
Showing 6 changed files with 255 additions and 21 deletions.
53 changes: 32 additions & 21 deletions zio-http-gen/src/main/scala/zio/http/gen/openapi/EndpointGen.scala
Original file line number Diff line number Diff line change
Expand Up @@ -191,7 +191,11 @@ final case class EndpointGen(config: Config) {
cf.copy(
objects = cf.objects.map(mapType(_.name, subtypeToTraits, aliasedPrimitives)),
caseClasses = cf.caseClasses.map(mapType(_.name, subtypeToTraits, aliasedPrimitives)),
enums = cf.enums.map(mapType(_.name, subtypeToTraits, aliasedPrimitives)),
enums = cf.enums.map(enm =>
mapType[Code.Enum](_.name, subtypeToTraits, aliasedPrimitives)(enm).copy(
abstractMembers = enm.abstractMembers.map(mapField(subtypeToTraits, aliasedPrimitives, enm.name)),
),
),
)
}
}
Expand Down Expand Up @@ -227,29 +231,36 @@ final case class EndpointGen(config: Config) {
codeStructureToAlter: T,
): T =
mapCaseClasses { cc =>
cc.copy(fields = cc.fields.foldRight(List.empty[Code.Field]) { case (f @ Code.Field(_, scalaType, _), tail) =>
f.copy(fieldType = mapTypeRef(scalaType) { case originalType @ Code.TypeRef(tName) =>
// We use the subtypeToTraits map to check if the type is a concrete subtype of a sealed trait.
// As of the time of writing this code, there should be only a single trait.
// In case future code generalizes to allow multiple mixins, this code should be updated.
//
// If no mixins are found, we try to check maybe we deal with an aliased primitive,
// which in this case we should use the provided alias (with ".Type" appended).
//
// If no alias, and no mixins, we return the original type.
subtypeToTraits
.get(tName)
.fold(aliasedPrimitives.getOrElse(tName, originalType)) { set =>
// If the type parameter has exactly 1 super type trait,
// and that trait's name is different from our enclosing object's name,
// then we should alter the type to include the object's name.
if (set.size != 1 || set.head == getEncapsulatingName(codeStructureToAlter)) originalType
else Code.TypeRef(set.head + "." + tName)
}
}) :: tail
cc.copy(fields = cc.fields.foldRight(List.empty[Code.Field]) { case (field, tail) =>
mapField(subtypeToTraits, aliasedPrimitives, getEncapsulatingName(codeStructureToAlter))(field) :: tail
})
}(codeStructureToAlter)

def mapField(
subtypeToTraits: Map[String, Set[String]],
aliasedPrimitives: Map[String, ScalaType],
encapsulatingName: => String,
): Code.Field => Code.Field = (f: Code.Field) =>
f.copy(fieldType = mapTypeRef(f.fieldType) { case originalType @ Code.TypeRef(tName) =>
// We use the subtypeToTraits map to check if the type is a concrete subtype of a sealed trait.
// As of the time of writing this code, there should be only a single trait.
// In case future code generalizes to allow multiple mixins, this code should be updated.
//
// If no mixins are found, we try to check maybe we deal with an aliased primitive,
// which in this case we should use the provided alias (with ".Type" appended).
//
// If no alias, and no mixins, we return the original type.
subtypeToTraits
.get(tName)
.fold(aliasedPrimitives.getOrElse(tName, originalType)) { set =>
// If the type parameter has exactly 1 super type trait,
// and that trait's name is different from our enclosing object's name,
// then we should alter the type to include the object's name.
if (set.size != 1 || set.head == encapsulatingName) originalType
else Code.TypeRef(set.head + "." + tName)
}
})

/**
* Given the type parameter of a field, we may want to alter it, e.g. by
* prepending the enclosing trait/object's name. This function will
Expand Down
8 changes: 8 additions & 0 deletions zio-http-gen/src/test/resources/ComponentAliasWeight.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package test.component

import zio.prelude.Newtype
import zio.schema.Schema

object Weight extends Newtype[Float] {
implicit val schema: Schema[Weight.Type] = Schema.primitive[Float].transform(wrap, unwrap)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package test.component

import zio.schema._
import zio.schema.annotation._

@noDiscriminator
sealed trait Animal {
def age: Age.Type
def weight: Weight.Type
}
object Animal {

implicit val codec: Schema[Animal] = DeriveSchema.gen[Animal]
case class Alligator(
age: Age.Type,
weight: Weight.Type,
num_teeth: Int,
) extends Animal
object Alligator {
implicit val codec: Schema[Alligator] = DeriveSchema.gen[Alligator]
}
case class Zebra(
age: Age.Type,
weight: Weight.Type,
num_stripes: Int,
) extends Animal
object Zebra {
implicit val codec: Schema[Zebra] = DeriveSchema.gen[Zebra]
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package test.component

import zio.schema._
import zio.schema.annotation._

@noDiscriminator
sealed trait Animal {
def age: Int
def weight: Float
}
object Animal {

implicit val codec: Schema[Animal] = DeriveSchema.gen[Animal]
case class Alligator(
age: Int,
weight: Float,
num_teeth: Int,
) extends Animal
object Alligator {
implicit val codec: Schema[Alligator] = DeriveSchema.gen[Alligator]
}
case class Zebra(
age: Int,
weight: Float,
num_stripes: Int,
) extends Animal
object Zebra {
implicit val codec: Schema[Zebra] = DeriveSchema.gen[Zebra]
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
info:
title: Animals Service
version: 0.0.1
tags:
- name: Animals_API
paths:
/api/v1/zoo/{animal}:
get:
operationId: get_animal
parameters:
- in: path
name: animal
schema:
type: string
required: true
tags:
- Animals_API
description: Get animals by species name
responses:
"200":
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/Animal'
description: OK
"500":
content:
application/json:
schema:
$ref: '#/components/schemas/HttpError'
description: Internal Server Error
openapi: 3.0.3
components:
schemas:
Age:
type: integer
format: int32
Weight:
type: number
format: float
Animal:
oneOf:
- $ref: '#/components/schemas/Alligator'
- $ref: '#/components/schemas/Zebra'
AnimalSharedFields:
type: object
required:
- age
- weight
properties:
age:
$ref: '#/components/schemas/Age'
weight:
$ref: '#/components/schemas/Weight'
Alligator:
allOf:
- $ref: '#/components/schemas/AnimalSharedFields'
- type: object
required:
- num_teeth
properties:
num_teeth:
type: integer
format: int32
Zebra:
allOf:
- $ref: '#/components/schemas/AnimalSharedFields'
- type: object
required:
- num_stripes
properties:
num_stripes:
type: integer
format: int32
HttpError:
type: object
properties:
messages:
type: string
74 changes: 74 additions & 0 deletions zio-http-gen/src/test/scala/zio/http/gen/scala/CodeGenSpec.scala
Original file line number Diff line number Diff line change
Expand Up @@ -332,6 +332,80 @@ object CodeGenSpec extends ZIOSpecDefault {
}
}
} @@ TestAspect.exceptScala3, // for some reason, the temp dir is empty in Scala 3
test("OpenAPI spec with inline schema response body of sum-type with reusable aliased fields") {
val openAPIString = stringFromResource("/inline_schema_sumtype_with_reusable_aliased_fields.yaml")

openApiFromYamlString(openAPIString) { oapi =>
codeGenFromOpenAPI(
oapi,
Config.default.copy(commonFieldsOnSuperType = true, generateSafeTypeAliases = true),
) { testDir =>
allFilesShouldBe(
testDir.toFile,
List(
"api/v1/zoo/Animal.scala",
"component/Animal.scala",
"component/AnimalSharedFields.scala",
"component/Age.scala",
"component/Weight.scala",
"component/HttpError.scala",
),
) && fileShouldBe(
testDir,
"api/v1/zoo/Animal.scala",
"/EndpointForZoo.scala",
) && fileShouldBe(
testDir,
"component/Age.scala",
"/ComponentAliasAge.scala",
) && fileShouldBe(
testDir,
"component/Weight.scala",
"/ComponentAliasWeight.scala",
) && fileShouldBe(
testDir,
"component/Animal.scala",
"/ComponentAnimalWithAbstractAliasedMembers.scala",
) && fileShouldBe(
testDir,
"component/HttpError.scala",
"/ComponentHttpError.scala",
)
}
}
} @@ TestAspect.exceptScala3, // for some reason, the temp dir is empty in Scala 3
test("OpenAPI spec with inline schema response body of sum-type with reusable un-aliased fields") {
val openAPIString = stringFromResource("/inline_schema_sumtype_with_reusable_aliased_fields.yaml")

openApiFromYamlString(openAPIString) { oapi =>
codeGenFromOpenAPI(
oapi,
Config.default.copy(commonFieldsOnSuperType = true, generateSafeTypeAliases = false),
) { testDir =>
allFilesShouldBe(
testDir.toFile,
List(
"api/v1/zoo/Animal.scala",
"component/Animal.scala",
"component/AnimalSharedFields.scala",
"component/HttpError.scala",
),
) && fileShouldBe(
testDir,
"api/v1/zoo/Animal.scala",
"/EndpointForZoo.scala",
) && fileShouldBe(
testDir,
"component/Animal.scala",
"/ComponentAnimalWithAbstractUnAliasedMembers.scala",
) && fileShouldBe(
testDir,
"component/HttpError.scala",
"/ComponentHttpError.scala",
)
}
}
} @@ TestAspect.exceptScala3, // for some reason, the temp dir is empty in Scala 3
test("OpenAPI spec with inline schema response body of sum-type with multiple reusable fields") {
val openAPIString = stringFromResource("/inline_schema_sumtype_with_multiple_reusable_fields.yaml")

Expand Down

0 comments on commit 1d092b9

Please sign in to comment.