Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 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,207 @@ 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 = countModelReferences(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 times each model is referenced - both as a oneOf member and as a
* property type. A child can only be inlined if it's referenced exactly once (by its
* oneOf parent) and not used as a property type elsewhere.
*/
private Map<String, Integer> countModelReferences(Map<String, CodegenModel> allModels) {
Map<String, Integer> counts = new HashMap<>();

// Count oneOf parent references
allModels.values().stream()
.flatMap(m -> m.oneOf.stream())
.forEach(name -> counts.merge(name, 1, Integer::sum));

// Count property-type references (prevents inlining models used as field types).
// Check both dataType and complexType
allModels.values().stream()
.flatMap(m -> m.vars.stream())
.forEach(prop -> {
if (prop.dataType != null && allModels.containsKey(prop.dataType)) {
counts.merge(prop.dataType, 1, Integer::sum);
}
if (prop.complexType != null && allModels.containsKey(prop.complexType)) {
counts.merge(prop.complexType, 1, Integer::sum);
}
});

return counts;
}

/**
* 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) continue;

// All children extend the parent trait
child.getVendorExtensions().put("x-oneOfParent", parent.classname);
Comment thread
nikhilsu marked this conversation as resolved.
if (parent.discriminator != null) {
child.getVendorExtensions().put("x-parentDiscriminatorName",
parent.discriminator.getPropertyName());
}

if (isInlineable(child, refCounts)) {
child.getVendorExtensions().put("x-isOneOfMember", true);
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 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");
}
}

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
Loading
Loading