Fix the null pointer exception when generating examples for schemas in python-experimental (#12019)

* Improves example generator for python-experimental

* Fixes quotes around date example

* Improves object schema examplple gen

* Samples regenerated

* Adds back in AnyType + oneOf discriminator handling

* Reverts version file

* Returns early in example gen for array composed schemas

* Adds toExampleValue method to python-exp

* Improves pattern regex sample generation in python-experimental

* Fixes comment typo
This commit is contained in:
Justin Black
2022-04-07 11:17:37 -07:00
committed by GitHub
parent 510f79989a
commit 2e19371a26
4 changed files with 210 additions and 116 deletions

View File

@@ -17,6 +17,8 @@
package org.openapitools.codegen.languages;
import com.github.curiousoddman.rgxgen.RgxGen;
import com.github.curiousoddman.rgxgen.config.RgxGenOption;
import com.github.curiousoddman.rgxgen.config.RgxGenProperties;
import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.Operation;
import io.swagger.v3.oas.models.servers.Server;
@@ -611,7 +613,7 @@ public class PythonExperimentalClientCodegen extends AbstractPythonCodegen {
} else {
strValue = dateValue.toString();
}
return "isoparse('" + strValue + "').date()";
return strValue;
}
public String pythonDateTime(Object dateTimeValue) {
@@ -628,7 +630,7 @@ public class PythonExperimentalClientCodegen extends AbstractPythonCodegen {
} else {
strValue = dateTimeValue.toString();
}
return "isoparse('" + strValue + "')";
return strValue;
}
/**
@@ -1355,6 +1357,13 @@ public class PythonExperimentalClientCodegen extends AbstractPythonCodegen {
return "\"" + in + "\"";
}
@Override
public String toExampleValue(Schema schema) {
String modelName = getModelName(schema);
Object objExample = getObjectExample(schema);
return toExampleValueRecursive(modelName, schema, objExample, 1, "", 0, new ArrayList<>());
}
public String toExampleValue(Schema schema, Object objExample) {
String modelName = getModelName(schema);
return toExampleValueRecursive(modelName, schema, objExample, 1, "", 0, new ArrayList<>());
@@ -1447,12 +1456,66 @@ public class PythonExperimentalClientCodegen extends AbstractPythonCodegen {
}
String refModelName = getModelName(schema);
return toExampleValueRecursive(refModelName, refSchema, objExample, indentationLevel, prefix, exampleLine, includedSchemas);
} else if (ModelUtils.isNullType(schema) || isAnyTypeSchema(schema)) {
} else if (ModelUtils.isNullType(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.
return fullPrefix + "None" + closeChars;
} else if (ModelUtils.isAnyType(schema)) {
/*
This schema may be a composed schema
TODO generate examples for some of these use cases in the future like
only oneOf without a discriminator
*/
Boolean hasProperties = (schema.getProperties() != null && !schema.getProperties().isEmpty());
CodegenDiscriminator disc = createDiscriminator(modelName, schema, openAPI);
if (ModelUtils.isComposedSchema(schema)) {
// complex composed object type schemas not yet handled and the code returns early
if (hasProperties) {
// what if this composed schema defined properties + allOf?
// or items + properties, both a ist and a dict could be accepted as payloads
return fullPrefix + "{}" + closeChars;
}
ComposedSchema cs = (ComposedSchema) schema;
Integer allOfExists = 0;
if (cs.getAllOf() != null && !cs.getAllOf().isEmpty()) {
allOfExists = 1;
}
Integer anyOfExists = 0;
if (cs.getAnyOf() != null && !cs.getAnyOf().isEmpty()) {
anyOfExists = 1;
}
Integer oneOfExists = 0;
if (cs.getOneOf() != null && !cs.getOneOf().isEmpty()) {
oneOfExists = 1;
}
if (allOfExists + anyOfExists + oneOfExists > 1) {
// what if it needs one oneOf schema, one anyOf schema, and two allOf schemas?
return fullPrefix + "None" + closeChars;
}
// for now only oneOf with discriminator is supported
if (oneOfExists == 1 && disc != null) {
;
} else {
return fullPrefix + "None" + closeChars;
}
}
if (disc != null) {
// a discriminator means that the type must be object
MappedModel mm = getDiscriminatorMappedModel(disc);
if (mm == null) {
return fullPrefix + "None" + closeChars;
}
String discPropNameValue = mm.getMappingName();
String chosenModelName = mm.getModelName();
Schema modelSchema = getModelNameToSchemaCache().get(chosenModelName);
CodegenProperty cp = new CodegenProperty();
cp.setName(disc.getPropertyName());
cp.setExample(discPropNameValue);
return exampleForObjectModel(modelSchema, fullPrefix, closeChars, cp, indentationLevel, exampleLine, closingIndentation, includedSchemas);
}
return fullPrefix + "None" + closeChars;
} else if (ModelUtils.isBooleanSchema(schema)) {
if (objExample == null) {
if (example == null) {
example = "True";
} else {
if ("false".equalsIgnoreCase(objExample.toString())) {
@@ -1462,22 +1525,24 @@ public class PythonExperimentalClientCodegen extends AbstractPythonCodegen {
}
}
return fullPrefix + example + closeChars;
} else if (ModelUtils.isDateSchema(schema)) {
} else if (ModelUtils.isStringSchema(schema)) {
if (example != null) {
return fullPrefix + ensureQuotes(example) + closeChars;
}
if (ModelUtils.isDateSchema(schema)) {
if (objExample == null) {
example = pythonDate("1970-01-01");
} else {
example = pythonDate(objExample);
}
return fullPrefix + example + closeChars;
} else if (ModelUtils.isDateTimeSchema(schema)) {
if (objExample == null) {
example = pythonDateTime("1970-01-01T00:00:00.00Z");
} else {
example = pythonDateTime(objExample);
}
return fullPrefix + example + closeChars;
} else if (ModelUtils.isBinarySchema(schema)) {
if (objExample == null) {
if (example == null) {
example = "/path/to/file";
}
example = "open('" + example + "', 'rb')";
@@ -1486,26 +1551,47 @@ public class PythonExperimentalClientCodegen extends AbstractPythonCodegen {
if (objExample == null) {
example = "'YQ=='";
}
return fullPrefix + example + closeChars;
} else if (ModelUtils.isStringSchema(schema)) {
if (objExample == null) {
} else if ("Number".equalsIgnoreCase(schema.getFormat())) {
// a BigDecimal:
if ("Number".equalsIgnoreCase(schema.getFormat())) {
example = "2";
return fullPrefix + example + closeChars;
} else if (StringUtils.isNotBlank(schema.getPattern())) {
String pattern = schema.getPattern();
RgxGen rgxGen = new RgxGen(pattern);
/*
RxGen does not support our ECMA dialect https://github.com/curious-odd-man/RgxGen/issues/56
So strip off the leading / and trailing / and turn on ignore case if we have it
*/
Pattern valueExtractor = Pattern.compile("^/?(.+?)/?(.?)$");
Matcher m = valueExtractor.matcher(pattern);
RgxGen rgxGen = null;
if (m.find()) {
int groupCount = m.groupCount();
if (groupCount == 1) {
// only pattern found
String isolatedPattern = m.group(1);
rgxGen = new RgxGen(isolatedPattern);
} else if (groupCount == 2) {
// patterns and flag found
String isolatedPattern = m.group(1);
String flags = m.group(2);
if (flags.contains("i")) {
rgxGen = new RgxGen(isolatedPattern);
RgxGenProperties properties = new RgxGenProperties();
RgxGenOption.CASE_INSENSITIVE.setInProperties(properties, true);
rgxGen.setProperties(properties);
} else {
rgxGen = new RgxGen(isolatedPattern);
}
}
} else {
rgxGen = new RgxGen(pattern);
}
// this seed makes it so if we have [a-z] we pick a
Random random = new Random(18);
String sample = rgxGen.generate(random);
// omit leading / and trailing /, omit trailing /i
Pattern valueExtractor = Pattern.compile("^/?(.+?)/?.?$");
Matcher m = valueExtractor.matcher(sample);
if (m.find()) {
example = m.group(m.groupCount());
if (rgxGen != null) {
example = rgxGen.generate(random);
} else {
example = "";
throw new RuntimeException("rgxGen cannot be null. Please open an issue in the openapi-generator github repo.");
}
} else if (schema.getMinLength() != null) {
example = "";
@@ -1516,7 +1602,6 @@ public class PythonExperimentalClientCodegen extends AbstractPythonCodegen {
} else {
example = "string_example";
}
}
return fullPrefix + ensureQuotes(example) + closeChars;
} else if (ModelUtils.isIntegerSchema(schema)) {
if (objExample == null) {
@@ -1539,7 +1624,11 @@ public class PythonExperimentalClientCodegen extends AbstractPythonCodegen {
} else if (ModelUtils.isArraySchema(schema)) {
if (objExample instanceof Iterable) {
// If the example is already a list, return it directly instead of wrongly wrap it in another list
return fullPrefix + objExample.toString();
return fullPrefix + objExample.toString() + closeChars;
}
if (ModelUtils.isComposedSchema(schema)) {
// complex composed array type schemas not yet handled and the code returns early
return fullPrefix + "[]" + closeChars;
}
ArraySchema arrayschema = (ArraySchema) schema;
Schema itemSchema = arrayschema.getItems();
@@ -1547,18 +1636,68 @@ public class PythonExperimentalClientCodegen extends AbstractPythonCodegen {
includedSchemas.add(schema);
String itemExample = toExampleValueRecursive(itemModelName, itemSchema, objExample, indentationLevel + 1, "", exampleLine + 1, includedSchemas);
if (StringUtils.isEmpty(itemExample) || cycleFound) {
return fullPrefix + "[]";
return fullPrefix + "[]" + closeChars;
} else {
return fullPrefix + "[" + "\n" + itemExample + "\n" + closingIndentation + "]" + closeChars;
}
} else if (ModelUtils.isMapSchema(schema)) {
} else if (ModelUtils.isTypeObjectSchema(schema)) {
if (modelName == null) {
fullPrefix += "dict(";
closeChars = ")";
}
if (cycleFound) {
return fullPrefix + closeChars;
}
Boolean hasProperties = (schema.getProperties() != null && !schema.getProperties().isEmpty());
CodegenDiscriminator disc = createDiscriminator(modelName, schema, openAPI);
if (ModelUtils.isComposedSchema(schema)) {
// complex composed object type schemas not yet handled and the code returns early
if (hasProperties) {
// what if this composed schema defined properties + allOf?
return fullPrefix + closeChars;
}
ComposedSchema cs = (ComposedSchema) schema;
Integer allOfExists = 0;
if (cs.getAllOf() != null && !cs.getAllOf().isEmpty()) {
allOfExists = 1;
}
Integer anyOfExists = 0;
if (cs.getAnyOf() != null && !cs.getAnyOf().isEmpty()) {
anyOfExists = 1;
}
Integer oneOfExists = 0;
if (cs.getOneOf() != null && !cs.getOneOf().isEmpty()) {
oneOfExists = 1;
}
if (allOfExists + anyOfExists + oneOfExists > 1) {
// what if it needs one oneOf schema, one anyOf schema, and two allOf schemas?
return fullPrefix + closeChars;
}
// for now only oneOf with discriminator is supported
if (oneOfExists == 1 && disc != null) {
;
} else {
return fullPrefix + closeChars;
}
}
if (disc != null) {
MappedModel mm = getDiscriminatorMappedModel(disc);
if (mm == null) {
return fullPrefix + closeChars;
}
String discPropNameValue = mm.getMappingName();
String chosenModelName = mm.getModelName();
Schema modelSchema = getModelNameToSchemaCache().get(chosenModelName);
CodegenProperty cp = new CodegenProperty();
cp.setName(disc.getPropertyName());
cp.setExample(discPropNameValue);
return exampleForObjectModel(modelSchema, fullPrefix, closeChars, cp, indentationLevel, exampleLine, closingIndentation, includedSchemas);
}
Object addPropsObj = schema.getAdditionalProperties();
if (hasProperties) {
return exampleForObjectModel(schema, fullPrefix, closeChars, null, indentationLevel, exampleLine, closingIndentation, includedSchemas);
} else if (addPropsObj instanceof Schema) {
// TODO handle true case for additionalProperties
if (addPropsObj instanceof Schema && !cycleFound) {
Schema addPropsSchema = (Schema) addPropsObj;
String key = "key";
Object addPropsExample = getObjectExample(addPropsSchema);
@@ -1576,51 +1715,6 @@ public class PythonExperimentalClientCodegen extends AbstractPythonCodegen {
} else {
example = fullPrefix + closeChars;
}
return example;
} else if (ModelUtils.isObjectSchema(schema)) {
if (modelName == null) {
fullPrefix += "dict(";
closeChars = ")";
}
if (cycleFound) {
return fullPrefix + closeChars;
}
CodegenDiscriminator disc = createDiscriminator(modelName, schema, openAPI);
if (disc != null) {
MappedModel mm = getDiscriminatorMappedModel(disc);
if (mm != null) {
String discPropNameValue = mm.getMappingName();
String chosenModelName = mm.getModelName();
// TODO handle this case in the future, this is when the discriminated
// schema allOf includes this schema, like Cat allOf includes Pet
// so this is the composed schema use case
} else {
return fullPrefix + closeChars;
}
}
return exampleForObjectModel(schema, fullPrefix, closeChars, null, indentationLevel, exampleLine, closingIndentation, includedSchemas);
} else if (ModelUtils.isComposedSchema(schema)) {
if (cycleFound) {
return fullPrefix + closeChars;
}
// TODO add examples for composed schema models without discriminators
CodegenDiscriminator disc = createDiscriminator(modelName, schema, openAPI);
if (disc != null) {
MappedModel mm = getDiscriminatorMappedModel(disc);
if (mm != null) {
String discPropNameValue = mm.getMappingName();
String chosenModelName = mm.getModelName();
Schema modelSchema = getModelNameToSchemaCache().get(chosenModelName);
CodegenProperty cp = new CodegenProperty();
cp.setName(disc.getPropertyName());
cp.setExample(discPropNameValue);
return exampleForObjectModel(modelSchema, fullPrefix, closeChars, cp, indentationLevel, exampleLine, closingIndentation, includedSchemas);
} else {
return fullPrefix + closeChars;
}
}
return fullPrefix + closeChars;
} else {
LOGGER.warn("Type " + schema.getType() + " not handled properly in toExampleValue");
}

View File

@@ -2,7 +2,7 @@
Type | Description | Notes
------------- | ------------- | -------------
**datetime** | | defaults to isoparse('2010-01-01T10:10:10.000111+01:00')
**datetime** | | defaults to 2010-01-01T10:10:10.000111+01:00
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)

View File

@@ -766,7 +766,7 @@ with petstore_api.ApiClient(configuration) as api_client:
api_instance = fake_api.FakeApi(api_client)
# example passing only optional values
body = ComposedOneOfDifferentTypes()
body = ComposedOneOfDifferentTypes(None)
try:
api_response = api_instance.composed_one_of_different_types(
body=body,
@@ -866,12 +866,12 @@ with petstore_api.ApiClient(configuration) as api_client:
number=32.1,
_float=3.14,
double=67.8,
string="a",
pattern_without_delimiter="AUR,rZ#UM/?R,Fp^l6$ARjbhJk C",
string="A",
pattern_without_delimiter="Aj",
byte='YQ==',
binary=open('/path/to/file', 'rb'),
date=isoparse('1970-01-01').date(),
date_time=isoparse('2020-02-02T20:20:20.22222Z'),
date="1970-01-01",
date_time="2020-02-02T20:20:20.222220Z",
password="password_example",
callback="callback_example",
)
@@ -911,7 +911,7 @@ Name | Type | Description | Notes
**byte** | **str** | None |
**binary** | **file_type** | None | [optional]
**date** | **date** | None | [optional]
**dateTime** | **datetime** | None | [optional] if omitted the server will use the default value of isoparse('2010-02-01T10:20:10.11111+01:00')
**dateTime** | **datetime** | None | [optional] if omitted the server will use the default value of 2010-02-01T10:20:10.11111+01:00
**password** | **str** | None | [optional]
**callback** | **str** | None | [optional]
**any string name** | **bool, date, datetime, dict, float, int, list, str, none_type** | any string name can be used but the value must be the correct type | [optional]
@@ -1434,12 +1434,12 @@ with petstore_api.ApiClient(configuration) as api_client:
# example passing only optional values
query_params = {
'compositionAtRoot': ,
'compositionAtRoot': None,
'compositionInProperty': dict(
some_prop=,
some_prop=None,
),
}
body =
body = None
try:
# testing composed schemas at inline locations
api_response = api_instance.inline_composition(

View File

@@ -314,7 +314,7 @@ with petstore_api.ApiClient(configuration) as api_client:
id=1,
pet_id=1,
quantity=1,
ship_date=isoparse('2020-02-02T20:20:20.000222Z'),
ship_date="2020-02-02T20:20:20.000222Z",
status="placed",
complete=False,
)