[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>
This commit is contained in:
Slavek Kabrda 2020-02-10 22:26:32 +01:00 committed by GitHub
parent cd91a15953
commit 0693a83cfe
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 556 additions and 272 deletions

View File

@ -26,6 +26,7 @@ import com.samskivert.mustache.Mustache.Lambda;
import io.swagger.v3.core.util.Json; import io.swagger.v3.core.util.Json;
import io.swagger.v3.oas.models.OpenAPI; import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.Operation; import io.swagger.v3.oas.models.Operation;
import io.swagger.v3.oas.models.PathItem;
import io.swagger.v3.oas.models.callbacks.Callback; import io.swagger.v3.oas.models.callbacks.Callback;
import io.swagger.v3.oas.models.examples.Example; import io.swagger.v3.oas.models.examples.Example;
import io.swagger.v3.oas.models.headers.Header; import io.swagger.v3.oas.models.headers.Header;
@ -59,6 +60,7 @@ import org.openapitools.codegen.templating.mustache.LowercaseLambda;
import org.openapitools.codegen.templating.mustache.TitlecaseLambda; import org.openapitools.codegen.templating.mustache.TitlecaseLambda;
import org.openapitools.codegen.templating.mustache.UppercaseLambda; import org.openapitools.codegen.templating.mustache.UppercaseLambda;
import org.openapitools.codegen.utils.ModelUtils; import org.openapitools.codegen.utils.ModelUtils;
import org.openapitools.codegen.utils.OneOfImplementorAdditionalData;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
@ -187,6 +189,11 @@ public class DefaultCodegen implements CodegenConfig {
// flag to indicate whether to use environment variable to post process file // flag to indicate whether to use environment variable to post process file
protected boolean enablePostProcessFile = false; protected boolean enablePostProcessFile = false;
private TemplatingEngineAdapter templatingEngine = new MustacheEngineAdapter(); private TemplatingEngineAdapter templatingEngine = new MustacheEngineAdapter();
// flag to indicate whether to use the utils.OneOfImplementorAdditionalData related logic
protected boolean useOneOfInterfaces = false;
// whether or not the oneOf imports machinery should add oneOf interfaces as imports in implementing classes
protected boolean addOneOfInterfaceImports = false;
protected List<CodegenModel> addOneOfInterfaces = new ArrayList<CodegenModel>();
// flag to indicate whether to only update files whose contents have changed // flag to indicate whether to only update files whose contents have changed
protected boolean enableMinimalUpdate = false; protected boolean enableMinimalUpdate = false;
@ -326,6 +333,65 @@ public class DefaultCodegen implements CodegenConfig {
// override with any special post-processing for all models // override with any special post-processing for all models
@SuppressWarnings({"static-method", "unchecked"}) @SuppressWarnings({"static-method", "unchecked"})
public Map<String, Object> postProcessAllModels(Map<String, Object> objs) { public Map<String, Object> postProcessAllModels(Map<String, Object> objs) {
if (this.useOneOfInterfaces) {
// First, add newly created oneOf interfaces
for (CodegenModel cm : addOneOfInterfaces) {
Map<String, Object> modelValue = new HashMap<String, Object>() {{
putAll(additionalProperties());
put("model", cm);
}};
List<Object> modelsValue = Arrays.asList(modelValue);
List<Map<String, String>> importsValue = new ArrayList<Map<String, String>>();
Map<String, Object> objsValue = new HashMap<String, Object>() {{
put("models", modelsValue);
put("package", modelPackage());
put("imports", importsValue);
put("classname", cm.classname);
putAll(additionalProperties);
}};
objs.put(cm.name, objsValue);
}
// Gather data from all the models that contain oneOf into OneOfImplementorAdditionalData classes
// (see docstring of that class to find out what information is gathered and why)
Map<String, OneOfImplementorAdditionalData> additionalDataMap = new HashMap<String, OneOfImplementorAdditionalData>();
for (Map.Entry modelsEntry : objs.entrySet()) {
Map<String, Object> modelsAttrs = (Map<String, Object>) modelsEntry.getValue();
List<Object> models = (List<Object>) modelsAttrs.get("models");
List<Map<String, String>> modelsImports = (List<Map<String, String>>) modelsAttrs.getOrDefault("imports", new ArrayList<Map<String, String>>());
for (Object _mo : models) {
Map<String, Object> mo = (Map<String, Object>) _mo;
CodegenModel cm = (CodegenModel) mo.get("model");
if (cm.oneOf.size() > 0) {
cm.vendorExtensions.put("x-is-one-of-interface", true);
for (String one : cm.oneOf) {
if (!additionalDataMap.containsKey(one)) {
additionalDataMap.put(one, new OneOfImplementorAdditionalData(one));
}
additionalDataMap.get(one).addFromInterfaceModel(cm, modelsImports);
}
// if this is oneOf interface, make sure we include the necessary imports for it
addImportsToOneOfInterface(modelsImports);
}
}
}
// Add all the data from OneOfImplementorAdditionalData classes to the implementing models
for (Map.Entry modelsEntry : objs.entrySet()) {
Map<String, Object> modelsAttrs = (Map<String, Object>) modelsEntry.getValue();
List<Object> models = (List<Object>) modelsAttrs.get("models");
List<Map<String, String>> imports = (List<Map<String, String>>) modelsAttrs.get("imports");
for (Object _implmo : models) {
Map<String, Object> implmo = (Map<String, Object>) _implmo;
CodegenModel implcm = (CodegenModel) implmo.get("model");
String modelName = toModelName(implcm.name);
if (additionalDataMap.containsKey(modelName)) {
additionalDataMap.get(modelName).addToImplementor(this, implcm, imports, addOneOfInterfaceImports);
}
}
}
}
return objs; return objs;
} }
@ -626,6 +692,62 @@ public class DefaultCodegen implements CodegenConfig {
//override with any special handling of the entire OpenAPI spec document //override with any special handling of the entire OpenAPI spec document
@SuppressWarnings("unused") @SuppressWarnings("unused")
public void preprocessOpenAPI(OpenAPI openAPI) { public void preprocessOpenAPI(OpenAPI openAPI) {
if (useOneOfInterfaces) {
// we process the openapi schema here to find oneOf schemas and create interface models for them
Map<String, Schema> schemas = new HashMap<String, Schema>(openAPI.getComponents().getSchemas());
if (schemas == null) {
schemas = new HashMap<String, Schema>();
}
Map<String, PathItem> pathItems = openAPI.getPaths();
// we need to add all request and response bodies to processed schemas
if (pathItems != null) {
for (Map.Entry<String, PathItem> e : pathItems.entrySet()) {
for (Map.Entry<PathItem.HttpMethod, Operation> op : e.getValue().readOperationsMap().entrySet()) {
String opId = getOrGenerateOperationId(op.getValue(), e.getKey(), op.getKey().toString());
// process request body
RequestBody b = ModelUtils.getReferencedRequestBody(openAPI, op.getValue().getRequestBody());
Schema requestSchema = null;
if (b != null) {
requestSchema = ModelUtils.getSchemaFromRequestBody(b);
}
if (requestSchema != null) {
schemas.put(opId, requestSchema);
}
// process all response bodies
for (Map.Entry<String, ApiResponse> ar : op.getValue().getResponses().entrySet()) {
ApiResponse a = ModelUtils.getReferencedApiResponse(openAPI, ar.getValue());
Schema responseSchema = ModelUtils.getSchemaFromResponse(a);
if (responseSchema != null) {
schemas.put(opId + ar.getKey(), responseSchema);
}
}
}
}
}
// go through all gathered schemas and add them as interfaces to be created
for (Map.Entry<String, Schema> e : schemas.entrySet()) {
String n = toModelName(e.getKey());
Schema s = e.getValue();
String nOneOf = toModelName(n + "OneOf");
if (ModelUtils.isComposedSchema(s)) {
addOneOfNameExtension((ComposedSchema) s, n);
} else if (ModelUtils.isArraySchema(s)) {
Schema items = ((ArraySchema) s).getItems();
if (ModelUtils.isComposedSchema(items)) {
addOneOfNameExtension((ComposedSchema) items, nOneOf);
addOneOfInterfaceModel((ComposedSchema) items, nOneOf);
}
} else if (ModelUtils.isMapSchema(s)) {
Schema addProps = ModelUtils.getAdditionalProperties(s);
if (addProps != null && ModelUtils.isComposedSchema(addProps)) {
addOneOfNameExtension((ComposedSchema) addProps, nOneOf);
addOneOfInterfaceModel((ComposedSchema) addProps, nOneOf);
}
}
}
}
} }
// override with any special handling of the entire OpenAPI spec document // override with any special handling of the entire OpenAPI spec document
@ -950,6 +1072,12 @@ public class DefaultCodegen implements CodegenConfig {
this.allowUnicodeIdentifiers = allowUnicodeIdentifiers; this.allowUnicodeIdentifiers = allowUnicodeIdentifiers;
} }
public Boolean getUseOneOfInterfaces() { return useOneOfInterfaces; }
public void setUseOneOfInterfaces(Boolean useOneOfInterfaces) {
this.useOneOfInterfaces = useOneOfInterfaces;
}
/** /**
* Return the regular expression/JSON schema pattern (http://json-schema.org/latest/json-schema-validation.html#anchor33) * Return the regular expression/JSON schema pattern (http://json-schema.org/latest/json-schema-validation.html#anchor33)
* *
@ -5534,4 +5662,49 @@ public class DefaultCodegen implements CodegenConfig {
public void setRemoveEnumValuePrefix(final boolean removeEnumValuePrefix) { public void setRemoveEnumValuePrefix(final boolean removeEnumValuePrefix) {
this.removeEnumValuePrefix = removeEnumValuePrefix; this.removeEnumValuePrefix = removeEnumValuePrefix;
} }
//// Following methods are related to the "useOneOfInterfaces" feature
/**
* Add "x-oneOf-name" extension to a given oneOf schema (assuming it has at least 1 oneOf elements)
* @param s schema to add the extension to
* @param name name of the parent oneOf schema
*/
public void addOneOfNameExtension(ComposedSchema s, String name) {
if (s.getOneOf() != null && s.getOneOf().size() > 0) {
s.addExtension("x-oneOf-name", name);
}
}
/**
* Add a given ComposedSchema as an interface model to be generated
* @param cs ComposedSchema object to create as interface model
* @param type name to use for the generated interface model
*/
public void addOneOfInterfaceModel(ComposedSchema cs, String type) {
CodegenModel cm = new CodegenModel();
cm.discriminator = createDiscriminator("", (Schema) cs);
for (Schema o : cs.getOneOf()) {
if (o.get$ref() == null) {
if (cm.discriminator != null && o.get$ref() == null) {
// OpenAPI spec states that inline objects should not be considered when discriminator is used
// https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#discriminatorObject
LOGGER.warn("Ignoring inline object in oneOf definition of {}, since discriminator is used", type);
} else {
LOGGER.warn("Inline models are not supported in oneOf definition right now");
}
continue;
}
cm.oneOf.add(toModelName(ModelUtils.getSimpleRef(o.get$ref())));
}
cm.name = type;
cm.classname = type;
cm.vendorExtensions.put("x-is-one-of-interface", true);
cm.interfaceModels = new ArrayList<CodegenModel>();
addOneOfInterfaces.add(cm);
}
public void addImportsToOneOfInterface(List<Map<String, String>> imports) {}
//// End of methods related to the "useOneOfInterfaces" feature
} }

View File

@ -522,6 +522,7 @@ public abstract class AbstractJavaCodegen extends DefaultCodegen implements Code
@Override @Override
public Map<String, Object> postProcessAllModels(Map<String, Object> objs) { public Map<String, Object> postProcessAllModels(Map<String, Object> objs) {
objs = super.postProcessAllModels(objs);
objs = super.updateAllModels(objs); objs = super.updateAllModels(objs);
if (!additionalModelTypeAnnotations.isEmpty()) { if (!additionalModelTypeAnnotations.isEmpty()) {
@ -1067,6 +1068,7 @@ public abstract class AbstractJavaCodegen extends DefaultCodegen implements Code
@Override @Override
public void preprocessOpenAPI(OpenAPI openAPI) { public void preprocessOpenAPI(OpenAPI openAPI) {
super.preprocessOpenAPI(openAPI);
if (openAPI == null) { if (openAPI == null) {
return; return;
} }

View File

@ -27,6 +27,8 @@ import org.openapitools.codegen.utils.ProcessUtils;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
@ -42,6 +44,7 @@ public class GoClientExperimentalCodegen extends GoClientCodegen {
embeddedTemplateDir = templateDir = "go-experimental"; embeddedTemplateDir = templateDir = "go-experimental";
usesOptionals = false; usesOptionals = false;
useOneOfInterfaces = true;
generatorMetadata = GeneratorMetadata.newBuilder(generatorMetadata).stability(Stability.EXPERIMENTAL).build(); generatorMetadata = GeneratorMetadata.newBuilder(generatorMetadata).stability(Stability.EXPERIMENTAL).build();
} }
@ -57,6 +60,11 @@ public class GoClientExperimentalCodegen extends GoClientCodegen {
return "go-experimental"; return "go-experimental";
} }
@Override
public String toGetter(String name) {
return "Get" + getterAndSetterCapitalize(name);
}
/** /**
* Returns human-friendly help for the generator. Provide the consumer with help * Returns human-friendly help for the generator. Provide the consumer with help
* tips, parameters here * tips, parameters here
@ -125,4 +133,16 @@ public class GoClientExperimentalCodegen extends GoClientCodegen {
objs = super.postProcessModels(objs); objs = super.postProcessModels(objs);
return objs; return objs;
} }
@Override
public void addImportsToOneOfInterface(List<Map<String, String>> imports) {
for (String i : Arrays.asList("fmt")) {
Map<String, String> oneImport = new HashMap<String, String>() {{
put("import", i);
}};
if (!imports.contains(oneImport)) {
imports.add(oneImport);
}
}
}
} }

View File

@ -25,19 +25,12 @@ import org.openapitools.codegen.languages.features.GzipFeatures;
import org.openapitools.codegen.languages.features.PerformBeanValidationFeatures; import org.openapitools.codegen.languages.features.PerformBeanValidationFeatures;
import org.openapitools.codegen.meta.features.DocumentationFeature; import org.openapitools.codegen.meta.features.DocumentationFeature;
import org.openapitools.codegen.templating.mustache.CaseFormatLambda; import org.openapitools.codegen.templating.mustache.CaseFormatLambda;
import org.openapitools.codegen.utils.ModelUtils;
import org.openapitools.codegen.utils.ProcessUtils; import org.openapitools.codegen.utils.ProcessUtils;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import io.swagger.v3.oas.models.Operation; import io.swagger.v3.oas.models.Operation;
import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.PathItem;
import io.swagger.v3.oas.models.media.ArraySchema;
import io.swagger.v3.oas.models.media.ComposedSchema;
import io.swagger.v3.oas.models.media.Schema; import io.swagger.v3.oas.models.media.Schema;
import io.swagger.v3.oas.models.parameters.RequestBody;
import io.swagger.v3.oas.models.responses.ApiResponse;
import java.io.File; import java.io.File;
import java.util.*; import java.util.*;
@ -111,9 +104,6 @@ public class JavaClientCodegen extends AbstractJavaCodegen
protected String authFolder; protected String authFolder;
protected String serializationLibrary = null; protected String serializationLibrary = null;
protected boolean useOneOfInterfaces = false;
protected List<CodegenModel> addOneOfInterfaces = new ArrayList<CodegenModel>();
public JavaClientCodegen() { public JavaClientCodegen() {
super(); super();
@ -501,6 +491,7 @@ public class JavaClientCodegen extends AbstractJavaCodegen
if (additionalProperties.containsKey(SERIALIZATION_LIBRARY_JACKSON)) { if (additionalProperties.containsKey(SERIALIZATION_LIBRARY_JACKSON)) {
useOneOfInterfaces = true; useOneOfInterfaces = true;
addOneOfInterfaceImports = true;
} }
} }
@ -846,259 +837,6 @@ public class JavaClientCodegen extends AbstractJavaCodegen
} }
} }
public void addOneOfNameExtension(Schema s, String name) {
ComposedSchema cs = (ComposedSchema) s;
if (cs.getOneOf() != null && cs.getOneOf().size() > 0) {
cs.addExtension("x-oneOf-name", name);
}
}
public void addOneOfInterfaceModel(ComposedSchema cs, String type) {
CodegenModel cm = new CodegenModel();
// TODO: 5.0: Remove the camelCased vendorExtension below and ensure templates use the newer property naming.
once(LOGGER).warn("4.3.0 has deprecated the use of vendor extensions which don't follow lower-kebab casing standards with x- prefix.");
for (Schema o : cs.getOneOf()) {
// TODO: inline objects
cm.oneOf.add(toModelName(ModelUtils.getSimpleRef(o.get$ref())));
}
cm.name = type;
cm.classname = type;
cm.vendorExtensions.put("isOneOfInterface", true); // TODO: 5.0 Remove
cm.vendorExtensions.put("x-is-one-of-interface", true);
cm.discriminator = createDiscriminator("", (Schema) cs);
cm.interfaceModels = new ArrayList<CodegenModel>();
addOneOfInterfaces.add(cm);
}
@Override
public void preprocessOpenAPI(OpenAPI openAPI) {
// we process the openapi schema here to find oneOf schemas here and create interface models for them
super.preprocessOpenAPI(openAPI);
Map<String, Schema> schemas = new HashMap<String, Schema>(openAPI.getComponents().getSchemas());
if (schemas == null) {
schemas = new HashMap<String, Schema>();
}
Map<String, PathItem> pathItems = openAPI.getPaths();
// we need to add all request and response bodies to processed schemas
if (pathItems != null) {
for (Map.Entry<String, PathItem> e : pathItems.entrySet()) {
for (Map.Entry<PathItem.HttpMethod, Operation> op : e.getValue().readOperationsMap().entrySet()) {
String opId = getOrGenerateOperationId(op.getValue(), e.getKey(), op.getKey().toString());
// process request body
RequestBody b = ModelUtils.getReferencedRequestBody(openAPI, op.getValue().getRequestBody());
Schema requestSchema = null;
if (b != null) {
requestSchema = ModelUtils.getSchemaFromRequestBody(b);
}
if (requestSchema != null) {
schemas.put(opId, requestSchema);
}
// process all response bodies
for (Map.Entry<String, ApiResponse> ar : op.getValue().getResponses().entrySet()) {
ApiResponse a = ModelUtils.getReferencedApiResponse(openAPI, ar.getValue());
Schema responseSchema = ModelUtils.getSchemaFromResponse(a);
if (responseSchema != null) {
schemas.put(opId + ar.getKey(), responseSchema);
}
}
}
}
}
for (Map.Entry<String, Schema> e : schemas.entrySet()) {
String n = toModelName(e.getKey());
Schema s = e.getValue();
String nOneOf = toModelName(n + "OneOf");
if (ModelUtils.isComposedSchema(s)) {
addOneOfNameExtension(s, n);
} else if (ModelUtils.isArraySchema(s)) {
Schema items = ((ArraySchema) s).getItems();
if (ModelUtils.isComposedSchema(items)) {
addOneOfNameExtension(items, nOneOf);
addOneOfInterfaceModel((ComposedSchema) items, nOneOf);
}
} else if (ModelUtils.isMapSchema(s)) {
Schema addProps = ModelUtils.getAdditionalProperties(s);
if (addProps != null && ModelUtils.isComposedSchema(addProps)) {
addOneOfNameExtension(addProps, nOneOf);
addOneOfInterfaceModel((ComposedSchema) addProps, nOneOf);
}
}
}
}
private class OneOfImplementorAdditionalData {
private String implementorName;
private List<String> additionalInterfaces = new ArrayList<String>();
private List<CodegenProperty> additionalProps = new ArrayList<CodegenProperty>();
private List<Map<String, String>> additionalImports = new ArrayList<Map<String, String>>();
public OneOfImplementorAdditionalData(String implementorName) {
this.implementorName = implementorName;
}
public String getImplementorName() {
return implementorName;
}
public void addFromInterfaceModel(CodegenModel cm, List<Map<String, String>> modelsImports) {
// Add cm as implemented interface
additionalInterfaces.add(cm.classname);
// Add all vars defined on cm
// a "oneOf" model (cm) by default inherits all properties from its "interfaceModels",
// but we only want to add properties defined on cm itself
List<CodegenProperty> toAdd = new ArrayList<CodegenProperty>(cm.vars);
// note that we can't just toAdd.removeAll(m.vars) for every interfaceModel,
// as they might have different value of `hasMore` and thus are not equal
List<String> omitAdding = new ArrayList<String>();
for (CodegenModel m : cm.interfaceModels) {
for (CodegenProperty v : m.vars) {
omitAdding.add(v.baseName);
}
}
for (CodegenProperty v : toAdd) {
if (!omitAdding.contains(v.baseName)) {
additionalProps.add(v.clone());
}
}
// Add all imports of cm
for (Map<String, String> importMap : modelsImports) {
// we're ok with shallow clone here, because imports are strings only
additionalImports.add(new HashMap<String, String>(importMap));
}
}
public void addToImplementor(CodegenModel implcm, List<Map<String, String>> implImports) {
implcm.getVendorExtensions().putIfAbsent("implements", new ArrayList<String>());
// Add implemented interfaces
for (String intf : additionalInterfaces) {
List<String> impl = (List<String>) implcm.getVendorExtensions().get("implements");
impl.add(intf);
// Add imports for interfaces
implcm.imports.add(intf);
Map<String, String> importsItem = new HashMap<String, String>();
importsItem.put("import", toModelImport(intf));
implImports.add(importsItem);
}
// Add oneOf-containing models properties - we need to properly set the hasMore values to make renderind correct
if (implcm.vars.size() > 0 && additionalProps.size() > 0) {
implcm.vars.get(implcm.vars.size() - 1).hasMore = true;
}
for (int i = 0; i < additionalProps.size(); i++) {
CodegenProperty var = additionalProps.get(i);
if (i == additionalProps.size() - 1) {
var.hasMore = false;
} else {
var.hasMore = true;
}
implcm.vars.add(var);
}
// Add imports
for (Map<String, String> oneImport : additionalImports) {
// exclude imports from this package - these are imports that only the oneOf interface needs
if (!implImports.contains(oneImport) && !oneImport.getOrDefault("import", "").startsWith(modelPackage())) {
implImports.add(oneImport);
}
}
}
}
@Override
public Map<String, Object> postProcessAllModels(Map<String, Object> objs) {
objs = super.postProcessAllModels(objs);
// TODO: 5.0: Remove the camelCased vendorExtension below and ensure templates use the newer property naming.
once(LOGGER).warn("4.3.0 has deprecated the use of vendor extensions which don't follow lower-kebab casing standards with x- prefix.");
if (this.useOneOfInterfaces) {
// First, add newly created oneOf interfaces
for (CodegenModel cm : addOneOfInterfaces) {
Map<String, Object> modelValue = new HashMap<String, Object>() {{
putAll(additionalProperties());
put("model", cm);
}};
List<Object> modelsValue = Arrays.asList(modelValue);
List<Map<String, String>> importsValue = new ArrayList<Map<String, String>>();
for (String i : Arrays.asList("JsonSubTypes", "JsonTypeInfo")) {
Map<String, String> oneImport = new HashMap<String, String>() {{
put("import", importMapping.get(i));
}};
importsValue.add(oneImport);
}
Map<String, Object> objsValue = new HashMap<String, Object>() {{
put("models", modelsValue);
put("package", modelPackage());
put("imports", importsValue);
put("classname", cm.classname);
putAll(additionalProperties);
}};
objs.put(cm.name, objsValue);
}
// - Add all "oneOf" models as interfaces to be implemented by the models that
// are the choices in "oneOf"; also mark the models containing "oneOf" as interfaces
// - Add all properties of "oneOf" to the implementing classes (NOTE that this
// would be problematic if the class was in multiple such "oneOf" models, in which
// case it would get all their properties, but it's probably better than not doing this)
// - Add all imports of "oneOf" model to all the implementing classes (this might not
// be optimal, as it can contain more than necessary, but it's good enough)
Map<String, OneOfImplementorAdditionalData> additionalDataMap = new HashMap<String, OneOfImplementorAdditionalData>();
for (Map.Entry modelsEntry : objs.entrySet()) {
Map<String, Object> modelsAttrs = (Map<String, Object>) modelsEntry.getValue();
List<Object> models = (List<Object>) modelsAttrs.get("models");
List<Map<String, String>> modelsImports = (List<Map<String, String>>) modelsAttrs.getOrDefault("imports", new ArrayList<Map<String, String>>());
for (Object _mo : models) {
Map<String, Object> mo = (Map<String, Object>) _mo;
CodegenModel cm = (CodegenModel) mo.get("model");
if (cm.oneOf.size() > 0) {
cm.vendorExtensions.put("isOneOfInterface", true); // TODO: 5.0 Remove
cm.vendorExtensions.put("x-is-one-of-interface", true);
// if this is oneOf interface, make sure we include the necessary jackson imports for it
for (String s : Arrays.asList("JsonTypeInfo", "JsonSubTypes")) {
Map<String, String> i = new HashMap<String, String>() {{
put("import", importMapping.get(s));
}};
if (!modelsImports.contains(i)) {
modelsImports.add(i);
}
}
for (String one : cm.oneOf) {
if (!additionalDataMap.containsKey(one)) {
additionalDataMap.put(one, new OneOfImplementorAdditionalData(one));
}
additionalDataMap.get(one).addFromInterfaceModel(cm, modelsImports);
}
}
}
}
for (Map.Entry modelsEntry : objs.entrySet()) {
Map<String, Object> modelsAttrs = (Map<String, Object>) modelsEntry.getValue();
List<Object> models = (List<Object>) modelsAttrs.get("models");
List<Map<String, String>> imports = (List<Map<String, String>>) modelsAttrs.get("imports");
for (Object _implmo : models) {
Map<String, Object> implmo = (Map<String, Object>) _implmo;
CodegenModel implcm = (CodegenModel) implmo.get("model");
if (additionalDataMap.containsKey(implcm.name)) {
additionalDataMap.get(implcm.name).addToImplementor(implcm, imports);
}
}
}
}
return objs;
}
public void forceSerializationLibrary(String serializationLibrary) { public void forceSerializationLibrary(String serializationLibrary) {
if((this.serializationLibrary != null) && !this.serializationLibrary.equalsIgnoreCase(serializationLibrary)) { if((this.serializationLibrary != null) && !this.serializationLibrary.equalsIgnoreCase(serializationLibrary)) {
LOGGER.warn("The configured serializationLibrary '" + this.serializationLibrary + "', is not supported by the library: '" + getLibrary() + "', switching back to: " + serializationLibrary); LOGGER.warn("The configured serializationLibrary '" + this.serializationLibrary + "', is not supported by the library: '" + getLibrary() + "', switching back to: " + serializationLibrary);
@ -1138,4 +876,16 @@ public class JavaClientCodegen extends AbstractJavaCodegen
} }
return apiVarName; return apiVarName;
} }
@Override
public void addImportsToOneOfInterface(List<Map<String, String>> imports) {
for (String i : Arrays.asList("JsonSubTypes", "JsonTypeInfo")) {
Map<String, String> oneImport = new HashMap<String, String>() {{
put("import", importMapping.get(i));
}};
if (!imports.contains(oneImport)) {
imports.add(oneImport);
}
}
}
} }

View File

@ -0,0 +1,147 @@
package org.openapitools.codegen.utils;
import org.openapitools.codegen.CodegenConfig;
import org.openapitools.codegen.CodegenModel;
import org.openapitools.codegen.CodegenProperty;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* This class holds data to add to `oneOf` members. Let's consider this example:
*
* Foo:
* properties:
* x:
* oneOf:
* - $ref: "#/components/schemas/One
* - $ref: "#/components/schemas/Two
* y:
* type: string
* One:
* properties:
* z:
* type: string
* Two:
* properties:
* a:
* type: string
*
* In codegens that use this mechanism, `Foo` will become an interface and `One` will
* become its implementing class. This class carries all data necessary to properly modify
* the implementing class model. Specifically:
*
* * Interfaces that the implementing classes have to implement (in the example above, `One` and `Two` will implement `Foo`)
* * Properties that need to be added to implementing classes (as `Foo` is interface, the `y` property will get pushed
* to implementing classes `One` and `Two`)
* * Imports that need to be added to implementing classes (e.g. if type of property `y` needs a specific import, it
* needs to be added to `One` and `Two` because of the above point)
*/
public class OneOfImplementorAdditionalData {
private String implementorName;
private List<String> additionalInterfaces = new ArrayList<String>();
private List<CodegenProperty> additionalProps = new ArrayList<CodegenProperty>();
private List<Map<String, String>> additionalImports = new ArrayList<Map<String, String>>();
public OneOfImplementorAdditionalData(String implementorName) {
this.implementorName = implementorName;
}
public String getImplementorName() {
return implementorName;
}
/**
* Add data from a given CodegenModel that the oneOf implementor should implement. For example:
*
* @param cm model that the implementor should implement
* @param modelsImports imports of the given `cm`
*/
public void addFromInterfaceModel(CodegenModel cm, List<Map<String, String>> modelsImports) {
// Add cm as implemented interface
additionalInterfaces.add(cm.classname);
// Add all vars defined on cm
// a "oneOf" model (cm) by default inherits all properties from its "interfaceModels",
// but we only want to add properties defined on cm itself
List<CodegenProperty> toAdd = new ArrayList<CodegenProperty>(cm.vars);
// note that we can't just toAdd.removeAll(m.vars) for every interfaceModel,
// as they might have different value of `hasMore` and thus are not equal
List<String> omitAdding = new ArrayList<String>();
for (CodegenModel m : cm.interfaceModels) {
for (CodegenProperty v : m.vars) {
omitAdding.add(v.baseName);
}
}
for (CodegenProperty v : toAdd) {
if (!omitAdding.contains(v.baseName)) {
additionalProps.add(v.clone());
}
}
// Add all imports of cm
for (Map<String, String> importMap : modelsImports) {
// we're ok with shallow clone here, because imports are strings only
additionalImports.add(new HashMap<String, String>(importMap));
}
}
/**
* Adds stored data to given implementing model
*
* @param cc CodegenConfig running this operation
* @param implcm the implementing model
* @param implImports imports of the implementing model
* @param addInterfaceImports whether or not to add the interface model as import (will vary by language)
*/
public void addToImplementor(CodegenConfig cc, CodegenModel implcm, List<Map<String, String>> implImports, boolean addInterfaceImports) {
implcm.getVendorExtensions().putIfAbsent("implements", new ArrayList<String>());
// Add implemented interfaces
for (String intf : additionalInterfaces) {
List<String> impl = (List<String>) implcm.getVendorExtensions().get("implements");
impl.add(intf);
if (addInterfaceImports) {
// Add imports for interfaces
implcm.imports.add(intf);
Map<String, String> importsItem = new HashMap<String, String>();
importsItem.put("import", cc.toModelImport(intf));
implImports.add(importsItem);
}
}
// Add oneOf-containing models properties - we need to properly set the hasMore values to make rendering correct
if (implcm.vars.size() > 0 && additionalProps.size() > 0) {
implcm.vars.get(implcm.vars.size() - 1).hasMore = true;
}
for (int i = 0; i < additionalProps.size(); i++) {
CodegenProperty var = additionalProps.get(i);
if (i == additionalProps.size() - 1) {
var.hasMore = false;
} else {
var.hasMore = true;
}
implcm.vars.add(var);
}
// Add imports
for (Map<String, String> oneImport : additionalImports) {
// exclude imports from this package - these are imports that only the oneOf interface needs
if (!implImports.contains(oneImport) && !oneImport.getOrDefault("import", "").startsWith(cc.modelPackage())) {
implImports.add(oneImport);
}
}
}
@Override
public String toString() {
return "OneOfImplementorAdditionalData{" +
"implementorName='" + implementorName + '\'' +
", additionalInterfaces=" + additionalInterfaces +
", additionalProps=" + additionalProps +
", additionalImports=" + additionalImports +
'}';
}
}

View File

@ -42,6 +42,6 @@ import org.hibernate.validator.constraints.*;
{{#models}} {{#models}}
{{#model}} {{#model}}
{{#isEnum}}{{>modelEnum}}{{/isEnum}}{{^isEnum}}{{#vendorExtensions.isOneOfInterface}}{{>oneof_interface}}{{/vendorExtensions.isOneOfInterface}}{{^vendorExtensions.isOneOfInterface}}{{>pojo}}{{/vendorExtensions.isOneOfInterface}}{{/isEnum}} {{#isEnum}}{{>modelEnum}}{{/isEnum}}{{^isEnum}}{{#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}}{{/isEnum}}
{{/model}} {{/model}}
{{/models}} {{/models}}

View File

@ -21,7 +21,7 @@ import java.util.Map;
public class {{classname}}Test { public class {{classname}}Test {
{{#models}} {{#models}}
{{#model}} {{#model}}
{{^vendorExtensions.isOneOfInterface}} {{^vendorExtensions.x-is-one-of-interface}}
{{^isEnum}} {{^isEnum}}
private final {{classname}} model = new {{classname}}(); private final {{classname}} model = new {{classname}}();
@ -44,7 +44,7 @@ public class {{classname}}Test {
} }
{{/allVars}} {{/allVars}}
{{/vendorExtensions.isOneOfInterface}} {{/vendorExtensions.x-is-one-of-interface}}
{{/model}} {{/model}}
{{/models}} {{/models}}
} }

View File

@ -1,8 +1,8 @@
# {{#vendorExtensions.isOneOfInterface}}Interface {{/vendorExtensions.isOneOfInterface}}{{classname}} # {{#vendorExtensions.x-is-one-of-interface}}Interface {{/vendorExtensions.x-is-one-of-interface}}{{classname}}
{{#description}}{{&description}} {{#description}}{{&description}}
{{/description}} {{/description}}
{{^vendorExtensions.isOneOfInterface}} {{^vendorExtensions.x-is-one-of-interface}}
## Properties ## Properties
Name | Type | Description | Notes Name | Type | Description | Notes
@ -26,11 +26,11 @@ Name | Value
* {{{.}}} * {{{.}}}
{{/vendorExtensions.implements}} {{/vendorExtensions.implements}}
{{/vendorExtensions.implements.0}} {{/vendorExtensions.implements.0}}
{{/vendorExtensions.isOneOfInterface}} {{/vendorExtensions.x-is-one-of-interface}}
{{#vendorExtensions.isOneOfInterface}} {{#vendorExtensions.x-is-one-of-interface}}
## Implementing Classes ## Implementing Classes
{{#oneOf}} {{#oneOf}}
* {{{.}}} * {{{.}}}
{{/oneOf}} {{/oneOf}}
{{/vendorExtensions.isOneOfInterface}} {{/vendorExtensions.x-is-one-of-interface}}

View File

@ -29,6 +29,10 @@ const (
{{^isEnum}} {{^isEnum}}
// {{classname}}{{#description}} {{{description}}}{{/description}}{{^description}} struct for {{{classname}}}{{/description}} // {{classname}}{{#description}} {{{description}}}{{/description}}{{^description}} struct for {{{classname}}}{{/description}}
type {{classname}} struct { type {{classname}} struct {
{{#vendorExtensions.x-is-one-of-interface}}
{{classname}}Interface interface { {{#discriminator}}{{propertyGetter}}() {{propertyType}}{{/discriminator}} }
{{/vendorExtensions.x-is-one-of-interface}}
{{^vendorExtensions.x-is-one-of-interface}}
{{#parent}} {{#parent}}
{{^isMapModel}} {{^isMapModel}}
{{{parent}}} {{{parent}}}
@ -42,10 +46,12 @@ type {{classname}} struct {
{{/description}} {{/description}}
{{name}} {{^required}}*{{/required}}{{{dataType}}} `json:"{{baseName}}{{^required}},omitempty{{/required}}"{{#withXml}} xml:"{{baseName}}{{#isXmlAttribute}},attr{{/isXmlAttribute}}"{{/withXml}}{{#vendorExtensions.x-go-custom-tag}} {{{.}}}{{/vendorExtensions.x-go-custom-tag}}` {{name}} {{^required}}*{{/required}}{{{dataType}}} `json:"{{baseName}}{{^required}},omitempty{{/required}}"{{#withXml}} xml:"{{baseName}}{{#isXmlAttribute}},attr{{/isXmlAttribute}}"{{/withXml}}{{#vendorExtensions.x-go-custom-tag}} {{{.}}}{{/vendorExtensions.x-go-custom-tag}}`
{{/vars}} {{/vars}}
{{/vendorExtensions.x-is-one-of-interface}}
} }
{{/isEnum}} {{/isEnum}}
{{^isEnum}} {{^isEnum}}
{{^vendorExtensions.x-is-one-of-interface}}
{{#vars}} {{#vars}}
{{#required}} {{#required}}
// Get{{name}} returns the {{name}} field value // Get{{name}} returns the {{name}} field value
@ -100,6 +106,58 @@ func (o *{{classname}}) Set{{name}}(v {{dataType}}) {
{{/required}} {{/required}}
{{/vars}} {{/vars}}
{{/vendorExtensions.x-is-one-of-interface}}
{{#vendorExtensions.x-is-one-of-interface}}
func (s *{{classname}}) MarshalJSON() ([]byte, error) {
return json.Marshal(s.{{classname}}Interface)
}
func (s *{{classname}}) UnmarshalJSON(src []byte) error {
var err error
{{#discriminator}}
var unmarshaled map[string]interface{}
err = json.Unmarshal(src, &unmarshaled)
if err != nil {
return err
}
if v, ok := unmarshaled["{{discriminator.propertyBaseName}}"]; ok {
switch v {
{{#discriminator.mappedModels}}
case "{{mappingName}}":
var result *{{modelName}} = &{{modelName}}{}
err = json.Unmarshal(src, result)
if err != nil {
return err
}
s.{{classname}}Interface = result
return nil
{{/discriminator.mappedModels}}
default:
return fmt.Errorf("No oneOf model has '{{discriminator.propertyBaseName}}' equal to %s", v)
}
} else {
return fmt.Errorf("Discriminator property '{{discriminator.propertyBaseName}}' not found in unmarshaled payload: %+v", unmarshaled)
}
{{/discriminator}}
{{^discriminator}}
{{#oneOf}}
var unmarshaled{{{.}}} *{{{.}}} = &{{{.}}}{}
err = json.Unmarshal(src, unmarshaled{{{.}}})
if err == nil {
s.{{classname}}Interface = unmarshaled{{{.}}}
return nil
}
{{/oneOf}}
return fmt.Errorf("No oneOf model could be deserialized from payload: %s", string(src))
{{/discriminator}}
}
{{/vendorExtensions.x-is-one-of-interface}}
{{#vendorExtensions.implements}}
// As{{{.}}} wraps this instance of {{classname}} in {{{.}}}
func (s *{{classname}}) As{{{.}}}() {{{.}}} {
return {{{.}}}{ {{{.}}}Interface: s }
}
{{/vendorExtensions.implements}}
{{/isEnum}} {{/isEnum}}
type Nullable{{{classname}}} struct { type Nullable{{{classname}}} struct {
Value {{{classname}}} Value {{{classname}}}

View File

@ -4,12 +4,18 @@
Name | Type | Description | Notes Name | Type | Description | Notes
------------ | ------------- | ------------- | ------------- ------------ | ------------- | ------------- | -------------
{{#vendorExtensions.x-is-one-of-interface}}
**{{classname}}Interface** | **interface { {{#discriminator}}{{propertyGetter}}() {{propertyType}}{{/discriminator}} }** | An interface that can hold any of the proper implementing types |
{{/vendorExtensions.x-is-one-of-interface}}
{{^vendorExtensions.x-is-one-of-interface}}
{{#vars}}**{{name}}** | Pointer to {{#isPrimitiveType}}**{{{dataType}}}**{{/isPrimitiveType}}{{^isPrimitiveType}}[**{{{dataType}}}**]({{complexType}}.md){{/isPrimitiveType}} | {{description}} | {{^required}}[optional] {{/required}}{{#isReadOnly}}[readonly] {{/isReadOnly}}{{#defaultValue}}[default to {{{.}}}]{{/defaultValue}} {{#vars}}**{{name}}** | Pointer to {{#isPrimitiveType}}**{{{dataType}}}**{{/isPrimitiveType}}{{^isPrimitiveType}}[**{{{dataType}}}**]({{complexType}}.md){{/isPrimitiveType}} | {{description}} | {{^required}}[optional] {{/required}}{{#isReadOnly}}[readonly] {{/isReadOnly}}{{#defaultValue}}[default to {{{.}}}]{{/defaultValue}}
{{/vars}} {{/vars}}
{{/vendorExtensions.x-is-one-of-interface}}
{{^isEnum}} {{^isEnum}}
## Methods ## Methods
{{^vendorExtensions.x-is-one-of-interface}}
{{#vars}} {{#vars}}
### Get{{name}} ### Get{{name}}
@ -46,6 +52,15 @@ when serializing to JSON (pass true as argument to set this, false to unset)
The {{name}} value is set to nil even if false is passed The {{name}} value is set to nil even if false is passed
{{/isNullable}} {{/isNullable}}
{{/vars}} {{/vars}}
{{#vendorExtensions.implements}}
### As{{{.}}}
`func (s *{{classname}}) As{{{.}}}() {{{.}}}`
Convenience method to wrap this instance of {{classname}} in {{{.}}}
{{/vendorExtensions.implements}}
{{/vendorExtensions.x-is-one-of-interface}}
{{/isEnum}} {{/isEnum}}
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) [[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)

View File

@ -1301,4 +1301,42 @@ public class DefaultCodegenTest {
Assert.assertTrue(roundCNext.isCircularReference); Assert.assertTrue(roundCNext.isCircularReference);
Assert.assertFalse(roundCOut.isCircularReference); Assert.assertFalse(roundCOut.isCircularReference);
} }
@Test
public void testUseOneOfInterfaces() {
final OpenAPI openAPI = TestUtils.parseSpec("src/test/resources/3_0/composed-oneof.yaml");
final DefaultCodegen cg = new DefaultCodegen();
cg.setUseOneOfInterfaces(true);
cg.preprocessOpenAPI(openAPI);
// assert names of the response/request schema oneOf interfaces are as expected
Assert.assertEquals(
openAPI.getPaths()
.get("/state")
.getPost()
.getRequestBody()
.getContent()
.get("application/json")
.getSchema()
.getExtensions()
.get("x-oneOf-name"),
"CreateState"
);
Assert.assertEquals(
openAPI.getPaths()
.get("/state")
.getGet()
.getResponses()
.get("200")
.getContent()
.get("application/json")
.getSchema()
.getExtensions()
.get("x-oneOf-name"),
"GetState200"
);
// for the array schema, assert that a oneOf interface was added to schema map
Schema items = ((ArraySchema) openAPI.getComponents().getSchemas().get("CustomOneOfArraySchema")).getItems();
Assert.assertEquals(items.getExtensions().get("x-oneOf-name"), "CustomOneOfArraySchemaOneOf");
}
} }

View File

@ -0,0 +1,61 @@
package org.openapitools.codegen.utils;
import org.openapitools.codegen.CodegenConfig;
import org.openapitools.codegen.CodegenModel;
import org.openapitools.codegen.CodegenProperty;
import org.openapitools.codegen.languages.GoClientExperimentalCodegen;
import org.testng.Assert;
import org.testng.annotations.Test;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class OneOfImplementorAdditionalDataTest {
@Test
public void testGeneralUsage() {
OneOfImplementorAdditionalData o = new OneOfImplementorAdditionalData("Implementor");
// set up all the necessary inputs for `o.addFromInterfaceModel`
CodegenModel oneOfModel = new CodegenModel();
oneOfModel.classname = "OneOfModel";
oneOfModel.vars = new ArrayList<>();
CodegenProperty cp1 = new CodegenProperty();
cp1.baseName = "OneOfModelProperty";
oneOfModel.vars.add(cp1);
CodegenProperty cp2 = new CodegenProperty();
cp2.baseName = "InterfaceModelProperty";
oneOfModel.vars.add(cp2);
// if the OneOfModel has interface models, we want to verify that their properties don't get
// added to the oneOf-implementing model
CodegenModel interfaceModel = new CodegenModel();
interfaceModel.vars.add(cp2.clone());
oneOfModel.interfaceModels = new ArrayList<>();
oneOfModel.interfaceModels.add(interfaceModel);
List<Map<String, String>> interfaceModelImports = new ArrayList<>();
interfaceModelImports.add(new HashMap<String, String>(){{ put("import", "foo"); }});
o.addFromInterfaceModel(oneOfModel, interfaceModelImports);
// set up all the necessary inputs for `o.addToImplementor`
CodegenModel implModel = new CodegenModel();
implModel.vars = new ArrayList<>();
CodegenProperty cp3 = new CodegenProperty();
cp3.baseName = "OtherProperty";
cp3.hasMore = false;
implModel.vars.add(cp3);
List<Map<String, String>> implModelImports = new ArrayList<>();
GoClientExperimentalCodegen cc = new GoClientExperimentalCodegen();
cc.setModelPackage("openapi");
o.addToImplementor(cc, implModel, implModelImports, false);
// make sure all the additions were done correctly
Assert.assertEquals(implModel.getVendorExtensions().get("implements"), new ArrayList<String>(){{add(oneOfModel.classname);}});
Assert.assertEquals(implModelImports, interfaceModelImports);
Assert.assertEquals(implModel.vars, new ArrayList<CodegenProperty>(){{add(cp3); add(cp1);}});
Assert.assertTrue(implModel.vars.get(0).hasMore);
}
}

View File

@ -42,6 +42,26 @@ paths:
description: OK description: OK
components: components:
schemas: schemas:
CustomOneOfSchema:
oneOf:
- $ref: '#/components/schemas/ObjA'
- $ref: '#/components/schemas/ObjB'
discriminator:
propertyName: realtype
mapping:
a-type: '#/components/schemas/ObjA'
b-type: '#/components/schemas/ObjB'
CustomOneOfArraySchema:
type: array
items:
oneOf:
- $ref: '#/components/schemas/ObjA'
- $ref: '#/components/schemas/ObjB'
discriminator:
propertyName: realtype
mapping:
a-type: '#/components/schemas/ObjA'
b-type: '#/components/schemas/ObjB'
ObjA: ObjA:
type: object type: object
properties: properties: