[csharp] Treat enum models consistently (#6851)

* [csharp] Treat enum models consistently

C# works differently from most languages in that enums are not
considered objects. This means default(EnumType) will choose a default
of the first enum option. This isn't desirable because it breaks the
required = false functionality of swagger specs, which defines a
property which isn't required to exist in the message body.

Rather than force consumers to use enum values such as UNSPECIFIED, UNKNOWN,
NOT_SET, etc... we can treat enums as primitives. This means any
non-required enum will become Nullable<EnumType> regardless of whether
it is defined as an inline enum or a referenced enum model.

* Categorizing C# integration test for enums as general

* [csharp] Remove enum-ref integration test

* [csharp] Clean up general enum support integration test, validate different enum usage cases.
This commit is contained in:
Jim Schubert
2017-11-09 04:14:47 -05:00
committed by wing328
parent 8b251555ac
commit 92ac1edd78
17 changed files with 1148 additions and 33 deletions

View File

@@ -4,6 +4,14 @@ package io.swagger.codegen;
* A class for storing constants that are used throughout the project.
*/
public class CodegenConstants {
public static final String APIS = "apis";
public static final String MODELS = "models";
public static final String SUPPORTING_FILES = "supportingFiles";
public static final String MODEL_TESTS = "modelTests";
public static final String MODEL_DOCS = "modelDocs";
public static final String API_TESTS = "apiTests";
public static final String API_DOCS = "apiDocs";
public static final String API_PACKAGE = "apiPackage";
public static final String API_PACKAGE_DESC = "package for generated api classes";

View File

@@ -33,9 +33,11 @@ public class DefaultGenerator extends AbstractGenerator implements Generator {
private Boolean generateApiDocumentation = null;
private Boolean generateModelTests = null;
private Boolean generateModelDocumentation = null;
private Boolean generateSwaggerMetadata = true;
private String basePath;
private String basePathWithoutHost;
private String contextPath;
private Map<String, String> generatorPropertyDefaults = new HashMap<>();
@Override
public Generator opts(ClientOptInput opts) {
@@ -61,6 +63,38 @@ public class DefaultGenerator extends AbstractGenerator implements Generator {
return this;
}
/**
* Programmatically disable the output of .swagger-codegen/VERSION, .swagger-codegen-ignore,
* or other metadata files used by Swagger Codegen.
* @param generateSwaggerMetadata true: enable outputs, false: disable outputs
*/
@SuppressWarnings("WeakerAccess")
public void setGenerateSwaggerMetadata(Boolean generateSwaggerMetadata) {
this.generateSwaggerMetadata = generateSwaggerMetadata;
}
/**
* Set generator properties otherwise pulled from system properties.
* Useful for running tests in parallel without relying on System.properties.
* @param key The system property key
* @param value The system property value
*/
@SuppressWarnings("WeakerAccess")
public void setGeneratorPropertyDefault(final String key, final String value) {
this.generatorPropertyDefaults.put(key, value);
}
private Boolean getGeneratorPropertyDefaultSwitch(final String key, final Boolean defaultValue) {
String result = null;
if (this.generatorPropertyDefaults.containsKey(key)) {
result = this.generatorPropertyDefaults.get(key);
}
if (result != null) {
return Boolean.valueOf(result);
}
return defaultValue;
}
private String getScheme() {
String scheme;
if (swagger.getSchemes() != null && swagger.getSchemes().size() > 0) {
@@ -88,11 +122,11 @@ public class DefaultGenerator extends AbstractGenerator implements Generator {
}
private void configureGeneratorProperties() {
// allows generating only models by specifying a CSV of models to generate, or empty for all
generateApis = System.getProperty("apis") != null ? true : null;
generateModels = System.getProperty("models") != null ? true : null;
generateSupportingFiles = System.getProperty("supportingFiles") != null ? true : null;
// NOTE: Boolean.TRUE is required below rather than `true` because of JVM boxing constraints and type inference.
generateApis = System.getProperty(CodegenConstants.APIS) != null ? Boolean.TRUE : getGeneratorPropertyDefaultSwitch(CodegenConstants.APIS, null);
generateModels = System.getProperty(CodegenConstants.MODELS) != null ? Boolean.TRUE : getGeneratorPropertyDefaultSwitch(CodegenConstants.MODELS, null);
generateSupportingFiles = System.getProperty(CodegenConstants.SUPPORTING_FILES) != null ? Boolean.TRUE : getGeneratorPropertyDefaultSwitch(CodegenConstants.SUPPORTING_FILES, null);
if (generateApis == null && generateModels == null && generateSupportingFiles == null) {
// no specifics are set, generate everything
@@ -110,10 +144,10 @@ public class DefaultGenerator extends AbstractGenerator implements Generator {
}
// model/api tests and documentation options rely on parent generate options (api or model) and no other options.
// They default to true in all scenarios and can only be marked false explicitly
generateModelTests = System.getProperty("modelTests") != null ? Boolean.valueOf(System.getProperty("modelTests")) : true;
generateModelDocumentation = System.getProperty("modelDocs") != null ? Boolean.valueOf(System.getProperty("modelDocs")) : true;
generateApiTests = System.getProperty("apiTests") != null ? Boolean.valueOf(System.getProperty("apiTests")) : true;
generateApiDocumentation = System.getProperty("apiDocs") != null ? Boolean.valueOf(System.getProperty("apiDocs")) : true;
generateModelTests = System.getProperty(CodegenConstants.MODEL_TESTS) != null ? Boolean.valueOf(System.getProperty(CodegenConstants.MODEL_TESTS)) : getGeneratorPropertyDefaultSwitch(CodegenConstants.MODEL_TESTS, true);
generateModelDocumentation = System.getProperty(CodegenConstants.MODEL_DOCS) != null ? Boolean.valueOf(System.getProperty(CodegenConstants.MODEL_DOCS)) : getGeneratorPropertyDefaultSwitch(CodegenConstants.MODEL_DOCS, true);
generateApiTests = System.getProperty(CodegenConstants.API_TESTS) != null ? Boolean.valueOf(System.getProperty(CodegenConstants.API_TESTS)) : getGeneratorPropertyDefaultSwitch(CodegenConstants.API_TESTS, true);
generateApiDocumentation = System.getProperty(CodegenConstants.API_DOCS) != null ? Boolean.valueOf(System.getProperty(CodegenConstants.API_DOCS)) : getGeneratorPropertyDefaultSwitch(CodegenConstants.API_DOCS, true);
// Additional properties added for tests to exclude references in project related files
@@ -595,7 +629,7 @@ public class DefaultGenerator extends AbstractGenerator implements Generator {
final String swaggerCodegenIgnore = ".swagger-codegen-ignore";
String ignoreFileNameTarget = config.outputFolder() + File.separator + swaggerCodegenIgnore;
File ignoreFile = new File(ignoreFileNameTarget);
if (!ignoreFile.exists()) {
if (generateSwaggerMetadata && !ignoreFile.exists()) {
String ignoreFileNameSource = File.separator + config.getCommonTemplateDir() + File.separator + swaggerCodegenIgnore;
String ignoreFileContents = readResourceContents(ignoreFileNameSource);
try {
@@ -606,13 +640,15 @@ public class DefaultGenerator extends AbstractGenerator implements Generator {
files.add(ignoreFile);
}
final String swaggerVersionMetadata = config.outputFolder() + File.separator + ".swagger-codegen" + File.separator + "VERSION";
File swaggerVersionMetadataFile = new File(swaggerVersionMetadata);
try {
writeToFile(swaggerVersionMetadata, ImplementationVersion.read());
files.add(swaggerVersionMetadataFile);
} catch (IOException e) {
throw new RuntimeException("Could not generate supporting file '" + swaggerVersionMetadata + "'", e);
if(generateSwaggerMetadata) {
final String swaggerVersionMetadata = config.outputFolder() + File.separator + ".swagger-codegen" + File.separator + "VERSION";
File swaggerVersionMetadataFile = new File(swaggerVersionMetadata);
try {
writeToFile(swaggerVersionMetadata, ImplementationVersion.read());
files.add(swaggerVersionMetadataFile);
} catch (IOException e) {
throw new RuntimeException("Could not generate supporting file '" + swaggerVersionMetadata + "'", e);
}
}
/*

View File

@@ -1,6 +1,7 @@
package io.swagger.codegen.languages;
import io.swagger.codegen.*;
import io.swagger.codegen.utils.ModelUtils;
import io.swagger.models.properties.*;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
@@ -296,6 +297,11 @@ public abstract class AbstractCSharpCodegen extends DefaultCodegen implements Co
additionalProperties.put(CodegenConstants.INTERFACE_PREFIX, interfacePrefix);
}
@Override
public void postProcessModelProperty(CodegenModel model, CodegenProperty property) {
super.postProcessModelProperty(model, property);
}
@Override
public Map<String, Object> postProcessModels(Map<String, Object> objs) {
List<Object> models = (List<Object>) objs.get("models");
@@ -315,6 +321,61 @@ public abstract class AbstractCSharpCodegen extends DefaultCodegen implements Co
return postProcessModelsEnum(objs);
}
/**
* Invoked by {@link DefaultGenerator} after all models have been post-processed, allowing for a last pass of codegen-specific model cleanup.
*
* @param objs Current state of codegen object model.
* @return An in-place modified state of the codegen object model.
*/
@Override
public Map<String, Object> postProcessAllModels(Map<String, Object> objs) {
final Map<String, Object> processed = super.postProcessAllModels(objs);
postProcessEnumRefs(processed);
return processed;
}
/**
* C# differs from other languages in that Enums are not _true_ objects; enums are compiled to integral types.
* So, in C#, an enum is considers more like a user-defined primitive.
*
* When working with enums, we can't always assume a RefModel is a nullable type (where default(YourType) == null),
* so this post processing runs through all models to find RefModel'd enums. Then, it runs through all vars and modifies
* those vars referencing RefModel'd enums to work the same as inlined enums rather than as objects.
* @param models
*/
private void postProcessEnumRefs(final Map<String, Object> models) {
Map<String, CodegenModel> enumRefs = new HashMap<String, CodegenModel>();
for (Map.Entry<String, Object> entry : models.entrySet()) {
CodegenModel model = ModelUtils.getModelByName(entry.getKey(), models);
if (model.isEnum) {
enumRefs.put(entry.getKey(), model);
}
}
for (Map.Entry<String, Object> entry : models.entrySet()) {
String swaggerName = entry.getKey();
CodegenModel model = ModelUtils.getModelByName(swaggerName, models);
if (model != null) {
for (CodegenProperty var : model.allVars) {
if (enumRefs.containsKey(var.datatype)) {
// Handle any enum properties referred to by $ref.
// This is different in C# than most other generators, because enums in C# are compiled to integral types,
// while enums in many other languages are true objects.
CodegenModel refModel = enumRefs.get(var.datatype);
var.allowableValues = refModel.allowableValues;
updateCodegenPropertyEnum(var);
// We do these after updateCodegenPropertyEnum to avoid generalities that don't mesh with C#.
var.isPrimitiveType = true;
var.isEnum = true;
}
}
} else {
LOGGER.warn("Expected to retrieve model %s by name, but no model was found. Check your -Dmodels inclusions.", swaggerName);
}
}
}
@Override
public Map<String, Object> postProcessOperations(Map<String, Object> objs) {
super.postProcessOperations(objs);

View File

@@ -513,11 +513,6 @@ public class CSharpClientCodegen extends AbstractCSharpCodegen {
this.packageGuid = packageGuid;
}
@Override
public Map<String, Object> postProcessModels(Map<String, Object> objMap) {
return super.postProcessModels(objMap);
}
@Override
public void postProcessParameter(CodegenParameter parameter) {
postProcessPattern(parameter.pattern, parameter.vendorExtensions);
@@ -530,7 +525,6 @@ public class CSharpClientCodegen extends AbstractCSharpCodegen {
super.postProcessModelProperty(model, property);
}
/*
* The swagger pattern spec follows the Perl convention and style of modifiers. .NET
* does not support this syntax directly so we need to convert the pattern to a .NET compatible