diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/DefaultCodegen.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/DefaultCodegen.java index 4227e0b2b234..d5c2959e52c1 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/DefaultCodegen.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/DefaultCodegen.java @@ -26,6 +26,7 @@ import io.swagger.v3.core.util.Json; import io.swagger.v3.oas.models.OpenAPI; 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.examples.Example; import io.swagger.v3.oas.models.headers.Header; @@ -59,6 +60,7 @@ import org.openapitools.codegen.templating.mustache.TitlecaseLambda; import org.openapitools.codegen.templating.mustache.UppercaseLambda; import org.openapitools.codegen.utils.ModelUtils; +import org.openapitools.codegen.utils.OneOfImplementorAdditionalData; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -187,6 +189,11 @@ apiTemplateFiles are for API outputs only (controllers/handlers). // flag to indicate whether to use environment variable to post process file protected boolean enablePostProcessFile = false; 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 addOneOfInterfaces = new ArrayList(); // flag to indicate whether to only update files whose contents have changed protected boolean enableMinimalUpdate = false; @@ -326,6 +333,65 @@ private void registerMustacheLambdas() { // override with any special post-processing for all models @SuppressWarnings({"static-method", "unchecked"}) public Map postProcessAllModels(Map objs) { + if (this.useOneOfInterfaces) { + // First, add newly created oneOf interfaces + for (CodegenModel cm : addOneOfInterfaces) { + Map modelValue = new HashMap() {{ + putAll(additionalProperties()); + put("model", cm); + }}; + List modelsValue = Arrays.asList(modelValue); + List> importsValue = new ArrayList>(); + Map objsValue = new HashMap() {{ + 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 additionalDataMap = new HashMap(); + for (Map.Entry modelsEntry : objs.entrySet()) { + Map modelsAttrs = (Map) modelsEntry.getValue(); + List models = (List) modelsAttrs.get("models"); + List> modelsImports = (List>) modelsAttrs.getOrDefault("imports", new ArrayList>()); + for (Object _mo : models) { + Map mo = (Map) _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 modelsAttrs = (Map) modelsEntry.getValue(); + List models = (List) modelsAttrs.get("models"); + List> imports = (List>) modelsAttrs.get("imports"); + for (Object _implmo : models) { + Map implmo = (Map) _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; } @@ -626,6 +692,62 @@ public void postProcessParameter(CodegenParameter parameter) { //override with any special handling of the entire OpenAPI spec document @SuppressWarnings("unused") public void preprocessOpenAPI(OpenAPI openAPI) { + if (useOneOfInterfaces) { + // we process the openapi schema here to find oneOf schemas and create interface models for them + Map schemas = new HashMap(openAPI.getComponents().getSchemas()); + if (schemas == null) { + schemas = new HashMap(); + } + Map pathItems = openAPI.getPaths(); + + // we need to add all request and response bodies to processed schemas + if (pathItems != null) { + for (Map.Entry e : pathItems.entrySet()) { + for (Map.Entry 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 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 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 @@ -950,6 +1072,12 @@ public void setAllowUnicodeIdentifiers(Boolean 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) * @@ -5534,4 +5662,49 @@ public boolean isRemoveEnumValuePrefix() { public void setRemoveEnumValuePrefix(final boolean removeEnumValuePrefix) { this.removeEnumValuePrefix = removeEnumValuePrefix; } -} \ No newline at end of file + + //// 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(); + + addOneOfInterfaces.add(cm); + } + + public void addImportsToOneOfInterface(List> imports) {} + //// End of methods related to the "useOneOfInterfaces" feature +} diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/AbstractJavaCodegen.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/AbstractJavaCodegen.java index c79d747db80d..ebb6832f9826 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/AbstractJavaCodegen.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/AbstractJavaCodegen.java @@ -522,6 +522,7 @@ public void processOpts() { @Override public Map postProcessAllModels(Map objs) { + objs = super.postProcessAllModels(objs); objs = super.updateAllModels(objs); if (!additionalModelTypeAnnotations.isEmpty()) { @@ -1067,6 +1068,7 @@ public Map postProcessOperationsWithModels(Map o @Override public void preprocessOpenAPI(OpenAPI openAPI) { + super.preprocessOpenAPI(openAPI); if (openAPI == null) { return; } diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/GoClientExperimentalCodegen.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/GoClientExperimentalCodegen.java index 471600e6abc2..64801956c080 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/GoClientExperimentalCodegen.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/GoClientExperimentalCodegen.java @@ -27,6 +27,8 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.util.Arrays; +import java.util.HashMap; import java.util.List; import java.util.Map; @@ -42,6 +44,7 @@ public GoClientExperimentalCodegen() { embeddedTemplateDir = templateDir = "go-experimental"; usesOptionals = false; + useOneOfInterfaces = true; generatorMetadata = GeneratorMetadata.newBuilder(generatorMetadata).stability(Stability.EXPERIMENTAL).build(); } @@ -57,6 +60,11 @@ public String getName() { 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 * tips, parameters here @@ -125,4 +133,16 @@ public Map postProcessModels(Map objs) { objs = super.postProcessModels(objs); return objs; } + + @Override + public void addImportsToOneOfInterface(List> imports) { + for (String i : Arrays.asList("fmt")) { + Map oneImport = new HashMap() {{ + put("import", i); + }}; + if (!imports.contains(oneImport)) { + imports.add(oneImport); + } + } + } } diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/JavaClientCodegen.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/JavaClientCodegen.java index ff0d6605c1c2..6aa926c08579 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/JavaClientCodegen.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/JavaClientCodegen.java @@ -25,19 +25,12 @@ import org.openapitools.codegen.languages.features.PerformBeanValidationFeatures; import org.openapitools.codegen.meta.features.DocumentationFeature; import org.openapitools.codegen.templating.mustache.CaseFormatLambda; -import org.openapitools.codegen.utils.ModelUtils; import org.openapitools.codegen.utils.ProcessUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; 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.parameters.RequestBody; -import io.swagger.v3.oas.models.responses.ApiResponse; import java.io.File; import java.util.*; @@ -111,9 +104,6 @@ public class JavaClientCodegen extends AbstractJavaCodegen protected String authFolder; protected String serializationLibrary = null; - protected boolean useOneOfInterfaces = false; - protected List addOneOfInterfaces = new ArrayList(); - public JavaClientCodegen() { super(); @@ -501,6 +491,7 @@ else if (MICROPROFILE.equals(getLibrary())) { if (additionalProperties.containsKey(SERIALIZATION_LIBRARY_JACKSON)) { useOneOfInterfaces = true; + addOneOfInterfaceImports = true; } } @@ -846,259 +837,6 @@ public void setSerializationLibrary(String serializationLibrary) { } } - 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(); - - 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 schemas = new HashMap(openAPI.getComponents().getSchemas()); - if (schemas == null) { - schemas = new HashMap(); - } - Map pathItems = openAPI.getPaths(); - - // we need to add all request and response bodies to processed schemas - if (pathItems != null) { - for (Map.Entry e : pathItems.entrySet()) { - for (Map.Entry 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 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 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 additionalInterfaces = new ArrayList(); - private List additionalProps = new ArrayList(); - private List> additionalImports = new ArrayList>(); - - public OneOfImplementorAdditionalData(String implementorName) { - this.implementorName = implementorName; - } - - public String getImplementorName() { - return implementorName; - } - - public void addFromInterfaceModel(CodegenModel cm, List> 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 toAdd = new ArrayList(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 omitAdding = new ArrayList(); - 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 importMap : modelsImports) { - // we're ok with shallow clone here, because imports are strings only - additionalImports.add(new HashMap(importMap)); - } - } - - public void addToImplementor(CodegenModel implcm, List> implImports) { - implcm.getVendorExtensions().putIfAbsent("implements", new ArrayList()); - - // Add implemented interfaces - for (String intf : additionalInterfaces) { - List impl = (List) implcm.getVendorExtensions().get("implements"); - impl.add(intf); - // Add imports for interfaces - implcm.imports.add(intf); - Map importsItem = new HashMap(); - 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 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 postProcessAllModels(Map 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 modelValue = new HashMap() {{ - putAll(additionalProperties()); - put("model", cm); - }}; - List modelsValue = Arrays.asList(modelValue); - List> importsValue = new ArrayList>(); - for (String i : Arrays.asList("JsonSubTypes", "JsonTypeInfo")) { - Map oneImport = new HashMap() {{ - put("import", importMapping.get(i)); - }}; - importsValue.add(oneImport); - } - Map objsValue = new HashMap() {{ - 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 additionalDataMap = new HashMap(); - for (Map.Entry modelsEntry : objs.entrySet()) { - Map modelsAttrs = (Map) modelsEntry.getValue(); - List models = (List) modelsAttrs.get("models"); - List> modelsImports = (List>) modelsAttrs.getOrDefault("imports", new ArrayList>()); - for (Object _mo : models) { - Map mo = (Map) _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 i = new HashMap() {{ - 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 modelsAttrs = (Map) modelsEntry.getValue(); - List models = (List) modelsAttrs.get("models"); - List> imports = (List>) modelsAttrs.get("imports"); - for (Object _implmo : models) { - Map implmo = (Map) _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) { 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); @@ -1138,4 +876,16 @@ public String toApiVarName(String name) { } return apiVarName; } + + @Override + public void addImportsToOneOfInterface(List> imports) { + for (String i : Arrays.asList("JsonSubTypes", "JsonTypeInfo")) { + Map oneImport = new HashMap() {{ + put("import", importMapping.get(i)); + }}; + if (!imports.contains(oneImport)) { + imports.add(oneImport); + } + } + } } diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/utils/OneOfImplementorAdditionalData.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/utils/OneOfImplementorAdditionalData.java new file mode 100644 index 000000000000..9454633de6aa --- /dev/null +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/utils/OneOfImplementorAdditionalData.java @@ -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 additionalInterfaces = new ArrayList(); + private List additionalProps = new ArrayList(); + private List> additionalImports = new ArrayList>(); + + 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> 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 toAdd = new ArrayList(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 omitAdding = new ArrayList(); + 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 importMap : modelsImports) { + // we're ok with shallow clone here, because imports are strings only + additionalImports.add(new HashMap(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> implImports, boolean addInterfaceImports) { + implcm.getVendorExtensions().putIfAbsent("implements", new ArrayList()); + + // Add implemented interfaces + for (String intf : additionalInterfaces) { + List impl = (List) implcm.getVendorExtensions().get("implements"); + impl.add(intf); + if (addInterfaceImports) { + // Add imports for interfaces + implcm.imports.add(intf); + Map importsItem = new HashMap(); + 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 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 + + '}'; + } +} diff --git a/modules/openapi-generator/src/main/resources/Java/model.mustache b/modules/openapi-generator/src/main/resources/Java/model.mustache index 8763e3bd08bb..d4d1447a1d4d 100644 --- a/modules/openapi-generator/src/main/resources/Java/model.mustache +++ b/modules/openapi-generator/src/main/resources/Java/model.mustache @@ -42,6 +42,6 @@ import org.hibernate.validator.constraints.*; {{#models}} {{#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}} {{/models}} diff --git a/modules/openapi-generator/src/main/resources/Java/model_test.mustache b/modules/openapi-generator/src/main/resources/Java/model_test.mustache index 0258f350074c..07468db56572 100644 --- a/modules/openapi-generator/src/main/resources/Java/model_test.mustache +++ b/modules/openapi-generator/src/main/resources/Java/model_test.mustache @@ -21,7 +21,7 @@ import java.util.Map; public class {{classname}}Test { {{#models}} {{#model}} - {{^vendorExtensions.isOneOfInterface}} + {{^vendorExtensions.x-is-one-of-interface}} {{^isEnum}} private final {{classname}} model = new {{classname}}(); @@ -44,7 +44,7 @@ public class {{classname}}Test { } {{/allVars}} - {{/vendorExtensions.isOneOfInterface}} + {{/vendorExtensions.x-is-one-of-interface}} {{/model}} {{/models}} } diff --git a/modules/openapi-generator/src/main/resources/Java/pojo_doc.mustache b/modules/openapi-generator/src/main/resources/Java/pojo_doc.mustache index e63db5e27209..4c15d09eadb6 100644 --- a/modules/openapi-generator/src/main/resources/Java/pojo_doc.mustache +++ b/modules/openapi-generator/src/main/resources/Java/pojo_doc.mustache @@ -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}} -{{^vendorExtensions.isOneOfInterface}} +{{^vendorExtensions.x-is-one-of-interface}} ## Properties Name | Type | Description | Notes @@ -26,11 +26,11 @@ Name | Value * {{{.}}} {{/vendorExtensions.implements}} {{/vendorExtensions.implements.0}} -{{/vendorExtensions.isOneOfInterface}} -{{#vendorExtensions.isOneOfInterface}} +{{/vendorExtensions.x-is-one-of-interface}} +{{#vendorExtensions.x-is-one-of-interface}} ## Implementing Classes {{#oneOf}} * {{{.}}} {{/oneOf}} -{{/vendorExtensions.isOneOfInterface}} \ No newline at end of file +{{/vendorExtensions.x-is-one-of-interface}} \ No newline at end of file diff --git a/modules/openapi-generator/src/main/resources/go-experimental/model.mustache b/modules/openapi-generator/src/main/resources/go-experimental/model.mustache index ac200f650a5a..ce8491a4bf10 100644 --- a/modules/openapi-generator/src/main/resources/go-experimental/model.mustache +++ b/modules/openapi-generator/src/main/resources/go-experimental/model.mustache @@ -29,6 +29,10 @@ const ( {{^isEnum}} // {{classname}}{{#description}} {{{description}}}{{/description}}{{^description}} struct for {{{classname}}}{{/description}} 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}} {{^isMapModel}} {{{parent}}} @@ -42,10 +46,12 @@ type {{classname}} struct { {{/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}}` {{/vars}} +{{/vendorExtensions.x-is-one-of-interface}} } {{/isEnum}} {{^isEnum}} +{{^vendorExtensions.x-is-one-of-interface}} {{#vars}} {{#required}} // Get{{name}} returns the {{name}} field value @@ -100,6 +106,58 @@ func (o *{{classname}}) Set{{name}}(v {{dataType}}) { {{/required}} {{/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}} type Nullable{{{classname}}} struct { Value {{{classname}}} diff --git a/modules/openapi-generator/src/main/resources/go-experimental/model_doc.mustache b/modules/openapi-generator/src/main/resources/go-experimental/model_doc.mustache index 523122f4e0d9..c6f3b8640843 100644 --- a/modules/openapi-generator/src/main/resources/go-experimental/model_doc.mustache +++ b/modules/openapi-generator/src/main/resources/go-experimental/model_doc.mustache @@ -4,12 +4,18 @@ 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}} +{{/vendorExtensions.x-is-one-of-interface}} {{^isEnum}} ## Methods +{{^vendorExtensions.x-is-one-of-interface}} {{#vars}} ### 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 {{/isNullable}} {{/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}} [[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) diff --git a/modules/openapi-generator/src/test/java/org/openapitools/codegen/DefaultCodegenTest.java b/modules/openapi-generator/src/test/java/org/openapitools/codegen/DefaultCodegenTest.java index a5507dcc5212..c2dc650ed868 100644 --- a/modules/openapi-generator/src/test/java/org/openapitools/codegen/DefaultCodegenTest.java +++ b/modules/openapi-generator/src/test/java/org/openapitools/codegen/DefaultCodegenTest.java @@ -1301,4 +1301,42 @@ public void testCircularReferencesDetection() { Assert.assertTrue(roundCNext.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"); + } } diff --git a/modules/openapi-generator/src/test/java/org/openapitools/codegen/utils/OneOfImplementorAdditionalDataTest.java b/modules/openapi-generator/src/test/java/org/openapitools/codegen/utils/OneOfImplementorAdditionalDataTest.java new file mode 100644 index 000000000000..9f8f969b49a2 --- /dev/null +++ b/modules/openapi-generator/src/test/java/org/openapitools/codegen/utils/OneOfImplementorAdditionalDataTest.java @@ -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> interfaceModelImports = new ArrayList<>(); + interfaceModelImports.add(new HashMap(){{ 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> 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(){{add(oneOfModel.classname);}}); + Assert.assertEquals(implModelImports, interfaceModelImports); + Assert.assertEquals(implModel.vars, new ArrayList(){{add(cp3); add(cp1);}}); + Assert.assertTrue(implModel.vars.get(0).hasMore); + } +} diff --git a/modules/openapi-generator/src/test/resources/3_0/composed-oneof.yaml b/modules/openapi-generator/src/test/resources/3_0/composed-oneof.yaml index 61bfc1da50ac..da6bd9788b49 100644 --- a/modules/openapi-generator/src/test/resources/3_0/composed-oneof.yaml +++ b/modules/openapi-generator/src/test/resources/3_0/composed-oneof.yaml @@ -42,6 +42,26 @@ paths: description: OK components: 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: type: object properties: