Skip to content

Commit a83d4e7

Browse files
committed
Add Spring one-of support from Martin Bucinskas
OpenAPITools#10993
1 parent 28cc286 commit a83d4e7

File tree

15 files changed

+656
-12
lines changed

15 files changed

+656
-12
lines changed

modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/SpringCodegen.java

Lines changed: 110 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,14 +29,20 @@
2929
import java.util.List;
3030
import java.util.Locale;
3131
import java.util.Map;
32+
import java.util.Objects;
33+
import java.util.Optional;
3234
import java.util.regex.Matcher;
3335
import java.util.stream.Collectors;
3436

37+
import io.swagger.v3.oas.models.media.ComposedSchema;
38+
import io.swagger.v3.oas.models.media.Content;
39+
import io.swagger.v3.oas.models.media.Schema;
40+
import io.swagger.v3.oas.models.parameters.RequestBody;
41+
import io.swagger.v3.oas.models.responses.ApiResponse;
3542
import com.samskivert.mustache.Mustache;
3643
import io.swagger.v3.oas.models.OpenAPI;
3744
import io.swagger.v3.oas.models.Operation;
3845
import io.swagger.v3.oas.models.PathItem;
39-
import io.swagger.v3.oas.models.media.Schema;
4046
import io.swagger.v3.oas.models.servers.Server;
4147
import org.apache.commons.lang3.tuple.Pair;
4248
import org.openapitools.codegen.CliOption;
@@ -60,6 +66,7 @@
6066
import org.openapitools.codegen.meta.features.WireFormatFeature;
6167
import org.openapitools.codegen.templating.mustache.SplitStringLambda;
6268
import org.openapitools.codegen.templating.mustache.TrimWhitespaceLambda;
69+
import org.openapitools.codegen.utils.ModelUtils;
6370
import org.openapitools.codegen.utils.URLPathUtils;
6471
import org.slf4j.Logger;
6572
import org.slf4j.LoggerFactory;
@@ -142,6 +149,8 @@ public SpringCodegen() {
142149
modelPackage = "org.openapitools.model";
143150
invokerPackage = "org.openapitools.api";
144151
artifactId = "openapi-spring";
152+
useOneOfInterfaces = true;
153+
addOneOfInterfaceImports = true;
145154

146155
// clioOptions default redefinition need to be updated
147156
updateOption(CodegenConstants.INVOKER_PACKAGE, this.getInvokerPackage());
@@ -603,7 +612,11 @@ public void addOperationToGroup(String tag, String resourcePath, Operation opera
603612

604613
@Override
605614
public void preprocessOpenAPI(OpenAPI openAPI) {
606-
super.preprocessOpenAPI(openAPI);
615+
if (openAPI.getComponents() != null) {
616+
preprocessInlineOneOf(openAPI);
617+
super.preprocessOpenAPI(openAPI);
618+
}
619+
607620
/*
608621
* TODO the following logic should not need anymore in OAS 3.0 if
609622
* ("/".equals(swagger.getBasePath())) { swagger.setBasePath(""); }
@@ -1016,4 +1029,99 @@ public void setPerformBeanValidation(boolean performBeanValidation) {
10161029
public void setUseOptional(boolean useOptional) {
10171030
this.useOptional = useOptional;
10181031
}
1032+
1033+
@Override
1034+
public void addImportsToOneOfInterface(List<Map<String, String>> imports) {
1035+
if (additionalProperties.containsKey(JACKSON)) {
1036+
for (String i : Arrays.asList("JsonSubTypes", "JsonTypeInfo")) {
1037+
Map<String, String> oneImport = new HashMap<>();
1038+
oneImport.put("import", importMapping.get(i));
1039+
if (!imports.contains(oneImport)) {
1040+
imports.add(oneImport);
1041+
}
1042+
}
1043+
}
1044+
}
1045+
1046+
@Override
1047+
public Map<String, Object> postProcessAllModels(Map<String, Object> objs) {
1048+
Map<String, Object> postProcessedModels = super.postProcessAllModels(objs);
1049+
1050+
for (Map.Entry<String, Object> modelsEntry : objs.entrySet()) {
1051+
Map<String, Object> modelsAttrs = (Map<String, Object>) modelsEntry.getValue();
1052+
List<Object> models = (List<Object>) modelsAttrs.get("models");
1053+
for (Object _mo : models) {
1054+
Map<String, Object> mo = (Map<String, Object>) _mo;
1055+
CodegenModel cm = (CodegenModel) mo.get("model");
1056+
if (cm.oneOf.size() > 0) {
1057+
cm.vendorExtensions.put("x-deduction", true);
1058+
cm.vendorExtensions.put("x-deduction-model-names", cm.oneOf.toArray());
1059+
}
1060+
}
1061+
}
1062+
1063+
return postProcessedModels;
1064+
}
1065+
1066+
private void preprocessInlineOneOf(OpenAPI openAPI) {
1067+
if (openAPI != null) {
1068+
if (openAPI.getPaths() != null) {
1069+
for (Map.Entry<String, PathItem> openAPIGetPathsEntry : openAPI.getPaths().entrySet()) {
1070+
String pathname = openAPIGetPathsEntry.getKey();
1071+
PathItem path = openAPIGetPathsEntry.getValue();
1072+
if (path.readOperations() == null) {
1073+
continue;
1074+
}
1075+
for (Operation operation : path.readOperations()) {
1076+
boolean hasBodyParameter = hasBodyParameter(openAPI, operation);
1077+
1078+
// OpenAPI parser do not add Inline One Of models in Operations to Components/Schemas
1079+
if (hasBodyParameter(openAPI, operation)) {
1080+
Optional.ofNullable(operation.getRequestBody())
1081+
.map(RequestBody::getContent)
1082+
.ifPresent(this::repairInlineOneOf);
1083+
}
1084+
if (operation.getResponses() != null) {
1085+
operation.getResponses().values().stream().map(ApiResponse::getContent)
1086+
.filter(Objects::nonNull)
1087+
.forEach(this::repairInlineOneOf);
1088+
}
1089+
}
1090+
}
1091+
}
1092+
}
1093+
}
1094+
1095+
/**
1096+
* Add all OneOf schemas to #/components/schemas and replace them in the original content by ref schema
1097+
* Replace OneOf with unmodifiable types with an empty Schema
1098+
*
1099+
* OpenAPI Parser does not add inline OneOf schemas to models to generate
1100+
*
1101+
* @param content a 'content' section in the OAS specification.
1102+
*/
1103+
private void repairInlineOneOf(final Content content) {
1104+
content.values().forEach(mediaType -> {
1105+
final Schema<?> replacingSchema = mediaType.getSchema();
1106+
if (isOneOfSchema(replacingSchema)) {
1107+
if (ModelUtils.isSchemaOneOfConsistsOfCustomTypes(openAPI, replacingSchema)) {
1108+
final String oneOfModelName = (String) replacingSchema.getExtensions().get("x-one-of-name");
1109+
final Schema<?> newRefSchema = new Schema<>().$ref("#/components/schemas/" + oneOfModelName);
1110+
mediaType.setSchema(newRefSchema);
1111+
ModelUtils.getSchemas(openAPI).put(oneOfModelName, replacingSchema);
1112+
} else {
1113+
mediaType.setSchema(new Schema().type("object"));
1114+
}
1115+
}
1116+
});
1117+
}
1118+
1119+
private static boolean isOneOfSchema(final Schema<?> schema) {
1120+
if (schema instanceof ComposedSchema) {
1121+
ComposedSchema composedSchema = (ComposedSchema) schema;
1122+
return Optional.ofNullable(composedSchema.getProperties()).map(Map::isEmpty).orElse(true)
1123+
&& Optional.ofNullable(schema.getExtensions()).map(m -> m.containsKey("x-one-of-name")).orElse(false);
1124+
}
1125+
return false;
1126+
}
10191127
}

modules/openapi-generator/src/main/java/org/openapitools/codegen/utils/ModelUtils.java

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1672,4 +1672,25 @@ public static SemVer getOpenApiVersion(OpenAPI openAPI, String location, List<Au
16721672

16731673
return new SemVer(version);
16741674
}
1675+
1676+
public static boolean isSchemaOneOfConsistsOfCustomTypes(OpenAPI openAPI, Schema<?> schema) {
1677+
if (schema instanceof ComposedSchema) {
1678+
ComposedSchema composedSchema = (ComposedSchema) schema;
1679+
if (composedSchema.getOneOf() == null || composedSchema.getOneOf().isEmpty()) {
1680+
return false;
1681+
}
1682+
for (Schema<?> oneOfSchema : composedSchema.getOneOf()) {
1683+
if (oneOfSchema.get$ref() != null) {
1684+
oneOfSchema = ModelUtils.getReferencedSchema(openAPI, schema);
1685+
}
1686+
if (!(oneOfSchema instanceof ComposedSchema
1687+
|| oneOfSchema instanceof MapSchema
1688+
|| oneOfSchema instanceof ArraySchema
1689+
|| oneOfSchema instanceof ObjectSchema)) {
1690+
return false;
1691+
}
1692+
}
1693+
}
1694+
return true;
1695+
}
16751696
}

modules/openapi-generator/src/main/resources/JavaSpring/model.mustache

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ import javax.annotation.Generated;
4848
{{>enumOuterClass}}
4949
{{/isEnum}}
5050
{{^isEnum}}
51-
{{>pojo}}
51+
{{#vendorExtensions.x-is-one-of-interface}}{{>oneof_interface}}{{/vendorExtensions.x-is-one-of-interface}}{{^vendorExtensions.x-is-one-of-interface}}{{>pojo}}{{/vendorExtensions.x-is-one-of-interface}}
5252
{{/isEnum}}
5353
{{/model}}
5454
{{/models}}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{{>additionalModelTypeAnnotations}}{{>generatedAnnotation}}{{>typeInfoAnnotation}}{{>xmlAnnotation}}
2+
public interface {{classname}} {{#vendorExtensions.x-implements}}{{#-first}}extends {{{.}}}{{/-first}}{{^-first}}, {{{.}}}{{/-first}}{{/vendorExtensions.x-implements}} {
3+
{{^vendorExtensions.x-deduction}}{{#discriminator}}
4+
public {{propertyType}} {{propertyGetter}}();
5+
{{/discriminator}}{{/vendorExtensions.x-deduction}}
6+
}

modules/openapi-generator/src/main/resources/JavaSpring/pojo.mustache

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
{{>xmlAnnotation}}
1818
{{/withXml}}
1919
{{>generatedAnnotation}}
20-
public class {{classname}} {{#parent}}extends {{{.}}}{{/parent}}{{^parent}}{{#hateoas}}extends RepresentationModel<{{classname}}> {{/hateoas}}{{/parent}} {{#serializableModel}}implements Serializable{{/serializableModel}} {
20+
public class {{classname}} {{#parent}}extends {{{.}}}{{/parent}}{{^parent}}{{#hateoas}}extends RepresentationModel<{{classname}}> {{/hateoas}}{{/parent}} {{^vendorExtensions.x-implements}}{{#serializableModel}}implements Serializable{{/serializableModel}}{{/vendorExtensions.x-implements}} {{#vendorExtensions.x-implements}}{{#-first}}implements {{#serializableModel}}Serializable, {{/serializableModel}}{{{.}}}{{/-first}}{{^-first}}, {{{.}}}{{/-first}}{{#-last}} {{/-last}}{{/vendorExtensions.x-implements}}{
2121
{{#serializableModel}}
2222

2323
private static final long serialVersionUID = 1L;
@@ -98,9 +98,7 @@ public class {{classname}} {{#parent}}extends {{{.}}}{{/parent}}{{^parent}}{{#ha
9898
{{/openApiNullable}}
9999
return this;
100100
}
101-
{{/isArray}}
102-
{{#isMap}}
103-
101+
{{/isArray}}{{#isMap}}{{#items.datatypeWithEnum}}
104102
public {{classname}} put{{nameInCamelCase}}Item(String key, {{{items.datatypeWithEnum}}} {{name}}Item) {
105103
{{^required}}
106104
if (this.{{name}} == null) {
@@ -110,10 +108,8 @@ public class {{classname}} {{#parent}}extends {{{.}}}{{/parent}}{{^parent}}{{#ha
110108
this.{{name}}.put(key, {{name}}Item);
111109
return this;
112110
}
113-
{{/isMap}}
114-
{{! end feature: fluent setter methods }}
111+
{{/items.datatypeWithEnum}}{{/isMap}}{{! end feature: fluent setter methods }}
115112
{{! begin feature: getter and setter }}
116-
117113
/**
118114
{{#description}}
119115
* {{{.}}}
Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,13 @@
11
{{#jackson}}
2-
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.EXISTING_PROPERTY, property = "{{{discriminator.propertyBaseName}}}", visible = true)
2+
3+
{{^vendorExtensions.x-deduction}}
4+
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.EXISTING_PROPERTY, property = "{{{discriminator.propertyBaseName}}}", visible = true){{/vendorExtensions.x-deduction}}{{#vendorExtensions.x-deduction}}
5+
@JsonTypeInfo(use = JsonTypeInfo.Id.DEDUCTION, visible = true){{/vendorExtensions.x-deduction}}
36
@JsonSubTypes({
47
{{#discriminator.mappedModels}}
58
@JsonSubTypes.Type(value = {{modelName}}.class, name = "{{^vendorExtensions.x-discriminator-value}}{{mappingName}}{{/vendorExtensions.x-discriminator-value}}{{#vendorExtensions.x-discriminator-value}}{{{vendorExtensions.x-discriminator-value}}}{{/vendorExtensions.x-discriminator-value}}"),
69
{{/discriminator.mappedModels}}
7-
}){{/jackson}}
10+
{{#vendorExtensions.x-deduction-model-names}}
11+
@JsonSubTypes.Type(value = {{.}}.class, name = "{{.}}"),
12+
{{/vendorExtensions.x-deduction-model-names}}
13+
}){{/jackson}}

0 commit comments

Comments
 (0)