Skip to content

Commit 0693a83

Browse files
Slavek Kabrdajirikuncar
andauthored
[go-experimental] Add oneOf support (#5150)
* [go-experimental] Add oneOf support * Fix docs for the oneOf models * isOneOfInterface => x-is-one-of-interface * Add proper warnings when inline models are used in oneOf choices * Add a convenience method to oneOf implementing structs to cast them as the oneOf interface * Update modules/openapi-generator/src/main/resources/go-experimental/model.mustache Co-Authored-By: Jiri Kuncar <jiri.kuncar@gmail.com> * Fix retrieving data from additionalDataMap * Add basic tests Co-authored-by: Jiri Kuncar <jiri.kuncar@gmail.com>
1 parent cd91a15 commit 0693a83

File tree

13 files changed

+556
-272
lines changed

13 files changed

+556
-272
lines changed

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

Lines changed: 174 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
import io.swagger.v3.core.util.Json;
2727
import io.swagger.v3.oas.models.OpenAPI;
2828
import io.swagger.v3.oas.models.Operation;
29+
import io.swagger.v3.oas.models.PathItem;
2930
import io.swagger.v3.oas.models.callbacks.Callback;
3031
import io.swagger.v3.oas.models.examples.Example;
3132
import io.swagger.v3.oas.models.headers.Header;
@@ -59,6 +60,7 @@
5960
import org.openapitools.codegen.templating.mustache.TitlecaseLambda;
6061
import org.openapitools.codegen.templating.mustache.UppercaseLambda;
6162
import org.openapitools.codegen.utils.ModelUtils;
63+
import org.openapitools.codegen.utils.OneOfImplementorAdditionalData;
6264
import org.slf4j.Logger;
6365
import org.slf4j.LoggerFactory;
6466

@@ -187,6 +189,11 @@ apiTemplateFiles are for API outputs only (controllers/handlers).
187189
// flag to indicate whether to use environment variable to post process file
188190
protected boolean enablePostProcessFile = false;
189191
private TemplatingEngineAdapter templatingEngine = new MustacheEngineAdapter();
192+
// flag to indicate whether to use the utils.OneOfImplementorAdditionalData related logic
193+
protected boolean useOneOfInterfaces = false;
194+
// whether or not the oneOf imports machinery should add oneOf interfaces as imports in implementing classes
195+
protected boolean addOneOfInterfaceImports = false;
196+
protected List<CodegenModel> addOneOfInterfaces = new ArrayList<CodegenModel>();
190197

191198
// flag to indicate whether to only update files whose contents have changed
192199
protected boolean enableMinimalUpdate = false;
@@ -326,6 +333,65 @@ private void registerMustacheLambdas() {
326333
// override with any special post-processing for all models
327334
@SuppressWarnings({"static-method", "unchecked"})
328335
public Map<String, Object> postProcessAllModels(Map<String, Object> objs) {
336+
if (this.useOneOfInterfaces) {
337+
// First, add newly created oneOf interfaces
338+
for (CodegenModel cm : addOneOfInterfaces) {
339+
Map<String, Object> modelValue = new HashMap<String, Object>() {{
340+
putAll(additionalProperties());
341+
put("model", cm);
342+
}};
343+
List<Object> modelsValue = Arrays.asList(modelValue);
344+
List<Map<String, String>> importsValue = new ArrayList<Map<String, String>>();
345+
Map<String, Object> objsValue = new HashMap<String, Object>() {{
346+
put("models", modelsValue);
347+
put("package", modelPackage());
348+
put("imports", importsValue);
349+
put("classname", cm.classname);
350+
putAll(additionalProperties);
351+
}};
352+
objs.put(cm.name, objsValue);
353+
}
354+
355+
// Gather data from all the models that contain oneOf into OneOfImplementorAdditionalData classes
356+
// (see docstring of that class to find out what information is gathered and why)
357+
Map<String, OneOfImplementorAdditionalData> additionalDataMap = new HashMap<String, OneOfImplementorAdditionalData>();
358+
for (Map.Entry modelsEntry : objs.entrySet()) {
359+
Map<String, Object> modelsAttrs = (Map<String, Object>) modelsEntry.getValue();
360+
List<Object> models = (List<Object>) modelsAttrs.get("models");
361+
List<Map<String, String>> modelsImports = (List<Map<String, String>>) modelsAttrs.getOrDefault("imports", new ArrayList<Map<String, String>>());
362+
for (Object _mo : models) {
363+
Map<String, Object> mo = (Map<String, Object>) _mo;
364+
CodegenModel cm = (CodegenModel) mo.get("model");
365+
if (cm.oneOf.size() > 0) {
366+
cm.vendorExtensions.put("x-is-one-of-interface", true);
367+
for (String one : cm.oneOf) {
368+
if (!additionalDataMap.containsKey(one)) {
369+
additionalDataMap.put(one, new OneOfImplementorAdditionalData(one));
370+
}
371+
additionalDataMap.get(one).addFromInterfaceModel(cm, modelsImports);
372+
}
373+
// if this is oneOf interface, make sure we include the necessary imports for it
374+
addImportsToOneOfInterface(modelsImports);
375+
}
376+
}
377+
}
378+
379+
// Add all the data from OneOfImplementorAdditionalData classes to the implementing models
380+
for (Map.Entry modelsEntry : objs.entrySet()) {
381+
Map<String, Object> modelsAttrs = (Map<String, Object>) modelsEntry.getValue();
382+
List<Object> models = (List<Object>) modelsAttrs.get("models");
383+
List<Map<String, String>> imports = (List<Map<String, String>>) modelsAttrs.get("imports");
384+
for (Object _implmo : models) {
385+
Map<String, Object> implmo = (Map<String, Object>) _implmo;
386+
CodegenModel implcm = (CodegenModel) implmo.get("model");
387+
String modelName = toModelName(implcm.name);
388+
if (additionalDataMap.containsKey(modelName)) {
389+
additionalDataMap.get(modelName).addToImplementor(this, implcm, imports, addOneOfInterfaceImports);
390+
}
391+
}
392+
}
393+
}
394+
329395
return objs;
330396
}
331397

@@ -626,6 +692,62 @@ public void postProcessParameter(CodegenParameter parameter) {
626692
//override with any special handling of the entire OpenAPI spec document
627693
@SuppressWarnings("unused")
628694
public void preprocessOpenAPI(OpenAPI openAPI) {
695+
if (useOneOfInterfaces) {
696+
// we process the openapi schema here to find oneOf schemas and create interface models for them
697+
Map<String, Schema> schemas = new HashMap<String, Schema>(openAPI.getComponents().getSchemas());
698+
if (schemas == null) {
699+
schemas = new HashMap<String, Schema>();
700+
}
701+
Map<String, PathItem> pathItems = openAPI.getPaths();
702+
703+
// we need to add all request and response bodies to processed schemas
704+
if (pathItems != null) {
705+
for (Map.Entry<String, PathItem> e : pathItems.entrySet()) {
706+
for (Map.Entry<PathItem.HttpMethod, Operation> op : e.getValue().readOperationsMap().entrySet()) {
707+
String opId = getOrGenerateOperationId(op.getValue(), e.getKey(), op.getKey().toString());
708+
// process request body
709+
RequestBody b = ModelUtils.getReferencedRequestBody(openAPI, op.getValue().getRequestBody());
710+
Schema requestSchema = null;
711+
if (b != null) {
712+
requestSchema = ModelUtils.getSchemaFromRequestBody(b);
713+
}
714+
if (requestSchema != null) {
715+
schemas.put(opId, requestSchema);
716+
}
717+
// process all response bodies
718+
for (Map.Entry<String, ApiResponse> ar : op.getValue().getResponses().entrySet()) {
719+
ApiResponse a = ModelUtils.getReferencedApiResponse(openAPI, ar.getValue());
720+
Schema responseSchema = ModelUtils.getSchemaFromResponse(a);
721+
if (responseSchema != null) {
722+
schemas.put(opId + ar.getKey(), responseSchema);
723+
}
724+
}
725+
}
726+
}
727+
}
728+
729+
// go through all gathered schemas and add them as interfaces to be created
730+
for (Map.Entry<String, Schema> e : schemas.entrySet()) {
731+
String n = toModelName(e.getKey());
732+
Schema s = e.getValue();
733+
String nOneOf = toModelName(n + "OneOf");
734+
if (ModelUtils.isComposedSchema(s)) {
735+
addOneOfNameExtension((ComposedSchema) s, n);
736+
} else if (ModelUtils.isArraySchema(s)) {
737+
Schema items = ((ArraySchema) s).getItems();
738+
if (ModelUtils.isComposedSchema(items)) {
739+
addOneOfNameExtension((ComposedSchema) items, nOneOf);
740+
addOneOfInterfaceModel((ComposedSchema) items, nOneOf);
741+
}
742+
} else if (ModelUtils.isMapSchema(s)) {
743+
Schema addProps = ModelUtils.getAdditionalProperties(s);
744+
if (addProps != null && ModelUtils.isComposedSchema(addProps)) {
745+
addOneOfNameExtension((ComposedSchema) addProps, nOneOf);
746+
addOneOfInterfaceModel((ComposedSchema) addProps, nOneOf);
747+
}
748+
}
749+
}
750+
}
629751
}
630752

631753
// override with any special handling of the entire OpenAPI spec document
@@ -950,6 +1072,12 @@ public void setAllowUnicodeIdentifiers(Boolean allowUnicodeIdentifiers) {
9501072
this.allowUnicodeIdentifiers = allowUnicodeIdentifiers;
9511073
}
9521074

1075+
public Boolean getUseOneOfInterfaces() { return useOneOfInterfaces; }
1076+
1077+
public void setUseOneOfInterfaces(Boolean useOneOfInterfaces) {
1078+
this.useOneOfInterfaces = useOneOfInterfaces;
1079+
}
1080+
9531081
/**
9541082
* Return the regular expression/JSON schema pattern (http://json-schema.org/latest/json-schema-validation.html#anchor33)
9551083
*
@@ -5534,4 +5662,49 @@ public boolean isRemoveEnumValuePrefix() {
55345662
public void setRemoveEnumValuePrefix(final boolean removeEnumValuePrefix) {
55355663
this.removeEnumValuePrefix = removeEnumValuePrefix;
55365664
}
5537-
}
5665+
5666+
//// Following methods are related to the "useOneOfInterfaces" feature
5667+
/**
5668+
* Add "x-oneOf-name" extension to a given oneOf schema (assuming it has at least 1 oneOf elements)
5669+
* @param s schema to add the extension to
5670+
* @param name name of the parent oneOf schema
5671+
*/
5672+
public void addOneOfNameExtension(ComposedSchema s, String name) {
5673+
if (s.getOneOf() != null && s.getOneOf().size() > 0) {
5674+
s.addExtension("x-oneOf-name", name);
5675+
}
5676+
}
5677+
5678+
/**
5679+
* Add a given ComposedSchema as an interface model to be generated
5680+
* @param cs ComposedSchema object to create as interface model
5681+
* @param type name to use for the generated interface model
5682+
*/
5683+
public void addOneOfInterfaceModel(ComposedSchema cs, String type) {
5684+
CodegenModel cm = new CodegenModel();
5685+
5686+
cm.discriminator = createDiscriminator("", (Schema) cs);
5687+
for (Schema o : cs.getOneOf()) {
5688+
if (o.get$ref() == null) {
5689+
if (cm.discriminator != null && o.get$ref() == null) {
5690+
// OpenAPI spec states that inline objects should not be considered when discriminator is used
5691+
// https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#discriminatorObject
5692+
LOGGER.warn("Ignoring inline object in oneOf definition of {}, since discriminator is used", type);
5693+
} else {
5694+
LOGGER.warn("Inline models are not supported in oneOf definition right now");
5695+
}
5696+
continue;
5697+
}
5698+
cm.oneOf.add(toModelName(ModelUtils.getSimpleRef(o.get$ref())));
5699+
}
5700+
cm.name = type;
5701+
cm.classname = type;
5702+
cm.vendorExtensions.put("x-is-one-of-interface", true);
5703+
cm.interfaceModels = new ArrayList<CodegenModel>();
5704+
5705+
addOneOfInterfaces.add(cm);
5706+
}
5707+
5708+
public void addImportsToOneOfInterface(List<Map<String, String>> imports) {}
5709+
//// End of methods related to the "useOneOfInterfaces" feature
5710+
}

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -522,6 +522,7 @@ public void processOpts() {
522522

523523
@Override
524524
public Map<String, Object> postProcessAllModels(Map<String, Object> objs) {
525+
objs = super.postProcessAllModels(objs);
525526
objs = super.updateAllModels(objs);
526527

527528
if (!additionalModelTypeAnnotations.isEmpty()) {
@@ -1067,6 +1068,7 @@ public Map<String, Object> postProcessOperationsWithModels(Map<String, Object> o
10671068

10681069
@Override
10691070
public void preprocessOpenAPI(OpenAPI openAPI) {
1071+
super.preprocessOpenAPI(openAPI);
10701072
if (openAPI == null) {
10711073
return;
10721074
}

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

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@
2727
import org.slf4j.Logger;
2828
import org.slf4j.LoggerFactory;
2929

30+
import java.util.Arrays;
31+
import java.util.HashMap;
3032
import java.util.List;
3133
import java.util.Map;
3234

@@ -42,6 +44,7 @@ public GoClientExperimentalCodegen() {
4244
embeddedTemplateDir = templateDir = "go-experimental";
4345

4446
usesOptionals = false;
47+
useOneOfInterfaces = true;
4548

4649
generatorMetadata = GeneratorMetadata.newBuilder(generatorMetadata).stability(Stability.EXPERIMENTAL).build();
4750
}
@@ -57,6 +60,11 @@ public String getName() {
5760
return "go-experimental";
5861
}
5962

63+
@Override
64+
public String toGetter(String name) {
65+
return "Get" + getterAndSetterCapitalize(name);
66+
}
67+
6068
/**
6169
* Returns human-friendly help for the generator. Provide the consumer with help
6270
* tips, parameters here
@@ -125,4 +133,16 @@ public Map<String, Object> postProcessModels(Map<String, Object> objs) {
125133
objs = super.postProcessModels(objs);
126134
return objs;
127135
}
136+
137+
@Override
138+
public void addImportsToOneOfInterface(List<Map<String, String>> imports) {
139+
for (String i : Arrays.asList("fmt")) {
140+
Map<String, String> oneImport = new HashMap<String, String>() {{
141+
put("import", i);
142+
}};
143+
if (!imports.contains(oneImport)) {
144+
imports.add(oneImport);
145+
}
146+
}
147+
}
128148
}

0 commit comments

Comments
 (0)