Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion bin/configs/scala-sttp-circe.yaml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
generatorName: scala-sttp
outputDir: samples/client/petstore/scala-sttp-circe
inputSpec: modules/openapi-generator/src/test/resources/3_0/scala/petstore.yaml
inputSpec: modules/openapi-generator/src/test/resources/3_0/scala-sttp-circe/petstore.yaml
templateDir: modules/openapi-generator/src/main/resources/scala-sttp
nameMappings:
_type: "`underscoreType`"
Expand Down
4 changes: 2 additions & 2 deletions docs/generators/scala-sttp.md
Original file line number Diff line number Diff line change
Expand Up @@ -226,9 +226,9 @@ These options may be applied as additional-properties (cli) or configOptions (pl
|Composite|✓|OAS2,OAS3
|Polymorphism|✗|OAS2,OAS3
|Union|✗|OAS3
|allOf||OAS2,OAS3
|allOf||OAS2,OAS3
|anyOf|✗|OAS3
|oneOf||OAS3
|oneOf||OAS3
|not|✗|OAS3

### Security Feature
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,10 @@ public ScalaSttpClientCodegen() {
.excludeSchemaSupportFeatures(
SchemaSupportFeature.Polymorphism
)
.includeSchemaSupportFeatures(
SchemaSupportFeature.oneOf,
SchemaSupportFeature.allOf
)
.excludeParameterFeatures(
ParameterFeature.Cookie
)
Expand Down Expand Up @@ -240,9 +244,186 @@ public ModelsMap postProcessModels(ModelsMap objs) {
*/
@Override
public Map<String, ModelsMap> postProcessAllModels(Map<String, ModelsMap> objs) {
final Map<String, ModelsMap> processed = super.postProcessAllModels(objs);
postProcessUpdateImports(processed);
return processed;
Map<String, ModelsMap> modelsMap = super.postProcessAllModels(objs);

Map<String, CodegenModel> allModels = collectAllModels(modelsMap);
synthesizeOneOfFromDiscriminator(allModels);
Map<String, Integer> refCounts = countOneOfReferences(allModels);
markOneOfTraits(modelsMap, allModels, refCounts);
removeInlinedModels(modelsMap);

postProcessUpdateImports(modelsMap);
return modelsMap;
}

/**
* Collect all CodegenModels by classname for lookup.
*/
private Map<String, CodegenModel> collectAllModels(Map<String, ModelsMap> modelsMap) {
return modelsMap.values().stream()
.flatMap(mm -> mm.getModels().stream())
.map(ModelMap::getModel)
.collect(java.util.stream.Collectors.toMap(m -> m.classname, m -> m, (a, b) -> a));
}

/**
* For specs that use allOf+discriminator (children reference parent via allOf, parent has
* discriminator.mapping but no oneOf), synthesize the oneOf set from the discriminator mapping.
* This allows the standard oneOf processing logic to handle both patterns uniformly.
*/
private void synthesizeOneOfFromDiscriminator(Map<String, CodegenModel> allModels) {
for (CodegenModel model : allModels.values()) {
if (!model.oneOf.isEmpty() || model.discriminator == null) {
continue;
}

if (model.discriminator.getMappedModels() != null
&& !model.discriminator.getMappedModels().isEmpty()) {
for (CodegenDiscriminator.MappedModel mapped : model.discriminator.getMappedModels()) {
model.oneOf.add(mapped.getModelName());
}
} else if (model.discriminator.getMapping() != null) {
for (String ref : model.discriminator.getMapping().values()) {
String modelName = ref.contains("/") ? ref.substring(ref.lastIndexOf('/') + 1) : ref;
if (allModels.containsKey(modelName)) {
model.oneOf.add(modelName);
}
}
}

if (!model.oneOf.isEmpty()) {
model.getVendorExtensions().put("x-synthesized-oneOf", true);
}
}
}

/**
* Count how many oneOf parents reference each child, used to determine
* whether a child can be inlined (only if referenced by exactly one parent).
*/
private Map<String, Integer> countOneOfReferences(Map<String, CodegenModel> allModels) {
return allModels.values().stream()
.flatMap(m -> m.oneOf.stream())
.collect(java.util.stream.Collectors.toMap(name -> name, name -> 1, Integer::sum));
}

/**
* Mark oneOf parents as sealed/regular traits with discriminator vendor extensions,
* and configure child models for inlining.
*/
private void markOneOfTraits(Map<String, ModelsMap> modelsMap,
Map<String, CodegenModel> allModels,
Map<String, Integer> refCounts) {
for (ModelsMap mm : modelsMap.values()) {
for (ModelMap modelMap : mm.getModels()) {
CodegenModel model = modelMap.getModel();

if (!model.oneOf.isEmpty()) {
configureOneOfModel(model, allModels, refCounts);
}

if (model.discriminator != null) {
model.getVendorExtensions().put("x-use-discr", true);
if (model.discriminator.getMapping() != null) {
model.getVendorExtensions().put("x-use-discr-mapping", true);
}
}
}
}
}

private void configureOneOfModel(CodegenModel parent,
Map<String, CodegenModel> allModels,
Map<String, Integer> refCounts) {
List<CodegenModel> inlineableMembers = new ArrayList<>();
Set<String> childImports = new HashSet<>();

for (String childName : parent.oneOf) {
CodegenModel child = allModels.get(childName);
if (child != null && isInlineable(child, refCounts)) {
markChildForInlining(child, parent);
inlineableMembers.add(child);
if (child.imports != null) {
childImports.addAll(child.imports);
}
}
}

buildDiscriminatorEntries(parent, allModels);

if (!inlineableMembers.isEmpty() && inlineableMembers.size() == parent.oneOf.size()) {
markAsSealedTrait(parent, inlineableMembers, childImports);
} else {
markAsRegularTrait(parent, inlineableMembers);
}
}

private boolean isInlineable(CodegenModel child, Map<String, Integer> refCounts) {
return (child.oneOf == null || child.oneOf.isEmpty())
&& refCounts.getOrDefault(child.classname, 0) == 1;
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
}

private void markChildForInlining(CodegenModel child, CodegenModel parent) {
child.getVendorExtensions().put("x-isOneOfMember", true);
child.getVendorExtensions().put("x-oneOfParent", parent.classname);
if (parent.discriminator != null) {
child.getVendorExtensions().put("x-parentDiscriminatorName",
parent.discriminator.getPropertyName());
}
}

private void buildDiscriminatorEntries(CodegenModel parent, Map<String, CodegenModel> allModels) {
List<Map<String, String>> entries = parent.oneOf.stream()
.map(allModels::get)
.filter(Objects::nonNull)
.map(child -> Map.of("classname", child.classname, "schemaName", child.name))
.collect(java.util.stream.Collectors.toList());
parent.getVendorExtensions().put("x-discriminator-entries", entries);
}

private void markAsSealedTrait(CodegenModel parent, List<CodegenModel> members,
Set<String> childImports) {
parent.getVendorExtensions().put("x-isSealedTrait", true);
parent.getVendorExtensions().put("x-oneOfMembers", members);

if (parent.getVendorExtensions().containsKey("x-synthesized-oneOf")
&& parent.vars != null && !parent.vars.isEmpty()) {
parent.getVendorExtensions().put("x-hasOwnVars", true);
}

mergeChildImports(parent, childImports);
}

private void markAsRegularTrait(CodegenModel parent, List<CodegenModel> partialMembers) {
parent.getVendorExtensions().put("x-isRegularTrait", true);
for (CodegenModel member : partialMembers) {
member.getVendorExtensions().remove("x-isOneOfMember");
member.getVendorExtensions().remove("x-oneOfParent");
member.getVendorExtensions().remove("x-parentDiscriminatorName");
}
}

private void mergeChildImports(CodegenModel parent, Set<String> childImports) {
if (childImports.isEmpty()) return;
Set<String> existing = parent.imports != null ? new HashSet<>(parent.imports) : new HashSet<>();
childImports.removeAll(existing);
if (!childImports.isEmpty()) {
if (parent.imports == null) {
parent.imports = new HashSet<>();
}
parent.imports.addAll(childImports);
}
}

/**
* Remove models that were inlined into their parent sealed trait -
* they don't need separate files.
*/
private void removeInlinedModels(Map<String, ModelsMap> modelsMap) {
modelsMap.entrySet().removeIf(entry ->
entry.getValue().getModels().stream()
.anyMatch(m -> m.getModel().getVendorExtensions().containsKey("x-isOneOfMember"))
);
}

/**
Expand Down
148 changes: 146 additions & 2 deletions modules/openapi-generator/src/main/resources/scala-sttp/model.mustache
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ package {{package}}
import {{import}}
{{/imports}}
{{#circe}}
import io.circe.{Decoder, Encoder, Json}
import io.circe.{Decoder, DecodingFailure, Encoder, Json}
import io.circe.syntax._
import {{invokerPackage}}.JsonSupport._
{{/circe}}
Expand All @@ -20,6 +20,148 @@ import {{invokerPackage}}.JsonSupport._
{{{description}}}
{{/javadocRenderer}}
{{/description}}
{{#vendorExtensions.x-isSealedTrait}}
sealed trait {{classname}}{{#vendorExtensions.x-hasOwnVars}} {
{{#vars}}
def {{{name}}}: {{^required}}Option[{{/required}}{{^isEnum}}{{dataType}}{{/isEnum}}{{#isEnum}}{{^isArray}}{{classname}}Enums.{{datatypeWithEnum}}{{/isArray}}{{#isArray}}Seq[{{classname}}Enums.{{datatypeWithEnum}}]{{/isArray}}{{/isEnum}}{{^required}}]{{/required}}
{{/vars}}
}{{/vendorExtensions.x-hasOwnVars}}
object {{classname}} {
{{#circe}}
{{#vendorExtensions.x-use-discr-mapping}}
{{#discriminator}}
implicit val encoder{{classname}}: Encoder[{{classname}}] = Encoder.instance {
{{#mappedModels}}
case obj: {{model.classname}} => obj.asJson.mapObject(("{{propertyName}}" -> "{{mappingName}}".asJson) +: _)
{{/mappedModels}}
}
implicit val decoder{{classname}}: Decoder[{{classname}}] = Decoder.instance { c =>
c.downField("{{propertyName}}").as[String].flatMap {
{{#mappedModels}}
case "{{mappingName}}" => c.as[{{model.classname}}]
{{/mappedModels}}
case other => Left(DecodingFailure(s"Unknown discriminator value: $other", c.history))
}
}
{{/discriminator}}
{{/vendorExtensions.x-use-discr-mapping}}
{{^vendorExtensions.x-use-discr-mapping}}
{{#vendorExtensions.x-use-discr}}
implicit val encoder{{classname}}: Encoder[{{classname}}] = Encoder.instance {
{{#vendorExtensions.x-discriminator-entries}}
case obj: {{classname}} => obj.asJson.mapObject(("{{discriminator.propertyName}}" -> "{{schemaName}}".asJson) +: _)
{{/vendorExtensions.x-discriminator-entries}}
}
implicit val decoder{{classname}}: Decoder[{{classname}}] = Decoder.instance { c =>
c.downField("{{discriminator.propertyName}}").as[String].flatMap {
{{#vendorExtensions.x-discriminator-entries}}
case "{{schemaName}}" => c.as[{{classname}}]
{{/vendorExtensions.x-discriminator-entries}}
case other => Left(DecodingFailure(s"Unknown discriminator value: $other", c.history))
}
}
{{/vendorExtensions.x-use-discr}}
{{^vendorExtensions.x-use-discr}}
implicit val encoder{{classname}}: Encoder[{{classname}}] = Encoder.instance {
{{#oneOf}}
case obj: {{.}} => obj.asJson
Comment thread
nikhilsu marked this conversation as resolved.
{{/oneOf}}
}
implicit val decoder{{classname}}: Decoder[{{classname}}] =
List[Decoder[{{classname}}]]({{#oneOf}}Decoder[{{.}}].map(x => x: {{classname}}){{^-last}}, {{/-last}}{{/oneOf}}).reduceLeft(_ or _)
{{/vendorExtensions.x-use-discr}}
{{/vendorExtensions.x-use-discr-mapping}}
{{/circe}}
}

{{#vendorExtensions.x-oneOfMembers}}
case class {{classname}}(
{{#vars}}
{{#description}}
/* {{{.}}} */
{{/description}}
{{{name}}}: {{^required}}Option[{{/required}}{{^isEnum}}{{dataType}}{{/isEnum}}{{#isEnum}}{{^isArray}}{{classname}}Enums.{{datatypeWithEnum}}{{/isArray}}{{#isArray}}Seq[{{classname}}Enums.{{datatypeWithEnum}}]{{/isArray}}{{/isEnum}}{{^required}}] = None{{/required}}{{^-last}},{{/-last}}
{{/vars}}
) extends {{vendorExtensions.x-oneOfParent}}
{{#circe}}
object {{classname}} {
implicit val encoder{{classname}}: Encoder[{{classname}}] = Encoder.instance { t =>
Json.fromFields{
Seq(
{{#vars}}
{{#required}}Some("{{baseName}}" -> t.{{{name}}}.asJson){{/required}}{{^required}}t.{{{name}}}.map(v => "{{baseName}}" -> v.asJson){{/required}}{{^-last}},{{/-last}}
{{/vars}}
).flatten
}
}
implicit val decoder{{classname}}: Decoder[{{classname}}] = Decoder.instance { c =>
for {
{{#vars}}
{{{name}}} <- c.downField("{{baseName}}").as[{{^required}}Option[{{/required}}{{^isEnum}}{{dataType}}{{/isEnum}}{{#isEnum}}{{^isArray}}{{classname}}Enums.{{datatypeWithEnum}}{{/isArray}}{{#isArray}}Seq[{{classname}}Enums.{{datatypeWithEnum}}]{{/isArray}}{{/isEnum}}{{^required}}]{{/required}}]
{{/vars}}
} yield {{classname}}(
{{#vars}}
{{{name}}} = {{{name}}}{{^-last}},{{/-last}}
{{/vars}}
)
}
}
{{/circe}}

{{/vendorExtensions.x-oneOfMembers}}
{{/vendorExtensions.x-isSealedTrait}}
{{#vendorExtensions.x-isRegularTrait}}
trait {{classname}}
object {{classname}} {
{{#circe}}
{{#vendorExtensions.x-use-discr-mapping}}
{{#discriminator}}
implicit val encoder{{classname}}: Encoder[{{classname}}] = Encoder.instance {
{{#mappedModels}}
case obj: {{model.classname}} => obj.asJson.mapObject(("{{propertyName}}" -> "{{mappingName}}".asJson) +: _)
{{/mappedModels}}
}
implicit val decoder{{classname}}: Decoder[{{classname}}] = Decoder.instance { c =>
c.downField("{{propertyName}}").as[String].flatMap {
{{#mappedModels}}
case "{{mappingName}}" => c.as[{{model.classname}}]
{{/mappedModels}}
case other => Left(DecodingFailure(s"Unknown discriminator value: $other", c.history))
}
}
{{/discriminator}}
{{/vendorExtensions.x-use-discr-mapping}}
{{^vendorExtensions.x-use-discr-mapping}}
{{#vendorExtensions.x-use-discr}}
implicit val encoder{{classname}}: Encoder[{{classname}}] = Encoder.instance {
{{#vendorExtensions.x-discriminator-entries}}
case obj: {{classname}} => obj.asJson.mapObject(("{{discriminator.propertyName}}" -> "{{schemaName}}".asJson) +: _)
{{/vendorExtensions.x-discriminator-entries}}
}
implicit val decoder{{classname}}: Decoder[{{classname}}] = Decoder.instance { c =>
c.downField("{{discriminator.propertyName}}").as[String].flatMap {
{{#vendorExtensions.x-discriminator-entries}}
case "{{schemaName}}" => c.as[{{classname}}]
{{/vendorExtensions.x-discriminator-entries}}
case other => Left(DecodingFailure(s"Unknown discriminator value: $other", c.history))
}
}
{{/vendorExtensions.x-use-discr}}
{{^vendorExtensions.x-use-discr}}
implicit val encoder{{classname}}: Encoder[{{classname}}] = Encoder.instance {
{{#oneOf}}
case obj: {{.}} => obj.asJson
{{/oneOf}}
}
implicit val decoder{{classname}}: Decoder[{{classname}}] =
List[Decoder[{{classname}}]]({{#oneOf}}Decoder[{{.}}].map(x => x: {{classname}}){{^-last}}, {{/-last}}{{/oneOf}}).reduceLeft(_ or _)
{{/vendorExtensions.x-use-discr}}
{{/vendorExtensions.x-use-discr-mapping}}
{{/circe}}
}
{{/vendorExtensions.x-isRegularTrait}}
{{^vendorExtensions.x-isSealedTrait}}
{{^vendorExtensions.x-isRegularTrait}}
{{^isEnum}}
case class {{classname}}(
{{#vars}}
Expand All @@ -28,7 +170,7 @@ case class {{classname}}(
{{/description}}
{{{name}}}: {{^required}}Option[{{/required}}{{^isEnum}}{{dataType}}{{/isEnum}}{{#isEnum}}{{^isArray}}{{classname}}Enums.{{datatypeWithEnum}}{{/isArray}}{{#isArray}}Seq[{{classname}}Enums.{{datatypeWithEnum}}]{{/isArray}}{{/isEnum}}{{^required}}] = None{{/required}}{{^-last}},{{/-last}}
{{/vars}}
)
){{#vendorExtensions.x-oneOfParent}} extends {{vendorExtensions.x-oneOfParent}}{{/vendorExtensions.x-oneOfParent}}
{{#circe}}
object {{classname}} {
{{#hasVars}}
Expand Down Expand Up @@ -64,6 +206,8 @@ object {{classname}} {
}
{{/circe}}
{{/isEnum}}
{{/vendorExtensions.x-isRegularTrait}}
{{/vendorExtensions.x-isSealedTrait}}

{{#isEnum}}
object {{classname}} extends Enumeration {
Expand Down
Loading
Loading