@@ -7,7 +7,7 @@ import sttp.tapir.codegen.json.JsonSerdeLib.{Circe, Jsoniter}
77import sttp .tapir .codegen .openapi .models .OpenapiModels .OpenapiDocument
88import sttp .tapir .codegen .openapi .models .{DefaultValueRenderer , OpenapiSchemaType , RenderConfig }
99import sttp .tapir .codegen .openapi .models .OpenapiSchemaType ._
10- import sttp .tapir .codegen .util .NameHelpers .indent
10+ import sttp .tapir .codegen .util .NameHelpers .{ indent , safeVariableName }
1111import sttp .tapir .codegen .util .{DocUtils , VersionedHelpers }
1212
1313case class GeneratedClassDefinitions (
@@ -82,7 +82,8 @@ class ClassDefinitionGenerator {
8282 xmlParamRefs : Set [String ] = Set .empty,
8383 useCustomJsoniterSerdes : Boolean = true ,
8484 packageReuse : PackageReuseContext = PackageReuseContext .none,
85- seperateFilesForModels : Boolean = false
85+ seperateFilesForModels : Boolean = false ,
86+ alwaysGenerateParamSupport : Boolean = false
8687 ): Option [GeneratedClassDefinitions ] = {
8788 val allSchemas : Map [String , OpenapiSchemaType ] = doc.components.toSeq.flatMap(_.schemas).toMap
8889 val allOneOfSchemas = allSchemas.collect { case (name, oneOf : OpenapiSchemaOneOf ) => name -> oneOf }.toSeq
@@ -123,7 +124,7 @@ class ClassDefinitionGenerator {
123124 val generatesQueryOrPathParamEnums = enumsDefinedOnEndpointParams ||
124125 allSchemas
125126 .collect { case (name, _ : OpenapiSchemaEnum ) => name }
126- .exists(queryOrPathParamRefs.contains)
127+ .exists(alwaysGenerateParamSupport || queryOrPathParamRefs.contains(_) )
127128 val enumSerdeHelper = if (! generatesQueryOrPathParamEnums) " " else EnumGenerator .enumSerdeHelperDefn(targetScala3)
128129
129130 def fetchTransitiveParamRefs (initialSet : Set [String ], toCheck : Seq [OpenapiSchemaType ]): Set [String ] = toCheck match {
@@ -136,14 +137,27 @@ class ClassDefinitionGenerator {
136137 jsonParamRefs.toSeq.flatMap(ref => allSchemas.get(ref.stripPrefix(" #/components/schemas/" )))
137138 )
138139
140+ def allChildrenDefineDiscriminator (d : String , s : OpenapiSchemaOneOf ): Boolean =
141+ s.types.forall {
142+ case t : OpenapiSchemaRef =>
143+ t.maybeResolved(doc).exists {
144+ case o : OpenapiSchemaObject => o.properties.contains(d)
145+ case _ => false
146+ }
147+ case _ => false
148+ }
149+
139150 val adtTypes =
140151 adtInheritanceMap
141152 .flatMap(_._2)
142153 .toSeq
143- .map(_._1)
154+ .map { s => s._1 -> s._2.discriminator.map (_.propertyName).filter(d => allChildrenDefineDiscriminator(d, s._2)) }
144155 .distinct
145- .filterNot(PackageReuseContext .isReusedSchema(_, packageReuse))
146- .map(name => s " sealed trait $name" )
156+ .filterNot(s => PackageReuseContext .isReusedSchema(s._1, packageReuse))
157+ .map { case (name, maybeDiscriminator) =>
158+ val maybeBody = maybeDiscriminator.map(d => s " { def ${safeVariableName(d)}: String } " ).getOrElse(" " )
159+ s " sealed trait $name$maybeBody"
160+ }
147161 .sorted
148162 .mkString(" " , " \n " , " \n " )
149163 val schemasWithAny = allSchemas.filter { case (_, schema) => schemaContainsAny(schema) }
@@ -174,7 +188,8 @@ class ClassDefinitionGenerator {
174188 schemasContainAny,
175189 useCustomJsoniterSerdes,
176190 packageReuse,
177- resolvableNonClassyOneOfSchemas
191+ resolvableNonClassyOneOfSchemas,
192+ seperateFilesForModels
178193 )
179194 val allTransitiveXmlParamRefs = fetchTransitiveParamRefs(
180195 xmlParamRefs,
@@ -206,12 +221,45 @@ class ClassDefinitionGenerator {
206221 _.schemas.map {
207222 case (name, _ : OpenapiSchemaEnum ) if PackageReuseContext .isReusedSchema(name, packageReuse) =>
208223 (name, (0 , Seq (PackageReuseContext .enumAliasType(name, packageReuse, seperateFilesForModels))))
224+ case (name, obj : OpenapiSchemaObject ) =>
225+ val isReusedSchema = PackageReuseContext .isReusedSchema(name, packageReuse)
226+ (
227+ name,
228+ (
229+ if (isReusedSchema) 0 else 2 ,
230+ generateClass(
231+ allSchemas,
232+ name,
233+ obj,
234+ allTransitiveJsonParamRefs,
235+ adtInheritanceMap,
236+ jsonSerdeLib,
237+ targetScala3,
238+ isReusedSchema,
239+ packageReuse,
240+ seperateFilesForModels,
241+ alwaysGenerateParamSupport
242+ )
243+ )
244+ )
209245 case (name, s) if PackageReuseContext .isReusedSchema(name, packageReuse) =>
210246 (name, (0 , Seq (PackageReuseContext .aliasType(name, packageReuse, seperateFilesForModels))))
211- case (name, obj : OpenapiSchemaObject ) =>
212- (name, (2 , generateClass(allSchemas, name, obj, allTransitiveJsonParamRefs, adtInheritanceMap, jsonSerdeLib, targetScala3)))
213247 case (name, obj : OpenapiSchemaEnum ) =>
214- (name, (1 , EnumGenerator .generateEnum(name, obj, targetScala3, queryOrPathParamRefs, jsonSerdeLib, allTransitiveJsonParamRefs)))
248+ (
249+ name,
250+ (
251+ 1 ,
252+ EnumGenerator .generateEnum(
253+ name,
254+ obj,
255+ targetScala3,
256+ queryOrPathParamRefs,
257+ jsonSerdeLib,
258+ allTransitiveJsonParamRefs,
259+ alwaysGenerateParamSupport
260+ )
261+ )
262+ )
215263 case (name, OpenapiSchemaMap (valueSchema, _, _)) =>
216264 (name, (0 , generateMap(name, valueSchema)))
217265 case (name, OpenapiSchemaArray (valueSchema, _, _, rs)) =>
@@ -256,14 +304,28 @@ class ClassDefinitionGenerator {
256304 addModel(name, defns.mkString(" \n " ))
257305 }
258306
259- adtGroups.toSeq.filterNot(p => isTypeAlias(p._1)).foreach { case (fileName, parentTraits) =>
260- val traits = parentTraits.map(p => s " sealed trait $p" ).mkString(" \n " )
307+ adtGroups.toSeq.foreach { case (fileName, parentTraits) =>
308+ val isReused = isTypeAlias(fileName)
309+ val traits =
310+ if (isReused) parentTraits.map(p => s " type $p = ${packageReuse.modelRoot(seperateFilesForModels)}. $p" ).mkString(" \n " )
311+ else parentTraits.map(p => s " sealed trait $p" ).mkString(" \n " )
261312 val children = childToAdtFile.filter(_._2 == fileName).keys.toSeq.sorted
262313 val childContent = children
263314 .flatMap { child =>
264315 allSchemas.get(child).collect { case obj : OpenapiSchemaObject =>
265- generateClass(allSchemas, child, obj, allTransitiveJsonParamRefs, adtInheritanceMap, jsonSerdeLib, targetScala3)
266- .mkString(" \n " )
316+ generateClass(
317+ allSchemas,
318+ child,
319+ obj,
320+ allTransitiveJsonParamRefs,
321+ adtInheritanceMap,
322+ jsonSerdeLib,
323+ targetScala3,
324+ isReused,
325+ packageReuse,
326+ seperateFilesForModels,
327+ alwaysGenerateParamSupport
328+ ).mkString(" \n " )
267329 }
268330 }
269331 .mkString(" \n " )
@@ -357,7 +419,11 @@ class ClassDefinitionGenerator {
357419 jsonParamRefs : Set [String ],
358420 adtInheritanceMap : Map [String , Seq [(String , OpenapiSchemaOneOf )]],
359421 jsonSerdeLib : JsonSerdeLib .JsonSerdeLib ,
360- targetScala3 : Boolean
422+ targetScala3 : Boolean ,
423+ isReused : Boolean ,
424+ packageReuse : PackageReuseContext ,
425+ seperateFilesForModels : Boolean ,
426+ alwaysGenerateParamSupport : Boolean
361427 ): Seq [String ] = try {
362428 val isJson = jsonParamRefs contains name
363429 def rec (className : String , schemaKey : String , obj : OpenapiSchemaObject , acc : List [String ]): Seq [String ] = {
@@ -395,7 +461,7 @@ class ClassDefinitionGenerator {
395461 val discriminatorDefBody = discriminatorDefFields.filter { case (n, _) => obj.properties.map(_._1).toSet.contains(n) } match {
396462 case Nil => " "
397463 case fields =>
398- val fs = fields.map { case (k, v) => s """ def ` $k ` : String = " $v" """ }.mkString(" \n " )
464+ val fs = fields.map { case (k, v) => s """ def ${safeVariableName(k)} : String = " $v" """ }.mkString(" \n " )
399465 s """ {
400466 | ${indent(2 )(fs)}
401467 |} """ .stripMargin
@@ -406,7 +472,7 @@ class ClassDefinitionGenerator {
406472 .map { case (key, OpenapiSchemaField (schemaType, maybeDefault, _)) =>
407473 val (tpe, maybeEnum) =
408474 mapSchemaTypeToType(className, key, obj.required.contains(key), schemaType, isJson, jsonSerdeLib, targetScala3)
409- val fixedKey = fixKey (key)
475+ val fixedKey = safeVariableName (key)
410476 val optional = schemaType.nullable || ! obj.required.contains(key)
411477 val maybeExplicitDefault =
412478 maybeDefault.map(
@@ -415,14 +481,24 @@ class ClassDefinitionGenerator {
415481 .render(allModels = allSchemas, thisType = schemaType, optional, RenderConfig (maybeEnum.map(_.enumName)))(_)
416482 )
417483 val default = maybeExplicitDefault getOrElse (if (optional) " = None" else " " )
418- s " $fixedKey: $tpe$default" -> maybeEnum.map(_.impl)
484+ if (isReused) " " -> maybeEnum.map { e =>
485+ val alias = s " ${e.enumName} = ${packageReuse.modelRoot(seperateFilesForModels)}. ${e.enumName}"
486+ s " type $alias\n val $alias"
487+ }
488+ else s " $fixedKey: $tpe$default" -> maybeEnum.map(_.impl)
419489 }
420490 .unzip
421491
422492 val enumDefn = maybeEnums.flatten.toList
423- s """ |case class $className (
424- | ${indent(2 )(properties.mkString(" ,\n " ))}
425- |) $parents$discriminatorDefBody""" .stripMargin :: innerClasses ::: enumDefn ::: acc
493+ val modelDefn =
494+ if (isReused) {
495+ val alias = s " $className = ${packageReuse.modelRoot(seperateFilesForModels)}. $className"
496+ s " type $alias\n val $alias"
497+ } else
498+ s """ |case class $className (
499+ | ${indent(2 )(properties.mkString(" ,\n " ))}
500+ |) $parents$discriminatorDefBody""" .stripMargin
501+ modelDefn :: innerClasses ::: enumDefn ::: acc
426502 }
427503
428504 rec(addName(" " , name), name, obj, Nil )
@@ -485,7 +561,8 @@ class ClassDefinitionGenerator {
485561 targetScala3,
486562 Set .empty,
487563 jsonSerdeLib,
488- if (isJson) Set (enumName) else Set .empty
564+ if (isJson) Set (enumName) else Set .empty,
565+ false
489566 )
490567 (enumName -> e.nullable, Some (InlineEnumDefn (enumName, enumDefn.mkString(" \n " ))))
491568
@@ -500,13 +577,6 @@ class ClassDefinitionGenerator {
500577
501578 private val reservedKeys = VersionedHelpers .reservedKeys
502579
503- private def fixKey (key : String ) = {
504- if (reservedKeys.contains(key))
505- s " ` $key` "
506- else
507- key
508- }
509-
510580 private def schemaContainsAny (schema : OpenapiSchemaType ): Boolean = schema match {
511581 case _ : OpenapiSchemaAny => true
512582 case OpenapiSchemaArray (items, _, _, _) => schemaContainsAny(items)
0 commit comments