Skip to content
GitLab
    • Explore Projects Groups Snippets
Projects Groups Snippets
  • /
  • Help
    • Help
    • Support
    • Community forum
    • Submit feedback
    • Contribute to GitLab
  • Sign in / Register
  • O openapi-generator
  • Project information
    • Project information
    • Activity
    • Labels
    • Members
  • Repository
    • Repository
    • Files
    • Commits
    • Branches
    • Tags
    • Contributors
    • Graph
    • Compare
  • Issues 3,476
    • Issues 3,476
    • List
    • Boards
    • Service Desk
    • Milestones
  • Merge requests 402
    • Merge requests 402
  • CI/CD
    • CI/CD
    • Pipelines
    • Jobs
    • Schedules
  • Deployments
    • Deployments
    • Environments
    • Releases
  • Packages and registries
    • Packages and registries
    • Package Registry
    • Infrastructure Registry
  • Monitor
    • Monitor
    • Incidents
  • Analytics
    • Analytics
    • Value stream
    • CI/CD
    • Repository
  • Wiki
    • Wiki
  • Snippets
    • Snippets
  • Activity
  • Graph
  • Create a new issue
  • Jobs
  • Commits
  • Issue Boards
Collapse sidebar
  • OpenAPI Tools
  • openapi-generator
  • Merge requests
  • !5150

[go-experimental] Add oneOf support

  • Review changes

  • Download
  • Email patches
  • Plain diff
Merged Administrator requested to merge github/fork/bkabrda/go-experimental-oneof-support into master 5 years ago
  • Overview 0
  • Commits 8
  • Pipelines 0
  • Changes 13

Created by: bkabrda

This PR adds oneOf support in the same way it was added for Java clients recently in #5120. It contains a lot of refactoring to be able to use the same mechanisms as were used to implement this feature for Java. Also see https://github.com/OpenAPITools/openapi-generator/pull/4785 for discussion on semantics of oneOf and thoughts on why it was implemented that way for Java.

Note that this PR doesn't contain any tests right now. I do want to add tests, but would like to do that after the general direction of this PR and the related refactoring is approved.

PR checklist

  • Read the contribution guidelines.
  • If contributing template-only or documentation-only changes which will change sample output, build the project before.
  • Run the shell script(s) under ./bin/ (or Windows batch scripts under.\bin\windows) to update Petstore samples related to your fix. This is important, as CI jobs will verify all generator outputs of your HEAD commit, and these must match the expectations made by your contribution. You only need to run ./bin/{LANG}-petstore.sh, ./bin/openapi3/{LANG}-petstore.sh if updating the code or mustache templates for a language ({LANG}) (e.g. php, ruby, python, etc).
  • File the PR against the correct branch: master, 4.3.x, 5.0.x. Default: master.
  • Copy the technical committee to review the pull request if your PR is targeting a particular programming language.

@antihax (2017/11) @bvwells (2017/12) @grokify (2018/07) @kemokemo (2018/09) @bkabrda (2019/07)

Compare
  • master (base)

and
  • latest version
    347eaf77
    8 commits, 2 years ago

13 files
+ 556
- 272

    Preferences

    File browser
    Compare changes
modules/openap‎i-generator/src‎
ma‎in‎
java/org/opena‎pitools/codegen‎
lang‎uages‎
AbstractJava‎Codegen.java‎ +2 -0
GoClientExperime‎ntalCodegen.java‎ +20 -0
JavaClientC‎odegen.java‎ +13 -263
ut‎ils‎
OneOfImplementorA‎dditionalData.java‎ +147 -0
DefaultCo‎degen.java‎ +174 -1
reso‎urces‎
Ja‎va‎
model.m‎ustache‎ +1 -1
model_tes‎t.mustache‎ +2 -2
pojo_doc‎.mustache‎ +5 -5
go-expe‎rimental‎
model.m‎ustache‎ +58 -0
model_doc‎.mustache‎ +15 -0
te‎st‎
java/org/opena‎pitools/codegen‎
ut‎ils‎
OneOfImplementorAdd‎itionalDataTest.java‎ +61 -0
DefaultCode‎genTest.java‎ +38 -0
resour‎ces/3_0‎
composed-‎oneof.yaml‎ +20 -0
modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/AbstractJavaCodegen.java
+ 2
- 0
  • View file @ 347eaf77

  • Edit in single-file editor

  • Open in Web IDE


@@ -522,6 +522,7 @@ public abstract class AbstractJavaCodegen extends DefaultCodegen implements Code
@Override
public Map<String, Object> postProcessAllModels(Map<String, Object> objs) {
objs = super.postProcessAllModels(objs);
objs = super.updateAllModels(objs);
if (!additionalModelTypeAnnotations.isEmpty()) {
@@ -1067,6 +1068,7 @@ public abstract class AbstractJavaCodegen extends DefaultCodegen implements Code
@Override
public void preprocessOpenAPI(OpenAPI openAPI) {
super.preprocessOpenAPI(openAPI);
if (openAPI == null) {
return;
}
modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/GoClientExperimentalCodegen.java
+ 20
- 0
  • View file @ 347eaf77

  • Edit in single-file editor

  • Open in Web IDE


@@ -27,6 +27,8 @@ import org.openapitools.codegen.utils.ProcessUtils;
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 class GoClientExperimentalCodegen extends GoClientCodegen {
embeddedTemplateDir = templateDir = "go-experimental";
usesOptionals = false;
useOneOfInterfaces = true;
generatorMetadata = GeneratorMetadata.newBuilder(generatorMetadata).stability(Stability.EXPERIMENTAL).build();
}
@@ -57,6 +60,11 @@ public class GoClientExperimentalCodegen extends GoClientCodegen {
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 class GoClientExperimentalCodegen extends GoClientCodegen {
objs = super.postProcessModels(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);
}
}
}
}
modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/JavaClientCodegen.java
+ 13
- 263
  • View file @ 347eaf77

  • Edit in single-file editor

  • Open in Web IDE


@@ -25,19 +25,12 @@ import org.openapitools.codegen.languages.features.GzipFeatures;
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<CodegenModel> addOneOfInterfaces = new ArrayList<CodegenModel>();
public JavaClientCodegen() {
super();
@@ -501,6 +491,7 @@ public class JavaClientCodegen extends AbstractJavaCodegen
if (additionalProperties.containsKey(SERIALIZATION_LIBRARY_JACKSON)) {
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) {
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 class JavaClientCodegen extends AbstractJavaCodegen
}
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);
}
}
}
}
modules/openapi-generator/src/main/java/org/openapitools/codegen/utils/OneOfImplementorAdditionalData.java 0 → 100644
+ 147
- 0
  • View file @ 347eaf77

  • Edit in single-file editor

  • Open in Web IDE

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 +
'}';
}
}
modules/openapi-generator/src/main/java/org/openapitools/codegen/DefaultCodegen.java
+ 174
- 1
  • View file @ 347eaf77

  • Edit in single-file editor

  • Open in Web IDE


@@ -26,6 +26,7 @@ import com.samskivert.mustache.Mustache.Lambda;
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.LowercaseLambda;
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 @@ public class DefaultCodegen implements CodegenConfig {
// 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<CodegenModel> addOneOfInterfaces = new ArrayList<CodegenModel>();
// flag to indicate whether to only update files whose contents have changed
protected boolean enableMinimalUpdate = false;
@@ -326,6 +333,65 @@ public class DefaultCodegen implements CodegenConfig {
// override with any special post-processing for all models
@SuppressWarnings({"static-method", "unchecked"})
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;
}
@@ -626,6 +692,62 @@ public class DefaultCodegen implements CodegenConfig {
//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<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
@@ -950,6 +1072,12 @@ public class DefaultCodegen implements CodegenConfig {
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 class DefaultCodegen implements CodegenConfig {
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<CodegenModel>();
addOneOfInterfaces.add(cm);
}
public void addImportsToOneOfInterface(List<Map<String, String>> imports) {}
//// End of methods related to the "useOneOfInterfaces" feature
}
modules/openapi-generator/src/main/resources/Java/model.mustache
+ 1
- 1
  • View file @ 347eaf77

  • Edit in single-file editor

  • Open in Web IDE


@@ -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}}
modules/openapi-generator/src/main/resources/Java/model_test.mustache
+ 2
- 2
  • View file @ 347eaf77

  • Edit in single-file editor

  • Open in Web IDE


@@ -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}}
}
modules/openapi-generator/src/main/resources/Java/pojo_doc.mustache
+ 5
- 5
  • View file @ 347eaf77

  • Edit in single-file editor

  • Open in Web IDE


# {{#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
modules/openapi-generator/src/main/resources/go-experimental/model.mustache
+ 58
- 0
  • View file @ 347eaf77

  • Edit in single-file editor

  • Open in Web IDE


@@ -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}}}
modules/openapi-generator/src/main/resources/go-experimental/model_doc.mustache
+ 15
- 0
  • View file @ 347eaf77

  • Edit in single-file editor

  • Open in Web IDE


@@ -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)
modules/openapi-generator/src/test/java/org/openapitools/codegen/utils/OneOfImplementorAdditionalDataTest.java 0 → 100644
+ 61
- 0
  • View file @ 347eaf77

  • Edit in single-file editor

  • Open in Web IDE

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);
}
}
modules/openapi-generator/src/test/java/org/openapitools/codegen/DefaultCodegenTest.java
+ 38
- 0
  • View file @ 347eaf77

  • Edit in single-file editor

  • Open in Web IDE


@@ -1301,4 +1301,42 @@ public class DefaultCodegenTest {
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");
}
}
0 Assignees
None
Assign to
0 Reviewers
None
Request review from
Labels
2
Client: Go Enhancement: General
2
Client: Go Enhancement: General
    Assign labels
  • Manage project labels

Milestone
4.3.0
4.3.0 (expired)
None
Time tracking
No estimate or time spent
Lock merge request
Unlocked
1
1 participant
Administrator
Reference: OpenAPITools/openapi-generator!5150
Source branch: github/fork/bkabrda/go-experimental-oneof-support

Menu

Explore Projects Groups Snippets