Adds generator unaliasSchema method, uses it to refactor python-experimental (#7274)

* Adds generator specific unaliasSchema method

* Adds unaliasSchema and hasValidation methods in python-experimental generator

* Removes primitive models with no validations, docs, tests, and models

* Uses unaliasSchema in getSchemaType and adds todos

* Deletes handleMethodResponse and fromResponse

* Simplifies fromRequestBody

* Removes unneeded handleMethodResponse

* Updates javadoc

* Updates fromModel

* Adds python-exp java test defaultSettingInPrimitiveModelWithValidations, removes model NumberWithValidationsAndDefault form v3 sample spec

* Deletes getSimpleTypeDeclaration

* Removes straggler file

* Deletes hasValidation and modelWillBeMade

* Uses super in fromFormProperty

* Regenerates samples for python-experimental

* Updates postProcessAllModels
This commit is contained in:
spacether 2020-08-27 08:07:33 -07:00 committed by GitHub
parent b4edfe477a
commit 892836f772
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 282 additions and 541 deletions

View File

@ -1971,6 +1971,10 @@ public class DefaultCodegen implements CodegenConfig {
return "oneOf<" + String.join(",", names) + ">";
}
protected Schema unaliasSchema(Schema schema, Map<String, String> usedImportMappings) {
return ModelUtils.unaliasSchema(this.openAPI, schema, usedImportMappings);
}
/**
* Return a string representation of the schema type, resolving aliasing and references if necessary.
*
@ -1978,7 +1982,7 @@ public class DefaultCodegen implements CodegenConfig {
* @return the string representation of the schema type.
*/
protected String getSingleSchemaType(Schema schema) {
Schema unaliasSchema = ModelUtils.unaliasSchema(this.openAPI, schema, importMapping);
Schema unaliasSchema = unaliasSchema(schema, importMapping);
if (StringUtils.isNotBlank(unaliasSchema.get$ref())) { // reference to another definition/schema
// get the schema/model name from $ref
@ -2216,7 +2220,7 @@ public class DefaultCodegen implements CodegenConfig {
}
// unalias schema
schema = ModelUtils.unaliasSchema(this.openAPI, schema, importMapping);
schema = unaliasSchema(schema, importMapping);
if (schema == null) {
LOGGER.warn("Schema {} not found", name);
return null;
@ -2330,7 +2334,7 @@ public class DefaultCodegen implements CodegenConfig {
m.interfaces = new ArrayList<String>();
for (Schema interfaceSchema : interfaces) {
interfaceSchema = ModelUtils.unaliasSchema(this.openAPI, interfaceSchema, importMapping);
interfaceSchema = unaliasSchema(interfaceSchema, importMapping);
if (StringUtils.isBlank(interfaceSchema.get$ref())) {
// primitive type
@ -2991,7 +2995,7 @@ public class DefaultCodegen implements CodegenConfig {
LOGGER.debug("debugging fromProperty for " + name + " : " + p);
// unalias schema
p = ModelUtils.unaliasSchema(this.openAPI, p, importMapping);
p = unaliasSchema(p, importMapping);
CodegenProperty property = CodegenModelFactory.newInstance(CodegenModelType.PROPERTY);
@ -3168,10 +3172,9 @@ public class DefaultCodegen implements CodegenConfig {
} else if (ModelUtils.isArraySchema(p)) {
// default to string if inner item is undefined
ArraySchema arraySchema = (ArraySchema) p;
Schema innerSchema = ModelUtils.unaliasSchema(this.openAPI, getSchemaItems(arraySchema), importMapping);
Schema innerSchema = unaliasSchema(getSchemaItems(arraySchema), importMapping);
} else if (ModelUtils.isMapSchema(p)) {
Schema innerSchema = ModelUtils.unaliasSchema(this.openAPI, getAdditionalProperties(p),
importMapping);
Schema innerSchema = unaliasSchema(getAdditionalProperties(p), importMapping);
if (innerSchema == null) {
LOGGER.error("Undefined map inner type for `{}`. Default to String.", p.getName());
innerSchema = new StringSchema().description("//TODO automatically added by openapi-generator due to undefined type");
@ -3251,7 +3254,7 @@ public class DefaultCodegen implements CodegenConfig {
itemName = property.name;
}
ArraySchema arraySchema = (ArraySchema) p;
Schema innerSchema = ModelUtils.unaliasSchema(this.openAPI, getSchemaItems(arraySchema), importMapping);
Schema innerSchema = unaliasSchema(getSchemaItems(arraySchema), importMapping);
CodegenProperty cp = fromProperty(itemName, innerSchema);
updatePropertyForArray(property, cp);
} else if (ModelUtils.isMapSchema(p)) {
@ -3263,8 +3266,7 @@ public class DefaultCodegen implements CodegenConfig {
property.maxItems = p.getMaxProperties();
// handle inner property
Schema innerSchema = ModelUtils.unaliasSchema(this.openAPI, getAdditionalProperties(p),
importMapping);
Schema innerSchema = unaliasSchema(getAdditionalProperties(p), importMapping);
if (innerSchema == null) {
LOGGER.error("Undefined map inner type for `{}`. Default to String.", p.getName());
innerSchema = new StringSchema().description("//TODO automatically added by openapi-generator due to undefined type");
@ -3504,7 +3506,7 @@ public class DefaultCodegen implements CodegenConfig {
CodegenOperation op,
ApiResponse methodResponse,
Map<String, String> importMappings) {
Schema responseSchema = ModelUtils.unaliasSchema(this.openAPI, ModelUtils.getSchemaFromResponse(methodResponse), importMappings);
Schema responseSchema = unaliasSchema(ModelUtils.getSchemaFromResponse(methodResponse), importMapping);
if (responseSchema != null) {
CodegenProperty cm = fromProperty("response", responseSchema);
@ -3908,8 +3910,7 @@ public class DefaultCodegen implements CodegenConfig {
}
Schema responseSchema;
if (this.openAPI != null && this.openAPI.getComponents() != null) {
responseSchema = ModelUtils.unaliasSchema(this.openAPI, ModelUtils.getSchemaFromResponse(response),
importMapping);
responseSchema = unaliasSchema(ModelUtils.getSchemaFromResponse(response), importMapping);
} else { // no model/alias defined
responseSchema = ModelUtils.getSchemaFromResponse(response);
}
@ -4150,7 +4151,7 @@ public class DefaultCodegen implements CodegenConfig {
}
if (parameterSchema != null) {
parameterSchema = ModelUtils.unaliasSchema(this.openAPI, parameterSchema);
parameterSchema = unaliasSchema(parameterSchema, Collections.<String, String>emptyMap());
if (parameterSchema == null) {
LOGGER.warn("warning! Schema not found for parameter \"" + parameter.getName() + "\", using String");
parameterSchema = new StringSchema().description("//TODO automatically added by openapi-generator due to missing type definition.");
@ -4690,7 +4691,7 @@ public class DefaultCodegen implements CodegenConfig {
private Map<String, Schema> unaliasPropertySchema(Map<String, Schema> properties) {
if (properties != null) {
for (String key : properties.keySet()) {
properties.put(key, ModelUtils.unaliasSchema(this.openAPI, properties.get(key), importMapping()));
properties.put(key, unaliasSchema(properties.get(key), importMapping()));
}
}
@ -5815,7 +5816,7 @@ public class DefaultCodegen implements CodegenConfig {
return codegenParameter;
}
private void addBodyModelSchema(CodegenParameter codegenParameter, String name, Schema schema, Set<String> imports, String bodyParameterName, boolean forceSimpleRef) {
protected void addBodyModelSchema(CodegenParameter codegenParameter, String name, Schema schema, Set<String> imports, String bodyParameterName, boolean forceSimpleRef) {
CodegenModel codegenModel = null;
if (StringUtils.isNotBlank(name)) {
schema.setName(name);

View File

@ -17,19 +17,16 @@
package org.openapitools.codegen.languages;
import io.swagger.v3.core.util.Json;
import io.swagger.v3.oas.models.Operation;
import io.swagger.v3.oas.models.media.*;
import io.swagger.v3.oas.models.media.ArraySchema;
import io.swagger.v3.oas.models.media.MediaType;
import io.swagger.v3.oas.models.media.Schema;
import io.swagger.v3.oas.models.parameters.Parameter;
import io.swagger.v3.oas.models.parameters.RequestBody;
import io.swagger.v3.oas.models.responses.ApiResponse;
import io.swagger.v3.oas.models.security.SecurityScheme;
import org.apache.commons.lang3.StringUtils;
import org.openapitools.codegen.*;
import org.openapitools.codegen.CodegenDiscriminator.MappedModel;
import org.openapitools.codegen.examples.ExampleGenerator;
import org.openapitools.codegen.meta.features.*;
import org.openapitools.codegen.utils.ModelUtils;
import org.openapitools.codegen.utils.ProcessUtils;
@ -225,6 +222,83 @@ public class PythonClientExperimentalCodegen extends PythonClientCodegen {
return "python-experimental";
}
@Override
protected Schema unaliasSchema(Schema schema, Map<String, String> usedImportMappings) {
Map<String, Schema> allSchemas = ModelUtils.getSchemas(openAPI);
if (allSchemas == null || allSchemas.isEmpty()) {
// skip the warning as the spec can have no model defined
//LOGGER.warn("allSchemas cannot be null/empty in unaliasSchema. Returned 'schema'");
return schema;
}
if (schema != null && StringUtils.isNotEmpty(schema.get$ref())) {
String simpleRef = ModelUtils.getSimpleRef(schema.get$ref());
if (usedImportMappings.containsKey(simpleRef)) {
LOGGER.debug("Schema unaliasing of {} omitted because aliased class is to be mapped to {}", simpleRef, usedImportMappings.get(simpleRef));
return schema;
}
Schema ref = allSchemas.get(simpleRef);
Boolean hasValidation = (
ref.getMaxItems() != null ||
ref.getMinLength() != null ||
ref.getMinItems() != null ||
ref.getMultipleOf() != null ||
ref.getPattern() != null ||
ref.getMaxLength() != null ||
ref.getMinimum() != null ||
ref.getMaximum() != null ||
ref.getExclusiveMaximum() != null ||
ref.getExclusiveMinimum() != null ||
ref.getUniqueItems() != null
);
if (ref == null) {
once(LOGGER).warn("{} is not defined", schema.get$ref());
return schema;
} else if (ref.getEnum() != null && !ref.getEnum().isEmpty()) {
// top-level enum class
return schema;
} else if (ModelUtils.isArraySchema(ref)) {
if (ModelUtils.isGenerateAliasAsModel(ref)) {
return schema; // generate a model extending array
} else {
return unaliasSchema(allSchemas.get(ModelUtils.getSimpleRef(schema.get$ref())),
usedImportMappings);
}
} else if (ModelUtils.isComposedSchema(ref)) {
return schema;
} else if (ModelUtils.isMapSchema(ref)) {
if (ref.getProperties() != null && !ref.getProperties().isEmpty()) // has at least one property
return schema; // treat it as model
else {
if (ModelUtils.isGenerateAliasAsModel(ref)) {
return schema; // generate a model extending map
} else {
// treat it as a typical map
return unaliasSchema(allSchemas.get(ModelUtils.getSimpleRef(schema.get$ref())),
usedImportMappings);
}
}
} else if (ModelUtils.isObjectSchema(ref)) { // model
if (ref.getProperties() != null && !ref.getProperties().isEmpty()) { // has at least one property
return schema;
} else { // free form object (type: object)
return unaliasSchema(allSchemas.get(ModelUtils.getSimpleRef(schema.get$ref())),
usedImportMappings);
}
} else if (hasValidation) {
// non object non array non map schemas that have validations
// are returned so we can generate those schemas as models
// we do this to:
// - preserve the validations in that model class in python
// - use those validations when we use this schema in composed oneOf schemas
return schema;
} else {
return unaliasSchema(allSchemas.get(ModelUtils.getSimpleRef(schema.get$ref())), usedImportMappings);
}
}
return schema;
}
public String pythonDate(Object dateValue) {
String strValue = null;
if (dateValue instanceof OffsetDateTime) {
@ -329,57 +403,58 @@ public class PythonClientExperimentalCodegen extends PythonClientCodegen {
return objs;
}
/**
/***
* Override with special post-processing for all models.
* we have a custom version of this method to:
* - remove any primitive models that do not contain validations
* these models are unaliased as inline definitions wherever the spec has them as refs
* this means that the generated client does not use these models
* because they are not used we do not write them
* - fix the model imports, go from model name to the full import string with toModelImport + globalImportFixer
*
* @param objs a map going from the model name to a object hoding the model info
* @return the updated objs
*/
@SuppressWarnings({"static-method", "unchecked"})
@Override
public Map<String, Object> postProcessAllModels(Map<String, Object> objs) {
super.postProcessAllModels(objs);
// loop through all models and delete ones where type!=object and the model has no validations and enums
// we will remove them because they are not needed
Map<String, Schema> modelSchemasToRemove = new HashMap<String, Schema>();
for (Object objModel: objs.values()) {
HashMap<String, Object> hmModel = (HashMap<String, Object>) objModel;
List<Map<String, Object>> models = (List<Map<String, Object>>) hmModel.get("models");
for (Map<String, Object> model : models) {
CodegenModel cm = (CodegenModel) model.get("model");
// remove model if it is a primitive with no validations
if (cm.isEnum || cm.isAlias) {
Schema modelSchema = ModelUtils.getSchema(this.openAPI, cm.name);
CodegenProperty modelProperty = fromProperty("_value", modelSchema);
if (!modelProperty.isEnum && !modelProperty.hasValidation && !cm.isArrayModel) {
// remove these models because they are aliases and do not have any enums or validations
modelSchemasToRemove.put(cm.name, modelSchema);
continue;
List<String> modelsToRemove = new ArrayList<>();
Map<String, Schema> allDefinitions = ModelUtils.getSchemas(this.openAPI);
for (String schemaName: allDefinitions.keySet()) {
Schema refSchema = new Schema().$ref("#/components/schemas/"+schemaName);
Schema unaliasedSchema = unaliasSchema(refSchema, importMapping);
String modelName = toModelName(schemaName);
if (unaliasedSchema.get$ref() == null) {
modelsToRemove.add(modelName);
} else {
HashMap<String, Object> objModel = (HashMap<String, Object>) objs.get(modelName);
List<Map<String, Object>> models = (List<Map<String, Object>>) objModel.get("models");
for (Map<String, Object> model : models) {
CodegenModel cm = (CodegenModel) model.get("model");
String[] importModelNames = cm.imports.toArray(new String[0]);
cm.imports.clear();
for (String importModelName : importModelNames) {
cm.imports.add(toModelImport(importModelName));
String globalImportFixer = "globals()['" + importModelName + "'] = " + importModelName;
cm.imports.add(globalImportFixer);
}
}
// fix model imports
if (cm.imports.size() == 0) {
continue;
}
String[] modelNames = cm.imports.toArray(new String[0]);
cm.imports.clear();
for (String modelName : modelNames) {
cm.imports.add(toModelImport(modelName));
String globalImportFixer = "globals()['" + modelName + "'] = " + modelName;
cm.imports.add(globalImportFixer);
}
}
}
// Remove modelSchemasToRemove models from objs
for (String modelName : modelSchemasToRemove.keySet()) {
for (String modelName : modelsToRemove) {
objs.remove(modelName);
}
return objs;
}
/**
* Convert OAS Property object to Codegen Property object
* We have a custom version of this method to always set allowableValues.enumVars on all enum variables
* Together with unaliasSchema this sets primitive types with validations as models
* This method is used by fromResponse
*
* @param name name of the property
* @param p OAS property object
@ -387,12 +462,17 @@ public class PythonClientExperimentalCodegen extends PythonClientCodegen {
*/
@Override
public CodegenProperty fromProperty(String name, Schema p) {
// we have a custom version of this function to always set allowableValues.enumVars on all enum variables
CodegenProperty result = super.fromProperty(name, p);
if (result.isEnum) {
updateCodegenPropertyEnum(result);
CodegenProperty cp = super.fromProperty(name, p);
if (cp.isEnum) {
updateCodegenPropertyEnum(cp);
}
return result;
if (cp.isPrimitiveType && p.get$ref() != null) {
cp.complexType = cp.dataType;
}
if (cp.isListContainer && cp.complexType == null && cp.mostInnerItems.complexType != null) {
cp.complexType = cp.mostInnerItems.complexType;
}
return cp;
}
/**
@ -439,167 +519,69 @@ public class PythonClientExperimentalCodegen extends PythonClientCodegen {
// overwriting defaultValue omitted from here
}
/***
* We have a custom version of this method to produce links to models when they are
* primitive type (not map, not array, not object) and include validations or are enums
*
* @param body requesst body
* @param imports import collection
* @param bodyParameterName body parameter name
* @return the resultant CodegenParameter
*/
@Override
public CodegenParameter fromRequestBody(RequestBody body, Set<String> imports, String bodyParameterName) {
CodegenParameter result = super.fromRequestBody(body, imports, bodyParameterName);
// if we generated a model with a non-object type because it has validations or enums,
// make sure that the datatype of that body parameter refers to our model class
Content content = body.getContent();
Set<String> keySet = content.keySet();
Object[] keyArray = (Object[]) keySet.toArray();
MediaType mediaType = content.get(keyArray[0]);
Schema schema = mediaType.getSchema();
String ref = schema.get$ref();
if (ref == null) {
return result;
CodegenParameter cp = super.fromRequestBody(body, imports, bodyParameterName);
Schema schema = ModelUtils.getSchemaFromRequestBody(body);
if (schema.get$ref() == null) {
return cp;
}
String modelName = ModelUtils.getSimpleRef(ref);
// the result lacks validation info so we need to make a CodegenProperty from the schema to check
// if we have validation and enum info exists
Schema realSchema = ModelUtils.getSchema(this.openAPI, modelName);
CodegenProperty modelProp = fromProperty("body", realSchema);
if (modelProp.isPrimitiveType && (modelProp.hasValidation || modelProp.isEnum)) {
String simpleDataType = result.dataType;
result.dataType = toModelName(modelName);
result.baseType = result.dataType;
Schema unaliasedSchema = unaliasSchema(schema, importMapping);
CodegenProperty unaliasedProp = fromProperty("body", unaliasedSchema);
Boolean dataTypeMismatch = !cp.dataType.equals(unaliasedProp.dataType);
Boolean baseTypeMismatch = !cp.baseType.equals(unaliasedProp.complexType) && unaliasedProp.complexType != null;
if (dataTypeMismatch || baseTypeMismatch) {
cp.dataType = unaliasedProp.dataType;
cp.baseType = unaliasedProp.complexType;
}
return result;
return cp;
}
/**
* Convert OAS Response object to Codegen Response object
/***
* Adds the body model schema to the body parameter
* We have a custom version of this method so we can flip forceSimpleRef
* to True based upon the results of unaliasSchema
* With this customization, we ensure that when schemas are passed to getSchemaType
* - if they have ref in them they are a model
* - if they do not have ref in them they are not a model
*
* @param responseCode HTTP response code
* @param response OAS Response object
* @return Codegen Response object
* @param codegenParameter the body parameter
* @param name model schema ref key in components
* @param schema the model schema (not refed)
* @param imports collection of imports
* @param bodyParameterName body parameter name
* @param forceSimpleRef if true use a model reference
*/
@Override
public CodegenResponse fromResponse(String responseCode, ApiResponse response) {
// if a response points at a model whose type != object and it has validations and/or enums, then we will
// generate the model, and response.baseType must be the name
// of the model. Point responses at models if the model is python class type ModelSimple
// When we serialize/deserialize ModelSimple models, validations and enums will be checked.
Schema responseSchema;
if (this.openAPI != null && this.openAPI.getComponents() != null) {
responseSchema = ModelUtils.unaliasSchema(this.openAPI, ModelUtils.getSchemaFromResponse(response), importMapping);
} else { // no model/alias defined
responseSchema = ModelUtils.getSchemaFromResponse(response);
}
String newBaseType = null;
if (responseSchema != null) {
CodegenProperty cp = fromProperty("response", responseSchema);
if (cp.complexType != null) {
String modelName = cp.complexType;
Schema modelSchema = ModelUtils.getSchema(this.openAPI, modelName);
if (modelSchema != null && !"object".equals(modelSchema.getType())) {
CodegenProperty modelProp = fromProperty("response", modelSchema);
if (modelProp.isEnum == true || modelProp.hasValidation == true) {
// this model has validations and/or enums so we will generate it
newBaseType = modelName;
}
}
} else {
if (cp.isEnum == true || cp.hasValidation == true) {
// this model has validations and/or enums so we will generate it
Schema sc = ModelUtils.getSchemaFromResponse(response);
newBaseType = toModelName(ModelUtils.getSimpleRef(sc.get$ref()));
}
protected void addBodyModelSchema(CodegenParameter codegenParameter, String name, Schema schema, Set<String> imports, String bodyParameterName, boolean forceSimpleRef) {
if (name != null) {
Schema bodySchema = new Schema().$ref("#/components/schemas/" + name);
Schema unaliased = unaliasSchema(bodySchema, importMapping);
if (unaliased.get$ref() != null) {
forceSimpleRef = true;
}
}
super.addBodyModelSchema(codegenParameter, name, schema, imports, bodyParameterName, forceSimpleRef);
CodegenResponse result = super.fromResponse(responseCode, response);
if (newBaseType != null) {
result.dataType = newBaseType;
// baseType is used to set the link to the model .md documentation
result.baseType = newBaseType;
}
return result;
}
/**
* Set op's returnBaseType, returnType, examples etc.
*
* @param operation endpoint Operation
* @param schemas a map of the schemas in the openapi spec
* @param op endpoint CodegenOperation
* @param methodResponse the default ApiResponse for the endpoint
*/
@Override
public void handleMethodResponse(Operation operation,
Map<String, Schema> schemas,
CodegenOperation op,
ApiResponse methodResponse) {
handleMethodResponse(operation, schemas, op, methodResponse, Collections.<String, String>emptyMap());
}
/**
* Set op's returnBaseType, returnType, examples etc.
*
* @param operation endpoint Operation
* @param schemas a map of the schemas in the openapi spec
* @param op endpoint CodegenOperation
* @param methodResponse the default ApiResponse for the endpoint
* @param importMappings mappings of external types to be omitted by unaliasing
*/
@Override
protected void handleMethodResponse(Operation operation,
Map<String, Schema> schemas,
CodegenOperation op,
ApiResponse methodResponse,
Map<String, String> importMappings) {
// we have a custom version of this method to handle endpoints that return models where
// type != object the model has validations and/or enums
// we do this by invoking our custom fromResponse method to create defaultResponse
// which we then use to set op.returnType and op.returnBaseType
CodegenResponse defaultResponse = fromResponse("defaultResponse", methodResponse);
Schema responseSchema = ModelUtils.unaliasSchema(this.openAPI, ModelUtils.getSchemaFromResponse(methodResponse), importMappings);
if (responseSchema != null) {
op.returnBaseType = defaultResponse.baseType;
// generate examples
String exampleStatusCode = "200";
for (String key : operation.getResponses().keySet()) {
if (operation.getResponses().get(key) == methodResponse && !key.equals("default")) {
exampleStatusCode = key;
}
}
op.examples = new ExampleGenerator(schemas, this.openAPI).generateFromResponseSchema(exampleStatusCode, responseSchema, getProducesInfo(this.openAPI, operation));
op.defaultResponse = toDefaultValue(responseSchema);
op.returnType = defaultResponse.dataType;
op.hasReference = schemas.containsKey(op.returnBaseType);
// lookup discriminator
Schema schema = schemas.get(op.returnBaseType);
if (schema != null) {
CodegenModel cmod = fromModel(op.returnBaseType, schema);
op.discriminator = cmod.discriminator;
}
if (defaultResponse.isListContainer) {
op.isListContainer = true;
} else if (defaultResponse.isMapContainer) {
op.isMapContainer = true;
} else {
op.returnSimpleType = true;
}
if (languageSpecificPrimitives().contains(op.returnBaseType) || op.returnBaseType == null) {
op.returnTypeIsPrimitive = true;
}
}
addHeaders(methodResponse, op.responseHeaders);
}
/**
* Return the sanitized variable name for enum
*
* @param value enum variable name
* @param datatype data type
* @return the sanitized variable name for enum
*/
/**
* Return the sanitized variable name for enum
*
* @param value enum variable name
* @param datatype data type
* @return the sanitized variable name for enum
*/
public String toEnumVarName(String value, String datatype) {
// our enum var names are keys in a python dict, so change spaces to underscores
if (value.length() == 0) {
@ -710,175 +692,62 @@ public class PythonClientExperimentalCodegen extends PythonClientCodegen {
/**
* Convert OAS Model object to Codegen Model object
* We have a custom version of this method so we can:
* - set the correct regex values for requiredVars + optionalVars
* - set model.defaultValue and model.hasRequired per the three use cases defined in this method
*
* @param name the name of the model
* @param schema OAS Model object
* @param sc OAS Model object
* @return Codegen Model object
*/
@Override
public CodegenModel fromModel(String name, Schema schema) {
// we have a custom version of this function so we can produce
// models for components whose type != object and which have validations and enums
// this ensures that:
// - endpoint (operation) responses with validations and type!=(object or array)
// - oneOf $ref components with validations and type!=(object or array)
// when endpoints receive payloads of these models
// that they will be converted into instances of these models
Map<String, String> propertyToModelName = new HashMap<String, String>();
Map<String, Schema> propertiesMap = schema.getProperties();
if (propertiesMap != null) {
for (Map.Entry<String, Schema> entry : propertiesMap.entrySet()) {
String schemaPropertyName = entry.getKey();
String pythonPropertyName = toVarName(schemaPropertyName);
Schema propertySchema = entry.getValue();
String ref = propertySchema.get$ref();
if (ref == null) {
continue;
}
Schema refSchema = ModelUtils.getReferencedSchema(this.openAPI, propertySchema);
String refType = refSchema.getType();
if (refType == null || refType.equals("object")) {
continue;
}
CodegenProperty modelProperty = fromProperty("_fake_name", refSchema);
if (modelProperty.isEnum == true || modelProperty.hasValidation == false) {
continue;
}
String modelName = ModelUtils.getSimpleRef(ref);
propertyToModelName.put(pythonPropertyName, toModelName(modelName));
}
public CodegenModel fromModel(String name, Schema sc) {
CodegenModel cm = super.fromModel(name, sc);
if (cm.requiredVars.size() > 0 && (cm.oneOf.size() > 0 || cm.anyOf.size() > 0)) {
addNullDefaultToOneOfAnyOfReqProps(sc, cm);
}
CodegenModel result = super.fromModel(name, schema);
// have oneOf point to the correct model
if (ModelUtils.isComposedSchema(schema)) {
ComposedSchema cs = (ComposedSchema) schema;
Map<String, Integer> importCounts = new HashMap<String, Integer>();
List<Schema> oneOfSchemas = cs.getOneOf();
if (oneOfSchemas != null) {
for (int i = 0; i < oneOfSchemas.size(); i++) {
Schema oneOfSchema = oneOfSchemas.get(i);
String languageType = getTypeDeclaration(oneOfSchema);
String ref = oneOfSchema.get$ref();
if (ref == null) {
Integer currVal = importCounts.getOrDefault(languageType, 0);
importCounts.put(languageType, currVal+1);
continue;
}
Schema refSchema = ModelUtils.getReferencedSchema(this.openAPI, oneOfSchema);
String refType = refSchema.getType();
if (refType == null || refType.equals("object")) {
Integer currVal = importCounts.getOrDefault(languageType, 0);
importCounts.put(languageType, currVal+1);
continue;
}
CodegenProperty modelProperty = fromProperty("_oneOfSchema", refSchema);
if (modelProperty.isEnum == true) {
Integer currVal = importCounts.getOrDefault(languageType, 0);
importCounts.put(languageType, currVal+1);
continue;
}
languageType = getTypeDeclaration(refSchema);
if (modelProperty.hasValidation == false) {
Integer currVal = importCounts.getOrDefault(languageType, 0);
importCounts.put(languageType, currVal+1);
continue;
}
Integer currVal = importCounts.getOrDefault(languageType, 0);
importCounts.put(languageType, currVal);
String modelName = toModelName(ModelUtils.getSimpleRef(ref));
result.imports.add(modelName);
result.oneOf.add(modelName);
currVal = importCounts.getOrDefault(modelName, 0);
importCounts.put(modelName, currVal+1);
}
}
for (Map.Entry<String, Integer> entry : importCounts.entrySet()) {
String importName = entry.getKey();
Integer importCount = entry.getValue();
if (importCount == 0) {
result.oneOf.remove(importName);
}
}
}
// this block handles models which have the python base class ModelSimple
// which are responsible for storing validations, enums, and an unnamed value
Schema modelSchema = ModelUtils.getSchema(this.openAPI, result.name);
CodegenProperty modelProperty = fromProperty("_value", modelSchema);
Boolean isPythonModelSimpleModel = (result.isEnum || result.isArrayModel || result.isAlias && modelProperty.hasValidation);
if (isPythonModelSimpleModel) {
// In python, classes which inherit from our ModelSimple class store one value,
// like a str, int, list and extra data about that value like validations and enums
if (result.isEnum) {
// if there is only one allowed value then we know that it should be set, so value is optional
// -> hasRequired = false
// if there are more than one allowed value then value is positional and required so
// -> hasRequired = true
ArrayList values = (ArrayList) result.allowableValues.get("values");
if (values != null && values.size() > 1) {
result.hasRequired = true;
}
if (modelProperty.defaultValue != null && result.defaultValue == null) {
result.defaultValue = modelProperty.defaultValue;
}
} else {
if (result.defaultValue == null) {
result.hasRequired = true;
}
}
}
// fix all property references to ModelSimple models, make those properties non-primitive and
// set their dataType and complexType to the model name, so documentation will refer to the correct model
// set regex values, before it was only done on model.vars
// NOTE: this is done for models of type != object which are not enums and have validations
ArrayList<List<CodegenProperty>> listOfLists = new ArrayList<List<CodegenProperty>>();
listOfLists.add(result.vars);
listOfLists.add(result.allVars);
listOfLists.add(result.requiredVars);
listOfLists.add(result.optionalVars);
listOfLists.add(result.readOnlyVars);
listOfLists.add(result.readWriteVars);
listOfLists.add(cm.requiredVars);
listOfLists.add(cm.optionalVars);
for (List<CodegenProperty> cpList : listOfLists) {
for (CodegenProperty cp : cpList) {
// set regex values, before it was only done on model.vars
postProcessModelProperty(result, cp);
// fix references to non-object models
if (!propertyToModelName.containsKey(cp.name)) {
continue;
}
cp.isPrimitiveType = false;
String modelName = propertyToModelName.get(cp.name);
cp.complexType = modelName;
cp.dataType = modelName;
cp.isEnum = false;
cp.hasValidation = false;
result.imports.add(modelName);
// sets regex values
postProcessModelProperty(cm, cp);
}
}
// if a class has a property of type self, remove the self import from imports
if (result.imports.contains(result.classname)) {
result.imports.remove(result.classname);
Boolean isNotPythonModelSimpleModel = (ModelUtils.isComposedSchema(sc) || ModelUtils.isObjectSchema(sc) || ModelUtils.isMapSchema(sc));
if (isNotPythonModelSimpleModel) {
return cm;
}
if (result.requiredVars.size() > 0 && (result.oneOf.size() > 0 || result.anyOf.size() > 0)) {
addNullDefaultToOneOfAnyOfReqProps(schema, result);
// Use cases for default values / enums of length one
// 1. no default exists
// schema does not contain default
// cm.defaultValue unset, cm.hasRequired = true
// 2. server has a default
// schema contains default
// cm.defaultValue set, cm.hasRequired = true
// different value here to differentiate between use case 3 below
// This defaultValue is used in the client docs only and is not sent to the server
// 3. only one value is allowed in an enum
// schema does not contain default
// cm.defaultValue set, cm.hasRequired = false
// because we know what value needs to be set so the user doesn't need to input it
// This defaultValue is used in the client and is sent to the server
String defaultValue = toDefaultValue(sc);
if (sc.getDefault() == null && defaultValue == null) {
cm.hasRequired = true;
} else if (sc.getDefault() != null) {
cm.defaultValue = defaultValue;
cm.hasRequired = true;
} else if (defaultValue != null && cm.defaultValue == null) {
cm.defaultValue = defaultValue;
cm.hasRequired = false;
}
return result;
return cm;
}
/**
* returns the OpenAPI type for the property. Use getAlias to handle $ref of primitive type
* We have a custom version of this function because for composed schemas we also want to return the model name
* In DefaultCodegen.java it returns a name built off of individual allOf/anyOf/oneOf which is not what
* python-experimental needs. Python-experimental needs the name of the composed schema
* Returns the python type for the property.
*
* @param schema property schema
* @return string presentation of the type
@ -886,102 +755,24 @@ public class PythonClientExperimentalCodegen extends PythonClientCodegen {
@SuppressWarnings("static-method")
@Override
public String getSchemaType(Schema schema) {
if (schema instanceof ComposedSchema) { // composed schema
Schema unaliasSchema = ModelUtils.unaliasSchema(this.openAPI, schema, importMapping);
String ref = unaliasSchema.get$ref();
if (ref != null) {
String schemaName = ModelUtils.getSimpleRef(unaliasSchema.get$ref());
if (StringUtils.isNotEmpty(schemaName) && importMapping.containsKey(schemaName)) {
return schemaName;
}
return getAlias(schemaName);
} else {
// we may have be processing the component schema rather than a schema with a $ref
// to a component schema
// so loop through component schemas and use the found one's name if we match
Map<String, Schema> schemas = ModelUtils.getSchemas(openAPI);
for (String thisSchemaName : schemas.keySet()) {
Schema thisSchema = schemas.get(thisSchemaName);
if (!ModelUtils.isComposedSchema(thisSchema)) {
continue;
}
if (thisSchema == unaliasSchema) {
if (importMapping.containsKey(thisSchemaName)) {
return thisSchemaName;
}
return getAlias(thisSchemaName);
}
}
LOGGER.warn("Error obtaining the datatype from ref:" + unaliasSchema.get$ref() + ". Default to 'object'");
return "object";
}
}
String openAPIType = getSingleSchemaType(schema);
if (typeMapping.containsKey(openAPIType)) {
String type = typeMapping.get(openAPIType);
if (languageSpecificPrimitives.contains(type)) {
return type;
}
} else {
return toModelName(openAPIType);
return type;
}
return openAPIType;
return toModelName(openAPIType);
}
public String getModelName(Schema sc) {
Boolean thisModelWillBeMade = modelWillBeMade(sc);
Map<String, Schema> schemas = ModelUtils.getSchemas(openAPI);
for (String thisSchemaName : schemas.keySet()) {
Schema thisSchema = schemas.get(thisSchemaName);
if (thisSchema == sc && thisModelWillBeMade) {
return toModelName(thisSchemaName);
if (sc.get$ref() != null) {
Schema unaliasedSchema = unaliasSchema(sc, importMapping);
if (unaliasedSchema.get$ref() != null) {
return toModelName(ModelUtils.getSimpleRef(sc.get$ref()));
}
}
return null;
}
/**
* Output the type declaration of the property
*
* @param schema property schema
* @return a string presentation of the property type
*/
public String getSimpleTypeDeclaration(Schema schema) {
String oasType = getSchemaType(schema);
if (typeMapping.containsKey(oasType)) {
return typeMapping.get(oasType);
}
return oasType;
}
public Boolean modelWillBeMade(Schema s) {
// only invoke this on $refed schemas
if (ModelUtils.isComposedSchema(s) || ModelUtils.isObjectSchema(s) || ModelUtils.isArraySchema(s) || ModelUtils.isMapSchema(s)) {
return true;
}
List<Object> enums = s.getEnum();
if (enums != null && !enums.isEmpty()) {
return true;
}
Boolean hasValidation = (
s.getMaxItems() != null ||
s.getMinLength() != null ||
s.getMinItems() != null ||
s.getMultipleOf() != null ||
s.getPattern() != null ||
s.getMaxLength() != null ||
s.getMinimum() != null ||
s.getMaximum() != null ||
s.getExclusiveMaximum() != null ||
s.getExclusiveMinimum() != null ||
s.getUniqueItems() != null
);
if (hasValidation) {
return true;
}
return false;
}
/**
* Return a string representation of the Python types for the specified OAS schema.
* Primitive types in the OAS specification are implemented in Python using the corresponding
@ -1011,8 +802,8 @@ public class PythonClientExperimentalCodegen extends PythonClientCodegen {
if (StringUtils.isNotEmpty(p.get$ref())) {
// The input schema is a reference. If the resolved schema is
// a composed schema, convert the name to a Python class.
Schema s = ModelUtils.getReferencedSchema(this.openAPI, p);
if (modelWillBeMade(s)) {
Schema unaliasedSchema = unaliasSchema(p, importMapping);
if (unaliasedSchema.get$ref() != null) {
String modelName = toModelName(ModelUtils.getSimpleRef(p.get$ref()));
if (referencedModelNames != null) {
referencedModelNames.add(modelName);
@ -1053,7 +844,7 @@ public class PythonClientExperimentalCodegen extends PythonClientCodegen {
if (ModelUtils.isFileSchema(p)) {
return prefix + "file_type" + fullSuffix;
}
String baseType = getSimpleTypeDeclaration(p);
String baseType = getSchemaType(p);
return prefix + baseType + fullSuffix;
}
@ -1133,7 +924,7 @@ public class PythonClientExperimentalCodegen extends PythonClientCodegen {
* @param in input string
* @return quoted string
*/
public String ensureQuotes(String in) {
private String ensureQuotes(String in) {
Pattern pattern = Pattern.compile("\r\n|\r|\n");
Matcher matcher = pattern.matcher(in);
if (matcher.find()) {
@ -1222,20 +1013,15 @@ public class PythonClientExperimentalCodegen extends PythonClientCodegen {
example = objExample.toString();
}
if (null != schema.get$ref()) {
// $ref case:
Map<String, Schema> allDefinitions = ModelUtils.getSchemas(this.openAPI);
String ref = ModelUtils.getSimpleRef(schema.get$ref());
if (allDefinitions != null) {
Schema refSchema = allDefinitions.get(ref);
if (null == refSchema) {
return fullPrefix + "None" + closeChars;
} else {
String refModelName = getModelName(refSchema);
return toExampleValueRecursive(refModelName, refSchema, objExample, indentationLevel, prefix, exampleLine);
}
} else {
LOGGER.warn("allDefinitions not defined in toExampleValue!\n");
Schema refSchema = allDefinitions.get(ref);
if (null == refSchema) {
LOGGER.warn("Unable to find referenced schema "+schema.get$ref()+"\n");
return fullPrefix + "None" + closeChars;
}
String refModelName = getModelName(schema);
return toExampleValueRecursive(refModelName, refSchema, objExample, indentationLevel, prefix, exampleLine);
} else if (ModelUtils.isNullType(schema) || isAnyTypeSchema(schema)) {
// The 'null' type is allowed in OAS 3.1 and above. It is not supported by OAS 3.0.x,
// though this tooling supports it.
@ -1537,92 +1323,12 @@ public class PythonClientExperimentalCodegen extends PythonClientCodegen {
*/
@Override
public CodegenParameter fromFormProperty(String name, Schema propertySchema, Set<String> imports) {
CodegenParameter codegenParameter = CodegenModelFactory.newInstance(CodegenModelType.PARAMETER);
LOGGER.debug("Debugging fromFormProperty {}: {}", name, propertySchema);
CodegenProperty codegenProperty = fromProperty(name, propertySchema);
ModelUtils.syncValidationProperties(propertySchema, codegenProperty);
codegenParameter.isFormParam = Boolean.TRUE;
codegenParameter.baseName = codegenProperty.baseName;
codegenParameter.paramName = toParamName((codegenParameter.baseName));
codegenParameter.baseType = codegenProperty.baseType;
codegenParameter.dataType = codegenProperty.dataType;
codegenParameter.dataFormat = codegenProperty.dataFormat;
codegenParameter.description = escapeText(codegenProperty.description);
codegenParameter.unescapedDescription = codegenProperty.getDescription();
codegenParameter.jsonSchema = Json.pretty(propertySchema);
codegenParameter.defaultValue = codegenProperty.getDefaultValue();
if (codegenProperty.getVendorExtensions() != null && !codegenProperty.getVendorExtensions().isEmpty()) {
codegenParameter.vendorExtensions = codegenProperty.getVendorExtensions();
}
if (propertySchema.getRequired() != null && !propertySchema.getRequired().isEmpty() && propertySchema.getRequired().contains(codegenProperty.baseName)) {
codegenParameter.required = Boolean.TRUE;
}
// non-array/map
updateCodegenPropertyEnum(codegenProperty);
codegenParameter.isEnum = codegenProperty.isEnum;
codegenParameter._enum = codegenProperty._enum;
codegenParameter.allowableValues = codegenProperty.allowableValues;
if (codegenProperty.isEnum) {
codegenParameter.datatypeWithEnum = codegenProperty.datatypeWithEnum;
codegenParameter.enumName = codegenProperty.enumName;
}
if (codegenProperty.items != null && codegenProperty.items.isEnum) {
codegenParameter.items = codegenProperty.items;
codegenParameter.mostInnerItems = codegenProperty.mostInnerItems;
}
// import
if (codegenProperty.complexType != null) {
imports.add(codegenProperty.complexType);
}
// validation
// handle maximum, minimum properly for int/long by removing the trailing ".0"
if (ModelUtils.isIntegerSchema(propertySchema)) {
codegenParameter.maximum = propertySchema.getMaximum() == null ? null : String.valueOf(propertySchema.getMaximum().longValue());
codegenParameter.minimum = propertySchema.getMinimum() == null ? null : String.valueOf(propertySchema.getMinimum().longValue());
} else {
codegenParameter.maximum = propertySchema.getMaximum() == null ? null : String.valueOf(propertySchema.getMaximum());
codegenParameter.minimum = propertySchema.getMinimum() == null ? null : String.valueOf(propertySchema.getMinimum());
}
codegenParameter.exclusiveMaximum = propertySchema.getExclusiveMaximum() == null ? false : propertySchema.getExclusiveMaximum();
codegenParameter.exclusiveMinimum = propertySchema.getExclusiveMinimum() == null ? false : propertySchema.getExclusiveMinimum();
codegenParameter.maxLength = propertySchema.getMaxLength();
codegenParameter.minLength = propertySchema.getMinLength();
codegenParameter.pattern = toRegularExpression(propertySchema.getPattern());
codegenParameter.maxItems = propertySchema.getMaxItems();
codegenParameter.minItems = propertySchema.getMinItems();
codegenParameter.uniqueItems = propertySchema.getUniqueItems() == null ? false : propertySchema.getUniqueItems();
codegenParameter.multipleOf = propertySchema.getMultipleOf();
// exclusive* are noop without corresponding min/max
if (codegenParameter.maximum != null || codegenParameter.minimum != null ||
codegenParameter.maxLength != null || codegenParameter.minLength != null ||
codegenParameter.maxItems != null || codegenParameter.minItems != null ||
codegenParameter.pattern != null || codegenParameter.multipleOf != null) {
codegenParameter.hasValidation = true;
}
setParameterBooleanFlagWithCodegenProperty(codegenParameter, codegenProperty);
CodegenParameter cp = super.fromFormProperty(name, propertySchema, imports);
Parameter p = new Parameter();
p.setSchema(propertySchema);
p.setName(codegenParameter.paramName);
setParameterExampleValue(codegenParameter, p);
// setParameterExampleValue(codegenParameter);
// set nullable
setParameterNullable(codegenParameter, codegenProperty);
//TODO collectionFormat for form parameter not yet supported
//codegenParameter.collectionFormat = getCollectionFormat(propertySchema);
return codegenParameter;
p.setName(cp.paramName);
setParameterExampleValue(cp, p);
return cp;
}
/**

View File

@ -21,7 +21,8 @@ import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.Operation;
import io.swagger.v3.oas.models.media.*;
import io.swagger.v3.parser.util.SchemaTypeUtil;
import java.time.OffsetDateTime;
import java.math.BigDecimal;
import java.util.Arrays;
import org.openapitools.codegen.*;
import org.openapitools.codegen.languages.PythonClientExperimentalCodegen;
import org.openapitools.codegen.utils.ModelUtils;
@ -341,4 +342,37 @@ public class PythonClientExperimentalTest {
Assert.assertEquals(importValue, "from models.special_model_name import SpecialModelName");
}
@Test(description = "format imports of models containing special characters")
public void defaultSettingInPrimitiveModelWithValidations() {
final PythonClientExperimentalCodegen codegen = new PythonClientExperimentalCodegen();
OpenAPI openAPI = TestUtils.createOpenAPI();
final Schema noDefault = new ArraySchema()
.type("number")
.minimum(new BigDecimal("10"));
final Schema hasDefault = new Schema()
.type("number")
.minimum(new BigDecimal("10"));
hasDefault.setDefault("15.0");
final Schema noDefaultEumLengthOne = new Schema()
.type("number")
.minimum(new BigDecimal("10"));
noDefaultEumLengthOne.setEnum(Arrays.asList("15.0"));
openAPI.getComponents().addSchemas("noDefaultModel", noDefault);
openAPI.getComponents().addSchemas("hasDefaultModel", hasDefault);
openAPI.getComponents().addSchemas("noDefaultEumLengthOneModel", noDefaultEumLengthOne);
codegen.setOpenAPI(openAPI);
final CodegenModel noDefaultModel = codegen.fromModel("noDefaultModel", noDefault);
Assert.assertEquals(noDefaultModel.defaultValue, null);
Assert.assertEquals(noDefaultModel.hasRequired, true);
final CodegenModel hasDefaultModel = codegen.fromModel("hasDefaultModel", hasDefault);
Assert.assertEquals(hasDefaultModel.defaultValue, "15.0");
Assert.assertEquals(hasDefaultModel.hasRequired, true);
final CodegenModel noDefaultEumLengthOneModel = codegen.fromModel("noDefaultEumLengthOneModel", noDefaultEumLengthOne);
Assert.assertEquals(noDefaultEumLengthOneModel.defaultValue, "15.0");
Assert.assertEquals(noDefaultEumLengthOneModel.hasRequired, false);
}
}