Skip to content

Commit

Permalink
codegen: implement oneOf support (#3624)
Browse files Browse the repository at this point in the history
  • Loading branch information
hughsimpson committed Apr 3, 2024
1 parent ff60ba6 commit 0196f34
Show file tree
Hide file tree
Showing 16 changed files with 823 additions and 164 deletions.
21 changes: 11 additions & 10 deletions doc/generator/sbt-openapi-codegen.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,15 +35,16 @@ defined case-classes and endpoint definitions.
The generator currently supports these settings, you can override them in the `build.sbt`;

```eval_rst
=============================== ==================================== =====================================================================
setting default value description
=============================== ==================================== =====================================================================
openapiSwaggerFile baseDirectory.value / "swagger.yaml" The swagger file with the api definitions.
openapiPackage sttp.tapir.generated The name for the generated package.
openapiObject TapirGeneratedEndpoints The name for the generated object.
openapiUseHeadTagForObjectName false If true, put endpoints in separate files based on first declared tag.
openapiJsonSerdeLib circe The json serde library to use.
=============================== ==================================== =====================================================================
===================================== ==================================== =======================================================================================
setting default value description
===================================== ==================================== =======================================================================================
openapiSwaggerFile baseDirectory.value / "swagger.yaml" The swagger file with the api definitions.
openapiPackage sttp.tapir.generated The name for the generated package.
openapiObject TapirGeneratedEndpoints The name for the generated object.
openapiUseHeadTagForObjectName false If true, put endpoints in separate files based on first declared tag.
openapiJsonSerdeLib circe The json serde library to use.
openapiValidateNonDiscriminatedOneOfs true Whether to fail if variants of a oneOf without a discriminator cannot be disambiguated.
===================================== ==================================== =======================================================================================
```

The general usage is;
Expand Down Expand Up @@ -114,7 +115,7 @@ representation types for the binary data
We currently miss a lot of OpenApi features like:

- tags
- ADTs
- anyOf/allOf
- missing model types and meta descriptions (like date, minLength)
- file handling

Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,15 @@ object GenScala {
private val headTagForNamesOpt: Opts[Boolean] =
Opts.flag("headTagForNames", "Whether to group generated endpoints by first declared tag", "t").orFalse

private val validateNonDiscriminatedOneOfsOpt: Opts[Boolean] =
Opts
.flag(
"validateNonDiscriminatedOneOfs",
"Whether to validate that all variants of oneOfs without discriminators can be disambiguated",
"v"
)
.orFalse

private val jsonLibOpt: Opts[Option[String]] =
Opts.option[String]("jsonLib", "Json library to use for serdes", "j").orNone

Expand All @@ -62,13 +71,21 @@ object GenScala {
}

val cmd: Command[IO[ExitCode]] = Command("genscala", "Generate Scala classes", helpFlag = true) {
(fileOpt, packageNameOpt, destDirOpt, objectNameOpt, targetScala3Opt, headTagForNamesOpt, jsonLibOpt).mapN {
case (file, packageName, destDir, maybeObjectName, targetScala3, headTagForNames, jsonLib) =>
(fileOpt, packageNameOpt, destDirOpt, objectNameOpt, targetScala3Opt, headTagForNamesOpt, jsonLibOpt, validateNonDiscriminatedOneOfsOpt)
.mapN { case (file, packageName, destDir, maybeObjectName, targetScala3, headTagForNames, jsonLib, validateNonDiscriminatedOneOfs) =>
val objectName = maybeObjectName.getOrElse(DefaultObjectName)

def generateCode(doc: OpenapiDocument): IO[Unit] = for {
contents <- IO.pure(
BasicGenerator.generateObjects(doc, packageName, objectName, targetScala3, headTagForNames, jsonLib.getOrElse("circe"))
BasicGenerator.generateObjects(
doc,
packageName,
objectName,
targetScala3,
headTagForNames,
jsonLib.getOrElse("circe"),
validateNonDiscriminatedOneOfs
)
)
destFiles <- contents.toVector.traverse { case (fileName, content) => writeGeneratedFile(destDir, fileName, content) }
_ <- IO.println(s"Generated endpoints written to: ${destFiles.mkString(", ")}")
Expand All @@ -81,7 +98,7 @@ object GenScala {
case Right(doc) => generateCode(doc).as(ExitCode.Success)
}
} yield exitCode
}
}
}

private def readFile(file: File): IO[String] = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,8 @@ object BasicGenerator {
objName: String,
targetScala3: Boolean,
useHeadTagForObjectNames: Boolean,
jsonSerdeLib: String
jsonSerdeLib: String,
validateNonDiscriminatedOneOfs: Boolean
): Map[String, String] = {
val normalisedJsonLib = jsonSerdeLib.toLowerCase match {
case "circe" => JsonSerdeLib.Circe
Expand All @@ -46,12 +47,30 @@ object BasicGenerator {
}

val EndpointDefs(endpointsByTag, queryParamRefs, jsonParamRefs) = endpointGenerator.endpointDefs(doc, useHeadTagForObjectNames)
val GeneratedClassDefinitions(classDefns, extras) =
classGenerator
.classDefs(
doc = doc,
targetScala3 = targetScala3,
queryParamRefs = queryParamRefs,
jsonSerdeLib = normalisedJsonLib,
jsonParamRefs = jsonParamRefs,
fullModelPath = s"$packagePath.$objName",
validateNonDiscriminatedOneOfs = validateNonDiscriminatedOneOfs
)
.getOrElse(GeneratedClassDefinitions("", None))
val isSplit = extras.nonEmpty
val internalImports =
if (isSplit)
s"""import $packagePath.$objName._
|import ${objName}JsonSerdes._""".stripMargin
else s"import $objName._"
val taggedObjs = endpointsByTag.collect {
case (Some(headTag), body) if body.nonEmpty =>
val taggedObj =
s"""package $packagePath
|
|import $objName._
|$internalImports
|
|object $headTag {
|
Expand All @@ -62,6 +81,15 @@ object BasicGenerator {
|}""".stripMargin
headTag -> taggedObj
}
val extraObj = extras.map { body =>
s"""package $packagePath
|
|object ${objName}JsonSerdes {
| import $packagePath.$objName._
|${indent(2)(body)}
|}""".stripMargin
}
val endpointsInMain = endpointsByTag.getOrElse(None, "")

val maybeSpecificationExtensionKeys = doc.paths
.flatMap { p =>
Expand All @@ -80,21 +108,23 @@ object BasicGenerator {
}
.mkString("\n")

val serdeImport = if (isSplit && endpointsInMain.nonEmpty) s"\nimport $packagePath.${objName}JsonSerdes._" else ""
val mainObj = s"""|
|package $packagePath
|
|object $objName {
|
|${indent(2)(imports(normalisedJsonLib))}
|${indent(2)(imports(normalisedJsonLib) + serdeImport)}
|
|${indent(2)(classGenerator.classDefs(doc, targetScala3, queryParamRefs, normalisedJsonLib, jsonParamRefs).getOrElse(""))}
|${indent(2)(classDefns)}
|
|${indent(2)(maybeSpecificationExtensionKeys)}
|
|${indent(2)(endpointsByTag.getOrElse(None, ""))}
|${indent(2)(endpointsInMain)}
|
|}
|""".stripMargin
taggedObjs + (objName -> mainObj)
taggedObjs ++ extraObj.map(s"${objName}JsonSerdes" -> _) + (objName -> mainObj)
}

private[codegen] def imports(jsonSerdeLib: JsonSerdeLib.JsonSerdeLib): String = {
Expand Down

0 comments on commit 0196f34

Please sign in to comment.