From b4954b0d80c1a182824bf8548ddf86868c4d98fa Mon Sep 17 00:00:00 2001 From: Sebastien Rosset Date: Sun, 17 May 2020 09:11:01 -0700 Subject: [PATCH] [codegen][python-experimental] Add configuration knob to disable JSON schema validation (#6227) * Add knob to disable JSON schema structural validation * Add knob to disable JSON schema structural validation * Fix formatting issues * execute sample scripts * execute sample scripts * fix multipleOf validation issue * Add validation log for multipleOf. Add customizable validation checks. add unit tests for JSON schema validation * Add validation log for multipleOf. Add customizable validation checks. add unit tests for JSON schema validation * Add validation log for multipleOf. Add customizable validation checks. add unit tests for JSON schema validation * Add validation log for multipleOf. Add customizable validation checks. add unit tests for JSON schema validation. Fix for python 2 * address review comments --- .../openapitools/codegen/DefaultCodegen.java | 12 +- .../resources/python/configuration.mustache | 29 ++++ .../python/python-experimental/api.mustache | 3 +- .../model_templates/classvars.mustache | 6 + .../method_set_attribute.mustache | 3 +- .../python-experimental/model_utils.mustache | 67 +++++++-- ...odels-for-testing-with-http-signature.yaml | 2 + .../petstore_api/configuration.py | 29 ++++ .../petstore_api/api/another_fake_api.py | 3 +- .../petstore_api/api/fake_api.py | 3 +- .../api/fake_classname_tags_123_api.py | 3 +- .../petstore_api/api/pet_api.py | 3 +- .../petstore_api/api/store_api.py | 3 +- .../petstore_api/api/user_api.py | 3 +- .../petstore_api/configuration.py | 29 ++++ .../petstore_api/model_utils.py | 70 ++++++++-- .../petstore_api/configuration.py | 29 ++++ .../python/petstore_api/configuration.py | 29 ++++ .../petstore_api/api/another_fake_api.py | 3 +- .../petstore_api/api/default_api.py | 3 +- .../petstore_api/api/fake_api.py | 3 +- .../api/fake_classname_tags_123_api.py | 3 +- .../petstore_api/api/pet_api.py | 3 +- .../petstore_api/api/store_api.py | 3 +- .../petstore_api/api/user_api.py | 3 +- .../petstore_api/configuration.py | 29 ++++ .../petstore_api/model_utils.py | 70 ++++++++-- .../petstore_api/models/format_test.py | 2 + .../tests/test_api_validation.py | 131 ++++++++++++++++++ .../python/petstore_api/configuration.py | 29 ++++ 30 files changed, 548 insertions(+), 60 deletions(-) create mode 100644 samples/openapi3/client/petstore/python-experimental/tests/test_api_validation.py diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/DefaultCodegen.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/DefaultCodegen.java index b3d02767d03..3f438659c0c 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/DefaultCodegen.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/DefaultCodegen.java @@ -2956,10 +2956,13 @@ public class DefaultCodegen implements CodegenConfig { if (p.getExclusiveMaximum() != null) { property.exclusiveMaximum = p.getExclusiveMaximum(); } + if (p.getMultipleOf() != null) { + property.multipleOf = p.getMultipleOf(); + } // check if any validation rule defined // exclusive* are noop without corresponding min/max - if (property.minimum != null || property.maximum != null) + if (property.minimum != null || property.maximum != null || p.getMultipleOf() != null) property.hasValidation = true; } else if (ModelUtils.isBooleanSchema(p)) { // boolean type @@ -3031,8 +3034,9 @@ public class DefaultCodegen implements CodegenConfig { // check if any validation rule defined // exclusive* are noop without corresponding min/max - if (property.minimum != null || property.maximum != null) + if (property.minimum != null || property.maximum != null || p.getMultipleOf() != null) { property.hasValidation = true; + } } else if (ModelUtils.isFreeFormObject(p)) { property.isFreeFormObject = true; @@ -4151,7 +4155,7 @@ public class DefaultCodegen implements CodegenConfig { if (codegenParameter.maximum != null || codegenParameter.minimum != null || codegenParameter.maxLength != null || codegenParameter.minLength != null || codegenParameter.maxItems != null || codegenParameter.minItems != null || - codegenParameter.pattern != null) { + codegenParameter.pattern != null || codegenParameter.multipleOf != null) { codegenParameter.hasValidation = true; } @@ -5629,7 +5633,7 @@ public class DefaultCodegen implements CodegenConfig { if (codegenParameter.maximum != null || codegenParameter.minimum != null || codegenParameter.maxLength != null || codegenParameter.minLength != null || codegenParameter.maxItems != null || codegenParameter.minItems != null || - codegenParameter.pattern != null) { + codegenParameter.pattern != null || codegenParameter.multipleOf != null) { codegenParameter.hasValidation = true; } diff --git a/modules/openapi-generator/src/main/resources/python/configuration.mustache b/modules/openapi-generator/src/main/resources/python/configuration.mustache index dceab63e67b..d1705eed25d 100644 --- a/modules/openapi-generator/src/main/resources/python/configuration.mustache +++ b/modules/openapi-generator/src/main/resources/python/configuration.mustache @@ -14,8 +14,15 @@ import urllib3 import six from six.moves import http_client as httplib +from {{packageName}}.exceptions import ApiValueError +JSON_SCHEMA_VALIDATION_KEYWORDS = { + 'multipleOf', 'maximum', 'exclusiveMaximum', + 'minimum', 'exclusiveMinimum', 'maxLength', + 'minLength', 'pattern', 'maxItems', 'minItems' +} + class Configuration(object): """NOTE: This class is auto generated by OpenAPI Generator @@ -43,6 +50,19 @@ class Configuration(object): then all undeclared properties received by the server are injected into the additional properties map. In that case, there are undeclared properties, and nothing to discard. + :param disabled_client_side_validations (string): Comma-separated list of + JSON schema validation keywords to disable JSON schema structural validation + rules. The following keywords may be specified: multipleOf, maximum, + exclusiveMaximum, minimum, exclusiveMinimum, maxLength, minLength, pattern, + maxItems, minItems. + By default, the validation is performed for data generated locally by the client + and data received from the server, independent of any validation performed by + the server side. If the input data does not satisfy the JSON schema validation + rules specified in the OpenAPI document, an exception is raised. + If disabled_client_side_validations is set, structural validation is + disabled. This can be useful to troubleshoot data validation problem, such as + when the OpenAPI document validation rules do not match the actual API data + received by the server. {{#hasHttpSignatureMethods}} :param signing_info: Configuration parameters for the HTTP signature security scheme. Must be an instance of {{{packageName}}}.signing.HttpSigningConfiguration @@ -139,6 +159,7 @@ conf = {{{packageName}}}.Configuration( api_key=None, api_key_prefix=None, username=None, password=None, discard_unknown_keys=False, + disabled_client_side_validations="", {{#hasHttpSignatureMethods}} signing_info=None, {{/hasHttpSignatureMethods}} @@ -172,6 +193,7 @@ conf = {{{packageName}}}.Configuration( """Password for HTTP basic authentication """ self.discard_unknown_keys = discard_unknown_keys + self.disabled_client_side_validations = disabled_client_side_validations {{#hasHttpSignatureMethods}} if signing_info is not None: signing_info.host = host @@ -277,6 +299,13 @@ conf = {{{packageName}}}.Configuration( def __setattr__(self, name, value): object.__setattr__(self, name, value) + if name == 'disabled_client_side_validations': + s = set(filter(None, value.split(','))) + for v in s: + if v not in JSON_SCHEMA_VALIDATION_KEYWORDS: + raise ApiValueError( + "Invalid keyword: '{0}''".format(v)) + self._disabled_client_side_validations = s {{#hasHttpSignatureMethods}} if name == "signing_info" and value is not None: # Ensure the host paramater from signing info is the same as diff --git a/modules/openapi-generator/src/main/resources/python/python-experimental/api.mustache b/modules/openapi-generator/src/main/resources/python/python-experimental/api.mustache index 4cf1d423e83..44fc3a89970 100644 --- a/modules/openapi-generator/src/main/resources/python/python-experimental/api.mustache +++ b/modules/openapi-generator/src/main/resources/python/python-experimental/api.mustache @@ -376,7 +376,8 @@ class Endpoint(object): check_validations( self.validations, (param,), - kwargs[param] + kwargs[param], + configuration=self.api_client.configuration ) if kwargs['_check_input_type'] is False: diff --git a/modules/openapi-generator/src/main/resources/python/python-experimental/model_templates/classvars.mustache b/modules/openapi-generator/src/main/resources/python/python-experimental/model_templates/classvars.mustache index fe54e34f5bb..471c16c10a9 100644 --- a/modules/openapi-generator/src/main/resources/python/python-experimental/model_templates/classvars.mustache +++ b/modules/openapi-generator/src/main/resources/python/python-experimental/model_templates/classvars.mustache @@ -57,6 +57,9 @@ {{#-first}}'flags': (re.{{.}}{{/-first}}{{^-first}} re.{{.}}{{/-first}}{{^-last}} | {{/-last}}{{#-last}}){{/-last}}{{/vendorExtensions.x-modifiers}} }, {{/pattern}} +{{#multipleOf}} + 'multiple_of': {{multipleOf}}, +{{/multipleOf}} }, {{/hasValidation}} {{/requiredVars}} @@ -87,6 +90,9 @@ {{#-first}}'flags': (re.{{.}}{{/-first}}{{^-first}} re.{{.}}{{/-first}}{{^-last}} | {{/-last}}{{#-last}}){{/-last}}{{/vendorExtensions.x-modifiers}} }, {{/pattern}} +{{#multipleOf}} + 'multiple_of': {{multipleOf}}, +{{/multipleOf}} }, {{/hasValidation}} {{/optionalVars}} diff --git a/modules/openapi-generator/src/main/resources/python/python-experimental/model_templates/method_set_attribute.mustache b/modules/openapi-generator/src/main/resources/python/python-experimental/model_templates/method_set_attribute.mustache index 15067fff80a..fddbc7c2ea7 100644 --- a/modules/openapi-generator/src/main/resources/python/python-experimental/model_templates/method_set_attribute.mustache +++ b/modules/openapi-generator/src/main/resources/python/python-experimental/model_templates/method_set_attribute.mustache @@ -45,6 +45,7 @@ check_validations( self.validations, (name,), - value + value, + self._configuration ) self.__dict__['_data_store'][name] = value \ No newline at end of file diff --git a/modules/openapi-generator/src/main/resources/python/python-experimental/model_utils.mustache b/modules/openapi-generator/src/main/resources/python/python-experimental/model_utils.mustache index ce0191f3ea1..1c163a915a2 100644 --- a/modules/openapi-generator/src/main/resources/python/python-experimental/model_utils.mustache +++ b/modules/openapi-generator/src/main/resources/python/python-experimental/model_utils.mustache @@ -369,17 +369,50 @@ def check_allowed_values(allowed_values, input_variable_path, input_values): ) -def check_validations(validations, input_variable_path, input_values): +def is_json_validation_enabled(schema_keyword, configuration=None): + """Returns true if JSON schema validation is enabled for the specified + validation keyword. This can be used to skip JSON schema structural validation + as requested in the configuration. + + Args: + schema_keyword (string): the name of a JSON schema validation keyword. + configuration (Configuration): the configuration class. + """ + + return (configuration is None or + not hasattr(configuration, '_disabled_client_side_validations') or + schema_keyword not in configuration._disabled_client_side_validations) + + +def check_validations( + validations, input_variable_path, input_values, + configuration=None): """Raises an exception if the input_values are invalid Args: - validations (dict): the validation dictionary - input_variable_path (tuple): the path to the input variable + validations (dict): the validation dictionary. + input_variable_path (tuple): the path to the input variable. input_values (list/str/int/float/date/datetime): the values that we - are checking + are checking. + configuration (Configuration): the configuration class. """ + current_validations = validations[input_variable_path] - if ('max_length' in current_validations and + if (is_json_validation_enabled('multipleOf', configuration) and + 'multiple_of' in current_validations and + isinstance(input_values, (int, float)) and + not (float(input_values) / current_validations['multiple_of']).is_integer()): + # Note 'multipleOf' will be as good as the floating point arithmetic. + raise ApiValueError( + "Invalid value for `%s`, value must be a multiple of " + "`%s`" % ( + input_variable_path[0], + current_validations['multiple_of'] + ) + ) + + if (is_json_validation_enabled('maxLength', configuration) and + 'max_length' in current_validations and len(input_values) > current_validations['max_length']): raise ApiValueError( "Invalid value for `%s`, length must be less than or equal to " @@ -389,7 +422,8 @@ def check_validations(validations, input_variable_path, input_values): ) ) - if ('min_length' in current_validations and + if (is_json_validation_enabled('minLength', configuration) and + 'min_length' in current_validations and len(input_values) < current_validations['min_length']): raise ApiValueError( "Invalid value for `%s`, length must be greater than or equal to " @@ -399,7 +433,8 @@ def check_validations(validations, input_variable_path, input_values): ) ) - if ('max_items' in current_validations and + if (is_json_validation_enabled('maxItems', configuration) and + 'max_items' in current_validations and len(input_values) > current_validations['max_items']): raise ApiValueError( "Invalid value for `%s`, number of items must be less than or " @@ -409,7 +444,8 @@ def check_validations(validations, input_variable_path, input_values): ) ) - if ('min_items' in current_validations and + if (is_json_validation_enabled('minItems', configuration) and + 'min_items' in current_validations and len(input_values) < current_validations['min_items']): raise ValueError( "Invalid value for `%s`, number of items must be greater than or " @@ -432,7 +468,8 @@ def check_validations(validations, input_variable_path, input_values): max_val = input_values min_val = input_values - if ('exclusive_maximum' in current_validations and + if (is_json_validation_enabled('exclusiveMaximum', configuration) and + 'exclusive_maximum' in current_validations and max_val >= current_validations['exclusive_maximum']): raise ApiValueError( "Invalid value for `%s`, must be a value less than `%s`" % ( @@ -441,7 +478,8 @@ def check_validations(validations, input_variable_path, input_values): ) ) - if ('inclusive_maximum' in current_validations and + if (is_json_validation_enabled('maximum', configuration) and + 'inclusive_maximum' in current_validations and max_val > current_validations['inclusive_maximum']): raise ApiValueError( "Invalid value for `%s`, must be a value less than or equal to " @@ -451,7 +489,8 @@ def check_validations(validations, input_variable_path, input_values): ) ) - if ('exclusive_minimum' in current_validations and + if (is_json_validation_enabled('exclusiveMinimum', configuration) and + 'exclusive_minimum' in current_validations and min_val <= current_validations['exclusive_minimum']): raise ApiValueError( "Invalid value for `%s`, must be a value greater than `%s`" % @@ -461,7 +500,8 @@ def check_validations(validations, input_variable_path, input_values): ) ) - if ('inclusive_minimum' in current_validations and + if (is_json_validation_enabled('minimum', configuration) and + 'inclusive_minimum' in current_validations and min_val < current_validations['inclusive_minimum']): raise ApiValueError( "Invalid value for `%s`, must be a value greater than or equal " @@ -471,7 +511,8 @@ def check_validations(validations, input_variable_path, input_values): ) ) flags = current_validations.get('regex', {}).get('flags', 0) - if ('regex' in current_validations and + if (is_json_validation_enabled('pattern', configuration) and + 'regex' in current_validations and not re.search(current_validations['regex']['pattern'], input_values, flags=flags)): err_msg = r"Invalid value for `%s`, must match regular expression `%s`" % ( diff --git a/modules/openapi-generator/src/test/resources/3_0/python-experimental/petstore-with-fake-endpoints-models-for-testing-with-http-signature.yaml b/modules/openapi-generator/src/test/resources/3_0/python-experimental/petstore-with-fake-endpoints-models-for-testing-with-http-signature.yaml index f4241f77798..9e01283cc8c 100644 --- a/modules/openapi-generator/src/test/resources/3_0/python-experimental/petstore-with-fake-endpoints-models-for-testing-with-http-signature.yaml +++ b/modules/openapi-generator/src/test/resources/3_0/python-experimental/petstore-with-fake-endpoints-models-for-testing-with-http-signature.yaml @@ -1426,6 +1426,7 @@ components: type: integer maximum: 100 minimum: 10 + multipleOf: 2 int32: type: integer format: int32 @@ -1438,6 +1439,7 @@ components: maximum: 543.2 minimum: 32.1 type: number + multipleOf: 32.5 float: type: number format: float diff --git a/samples/client/petstore/python-asyncio/petstore_api/configuration.py b/samples/client/petstore/python-asyncio/petstore_api/configuration.py index a93073e8142..5c860e8c954 100644 --- a/samples/client/petstore/python-asyncio/petstore_api/configuration.py +++ b/samples/client/petstore/python-asyncio/petstore_api/configuration.py @@ -19,8 +19,15 @@ import urllib3 import six from six.moves import http_client as httplib +from petstore_api.exceptions import ApiValueError +JSON_SCHEMA_VALIDATION_KEYWORDS = { + 'multipleOf', 'maximum', 'exclusiveMaximum', + 'minimum', 'exclusiveMinimum', 'maxLength', + 'minLength', 'pattern', 'maxItems', 'minItems' +} + class Configuration(object): """NOTE: This class is auto generated by OpenAPI Generator @@ -48,6 +55,19 @@ class Configuration(object): then all undeclared properties received by the server are injected into the additional properties map. In that case, there are undeclared properties, and nothing to discard. + :param disabled_client_side_validations (string): Comma-separated list of + JSON schema validation keywords to disable JSON schema structural validation + rules. The following keywords may be specified: multipleOf, maximum, + exclusiveMaximum, minimum, exclusiveMinimum, maxLength, minLength, pattern, + maxItems, minItems. + By default, the validation is performed for data generated locally by the client + and data received from the server, independent of any validation performed by + the server side. If the input data does not satisfy the JSON schema validation + rules specified in the OpenAPI document, an exception is raised. + If disabled_client_side_validations is set, structural validation is + disabled. This can be useful to troubleshoot data validation problem, such as + when the OpenAPI document validation rules do not match the actual API data + received by the server. :Example: @@ -93,6 +113,7 @@ conf = petstore_api.Configuration( api_key=None, api_key_prefix=None, username=None, password=None, discard_unknown_keys=False, + disabled_client_side_validations="", ): """Constructor """ @@ -123,6 +144,7 @@ conf = petstore_api.Configuration( """Password for HTTP basic authentication """ self.discard_unknown_keys = discard_unknown_keys + self.disabled_client_side_validations = disabled_client_side_validations self.access_token = None """access token for OAuth/Bearer """ @@ -201,6 +223,13 @@ conf = petstore_api.Configuration( def __setattr__(self, name, value): object.__setattr__(self, name, value) + if name == 'disabled_client_side_validations': + s = set(filter(None, value.split(','))) + for v in s: + if v not in JSON_SCHEMA_VALIDATION_KEYWORDS: + raise ApiValueError( + "Invalid keyword: '{0}''".format(v)) + self._disabled_client_side_validations = s @classmethod def set_default(cls, default): diff --git a/samples/client/petstore/python-experimental/petstore_api/api/another_fake_api.py b/samples/client/petstore/python-experimental/petstore_api/api/another_fake_api.py index 2db9b6de925..b7ea5e54932 100644 --- a/samples/client/petstore/python-experimental/petstore_api/api/another_fake_api.py +++ b/samples/client/petstore/python-experimental/petstore_api/api/another_fake_api.py @@ -249,7 +249,8 @@ class Endpoint(object): check_validations( self.validations, (param,), - kwargs[param] + kwargs[param], + configuration=self.api_client.configuration ) if kwargs['_check_input_type'] is False: diff --git a/samples/client/petstore/python-experimental/petstore_api/api/fake_api.py b/samples/client/petstore/python-experimental/petstore_api/api/fake_api.py index 8c50f5e15d5..e97902bb9a2 100644 --- a/samples/client/petstore/python-experimental/petstore_api/api/fake_api.py +++ b/samples/client/petstore/python-experimental/petstore_api/api/fake_api.py @@ -2192,7 +2192,8 @@ class Endpoint(object): check_validations( self.validations, (param,), - kwargs[param] + kwargs[param], + configuration=self.api_client.configuration ) if kwargs['_check_input_type'] is False: diff --git a/samples/client/petstore/python-experimental/petstore_api/api/fake_classname_tags_123_api.py b/samples/client/petstore/python-experimental/petstore_api/api/fake_classname_tags_123_api.py index ab817996b0e..863e64beb0b 100644 --- a/samples/client/petstore/python-experimental/petstore_api/api/fake_classname_tags_123_api.py +++ b/samples/client/petstore/python-experimental/petstore_api/api/fake_classname_tags_123_api.py @@ -251,7 +251,8 @@ class Endpoint(object): check_validations( self.validations, (param,), - kwargs[param] + kwargs[param], + configuration=self.api_client.configuration ) if kwargs['_check_input_type'] is False: diff --git a/samples/client/petstore/python-experimental/petstore_api/api/pet_api.py b/samples/client/petstore/python-experimental/petstore_api/api/pet_api.py index 829534ed790..3d289a66988 100644 --- a/samples/client/petstore/python-experimental/petstore_api/api/pet_api.py +++ b/samples/client/petstore/python-experimental/petstore_api/api/pet_api.py @@ -1256,7 +1256,8 @@ class Endpoint(object): check_validations( self.validations, (param,), - kwargs[param] + kwargs[param], + configuration=self.api_client.configuration ) if kwargs['_check_input_type'] is False: diff --git a/samples/client/petstore/python-experimental/petstore_api/api/store_api.py b/samples/client/petstore/python-experimental/petstore_api/api/store_api.py index 890474acc03..bcae5aab2a6 100644 --- a/samples/client/petstore/python-experimental/petstore_api/api/store_api.py +++ b/samples/client/petstore/python-experimental/petstore_api/api/store_api.py @@ -590,7 +590,8 @@ class Endpoint(object): check_validations( self.validations, (param,), - kwargs[param] + kwargs[param], + configuration=self.api_client.configuration ) if kwargs['_check_input_type'] is False: diff --git a/samples/client/petstore/python-experimental/petstore_api/api/user_api.py b/samples/client/petstore/python-experimental/petstore_api/api/user_api.py index 74389cbf242..1362c5e3110 100644 --- a/samples/client/petstore/python-experimental/petstore_api/api/user_api.py +++ b/samples/client/petstore/python-experimental/petstore_api/api/user_api.py @@ -1049,7 +1049,8 @@ class Endpoint(object): check_validations( self.validations, (param,), - kwargs[param] + kwargs[param], + configuration=self.api_client.configuration ) if kwargs['_check_input_type'] is False: diff --git a/samples/client/petstore/python-experimental/petstore_api/configuration.py b/samples/client/petstore/python-experimental/petstore_api/configuration.py index 32406df02f5..18085f8b748 100644 --- a/samples/client/petstore/python-experimental/petstore_api/configuration.py +++ b/samples/client/petstore/python-experimental/petstore_api/configuration.py @@ -20,8 +20,15 @@ import urllib3 import six from six.moves import http_client as httplib +from petstore_api.exceptions import ApiValueError +JSON_SCHEMA_VALIDATION_KEYWORDS = { + 'multipleOf', 'maximum', 'exclusiveMaximum', + 'minimum', 'exclusiveMinimum', 'maxLength', + 'minLength', 'pattern', 'maxItems', 'minItems' +} + class Configuration(object): """NOTE: This class is auto generated by OpenAPI Generator @@ -49,6 +56,19 @@ class Configuration(object): then all undeclared properties received by the server are injected into the additional properties map. In that case, there are undeclared properties, and nothing to discard. + :param disabled_client_side_validations (string): Comma-separated list of + JSON schema validation keywords to disable JSON schema structural validation + rules. The following keywords may be specified: multipleOf, maximum, + exclusiveMaximum, minimum, exclusiveMinimum, maxLength, minLength, pattern, + maxItems, minItems. + By default, the validation is performed for data generated locally by the client + and data received from the server, independent of any validation performed by + the server side. If the input data does not satisfy the JSON schema validation + rules specified in the OpenAPI document, an exception is raised. + If disabled_client_side_validations is set, structural validation is + disabled. This can be useful to troubleshoot data validation problem, such as + when the OpenAPI document validation rules do not match the actual API data + received by the server. :Example: @@ -94,6 +114,7 @@ conf = petstore_api.Configuration( api_key=None, api_key_prefix=None, username=None, password=None, discard_unknown_keys=False, + disabled_client_side_validations="", ): """Constructor """ @@ -124,6 +145,7 @@ conf = petstore_api.Configuration( """Password for HTTP basic authentication """ self.discard_unknown_keys = discard_unknown_keys + self.disabled_client_side_validations = disabled_client_side_validations self.access_token = None """access token for OAuth/Bearer """ @@ -205,6 +227,13 @@ conf = petstore_api.Configuration( def __setattr__(self, name, value): object.__setattr__(self, name, value) + if name == 'disabled_client_side_validations': + s = set(filter(None, value.split(','))) + for v in s: + if v not in JSON_SCHEMA_VALIDATION_KEYWORDS: + raise ApiValueError( + "Invalid keyword: '{0}''".format(v)) + self._disabled_client_side_validations = s @classmethod def set_default(cls, default): diff --git a/samples/client/petstore/python-experimental/petstore_api/model_utils.py b/samples/client/petstore/python-experimental/petstore_api/model_utils.py index 4482fdf71a1..baf22c61d19 100644 --- a/samples/client/petstore/python-experimental/petstore_api/model_utils.py +++ b/samples/client/petstore/python-experimental/petstore_api/model_utils.py @@ -112,7 +112,8 @@ class OpenApiModel(object): check_validations( self.validations, (name,), - value + value, + self._configuration ) self.__dict__['_data_store'][name] = value @@ -635,17 +636,50 @@ def check_allowed_values(allowed_values, input_variable_path, input_values): ) -def check_validations(validations, input_variable_path, input_values): +def is_json_validation_enabled(schema_keyword, configuration=None): + """Returns true if JSON schema validation is enabled for the specified + validation keyword. This can be used to skip JSON schema structural validation + as requested in the configuration. + + Args: + schema_keyword (string): the name of a JSON schema validation keyword. + configuration (Configuration): the configuration class. + """ + + return (configuration is None or + not hasattr(configuration, '_disabled_client_side_validations') or + schema_keyword not in configuration._disabled_client_side_validations) + + +def check_validations( + validations, input_variable_path, input_values, + configuration=None): """Raises an exception if the input_values are invalid Args: - validations (dict): the validation dictionary - input_variable_path (tuple): the path to the input variable + validations (dict): the validation dictionary. + input_variable_path (tuple): the path to the input variable. input_values (list/str/int/float/date/datetime): the values that we - are checking + are checking. + configuration (Configuration): the configuration class. """ + current_validations = validations[input_variable_path] - if ('max_length' in current_validations and + if (is_json_validation_enabled('multipleOf', configuration) and + 'multiple_of' in current_validations and + isinstance(input_values, (int, float)) and + not (float(input_values) / current_validations['multiple_of']).is_integer()): + # Note 'multipleOf' will be as good as the floating point arithmetic. + raise ApiValueError( + "Invalid value for `%s`, value must be a multiple of " + "`%s`" % ( + input_variable_path[0], + current_validations['multiple_of'] + ) + ) + + if (is_json_validation_enabled('maxLength', configuration) and + 'max_length' in current_validations and len(input_values) > current_validations['max_length']): raise ApiValueError( "Invalid value for `%s`, length must be less than or equal to " @@ -655,7 +689,8 @@ def check_validations(validations, input_variable_path, input_values): ) ) - if ('min_length' in current_validations and + if (is_json_validation_enabled('minLength', configuration) and + 'min_length' in current_validations and len(input_values) < current_validations['min_length']): raise ApiValueError( "Invalid value for `%s`, length must be greater than or equal to " @@ -665,7 +700,8 @@ def check_validations(validations, input_variable_path, input_values): ) ) - if ('max_items' in current_validations and + if (is_json_validation_enabled('maxItems', configuration) and + 'max_items' in current_validations and len(input_values) > current_validations['max_items']): raise ApiValueError( "Invalid value for `%s`, number of items must be less than or " @@ -675,7 +711,8 @@ def check_validations(validations, input_variable_path, input_values): ) ) - if ('min_items' in current_validations and + if (is_json_validation_enabled('minItems', configuration) and + 'min_items' in current_validations and len(input_values) < current_validations['min_items']): raise ValueError( "Invalid value for `%s`, number of items must be greater than or " @@ -698,7 +735,8 @@ def check_validations(validations, input_variable_path, input_values): max_val = input_values min_val = input_values - if ('exclusive_maximum' in current_validations and + if (is_json_validation_enabled('exclusiveMaximum', configuration) and + 'exclusive_maximum' in current_validations and max_val >= current_validations['exclusive_maximum']): raise ApiValueError( "Invalid value for `%s`, must be a value less than `%s`" % ( @@ -707,7 +745,8 @@ def check_validations(validations, input_variable_path, input_values): ) ) - if ('inclusive_maximum' in current_validations and + if (is_json_validation_enabled('maximum', configuration) and + 'inclusive_maximum' in current_validations and max_val > current_validations['inclusive_maximum']): raise ApiValueError( "Invalid value for `%s`, must be a value less than or equal to " @@ -717,7 +756,8 @@ def check_validations(validations, input_variable_path, input_values): ) ) - if ('exclusive_minimum' in current_validations and + if (is_json_validation_enabled('exclusiveMinimum', configuration) and + 'exclusive_minimum' in current_validations and min_val <= current_validations['exclusive_minimum']): raise ApiValueError( "Invalid value for `%s`, must be a value greater than `%s`" % @@ -727,7 +767,8 @@ def check_validations(validations, input_variable_path, input_values): ) ) - if ('inclusive_minimum' in current_validations and + if (is_json_validation_enabled('minimum', configuration) and + 'inclusive_minimum' in current_validations and min_val < current_validations['inclusive_minimum']): raise ApiValueError( "Invalid value for `%s`, must be a value greater than or equal " @@ -737,7 +778,8 @@ def check_validations(validations, input_variable_path, input_values): ) ) flags = current_validations.get('regex', {}).get('flags', 0) - if ('regex' in current_validations and + if (is_json_validation_enabled('pattern', configuration) and + 'regex' in current_validations and not re.search(current_validations['regex']['pattern'], input_values, flags=flags)): err_msg = r"Invalid value for `%s`, must match regular expression `%s`" % ( diff --git a/samples/client/petstore/python-tornado/petstore_api/configuration.py b/samples/client/petstore/python-tornado/petstore_api/configuration.py index 32406df02f5..18085f8b748 100644 --- a/samples/client/petstore/python-tornado/petstore_api/configuration.py +++ b/samples/client/petstore/python-tornado/petstore_api/configuration.py @@ -20,8 +20,15 @@ import urllib3 import six from six.moves import http_client as httplib +from petstore_api.exceptions import ApiValueError +JSON_SCHEMA_VALIDATION_KEYWORDS = { + 'multipleOf', 'maximum', 'exclusiveMaximum', + 'minimum', 'exclusiveMinimum', 'maxLength', + 'minLength', 'pattern', 'maxItems', 'minItems' +} + class Configuration(object): """NOTE: This class is auto generated by OpenAPI Generator @@ -49,6 +56,19 @@ class Configuration(object): then all undeclared properties received by the server are injected into the additional properties map. In that case, there are undeclared properties, and nothing to discard. + :param disabled_client_side_validations (string): Comma-separated list of + JSON schema validation keywords to disable JSON schema structural validation + rules. The following keywords may be specified: multipleOf, maximum, + exclusiveMaximum, minimum, exclusiveMinimum, maxLength, minLength, pattern, + maxItems, minItems. + By default, the validation is performed for data generated locally by the client + and data received from the server, independent of any validation performed by + the server side. If the input data does not satisfy the JSON schema validation + rules specified in the OpenAPI document, an exception is raised. + If disabled_client_side_validations is set, structural validation is + disabled. This can be useful to troubleshoot data validation problem, such as + when the OpenAPI document validation rules do not match the actual API data + received by the server. :Example: @@ -94,6 +114,7 @@ conf = petstore_api.Configuration( api_key=None, api_key_prefix=None, username=None, password=None, discard_unknown_keys=False, + disabled_client_side_validations="", ): """Constructor """ @@ -124,6 +145,7 @@ conf = petstore_api.Configuration( """Password for HTTP basic authentication """ self.discard_unknown_keys = discard_unknown_keys + self.disabled_client_side_validations = disabled_client_side_validations self.access_token = None """access token for OAuth/Bearer """ @@ -205,6 +227,13 @@ conf = petstore_api.Configuration( def __setattr__(self, name, value): object.__setattr__(self, name, value) + if name == 'disabled_client_side_validations': + s = set(filter(None, value.split(','))) + for v in s: + if v not in JSON_SCHEMA_VALIDATION_KEYWORDS: + raise ApiValueError( + "Invalid keyword: '{0}''".format(v)) + self._disabled_client_side_validations = s @classmethod def set_default(cls, default): diff --git a/samples/client/petstore/python/petstore_api/configuration.py b/samples/client/petstore/python/petstore_api/configuration.py index 32406df02f5..18085f8b748 100644 --- a/samples/client/petstore/python/petstore_api/configuration.py +++ b/samples/client/petstore/python/petstore_api/configuration.py @@ -20,8 +20,15 @@ import urllib3 import six from six.moves import http_client as httplib +from petstore_api.exceptions import ApiValueError +JSON_SCHEMA_VALIDATION_KEYWORDS = { + 'multipleOf', 'maximum', 'exclusiveMaximum', + 'minimum', 'exclusiveMinimum', 'maxLength', + 'minLength', 'pattern', 'maxItems', 'minItems' +} + class Configuration(object): """NOTE: This class is auto generated by OpenAPI Generator @@ -49,6 +56,19 @@ class Configuration(object): then all undeclared properties received by the server are injected into the additional properties map. In that case, there are undeclared properties, and nothing to discard. + :param disabled_client_side_validations (string): Comma-separated list of + JSON schema validation keywords to disable JSON schema structural validation + rules. The following keywords may be specified: multipleOf, maximum, + exclusiveMaximum, minimum, exclusiveMinimum, maxLength, minLength, pattern, + maxItems, minItems. + By default, the validation is performed for data generated locally by the client + and data received from the server, independent of any validation performed by + the server side. If the input data does not satisfy the JSON schema validation + rules specified in the OpenAPI document, an exception is raised. + If disabled_client_side_validations is set, structural validation is + disabled. This can be useful to troubleshoot data validation problem, such as + when the OpenAPI document validation rules do not match the actual API data + received by the server. :Example: @@ -94,6 +114,7 @@ conf = petstore_api.Configuration( api_key=None, api_key_prefix=None, username=None, password=None, discard_unknown_keys=False, + disabled_client_side_validations="", ): """Constructor """ @@ -124,6 +145,7 @@ conf = petstore_api.Configuration( """Password for HTTP basic authentication """ self.discard_unknown_keys = discard_unknown_keys + self.disabled_client_side_validations = disabled_client_side_validations self.access_token = None """access token for OAuth/Bearer """ @@ -205,6 +227,13 @@ conf = petstore_api.Configuration( def __setattr__(self, name, value): object.__setattr__(self, name, value) + if name == 'disabled_client_side_validations': + s = set(filter(None, value.split(','))) + for v in s: + if v not in JSON_SCHEMA_VALIDATION_KEYWORDS: + raise ApiValueError( + "Invalid keyword: '{0}''".format(v)) + self._disabled_client_side_validations = s @classmethod def set_default(cls, default): diff --git a/samples/openapi3/client/petstore/python-experimental/petstore_api/api/another_fake_api.py b/samples/openapi3/client/petstore/python-experimental/petstore_api/api/another_fake_api.py index fbf98194fcd..6c7e40c2482 100644 --- a/samples/openapi3/client/petstore/python-experimental/petstore_api/api/another_fake_api.py +++ b/samples/openapi3/client/petstore/python-experimental/petstore_api/api/another_fake_api.py @@ -249,7 +249,8 @@ class Endpoint(object): check_validations( self.validations, (param,), - kwargs[param] + kwargs[param], + configuration=self.api_client.configuration ) if kwargs['_check_input_type'] is False: diff --git a/samples/openapi3/client/petstore/python-experimental/petstore_api/api/default_api.py b/samples/openapi3/client/petstore/python-experimental/petstore_api/api/default_api.py index 3369688cb88..3e46d7928cb 100644 --- a/samples/openapi3/client/petstore/python-experimental/petstore_api/api/default_api.py +++ b/samples/openapi3/client/petstore/python-experimental/petstore_api/api/default_api.py @@ -235,7 +235,8 @@ class Endpoint(object): check_validations( self.validations, (param,), - kwargs[param] + kwargs[param], + configuration=self.api_client.configuration ) if kwargs['_check_input_type'] is False: diff --git a/samples/openapi3/client/petstore/python-experimental/petstore_api/api/fake_api.py b/samples/openapi3/client/petstore/python-experimental/petstore_api/api/fake_api.py index ad882a19c66..fd5096ba6de 100644 --- a/samples/openapi3/client/petstore/python-experimental/petstore_api/api/fake_api.py +++ b/samples/openapi3/client/petstore/python-experimental/petstore_api/api/fake_api.py @@ -2054,7 +2054,8 @@ class Endpoint(object): check_validations( self.validations, (param,), - kwargs[param] + kwargs[param], + configuration=self.api_client.configuration ) if kwargs['_check_input_type'] is False: diff --git a/samples/openapi3/client/petstore/python-experimental/petstore_api/api/fake_classname_tags_123_api.py b/samples/openapi3/client/petstore/python-experimental/petstore_api/api/fake_classname_tags_123_api.py index ec0225f1d1c..f2a13a0b92f 100644 --- a/samples/openapi3/client/petstore/python-experimental/petstore_api/api/fake_classname_tags_123_api.py +++ b/samples/openapi3/client/petstore/python-experimental/petstore_api/api/fake_classname_tags_123_api.py @@ -251,7 +251,8 @@ class Endpoint(object): check_validations( self.validations, (param,), - kwargs[param] + kwargs[param], + configuration=self.api_client.configuration ) if kwargs['_check_input_type'] is False: diff --git a/samples/openapi3/client/petstore/python-experimental/petstore_api/api/pet_api.py b/samples/openapi3/client/petstore/python-experimental/petstore_api/api/pet_api.py index 0a0849dfb95..1a0fdb48bf4 100644 --- a/samples/openapi3/client/petstore/python-experimental/petstore_api/api/pet_api.py +++ b/samples/openapi3/client/petstore/python-experimental/petstore_api/api/pet_api.py @@ -1259,7 +1259,8 @@ class Endpoint(object): check_validations( self.validations, (param,), - kwargs[param] + kwargs[param], + configuration=self.api_client.configuration ) if kwargs['_check_input_type'] is False: diff --git a/samples/openapi3/client/petstore/python-experimental/petstore_api/api/store_api.py b/samples/openapi3/client/petstore/python-experimental/petstore_api/api/store_api.py index 36fd9bd722c..46fce50d29d 100644 --- a/samples/openapi3/client/petstore/python-experimental/petstore_api/api/store_api.py +++ b/samples/openapi3/client/petstore/python-experimental/petstore_api/api/store_api.py @@ -592,7 +592,8 @@ class Endpoint(object): check_validations( self.validations, (param,), - kwargs[param] + kwargs[param], + configuration=self.api_client.configuration ) if kwargs['_check_input_type'] is False: diff --git a/samples/openapi3/client/petstore/python-experimental/petstore_api/api/user_api.py b/samples/openapi3/client/petstore/python-experimental/petstore_api/api/user_api.py index aeac9949361..e354babbfe5 100644 --- a/samples/openapi3/client/petstore/python-experimental/petstore_api/api/user_api.py +++ b/samples/openapi3/client/petstore/python-experimental/petstore_api/api/user_api.py @@ -1057,7 +1057,8 @@ class Endpoint(object): check_validations( self.validations, (param,), - kwargs[param] + kwargs[param], + configuration=self.api_client.configuration ) if kwargs['_check_input_type'] is False: diff --git a/samples/openapi3/client/petstore/python-experimental/petstore_api/configuration.py b/samples/openapi3/client/petstore/python-experimental/petstore_api/configuration.py index 92c53fb54ec..850cb907b09 100644 --- a/samples/openapi3/client/petstore/python-experimental/petstore_api/configuration.py +++ b/samples/openapi3/client/petstore/python-experimental/petstore_api/configuration.py @@ -20,8 +20,15 @@ import urllib3 import six from six.moves import http_client as httplib +from petstore_api.exceptions import ApiValueError +JSON_SCHEMA_VALIDATION_KEYWORDS = { + 'multipleOf', 'maximum', 'exclusiveMaximum', + 'minimum', 'exclusiveMinimum', 'maxLength', + 'minLength', 'pattern', 'maxItems', 'minItems' +} + class Configuration(object): """NOTE: This class is auto generated by OpenAPI Generator @@ -49,6 +56,19 @@ class Configuration(object): then all undeclared properties received by the server are injected into the additional properties map. In that case, there are undeclared properties, and nothing to discard. + :param disabled_client_side_validations (string): Comma-separated list of + JSON schema validation keywords to disable JSON schema structural validation + rules. The following keywords may be specified: multipleOf, maximum, + exclusiveMaximum, minimum, exclusiveMinimum, maxLength, minLength, pattern, + maxItems, minItems. + By default, the validation is performed for data generated locally by the client + and data received from the server, independent of any validation performed by + the server side. If the input data does not satisfy the JSON schema validation + rules specified in the OpenAPI document, an exception is raised. + If disabled_client_side_validations is set, structural validation is + disabled. This can be useful to troubleshoot data validation problem, such as + when the OpenAPI document validation rules do not match the actual API data + received by the server. :param signing_info: Configuration parameters for the HTTP signature security scheme. Must be an instance of petstore_api.signing.HttpSigningConfiguration @@ -135,6 +155,7 @@ conf = petstore_api.Configuration( api_key=None, api_key_prefix=None, username=None, password=None, discard_unknown_keys=False, + disabled_client_side_validations="", signing_info=None, ): """Constructor @@ -166,6 +187,7 @@ conf = petstore_api.Configuration( """Password for HTTP basic authentication """ self.discard_unknown_keys = discard_unknown_keys + self.disabled_client_side_validations = disabled_client_side_validations if signing_info is not None: signing_info.host = host self.signing_info = signing_info @@ -252,6 +274,13 @@ conf = petstore_api.Configuration( def __setattr__(self, name, value): object.__setattr__(self, name, value) + if name == 'disabled_client_side_validations': + s = set(filter(None, value.split(','))) + for v in s: + if v not in JSON_SCHEMA_VALIDATION_KEYWORDS: + raise ApiValueError( + "Invalid keyword: '{0}''".format(v)) + self._disabled_client_side_validations = s if name == "signing_info" and value is not None: # Ensure the host paramater from signing info is the same as # Configuration.host. diff --git a/samples/openapi3/client/petstore/python-experimental/petstore_api/model_utils.py b/samples/openapi3/client/petstore/python-experimental/petstore_api/model_utils.py index 4482fdf71a1..baf22c61d19 100644 --- a/samples/openapi3/client/petstore/python-experimental/petstore_api/model_utils.py +++ b/samples/openapi3/client/petstore/python-experimental/petstore_api/model_utils.py @@ -112,7 +112,8 @@ class OpenApiModel(object): check_validations( self.validations, (name,), - value + value, + self._configuration ) self.__dict__['_data_store'][name] = value @@ -635,17 +636,50 @@ def check_allowed_values(allowed_values, input_variable_path, input_values): ) -def check_validations(validations, input_variable_path, input_values): +def is_json_validation_enabled(schema_keyword, configuration=None): + """Returns true if JSON schema validation is enabled for the specified + validation keyword. This can be used to skip JSON schema structural validation + as requested in the configuration. + + Args: + schema_keyword (string): the name of a JSON schema validation keyword. + configuration (Configuration): the configuration class. + """ + + return (configuration is None or + not hasattr(configuration, '_disabled_client_side_validations') or + schema_keyword not in configuration._disabled_client_side_validations) + + +def check_validations( + validations, input_variable_path, input_values, + configuration=None): """Raises an exception if the input_values are invalid Args: - validations (dict): the validation dictionary - input_variable_path (tuple): the path to the input variable + validations (dict): the validation dictionary. + input_variable_path (tuple): the path to the input variable. input_values (list/str/int/float/date/datetime): the values that we - are checking + are checking. + configuration (Configuration): the configuration class. """ + current_validations = validations[input_variable_path] - if ('max_length' in current_validations and + if (is_json_validation_enabled('multipleOf', configuration) and + 'multiple_of' in current_validations and + isinstance(input_values, (int, float)) and + not (float(input_values) / current_validations['multiple_of']).is_integer()): + # Note 'multipleOf' will be as good as the floating point arithmetic. + raise ApiValueError( + "Invalid value for `%s`, value must be a multiple of " + "`%s`" % ( + input_variable_path[0], + current_validations['multiple_of'] + ) + ) + + if (is_json_validation_enabled('maxLength', configuration) and + 'max_length' in current_validations and len(input_values) > current_validations['max_length']): raise ApiValueError( "Invalid value for `%s`, length must be less than or equal to " @@ -655,7 +689,8 @@ def check_validations(validations, input_variable_path, input_values): ) ) - if ('min_length' in current_validations and + if (is_json_validation_enabled('minLength', configuration) and + 'min_length' in current_validations and len(input_values) < current_validations['min_length']): raise ApiValueError( "Invalid value for `%s`, length must be greater than or equal to " @@ -665,7 +700,8 @@ def check_validations(validations, input_variable_path, input_values): ) ) - if ('max_items' in current_validations and + if (is_json_validation_enabled('maxItems', configuration) and + 'max_items' in current_validations and len(input_values) > current_validations['max_items']): raise ApiValueError( "Invalid value for `%s`, number of items must be less than or " @@ -675,7 +711,8 @@ def check_validations(validations, input_variable_path, input_values): ) ) - if ('min_items' in current_validations and + if (is_json_validation_enabled('minItems', configuration) and + 'min_items' in current_validations and len(input_values) < current_validations['min_items']): raise ValueError( "Invalid value for `%s`, number of items must be greater than or " @@ -698,7 +735,8 @@ def check_validations(validations, input_variable_path, input_values): max_val = input_values min_val = input_values - if ('exclusive_maximum' in current_validations and + if (is_json_validation_enabled('exclusiveMaximum', configuration) and + 'exclusive_maximum' in current_validations and max_val >= current_validations['exclusive_maximum']): raise ApiValueError( "Invalid value for `%s`, must be a value less than `%s`" % ( @@ -707,7 +745,8 @@ def check_validations(validations, input_variable_path, input_values): ) ) - if ('inclusive_maximum' in current_validations and + if (is_json_validation_enabled('maximum', configuration) and + 'inclusive_maximum' in current_validations and max_val > current_validations['inclusive_maximum']): raise ApiValueError( "Invalid value for `%s`, must be a value less than or equal to " @@ -717,7 +756,8 @@ def check_validations(validations, input_variable_path, input_values): ) ) - if ('exclusive_minimum' in current_validations and + if (is_json_validation_enabled('exclusiveMinimum', configuration) and + 'exclusive_minimum' in current_validations and min_val <= current_validations['exclusive_minimum']): raise ApiValueError( "Invalid value for `%s`, must be a value greater than `%s`" % @@ -727,7 +767,8 @@ def check_validations(validations, input_variable_path, input_values): ) ) - if ('inclusive_minimum' in current_validations and + if (is_json_validation_enabled('minimum', configuration) and + 'inclusive_minimum' in current_validations and min_val < current_validations['inclusive_minimum']): raise ApiValueError( "Invalid value for `%s`, must be a value greater than or equal " @@ -737,7 +778,8 @@ def check_validations(validations, input_variable_path, input_values): ) ) flags = current_validations.get('regex', {}).get('flags', 0) - if ('regex' in current_validations and + if (is_json_validation_enabled('pattern', configuration) and + 'regex' in current_validations and not re.search(current_validations['regex']['pattern'], input_values, flags=flags)): err_msg = r"Invalid value for `%s`, must match regular expression `%s`" % ( diff --git a/samples/openapi3/client/petstore/python-experimental/petstore_api/models/format_test.py b/samples/openapi3/client/petstore/python-experimental/petstore_api/models/format_test.py index e3084b71911..24601b9499c 100644 --- a/samples/openapi3/client/petstore/python-experimental/petstore_api/models/format_test.py +++ b/samples/openapi3/client/petstore/python-experimental/petstore_api/models/format_test.py @@ -65,6 +65,7 @@ class FormatTest(ModelNormal): ('number',): { 'inclusive_maximum': 543.2, 'inclusive_minimum': 32.1, + 'multiple_of': 32.5, }, ('password',): { 'max_length': 64, @@ -73,6 +74,7 @@ class FormatTest(ModelNormal): ('integer',): { 'inclusive_maximum': 100, 'inclusive_minimum': 10, + 'multiple_of': 2, }, ('int32',): { 'inclusive_maximum': 200, diff --git a/samples/openapi3/client/petstore/python-experimental/tests/test_api_validation.py b/samples/openapi3/client/petstore/python-experimental/tests/test_api_validation.py new file mode 100644 index 00000000000..a8a7276142b --- /dev/null +++ b/samples/openapi3/client/petstore/python-experimental/tests/test_api_validation.py @@ -0,0 +1,131 @@ +# coding: utf-8 + +# flake8: noqa + +""" +Run the tests. +$ pip install nose (optional) +$ cd OpenAPIetstore-python +$ nosetests -v +""" + +import os +import time +import atexit +import datetime +import json +import sys +import weakref +import unittest +from dateutil.parser import parse +from collections import namedtuple + +import petstore_api +import petstore_api.configuration + +HOST = 'http://petstore.swagger.io/v2' +MockResponse = namedtuple('MockResponse', 'data') + + +class ApiClientTests(unittest.TestCase): + def setUp(self): + self.api_client = petstore_api.ApiClient() + + def test_configuration(self): + config = petstore_api.Configuration() + config.host = 'http://localhost/' + + config.disabled_client_side_validations = ("multipleOf,maximum,exclusiveMaximum,minimum,exclusiveMinimum," + "maxLength,minLength,pattern,maxItems,minItems") + with self.checkRaiseRegex(ValueError, "Invalid keyword: 'foo'"): + config.disabled_client_side_validations = 'foo' + config.disabled_client_side_validations = "" + + + def checkRaiseRegex(self, expected_exception, expected_regex): + if sys.version_info < (3, 0): + return self.assertRaisesRegexp(expected_exception, expected_regex) + + return self.assertRaisesRegex(expected_exception, expected_regex) + + + def test_multiple_of(self): + inst = petstore_api.FormatTest( + byte='3', + date=datetime.date(2000, 1, 1), + password="abcdefghijkl", + integer=30, + number=65.0, + float=62.4 + ) + assert isinstance(inst, petstore_api.FormatTest) + + with self.checkRaiseRegex(petstore_api.exceptions.ApiValueError, "Invalid value for `integer`, value must be a multiple of `2`"): + inst = petstore_api.FormatTest( + byte='3', + date=datetime.date(2000, 1, 1), + password="abcdefghijkl", + integer=31, # Value is supposed to be multiple of '2'. An error must be raised + number=65.0, + float=62.4 + ) + + def test_multiple_of_deserialization(self): + data = { + 'byte': '3', + 'date': '1970-01-01', + 'password': "abcdefghijkl", + 'integer': 30, + 'number': 65.0, + 'float': 62.4, + } + response = MockResponse(data=json.dumps(data)) + deserialized = self.api_client.deserialize(response, (petstore_api.FormatTest,), True) + self.assertTrue(isinstance(deserialized, petstore_api.FormatTest)) + + with self.checkRaiseRegex(petstore_api.exceptions.ApiValueError, "Invalid value for `integer`, value must be a multiple of `2`"): + data = { + 'byte': '3', + 'date': '1970-01-01', + 'password': "abcdefghijkl", + 'integer': 31, # Value is supposed to be multiple of '2'. An error must be raised + 'number': 65.0, + 'float': 62.4, + } + response = MockResponse(data=json.dumps(data)) + deserialized = self.api_client.deserialize(response, (petstore_api.FormatTest,), True) + + # Disable JSON schema validation. No error should be raised during deserialization. + config = petstore_api.Configuration() + config.disabled_client_side_validations = "multipleOf" + api_client = petstore_api.ApiClient(configuration=config) + + data = { + 'byte': '3', + 'date': '1970-01-01', + 'password': "abcdefghijkl", + 'integer': 31, # Value is supposed to be multiple of '2' + 'number': 65.0, + 'float': 62.4, + } + response = MockResponse(data=json.dumps(data)) + deserialized = api_client.deserialize(response, (petstore_api.FormatTest,), True) + self.assertTrue(isinstance(deserialized, petstore_api.FormatTest)) + + # Disable JSON schema validation but for a different keyword. + # An error should be raised during deserialization. + config = petstore_api.Configuration() + config.disabled_client_side_validations = "maxItems" + api_client = petstore_api.ApiClient(configuration=config) + + with self.checkRaiseRegex(petstore_api.exceptions.ApiValueError, "Invalid value for `integer`, value must be a multiple of `2`"): + data = { + 'byte': '3', + 'date': '1970-01-01', + 'password': "abcdefghijkl", + 'integer': 31, # Value is supposed to be multiple of '2' + 'number': 65.0, + 'float': 62.4, + } + response = MockResponse(data=json.dumps(data)) + deserialized = api_client.deserialize(response, (petstore_api.FormatTest,), True) diff --git a/samples/openapi3/client/petstore/python/petstore_api/configuration.py b/samples/openapi3/client/petstore/python/petstore_api/configuration.py index 92c53fb54ec..850cb907b09 100644 --- a/samples/openapi3/client/petstore/python/petstore_api/configuration.py +++ b/samples/openapi3/client/petstore/python/petstore_api/configuration.py @@ -20,8 +20,15 @@ import urllib3 import six from six.moves import http_client as httplib +from petstore_api.exceptions import ApiValueError +JSON_SCHEMA_VALIDATION_KEYWORDS = { + 'multipleOf', 'maximum', 'exclusiveMaximum', + 'minimum', 'exclusiveMinimum', 'maxLength', + 'minLength', 'pattern', 'maxItems', 'minItems' +} + class Configuration(object): """NOTE: This class is auto generated by OpenAPI Generator @@ -49,6 +56,19 @@ class Configuration(object): then all undeclared properties received by the server are injected into the additional properties map. In that case, there are undeclared properties, and nothing to discard. + :param disabled_client_side_validations (string): Comma-separated list of + JSON schema validation keywords to disable JSON schema structural validation + rules. The following keywords may be specified: multipleOf, maximum, + exclusiveMaximum, minimum, exclusiveMinimum, maxLength, minLength, pattern, + maxItems, minItems. + By default, the validation is performed for data generated locally by the client + and data received from the server, independent of any validation performed by + the server side. If the input data does not satisfy the JSON schema validation + rules specified in the OpenAPI document, an exception is raised. + If disabled_client_side_validations is set, structural validation is + disabled. This can be useful to troubleshoot data validation problem, such as + when the OpenAPI document validation rules do not match the actual API data + received by the server. :param signing_info: Configuration parameters for the HTTP signature security scheme. Must be an instance of petstore_api.signing.HttpSigningConfiguration @@ -135,6 +155,7 @@ conf = petstore_api.Configuration( api_key=None, api_key_prefix=None, username=None, password=None, discard_unknown_keys=False, + disabled_client_side_validations="", signing_info=None, ): """Constructor @@ -166,6 +187,7 @@ conf = petstore_api.Configuration( """Password for HTTP basic authentication """ self.discard_unknown_keys = discard_unknown_keys + self.disabled_client_side_validations = disabled_client_side_validations if signing_info is not None: signing_info.host = host self.signing_info = signing_info @@ -252,6 +274,13 @@ conf = petstore_api.Configuration( def __setattr__(self, name, value): object.__setattr__(self, name, value) + if name == 'disabled_client_side_validations': + s = set(filter(None, value.split(','))) + for v in s: + if v not in JSON_SCHEMA_VALIDATION_KEYWORDS: + raise ApiValueError( + "Invalid keyword: '{0}''".format(v)) + self._disabled_client_side_validations = s if name == "signing_info" and value is not None: # Ensure the host paramater from signing info is the same as # Configuration.host.