Skip to content

Commit

Permalink
Merge branch 'OpenAPITools:master' into welshm.add_enum_support_pytho…
Browse files Browse the repository at this point in the history
…n_codegen
  • Loading branch information
welshm authored Jun 6, 2024
2 parents 29ffe55 + 3aba427 commit 23300fb
Show file tree
Hide file tree
Showing 17 changed files with 263 additions and 97 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -2874,7 +2874,9 @@ private void mergeProperties(Map<String, Schema> existingProperties, Map<String,
if (null != existingProperties && null != newProperties) {
Schema existingType = existingProperties.get("type");
Schema newType = newProperties.get("type");
newProperties.forEach((key, value) -> existingProperties.put(key, ModelUtils.cloneSchema(value)));
newProperties.forEach((key, value) ->
existingProperties.put(key, ModelUtils.cloneSchema(value, specVersionGreaterThanOrEqualTo310(openAPI)))
);
if (null != existingType && null != newType && null != newType.getEnum() && !newType.getEnum().isEmpty()) {
for (Object e : newType.getEnum()) {
// ensure all interface enum types are added to schema
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -372,10 +372,17 @@ public String modelDocFileFolder() {

@Override
public String toVarName(String name) {
if (nameMapping.containsKey(name)) {
return nameMapping.get(name);
}

// replace - with _ e.g. created-at => created_at
name = name.replace("-", "_");

// always need to replace leading underscores first
if (name.equals("_")) {
return "underscore";
}
name = name.replaceAll("^_", "");

// if it's all upper case, do nothing
Expand Down Expand Up @@ -408,12 +415,20 @@ public String toVarName(String name) {

@Override
public String toParamName(String name) {
if (parameterNameMapping.containsKey(name)) {
return parameterNameMapping.get(name);
}

// should be the same as variable name
return toVarName(name);
}

@Override
public String toModelName(final String name) {
if (modelNameMapping.containsKey(name)) {
return modelNameMapping.get(name);
}

String sanitizedName = sanitizeName(name);

if (!StringUtils.isEmpty(modelNamePrefix)) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,9 @@

package org.openapitools.codegen.utils;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.jsontype.BasicPolymorphicTypeValidator;
import io.swagger.v3.core.util.AnnotationsUtils;
import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.Operation;
import io.swagger.v3.oas.models.PathItem;
Expand Down Expand Up @@ -64,6 +63,8 @@ public class ModelUtils {

private static final String URI_FORMAT = "uri";

private static final Set<String> OPENAPI_TYPES = Set.of("array", "integer", "number", "boolean", "string", "object");

private static final String generateAliasAsModelKey = "generateAliasAsModel";

// A vendor extension to track the value of the 'swagger' field in a 2.0 doc, if applicable.
Expand All @@ -76,16 +77,10 @@ public class ModelUtils {

private static final ObjectMapper JSON_MAPPER;
private static final ObjectMapper YAML_MAPPER;
private static final ObjectMapper TYPED_JSON_MAPPER = new ObjectMapper();

static {
JSON_MAPPER = ObjectMapperFactory.createJson();
YAML_MAPPER = ObjectMapperFactory.createYaml();

BasicPolymorphicTypeValidator ptv = BasicPolymorphicTypeValidator.builder()
.allowIfSubType(Object.class)
.build();
TYPED_JSON_MAPPER.activateDefaultTyping(ptv, ObjectMapper.DefaultTyping.EVERYTHING);
}

public static boolean isDisallowAdditionalPropertiesIfNotPresent() {
Expand Down Expand Up @@ -2186,13 +2181,19 @@ public static boolean isParent(Schema schema) {
return false;
}

public static Schema cloneSchema(Schema schema) {
try {
String json = TYPED_JSON_MAPPER.writeValueAsString(schema);
return TYPED_JSON_MAPPER.readValue(json, schema.getClass());
} catch (JsonProcessingException ex) {
LOGGER.error("Can't clone schema {}", schema, ex);
return schema;
public static Schema cloneSchema(Schema schema, boolean openapi31) {
if (openapi31) {
return AnnotationsUtils.clone(schema, openapi31);
} else {
// AnnotationsUtils.clone doesn't support custom schema types for OpenAPI < 3.1
String schemaType = schema.getType();
if (schemaType != null && !OPENAPI_TYPES.contains(schemaType)) {
schema.setType(null);
}
Schema result = AnnotationsUtils.clone(schema, openapi31);
schema.setType(schemaType);
result.setType(schemaType);
return result;
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1 +1 @@
{{#isPathParam}}{{#useBeanValidation}}{{>beanValidationPathParams}}{{/useBeanValidation}}{{#swagger2AnnotationLibrary}}@Parameter(description = "{{{description}}}"{{#required}}, required = true{{/required}}{{#allowableValues}}{{#defaultValue}}, schema = Schema(allowableValues = [{{#enumVars}}"{{#lambdaEscapeInNormalString}}{{{value}}}{{/lambdaEscapeInNormalString}}"{{^-last}}, {{/-last}}{{/enumVars}}]{{^isContainer}}, defaultValue = {{^isString}}"{{/isString}}{{#isString}}{{#isEnum}}"{{/isEnum}}{{/isString}}{{{defaultValue}}}{{^isString}}"{{/isString}}{{#isString}}{{#isEnum}}"{{/isEnum}}{{/isString}}{{/isContainer}}{{/defaultValue}}{{/allowableValues}}{{#allowableValues}}{{^defaultValue}}, schema = Schema(allowableValues = [{{#enumVars}}"{{#lambdaEscapeInNormalString}}{{{value}}}{{/lambdaEscapeInNormalString}}"{{^-last}}, {{/-last}}{{/enumVars}}]){{/defaultValue}}{{/allowableValues}}{{^allowableValues}}{{#defaultValue}}{{^isContainer}}, schema = Schema(defaultValue = {{^isString}}"{{/isString}}{{#isString}}{{#isEnum}}"{{/isEnum}}{{/isString}}{{{defaultValue}}}{{^isString}}"{{/isString}}{{#isString}}{{#isEnum}}"{{/isEnum}}{{/isString}}{{/isContainer}}{{/defaultValue}}{{/allowableValues}}){{/swagger2AnnotationLibrary}}{{#swagger1AnnotationLibrary}}@ApiParam(value = "{{{description}}}"{{#required}}, required = true{{/required}}{{#allowableValues}}, allowableValues = "{{#enumVars}}{{#lambda.escapeDoubleQuote}}{{{value}}}{{/lambda.escapeDoubleQuote}}{{^-last}}, {{/-last}}{{/enumVars}}"{{/allowableValues}}{{#defaultValue}}, defaultValue = "{{{.}}}"{{/defaultValue}}){{/swagger1AnnotationLibrary}} @PathVariable("{{baseName}}") {{{paramName}}}: {{>optionalDataType}}{{/isPathParam}}
{{#isPathParam}}{{#useBeanValidation}}{{>beanValidationPathParams}}{{/useBeanValidation}}{{#swagger2AnnotationLibrary}}@Parameter(description = "{{{description}}}"{{#required}}, required = true{{/required}}{{#allowableValues}}{{#defaultValue}}, schema = Schema(allowableValues = [{{#enumVars}}"{{#lambdaEscapeInNormalString}}{{{value}}}{{/lambdaEscapeInNormalString}}"{{^-last}}, {{/-last}}{{/enumVars}}]{{^isContainer}}, defaultValue = {{^isString}}"{{/isString}}{{#isString}}{{#isEnum}}"{{/isEnum}}{{/isString}}{{{defaultValue}}}{{^isString}}"{{/isString}}{{#isString}}{{#isEnum}}"{{/isEnum}}{{/isString}}{{/isContainer}}){{/defaultValue}}{{/allowableValues}}{{#allowableValues}}{{^defaultValue}}, schema = Schema(allowableValues = [{{#enumVars}}"{{#lambdaEscapeInNormalString}}{{{value}}}{{/lambdaEscapeInNormalString}}"{{^-last}}, {{/-last}}{{/enumVars}}]){{/defaultValue}}{{/allowableValues}}{{^allowableValues}}{{#defaultValue}}{{^isContainer}}, schema = Schema(defaultValue = {{^isString}}"{{/isString}}{{#isString}}{{#isEnum}}"{{/isEnum}}{{/isString}}{{{defaultValue}}}{{^isString}}"{{/isString}}{{#isString}}{{#isEnum}}"{{/isEnum}}{{/isString}}){{/isContainer}}{{/defaultValue}}{{/allowableValues}}){{/swagger2AnnotationLibrary}}{{#swagger1AnnotationLibrary}}@ApiParam(value = "{{{description}}}"{{#required}}, required = true{{/required}}{{#allowableValues}}, allowableValues = "{{#enumVars}}{{#lambda.escapeDoubleQuote}}{{{value}}}{{/lambda.escapeDoubleQuote}}{{^-last}}, {{/-last}}{{/enumVars}}"{{/allowableValues}}{{#defaultValue}}, defaultValue = "{{{.}}}"{{/defaultValue}}){{/swagger1AnnotationLibrary}} @PathVariable("{{baseName}}") {{{paramName}}}: {{>optionalDataType}}{{/isPathParam}}
Original file line number Diff line number Diff line change
Expand Up @@ -322,10 +322,7 @@ class ApiClient:
match = re.search(r"charset=([a-zA-Z\-\d]+)[\s;]?", content_type)
encoding = match.group(1) if match else "utf-8"
response_text = response_data.data.decode(encoding)
if response_type in ["bytearray", "str"]:
return_data = self.__deserialize_primitive(response_text, response_type)
else:
return_data = self.deserialize(response_text, response_type)
return_data = self.deserialize(response_text, response_type, content_type)
finally:
if not 200 <= response_data.status <= 299:
raise ApiException.from_response(
Expand Down Expand Up @@ -393,21 +390,35 @@ class ApiClient:
for key, val in obj_dict.items()
}

def deserialize(self, response_text, response_type):
def deserialize(self, response_text: str, response_type: str, content_type: Optional[str]):
"""Deserializes response into an object.

:param response: RESTResponse object to be deserialized.
:param response_type: class literal for
deserialized object, or string of class name.
:param content_type: content type of response.

:return: deserialized object.
"""

# fetch data from response object
try:
data = json.loads(response_text)
except ValueError:
if content_type is None:
try:
data = json.loads(response_text)
except ValueError:
data = response_text
elif content_type.startswith("application/json"):
if response_text == "":
data = ""
else:
data = json.loads(response_text)
elif content_type.startswith("text/plain"):
data = response_text
else:
raise ApiException(
status=0,
reason="Unsupported content type: {0}".format(content_type)
)

return self.__deserialize(data, response_type)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -399,51 +399,66 @@ public void testCloneNumberSchema() {
.name("test-schema")
.minimum(new BigDecimal(100));

Schema deepCopy = ModelUtils.cloneSchema(schema);
Schema deepCopy = ModelUtils.cloneSchema(schema, false);

Assert.assertEquals(schema, deepCopy);
Assert.assertEquals(deepCopy, schema);
Assert.assertNotSame(deepCopy, schema);
}

@Test
public void testCloneCustomSchema() {
Schema schema = new Schema().type("money");
Schema schema = new ObjectSchema().type("money");

Schema deepCopy = ModelUtils.cloneSchema(schema);
Schema deepCopy = ModelUtils.cloneSchema(schema, false);

Assert.assertEquals(schema, deepCopy);
Assert.assertEquals(deepCopy, schema);
Assert.assertNotSame(deepCopy, schema);
}

@Test
public void testCloneComposedSchema() {
Schema base1 = new Schema()
Schema base1 = new ObjectSchema()
.name("Base1")
.addProperty("foo", new StringSchema());
Schema base2 = new Schema()
Schema base2 = new ObjectSchema()
.name("Base2")
.addProperty("bar", new StringSchema());
Schema composedSchema = new ComposedSchema()
.name("Composed")
.allOf(List.of(base1, base2))
.addProperty("baz", new StringSchema());

var deepCopy = ModelUtils.cloneSchema(composedSchema);
Schema deepCopy = ModelUtils.cloneSchema(composedSchema, false);

Assert.assertEquals(composedSchema, deepCopy);
Assert.assertEquals(deepCopy, composedSchema);
Assert.assertNotSame(deepCopy, composedSchema);
}

@Test
public void testCloneArrayOfEnumsSchema() {
Schema arraySchema = new Schema()
Schema schema = new ArraySchema()
.name("ArrayType")
.type("array")
.items(new Schema()
.items(new StringSchema()
.type("string")
._enum(List.of("SUCCESS", "FAILURE", "SKIPPED"))
)
._default(List.of("SUCCESS", "FAILURE"));

var deepCopy = ModelUtils.cloneSchema(arraySchema);
Schema deepCopy = ModelUtils.cloneSchema(schema, false);

Assert.assertEquals(arraySchema, deepCopy);
Assert.assertEquals(deepCopy, schema);
Assert.assertNotSame(deepCopy, schema);
}

@Test
public void testCloneDateTimeSchemaWithExample() {
Schema schema = new DateTimeSchema()
.example("2020-02-02T20:20:20.000222Z");

Schema deepCopy = ModelUtils.cloneSchema(schema, false);

Assert.assertEquals(deepCopy, schema);
Assert.assertNotSame(deepCopy, schema);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -315,10 +315,7 @@ def response_deserialize(
match = re.search(r"charset=([a-zA-Z\-\d]+)[\s;]?", content_type)
encoding = match.group(1) if match else "utf-8"
response_text = response_data.data.decode(encoding)
if response_type in ["bytearray", "str"]:
return_data = self.__deserialize_primitive(response_text, response_type)
else:
return_data = self.deserialize(response_text, response_type)
return_data = self.deserialize(response_text, response_type, content_type)
finally:
if not 200 <= response_data.status <= 299:
raise ApiException.from_response(
Expand Down Expand Up @@ -386,21 +383,35 @@ def sanitize_for_serialization(self, obj):
for key, val in obj_dict.items()
}

def deserialize(self, response_text, response_type):
def deserialize(self, response_text: str, response_type: str, content_type: Optional[str]):
"""Deserializes response into an object.
:param response: RESTResponse object to be deserialized.
:param response_type: class literal for
deserialized object, or string of class name.
:param content_type: content type of response.
:return: deserialized object.
"""

# fetch data from response object
try:
data = json.loads(response_text)
except ValueError:
if content_type is None:
try:
data = json.loads(response_text)
except ValueError:
data = response_text
elif content_type.startswith("application/json"):
if response_text == "":
data = ""
else:
data = json.loads(response_text)
elif content_type.startswith("text/plain"):
data = response_text
else:
raise ApiException(
status=0,
reason="Unsupported content type: {0}".format(content_type)
)

return self.__deserialize(data, response_type)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -113,15 +113,12 @@ def testBodyParameter(self):
n = openapi_client.Pet.from_dict({"name": "testing", "photoUrls": ["http://1", "http://2"]})
api_instance = openapi_client.BodyApi()
api_response = api_instance.test_echo_body_pet_response_string(n)
self.assertEqual(api_response, '{"name": "testing", "photoUrls": ["http://1", "http://2"]}')
self.assertEqual(api_response, "{'name': 'testing', 'photoUrls': ['http://1', 'http://2']}")

t = openapi_client.Tag()
api_response = api_instance.test_echo_body_tag_response_string(t)
self.assertEqual(api_response, "{}") # assertion to ensure {} is sent in the body

api_response = api_instance.test_echo_body_tag_response_string(None)
self.assertEqual(api_response, "") # assertion to ensure emtpy string is sent in the body

api_response = api_instance.test_echo_body_free_form_object_response_string({})
self.assertEqual(api_response, "{}") # assertion to ensure {} is sent in the body

Expand Down
27 changes: 19 additions & 8 deletions samples/client/echo_api/python/openapi_client/api_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -315,10 +315,7 @@ def response_deserialize(
match = re.search(r"charset=([a-zA-Z\-\d]+)[\s;]?", content_type)
encoding = match.group(1) if match else "utf-8"
response_text = response_data.data.decode(encoding)
if response_type in ["bytearray", "str"]:
return_data = self.__deserialize_primitive(response_text, response_type)
else:
return_data = self.deserialize(response_text, response_type)
return_data = self.deserialize(response_text, response_type, content_type)
finally:
if not 200 <= response_data.status <= 299:
raise ApiException.from_response(
Expand Down Expand Up @@ -386,21 +383,35 @@ def sanitize_for_serialization(self, obj):
for key, val in obj_dict.items()
}

def deserialize(self, response_text, response_type):
def deserialize(self, response_text: str, response_type: str, content_type: Optional[str]):
"""Deserializes response into an object.
:param response: RESTResponse object to be deserialized.
:param response_type: class literal for
deserialized object, or string of class name.
:param content_type: content type of response.
:return: deserialized object.
"""

# fetch data from response object
try:
data = json.loads(response_text)
except ValueError:
if content_type is None:
try:
data = json.loads(response_text)
except ValueError:
data = response_text
elif content_type.startswith("application/json"):
if response_text == "":
data = ""
else:
data = json.loads(response_text)
elif content_type.startswith("text/plain"):
data = response_text
else:
raise ApiException(
status=0,
reason="Unsupported content type: {0}".format(content_type)
)

return self.__deserialize(data, response_type)

Expand Down
8 changes: 4 additions & 4 deletions samples/client/echo_api/python/tests/test_manual.py
Original file line number Diff line number Diff line change
Expand Up @@ -174,17 +174,17 @@ def testBodyParameter(self):
n = openapi_client.Pet.from_dict({"name": "testing", "photoUrls": ["http://1", "http://2"]})
api_instance = openapi_client.BodyApi()
api_response = api_instance.test_echo_body_pet_response_string(n)
self.assertEqual(api_response, '{"name": "testing", "photoUrls": ["http://1", "http://2"]}')
self.assertEqual(api_response, "{'name': 'testing', 'photoUrls': ['http://1', 'http://2']}")

t = openapi_client.Tag()
api_response = api_instance.test_echo_body_tag_response_string(t)
self.assertEqual(api_response, "{}") # assertion to ensure {} is sent in the body

api_response = api_instance.test_echo_body_tag_response_string(None)
self.assertEqual(api_response, "") # assertion to ensure emtpy string is sent in the body

api_response = api_instance.test_echo_body_free_form_object_response_string({})
self.assertEqual(api_response, "{}") # assertion to ensure {} is sent in the body

api_response = api_instance.test_echo_body_tag_response_string(None)
self.assertEqual(api_response, "") # assertion to ensure emtpy string is sent in the body

def testAuthHttpBasic(self):
api_instance = openapi_client.AuthApi()
Expand Down
Loading

0 comments on commit 23300fb

Please sign in to comment.