Skip to content

Commit

Permalink
[gen] sum types (enums) are now generated with cases in companion tha…
Browse files Browse the repository at this point in the history
…t actually extend the sealed trait.

      Sum types abstract members generation by reusable components in the schema, as an opt-in, configurable feature.
      Only generating sealed trait body if all subtypes include same reusable component, with it's fields as the abstract trait's members.
      Redundant duplicate classes for each case is now omitted.
      Validate fields in case classes and traits does not contain duplicates that cannot be reconciled.
      Existing tests has been amended to reflect the fix.
      New tests were added.
      Some utilities were added.
  • Loading branch information
hochgi committed May 23, 2024
1 parent 53e0f9f commit ef0d570
Show file tree
Hide file tree
Showing 19 changed files with 946 additions and 57 deletions.
1 change: 1 addition & 0 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -293,6 +293,7 @@ lazy val zioHttpGen = (project in file("zio-http-gen"))
`zio-test-sbt`,
scalafmt.cross(CrossVersion.for3Use2_13),
scalametaParsers.cross(CrossVersion.for3Use2_13).exclude("org.scala-lang.modules", "scala-collection-compat_2.13"),
`zio-json-yaml` % Test
),
)
.settings(
Expand Down
2 changes: 2 additions & 0 deletions project/Dependencies.scala
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ object Dependencies {
val ScalaCompactCollectionVersion = "2.12.0"
val ZioVersion = "2.1.1"
val ZioCliVersion = "0.5.0"
val ZioJsonVersion = "0.6.2"
val ZioSchemaVersion = "1.1.1"
val SttpVersion = "3.3.18"
val ZioConfigVersion = "4.0.2"
Expand Down Expand Up @@ -34,6 +35,7 @@ object Dependencies {

val zio = "dev.zio" %% "zio" % ZioVersion
val `zio-cli` = "dev.zio" %% "zio-cli" % ZioCliVersion
val `zio-json-yaml` = "dev.zio" %% "zio-json-yaml" % ZioCliVersion
val `zio-streams` = "dev.zio" %% "zio-streams" % ZioVersion
val `zio-schema` = "dev.zio" %% "zio-schema" % ZioSchemaVersion
val `zio-schema-json` = "dev.zio" %% "zio-schema-json" % ZioSchemaVersion
Expand Down
16 changes: 16 additions & 0 deletions zio-http-gen/src/main/scala/zio/http/gen/openapi/Config.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package zio.http.gen.openapi

import zio.{Config => zc}

final case class Config(commonFieldsOnSuperType: Boolean)
object Config {

val default: Config = Config(
commonFieldsOnSuperType = false,
)

lazy val config: zio.Config[Config] =
zc.boolean("common-fields-on-super-type")
.withDefault(Config.default.commonFieldsOnSuperType)
.map(Config.apply)
}
240 changes: 200 additions & 40 deletions zio-http-gen/src/main/scala/zio/http/gen/openapi/EndpointGen.scala

Large diffs are not rendered by default.

6 changes: 4 additions & 2 deletions zio-http-gen/src/main/scala/zio/http/gen/scala/Code.scala
Original file line number Diff line number Diff line change
Expand Up @@ -62,10 +62,11 @@ object Code {
Object(name, schema = false, endpoints, Nil, Nil, Nil)
}

final case class CaseClass(name: String, fields: List[Field], companionObject: Option[Object]) extends ScalaType
final case class CaseClass(name: String, fields: List[Field], companionObject: Option[Object], mixins: List[String])
extends ScalaType

object CaseClass {
def apply(name: String): CaseClass = CaseClass(name, Nil, None)
def apply(name: String, mixins: List[String]): CaseClass = CaseClass(name, Nil, None, mixins)
}

final case class Enum(
Expand All @@ -75,6 +76,7 @@ object Code {
discriminator: Option[String] = None,
noDiscriminator: Boolean = false,
schema: Boolean = true,
abstractMembers: List[Field] = Nil,
) extends ScalaType

sealed abstract case class Field private (name: String, fieldType: ScalaType) extends Code {
Expand Down
39 changes: 34 additions & 5 deletions zio-http-gen/src/main/scala/zio/http/gen/scala/CodeGen.scala
Original file line number Diff line number Diff line change
Expand Up @@ -86,20 +86,24 @@ object CodeGen {
"\n}"
Nil -> content

case Code.CaseClass(name, fields, companionObject) =>
case Code.CaseClass(name, fields, companionObject, mixins) =>
val (imports, contents) = fields.map(render(basePackage)).unzip
val (coImports, coContent) =
companionObject.map { co =>
val (coImports, coContent) = render(basePackage)(co)
(coImports, s"\n$coContent")
}.getOrElse(Nil -> "")
val mixinsString = mixins match {
case Nil => ""
case _ => mixins.mkString(" extends ", " with ", "")
}
val content =
s"case class $name(\n" +
contents.mkString(",\n").replace("val ", " ") +
"\n)" + coContent
"\n)" + mixinsString + coContent
(imports.flatten ++ coImports).distinct -> content

case Code.Enum(name, cases, caseNames, discriminator, noDiscriminator, schema) =>
case Code.Enum(name, cases, caseNames, discriminator, noDiscriminator, schema, abstractMembers) =>
val discriminatorAnnotation =
if (noDiscriminator) "@noDiscriminator\n" else ""
val discriminatorNameAnnotation =
Expand All @@ -118,15 +122,40 @@ object CodeGen {
imports -> contents.mkString("\n")
}

val (traitBodyImports, traitBody) = {
val traitBodyBuilder = new StringBuilder().append(' ')
var pre = '{'
val imports = abstractMembers.foldLeft(List.empty[Code.Import]) {
case (importsAcc, Code.Field(name, fieldType)) =>
val (imports, tpe) = render(basePackage)(fieldType)
if (tpe.isEmpty) importsAcc
else {
traitBodyBuilder += pre
pre = '\n'
traitBodyBuilder ++= "def "
traitBodyBuilder ++= name
traitBodyBuilder ++= ": "
traitBodyBuilder ++= tpe

imports ::: importsAcc
}
}
val body =
if (pre == '{') "\n"
else traitBodyBuilder.append("\n}\n").result()

imports -> body
}

val content =
discriminatorAnnotation +
discriminatorNameAnnotation +
s"sealed trait $name\n" +
s"sealed trait $name" + traitBody +
s"object $name {\n" +
(if (schema) s"\n\n implicit val codec: Schema[$name] = DeriveSchema.gen[$name]\n" else "") +
casesContent +
"\n}"
casesImports.flatten.distinct -> content
casesImports.foldRight(traitBodyImports)(_ ::: _).distinct -> content

case col: Code.Collection =>
col match {
Expand Down
31 changes: 31 additions & 0 deletions zio-http-gen/src/test/resources/ComponentAnimal.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package test.component

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

@noDiscriminator
sealed trait Animal
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,34 @@
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]

}
}
12 changes: 12 additions & 0 deletions zio-http-gen/src/test/resources/ComponentHttpError.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package test.component

import zio.schema._

case class HttpError(
messages: Option[String],
)
object HttpError {

implicit val codec: Schema[HttpError] = DeriveSchema.gen[HttpError]

}
15 changes: 15 additions & 0 deletions zio-http-gen/src/test/resources/EndpointForZoo.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package test.api.v1.zoo

import test.component._
import zio.Chunk

object Animal {
import zio.http._
import zio.http.endpoint._
import zio.http.codec._
val get_animal = Endpoint(Method.GET / "api" / "v1" / "zoo" / string("animal"))
.in[Unit]
.out[Chunk[Animal]](status = Status.Ok)
.outError[HttpError](status = Status.InternalServerError)

}
14 changes: 14 additions & 0 deletions zio-http-gen/src/test/resources/EndpointForZooNoError.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package test.api.v1.zoo

import test.component._
import zio.Chunk

object Animal {
import zio.http._
import zio.http.endpoint._
import zio.http.codec._
val get_animal = Endpoint(Method.GET / "api" / "v1" / "zoo" / string("animal"))
.in[Unit]
.out[Chunk[Animal]](status = Status.Ok)

}
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ object PaymentNamedDiscriminator {
case class Card(
number: String,
cvv: String,
)
) extends PaymentNamedDiscriminator
object Card {

implicit val codec: Schema[Card] = DeriveSchema.gen[Card]
Expand All @@ -21,7 +21,7 @@ object PaymentNamedDiscriminator {
@caseName("cash")
case class Cash(
amount: Int,
)
) extends PaymentNamedDiscriminator
object Cash {

implicit val codec: Schema[Cash] = DeriveSchema.gen[Cash]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,15 @@ object PaymentNoDiscriminator {
case class Card(
number: String,
cvv: String,
)
) extends PaymentNoDiscriminator
object Card {

implicit val codec: Schema[Card] = DeriveSchema.gen[Card]

}
case class Cash(
amount: Int,
)
) extends PaymentNoDiscriminator
object Cash {

implicit val codec: Schema[Cash] = DeriveSchema.gen[Cash]
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
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
openapi: 3.0.3
components:
schemas:
Animal:
oneOf:
- $ref: '#/components/schemas/Alligator'
- $ref: '#/components/schemas/Zebra'
HasAgeAndWeight:
type: object
required:
- age
properties:
age:
type: integer
format: int32
minimum: 0
weight:
type: number
format: float
minimum: 0
HasWeight:
type: object
required:
- weight
properties:
weight:
type: number
format: double
minimum: 0
Alligator:
allOf:
- $ref: '#/components/schemas/HasAgeAndWeight'
- $ref: '#/components/schemas/HasWeight'
- type: object
required:
- num_teeth
properties:
num_teeth:
type: integer
format: int32
minimum: 0
Zebra:
allOf:
- $ref: '#/components/schemas/HasAgeAndWeight'
- $ref: '#/components/schemas/HasWeight'
- type: object
required:
- num_stripes
properties:
num_stripes:
type: integer
format: int32
minimum: 0
Loading

0 comments on commit ef0d570

Please sign in to comment.