Skip to content

Commit 3f9c027

Browse files
authored
codegen: create a file per model (#5365)
Should address #5287 Relevant changes are clearer after #5366 which commits some incidental refactoring Adds a new `openapiSeperateFilesForModels` flag with default `false` so as not to needlessly break user code (my downstream refactor after enabling this was mostly just find+replace, but still chonky and with edge-cases) The `nifi_test` now uses this new flag; it's a pretty large spec, so this provides good coverage, whilst also reducing the number of lines in our expectations (this is the sole reason for the diff having more deleted lines than added). Writing the output files gets wrapped in a `Future.traverse` now because writing >1000 model files can be slower than it needs to be, otherwise. Type aliases are now sorted alphabetically in output. Inline enums now get written immediately after the 'container' object, rather than at the start of the file. This wasn't a particularly intentional change, but it aligns with the fact that, when 'splitting' to separate files, inline defns will live with the parent. The ordering change should have no impact on user code (although it did cause a bit of churn in test expectations...) Incidentally fixes an issue with 'deduplicated' file structure when using zio or circe serdes and reusing a json-y Map[String T] or Array[T] type alias
1 parent 4681cdb commit 3f9c027

40 files changed

Lines changed: 945 additions & 4605 deletions

doc/generator/sbt-openapi-codegen.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ openapiPackageDependencies Map.empty[String, String] Allow
5656
repeated schema declarations between the openapis, with the generated code for the 'key' package defining type (and sometimes val)
5757
aliases to the duplicates in the 'value' package. This is still experimental - significantly, the type is likely to change
5858
to a Map[String, Seq[String]] in the near future to permit multiple 'inheritance', and there may be bugs in the implementation.
59+
openapiSeperateFilesForModels false When true, models will be written to individual files under $pkg.models, with type aliases and helpers living under `package.scala` in a package object
5960
===================================== ==================================== ==================================================================================================
6061
```
6162

openapi-codegen/core/src/main/scala/sttp/tapir/codegen/ClassDefinitionGenerator.scala

Lines changed: 135 additions & 107 deletions
Large diffs are not rendered by default.

openapi-codegen/core/src/main/scala/sttp/tapir/codegen/EndpointGenerator.scala

Lines changed: 29 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package sttp.tapir.codegen
22
import io.circe.Json
3-
import sttp.tapir.codegen.RootGenerator.{indent, mapSchemaSimpleTypeToType, strippedToCamelCase}
3+
import sttp.tapir.codegen.RootGenerator.{mapSchemaSimpleTypeToType, strippedToCamelCase}
4+
import sttp.tapir.codegen.dedup.PackageReuseContext
45
import sttp.tapir.codegen.json.JsonSerdeLib.JsonSerdeLib
56
import sttp.tapir.codegen.XmlSerdeLib.XmlSerdeLib
67
import sttp.tapir.codegen.openapi.models.OpenapiModels.{
@@ -12,31 +13,12 @@ import sttp.tapir.codegen.openapi.models.OpenapiModels.{
1213
OpenapiResponseContent,
1314
OpenapiResponseDef
1415
}
15-
import sttp.tapir.codegen.openapi.models.OpenapiSchemaType.{
16-
AnyType,
17-
OpenapiSchemaAny,
18-
OpenapiSchemaArray,
19-
OpenapiSchemaBinary,
20-
OpenapiSchemaEnum,
21-
OpenapiSchemaMap,
22-
OpenapiSchemaObject,
23-
OpenapiSchemaOneOf,
24-
OpenapiSchemaRef,
25-
OpenapiSchemaSimpleType,
26-
OpenapiSchemaString
27-
}
16+
import sttp.tapir.codegen.openapi.models.OpenapiSchemaType._
2817
import sttp.tapir.codegen.openapi.models._
29-
import sttp.tapir.codegen.openapi.models.GenerationDirectives.{
30-
forceEager,
31-
forceReqEager,
32-
forceReqStreaming,
33-
forceRespEager,
34-
forceRespStreaming,
35-
forceStreaming,
36-
jsonBodyAsString,
37-
securityPrefixKey
38-
}
18+
import sttp.tapir.codegen.openapi.models.GenerationDirectives._
19+
import sttp.tapir.codegen.security.{SecurityDefn, SecurityGenerator, SecurityWrapperDefn}
3920
import sttp.tapir.codegen.util.ErrUtils.bail
21+
import sttp.tapir.codegen.util.NameHelpers.indent
4022
import sttp.tapir.codegen.util.{JavaEscape, Location, NameHelpers}
4123

4224
case class EndpointTypes(security: Seq[String], in: Seq[String], err: Seq[String], out: Seq[String]) {
@@ -140,7 +122,8 @@ class EndpointGenerator {
140122
generateEndpointTypes: Boolean,
141123
validators: ValidationDefns,
142124
generateValidators: Boolean,
143-
packageReuse: PackageReuseContext
125+
packageReuse: PackageReuseContext,
126+
seperateFilesForModels: Boolean
144127
): EndpointDefs = {
145128
val capabilities = capabilityImpl(streamingImplementation)
146129
val components = Option(doc.components).flatten
@@ -162,7 +145,8 @@ class EndpointGenerator {
162145
doc,
163146
validators,
164147
generateValidators,
165-
packageReuse
148+
packageReuse,
149+
seperateFilesForModels
166150
)
167151
)
168152
.foldLeft(GeneratedEndpoints(Nil, Set.empty, false, EndpointDetails.empty))(_ merge _)
@@ -203,7 +187,8 @@ class EndpointGenerator {
203187
doc: OpenapiDocument,
204188
validators: ValidationDefns,
205189
generateValidators: Boolean,
206-
packageReuse: PackageReuseContext
190+
packageReuse: PackageReuseContext,
191+
seperateFilesForModels: Boolean
207192
)(p: OpenapiPath): GeneratedEndpoints = {
208193
val parameters = components.map(_.parameters).getOrElse(Map.empty)
209194
val securitySchemes = components.map(_.securitySchemes).getOrElse(Map.empty)
@@ -255,7 +240,8 @@ class EndpointGenerator {
255240
validators,
256241
generateValidators,
257242
isReused,
258-
packageReuse
243+
packageReuse,
244+
seperateFilesForModels
259245
)
260246
val (outDecl, outTypes, errTypes, inlineDefns) =
261247
outs(
@@ -270,7 +256,8 @@ class EndpointGenerator {
270256
validators,
271257
generateValidators,
272258
isReused,
273-
packageReuse
259+
packageReuse,
260+
seperateFilesForModels
274261
)
275262
val allTypes = EndpointTypes(
276263
maybeSecurityPath.toSeq.flatMap(_._2) ++ securityTypes.toSeq,
@@ -504,8 +491,8 @@ class EndpointGenerator {
504491
case x => bail(s"Can't create non-simple params - found $x")
505492
}
506493

507-
private def aliases(packageReuse: PackageReuseContext, types: Seq[String]): String =
508-
types.map(s => s"type $s = ${packageReuse.dependencyModelPath}.$s").mkString("\n")
494+
private def aliases(packageReuse: PackageReuseContext, types: Seq[String], seperateFilesForModels: Boolean): String =
495+
types.map(PackageReuseContext.enumAliasType(_, packageReuse, seperateFilesForModels)).mkString("\n")
509496

510497
private def ins(
511498
parameters: Seq[OpenapiParameter],
@@ -520,7 +507,8 @@ class EndpointGenerator {
520507
validators: ValidationDefns,
521508
generateValidators: Boolean,
522509
isReused: Boolean,
523-
packageReuse: PackageReuseContext
510+
packageReuse: PackageReuseContext,
511+
seperateFilesForModels: Boolean
524512
)(implicit location: Location): (String, Option[String], Seq[String], Option[String]) = {
525513

526514
// .in(query[Limit]("limit").description("Maximum number of books to retrieve"))
@@ -557,7 +545,7 @@ class EndpointGenerator {
557545
validators
558546
)
559547

560-
val inlineDefn = maybeInlineDefn.map(d => if (isReused) aliases(packageReuse, inlineTypes) else d)
548+
val inlineDefn = maybeInlineDefn.map(d => if (isReused) aliases(packageReuse, inlineTypes, seperateFilesForModels) else d)
561549
(decl, tpe, inlineDefn)
562550
}
563551
val (rqBody, maybeReqType, maybeInlineDefns) = requestBody.flatMap { b =>
@@ -685,7 +673,8 @@ class EndpointGenerator {
685673
validators: ValidationDefns,
686674
generateValidators: Boolean,
687675
isReused: Boolean,
688-
packageReuse: PackageReuseContext
676+
packageReuse: PackageReuseContext,
677+
seperateFilesForModels: Boolean
689678
)(implicit
690679
location: Location
691680
) = {
@@ -721,7 +710,7 @@ class EndpointGenerator {
721710
validators
722711
)
723712
}
724-
val inlineDefn = maybeInlineDefn.map(d => if (isReused) aliases(packageReuse, inlineTypes) else d)
713+
val inlineDefn = maybeInlineDefn.map(d => if (isReused) aliases(packageReuse, inlineTypes, seperateFilesForModels) else d)
725714
(decl, tpe, inlineDefn)
726715
}
727716
resp.content match {
@@ -758,17 +747,18 @@ class EndpointGenerator {
758747
.groupBy(_._1)
759748
val aliasDefns =
760749
if (needsAliases && isReused) {
750+
val parentModelPath = packageReuse.modelRoot(seperateFilesForModels)
761751
val wrappers = declsByWrapperClassName
762752
.map { case (name, seq) =>
763-
s"type $name = ${packageReuse.dependencyModelPath}.$name\nval $name = ${packageReuse.dependencyModelPath}.$name\n"
753+
s"type $name = $parentModelPath.$name\nval $name = $parentModelPath.$name\n"
764754
}
765755
.toSeq
766756
.sorted
767757
.mkString("\n")
768758
Some(s"""
769-
|type $traitName = ${packageReuse.dependencyModelPath}.$traitName
770-
|type ${traitName}Full = ${packageReuse.dependencyModelPath}.${traitName}Full
771-
|val ${traitName}Full = ${packageReuse.dependencyModelPath}.${traitName}Full
759+
|type $traitName = $parentModelPath.$traitName
760+
|type ${traitName}Full = $parentModelPath.${traitName}Full
761+
|val ${traitName}Full = $parentModelPath.${traitName}Full
772762
|$wrappers
773763
|""".stripMargin)
774764
} else if (needsAliases) {

openapi-codegen/core/src/main/scala/sttp/tapir/codegen/EnumGenerator.scala

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,4 +66,55 @@ object EnumGenerator {
6666
|}""".stripMargin :: Nil
6767
}
6868
}
69+
70+
def enumSerdeHelperDefn(targetScala3: Boolean): String = {
71+
if (targetScala3)
72+
"""
73+
|def enumMap[E: enumextensions.EnumMirror]: Map[String, E] =
74+
| Map.from(
75+
| for e <- enumextensions.EnumMirror[E].values yield e.name.toUpperCase -> e
76+
| )
77+
|case class EnumExtraParamSupport[T: enumextensions.EnumMirror](eMap: Map[String, T]) extends ExtraParamSupport[T] {
78+
| // Case-insensitive mapping
79+
| def decode(s: String): sttp.tapir.DecodeResult[T] =
80+
| scala.util
81+
| .Try(eMap(s.toUpperCase))
82+
| .fold(
83+
| _ =>
84+
| sttp.tapir.DecodeResult.Error(
85+
| s,
86+
| new NoSuchElementException(
87+
| s"Could not find value $s for enum ${enumextensions.EnumMirror[T].mirroredName}, available values: ${enumextensions.EnumMirror[T].values.mkString(", ")}"
88+
| )
89+
| ),
90+
| sttp.tapir.DecodeResult.Value(_)
91+
| )
92+
| def encode(t: T): String = t.name
93+
|}
94+
|def extraCodecSupport[T: enumextensions.EnumMirror]: ExtraParamSupport[T] =
95+
| EnumExtraParamSupport(enumMap[T](using enumextensions.EnumMirror[T]))
96+
|""".stripMargin
97+
else
98+
"""
99+
|case class EnumExtraParamSupport[T <: enumeratum.EnumEntry](enumName: String, T: enumeratum.Enum[T]) extends ExtraParamSupport[T] {
100+
| // Case-insensitive mapping
101+
| def decode(s: String): sttp.tapir.DecodeResult[T] =
102+
| scala.util.Try(T.upperCaseNameValuesToMap(s.toUpperCase))
103+
| .fold(
104+
| _ =>
105+
| sttp.tapir.DecodeResult.Error(
106+
| s,
107+
| new NoSuchElementException(
108+
| s"Could not find value $s for enum ${enumName}, available values: ${T.values.mkString(", ")}"
109+
| )
110+
| ),
111+
| sttp.tapir.DecodeResult.Value(_)
112+
| )
113+
| def encode(t: T): String = t.entryName
114+
|}
115+
|def extraCodecSupport[T <: enumeratum.EnumEntry](enumName: String, T: enumeratum.Enum[T]): ExtraParamSupport[T] =
116+
| EnumExtraParamSupport(enumName, T)
117+
|""".stripMargin
118+
}
119+
69120
}

openapi-codegen/core/src/main/scala/sttp/tapir/codegen/PackageReuseContext.scala

Lines changed: 0 additions & 49 deletions
This file was deleted.

0 commit comments

Comments
 (0)