From c4c51637d29a68b995d7803f53837ccabfd4bdc9 Mon Sep 17 00:00:00 2001 From: p1c2u Date: Mon, 2 Sep 2019 23:36:30 +0100 Subject: [PATCH 1/4] Rename schema validate to obj_validate --- openapi_core/schema/media_types/models.py | 2 +- openapi_core/schema/parameters/models.py | 2 +- openapi_core/schema/schemas/models.py | 8 +- tests/unit/schema/test_schemas.py | 144 +++++++++++----------- 4 files changed, 78 insertions(+), 78 deletions(-) diff --git a/openapi_core/schema/media_types/models.py b/openapi_core/schema/media_types/models.py index 7c53945..5cc278b 100644 --- a/openapi_core/schema/media_types/models.py +++ b/openapi_core/schema/media_types/models.py @@ -47,7 +47,7 @@ class MediaType(object): raise InvalidMediaTypeValue(exc) try: - return self.schema.validate( + return self.schema.obj_validate( unmarshalled, custom_formatters=custom_formatters) except OpenAPISchemaError as exc: raise InvalidMediaTypeValue(exc) diff --git a/openapi_core/schema/parameters/models.py b/openapi_core/schema/parameters/models.py index 0565f66..f20b4f9 100644 --- a/openapi_core/schema/parameters/models.py +++ b/openapi_core/schema/parameters/models.py @@ -123,7 +123,7 @@ class Parameter(object): raise InvalidParameterValue(self.name, exc) try: - return self.schema.validate( + return self.schema.obj_validate( unmarshalled, custom_formatters=custom_formatters) except OpenAPISchemaError as exc: raise InvalidParameterValue(self.name, exc) diff --git a/openapi_core/schema/schemas/models.py b/openapi_core/schema/schemas/models.py index 5dfa329..17fa44f 100644 --- a/openapi_core/schema/schemas/models.py +++ b/openapi_core/schema/schemas/models.py @@ -406,7 +406,7 @@ class Schema(object): return defaultdict(lambda: default, mapping) - def validate(self, value, custom_formatters=None): + def obj_validate(self, value, custom_formatters=None): if value is None: if not self.nullable: raise InvalidSchemaValue("Null value for non-nullable schema of type {type}", value, self.type) @@ -453,7 +453,7 @@ class Schema(object): if self.unique_items and len(set(value)) != len(value): raise OpenAPISchemaError("Value may not contain duplicate items") - f = functools.partial(self.items.validate, + f = functools.partial(self.items.obj_validate, custom_formatters=custom_formatters) return list(map(f, value)) @@ -602,7 +602,7 @@ class Schema(object): if self.additional_properties is not True: for prop_name in extra_props: prop_value = value[prop_name] - self.additional_properties.validate( + self.additional_properties.obj_validate( prop_value, custom_formatters=custom_formatters) for prop_name, prop in iteritems(all_props): @@ -615,7 +615,7 @@ class Schema(object): continue prop_value = prop.default try: - prop.validate(prop_value, custom_formatters=custom_formatters) + prop.obj_validate(prop_value, custom_formatters=custom_formatters) except OpenAPISchemaError as exc: raise InvalidSchemaProperty(prop_name, original_exception=exc) diff --git a/tests/unit/schema/test_schemas.py b/tests/unit/schema/test_schemas.py index 0b747aa..0e7a2fe 100644 --- a/tests/unit/schema/test_schemas.py +++ b/tests/unit/schema/test_schemas.py @@ -328,7 +328,7 @@ class TestSchemaUnmarshal(object): assert schema.unmarshal('string') == 'string' -class TestSchemaValidate(object): +class TestSchemaObjValidate(object): @pytest.mark.parametrize('schema_type', [ 'boolean', 'array', 'integer', 'number', 'string', @@ -338,7 +338,7 @@ class TestSchemaValidate(object): value = None with pytest.raises(InvalidSchemaValue): - schema.validate(value) + schema.obj_validate(value) @pytest.mark.parametrize('schema_type', [ 'boolean', 'array', 'integer', 'number', 'string', @@ -347,7 +347,7 @@ class TestSchemaValidate(object): schema = Schema(schema_type, nullable=True) value = None - result = schema.validate(value) + result = schema.obj_validate(value) assert result is None @@ -355,7 +355,7 @@ class TestSchemaValidate(object): def test_boolean(self, value): schema = Schema('boolean') - result = schema.validate(value) + result = schema.obj_validate(value) assert result == value @@ -364,20 +364,20 @@ class TestSchemaValidate(object): schema = Schema('boolean') with pytest.raises(InvalidSchemaValue): - schema.validate(value) + schema.obj_validate(value) @pytest.mark.parametrize('value', [[1, 2], (3, 4)]) def test_array_no_schema(self, value): schema = Schema('array') with pytest.raises(OpenAPISchemaError): - schema.validate(value) + schema.obj_validate(value) @pytest.mark.parametrize('value', [[1, 2], (3, 4)]) def test_array(self, value): schema = Schema('array', items=Schema('integer')) - result = schema.validate(value) + result = schema.obj_validate(value) assert result == value @@ -386,13 +386,13 @@ class TestSchemaValidate(object): schema = Schema('array') with pytest.raises(InvalidSchemaValue): - schema.validate(value) + schema.obj_validate(value) @pytest.mark.parametrize('value', [1, 3]) def test_integer(self, value): schema = Schema('integer') - result = schema.validate(value) + result = schema.obj_validate(value) assert result == value @@ -401,20 +401,20 @@ class TestSchemaValidate(object): schema = Schema('integer') with pytest.raises(InvalidSchemaValue): - schema.validate(value) + schema.obj_validate(value) @pytest.mark.parametrize('value', [0, 1, 2]) def test_integer_minimum_invalid(self, value): schema = Schema('integer', minimum=3) with pytest.raises(InvalidSchemaValue): - schema.validate(value) + schema.obj_validate(value) @pytest.mark.parametrize('value', [4, 5, 6]) def test_integer_minimum(self, value): schema = Schema('integer', minimum=3) - result = schema.validate(value) + result = schema.obj_validate(value) assert result == value @@ -423,13 +423,13 @@ class TestSchemaValidate(object): schema = Schema('integer', maximum=3) with pytest.raises(InvalidSchemaValue): - schema.validate(value) + schema.obj_validate(value) @pytest.mark.parametrize('value', [0, 1, 2]) def test_integer_maximum(self, value): schema = Schema('integer', maximum=3) - result = schema.validate(value) + result = schema.obj_validate(value) assert result == value @@ -438,13 +438,13 @@ class TestSchemaValidate(object): schema = Schema('integer', multiple_of=3) with pytest.raises(InvalidSchemaValue): - schema.validate(value) + schema.obj_validate(value) @pytest.mark.parametrize('value', [3, 6, 18]) def test_integer_multiple_of(self, value): schema = Schema('integer', multiple_of=3) - result = schema.validate(value) + result = schema.obj_validate(value) assert result == value @@ -452,7 +452,7 @@ class TestSchemaValidate(object): def test_number(self, value): schema = Schema('number') - result = schema.validate(value) + result = schema.obj_validate(value) assert result == value @@ -461,20 +461,20 @@ class TestSchemaValidate(object): schema = Schema('number') with pytest.raises(InvalidSchemaValue): - schema.validate(value) + schema.obj_validate(value) @pytest.mark.parametrize('value', [0, 1, 2]) def test_number_minimum_invalid(self, value): schema = Schema('number', minimum=3) with pytest.raises(InvalidSchemaValue): - schema.validate(value) + schema.obj_validate(value) @pytest.mark.parametrize('value', [3, 4, 5]) def test_number_minimum(self, value): schema = Schema('number', minimum=3) - result = schema.validate(value) + result = schema.obj_validate(value) assert result == value @@ -483,13 +483,13 @@ class TestSchemaValidate(object): schema = Schema('number', minimum=3, exclusive_minimum=3) with pytest.raises(InvalidSchemaValue): - schema.validate(value) + schema.obj_validate(value) @pytest.mark.parametrize('value', [4, 5, 6]) def test_number_exclusive_minimum(self, value): schema = Schema('number', minimum=3) - result = schema.validate(value) + result = schema.obj_validate(value) assert result == value @@ -498,13 +498,13 @@ class TestSchemaValidate(object): schema = Schema('number', maximum=3) with pytest.raises(InvalidSchemaValue): - schema.validate(value) + schema.obj_validate(value) @pytest.mark.parametrize('value', [1, 2, 3]) def test_number_maximum(self, value): schema = Schema('number', maximum=3) - result = schema.validate(value) + result = schema.obj_validate(value) assert result == value @@ -513,13 +513,13 @@ class TestSchemaValidate(object): schema = Schema('number', maximum=3, exclusive_maximum=True) with pytest.raises(InvalidSchemaValue): - schema.validate(value) + schema.obj_validate(value) @pytest.mark.parametrize('value', [0, 1, 2]) def test_number_exclusive_maximum(self, value): schema = Schema('number', maximum=3, exclusive_maximum=True) - result = schema.validate(value) + result = schema.obj_validate(value) assert result == value @@ -528,13 +528,13 @@ class TestSchemaValidate(object): schema = Schema('number', multiple_of=3) with pytest.raises(InvalidSchemaValue): - schema.validate(value) + schema.obj_validate(value) @pytest.mark.parametrize('value', [3, 6, 18]) def test_number_multiple_of(self, value): schema = Schema('number', multiple_of=3) - result = schema.validate(value) + result = schema.obj_validate(value) assert result == value @@ -542,7 +542,7 @@ class TestSchemaValidate(object): def test_string(self, value): schema = Schema('string') - result = schema.validate(value) + result = schema.obj_validate(value) assert result == value @@ -551,7 +551,7 @@ class TestSchemaValidate(object): schema = Schema('string') with pytest.raises(InvalidSchemaValue): - schema.validate(value) + schema.obj_validate(value) @pytest.mark.parametrize('value', [ b('true'), u('test'), False, 1, 3.14, [1, 3], @@ -561,7 +561,7 @@ class TestSchemaValidate(object): schema = Schema('string', schema_format='date') with pytest.raises(InvalidSchemaValue): - schema.validate(value) + schema.obj_validate(value) @pytest.mark.parametrize('value', [ datetime.date(1989, 1, 2), datetime.date(2018, 1, 2), @@ -569,7 +569,7 @@ class TestSchemaValidate(object): def test_string_format_date(self, value): schema = Schema('string', schema_format='date') - result = schema.validate(value) + result = schema.obj_validate(value) assert result == value @@ -579,7 +579,7 @@ class TestSchemaValidate(object): def test_string_format_uuid(self, value): schema = Schema('string', schema_format='uuid') - result = schema.validate(value) + result = schema.obj_validate(value) assert result == value @@ -591,7 +591,7 @@ class TestSchemaValidate(object): schema = Schema('string', schema_format='uuid') with pytest.raises(InvalidSchemaValue): - schema.validate(value) + schema.obj_validate(value) @pytest.mark.parametrize('value', [ b('true'), u('true'), False, 1, 3.14, [1, 3], @@ -601,7 +601,7 @@ class TestSchemaValidate(object): schema = Schema('string', schema_format='date-time') with pytest.raises(InvalidSchemaValue): - schema.validate(value) + schema.obj_validate(value) @pytest.mark.parametrize('value', [ datetime.datetime(1989, 1, 2, 0, 0, 0), @@ -610,7 +610,7 @@ class TestSchemaValidate(object): def test_string_format_datetime(self, value): schema = Schema('string', schema_format='date-time') - result = schema.validate(value) + result = schema.obj_validate(value) assert result == value @@ -622,7 +622,7 @@ class TestSchemaValidate(object): schema = Schema('string', schema_format='binary') with pytest.raises(InvalidSchemaValue): - schema.validate(value) + schema.obj_validate(value) @pytest.mark.parametrize('value', [ b('stream'), b('text'), @@ -630,7 +630,7 @@ class TestSchemaValidate(object): def test_string_format_binary(self, value): schema = Schema('string', schema_format='binary') - result = schema.validate(value) + result = schema.obj_validate(value) assert result == value @@ -641,7 +641,7 @@ class TestSchemaValidate(object): schema = Schema('string', schema_format='byte') with pytest.raises(OpenAPISchemaError): - schema.validate(value) + schema.obj_validate(value) @pytest.mark.parametrize('value', [ u('tsssst'), u('dGVzdA=='), @@ -649,7 +649,7 @@ class TestSchemaValidate(object): def test_string_format_byte(self, value): schema = Schema('string', schema_format='byte') - result = schema.validate(value) + result = schema.obj_validate(value) assert result == value @@ -662,27 +662,27 @@ class TestSchemaValidate(object): schema = Schema('string', schema_format=unknown_format) with pytest.raises(OpenAPISchemaError): - schema.validate(value) + schema.obj_validate(value) @pytest.mark.parametrize('value', [u(""), ]) def test_string_min_length_invalid_schema(self, value): schema = Schema('string', min_length=-1) with pytest.raises(OpenAPISchemaError): - schema.validate(value) + schema.obj_validate(value) @pytest.mark.parametrize('value', [u(""), u("a"), u("ab")]) def test_string_min_length_invalid(self, value): schema = Schema('string', min_length=3) with pytest.raises(InvalidSchemaValue): - schema.validate(value) + schema.obj_validate(value) @pytest.mark.parametrize('value', [u("abc"), u("abcd")]) def test_string_min_length(self, value): schema = Schema('string', min_length=3) - result = schema.validate(value) + result = schema.obj_validate(value) assert result == value @@ -691,20 +691,20 @@ class TestSchemaValidate(object): schema = Schema('string', max_length=-1) with pytest.raises(OpenAPISchemaError): - schema.validate(value) + schema.obj_validate(value) @pytest.mark.parametrize('value', [u("ab"), u("abc")]) def test_string_max_length_invalid(self, value): schema = Schema('string', max_length=1) with pytest.raises(InvalidSchemaValue): - schema.validate(value) + schema.obj_validate(value) @pytest.mark.parametrize('value', [u(""), u("a")]) def test_string_max_length(self, value): schema = Schema('string', max_length=1) - result = schema.validate(value) + result = schema.obj_validate(value) assert result == value @@ -713,13 +713,13 @@ class TestSchemaValidate(object): schema = Schema('string', pattern='baz') with pytest.raises(InvalidSchemaValue): - schema.validate(value) + schema.obj_validate(value) @pytest.mark.parametrize('value', [u("bar"), u("foobar")]) def test_string_pattern(self, value): schema = Schema('string', pattern='bar') - result = schema.validate(value) + result = schema.obj_validate(value) assert result == value @@ -728,7 +728,7 @@ class TestSchemaValidate(object): schema = Schema('object') with pytest.raises(InvalidSchemaValue): - schema.validate(value) + schema.obj_validate(value) @pytest.mark.parametrize('value', [Model(), ]) def test_object_multiple_one_of(self, value): @@ -738,7 +738,7 @@ class TestSchemaValidate(object): schema = Schema('object', one_of=one_of) with pytest.raises(MultipleOneOfSchema): - schema.validate(value) + schema.obj_validate(value) @pytest.mark.parametrize('value', [Model(), ]) def test_object_defferent_type_one_of(self, value): @@ -748,7 +748,7 @@ class TestSchemaValidate(object): schema = Schema('object', one_of=one_of) with pytest.raises(MultipleOneOfSchema): - schema.validate(value) + schema.obj_validate(value) @pytest.mark.parametrize('value', [Model(), ]) def test_object_no_one_of(self, value): @@ -767,7 +767,7 @@ class TestSchemaValidate(object): schema = Schema('object', one_of=one_of) with pytest.raises(NoOneOfSchema): - schema.validate(value) + schema.obj_validate(value) @pytest.mark.parametrize('value', [ Model({ @@ -800,13 +800,13 @@ class TestSchemaValidate(object): ] schema = Schema('object', one_of=one_of) - schema.validate(value) + schema.obj_validate(value) @pytest.mark.parametrize('value', [Model(), ]) def test_object_default_property(self, value): schema = Schema('object', default='value1') - result = schema.validate(value) + result = schema.obj_validate(value) assert result == value @@ -815,7 +815,7 @@ class TestSchemaValidate(object): schema = Schema('object', min_properties=-1) with pytest.raises(OpenAPISchemaError): - schema.validate(value) + schema.obj_validate(value) @pytest.mark.parametrize('value', [ Model({'a': 1}), @@ -830,7 +830,7 @@ class TestSchemaValidate(object): ) with pytest.raises(InvalidSchemaValue): - schema.validate(value) + schema.obj_validate(value) @pytest.mark.parametrize('value', [ Model({'a': 1}), @@ -844,7 +844,7 @@ class TestSchemaValidate(object): min_properties=1, ) - result = schema.validate(value) + result = schema.obj_validate(value) assert result == value @@ -853,7 +853,7 @@ class TestSchemaValidate(object): schema = Schema('object', max_properties=-1) with pytest.raises(OpenAPISchemaError): - schema.validate(value) + schema.obj_validate(value) @pytest.mark.parametrize('value', [ Model({'a': 1}), @@ -868,7 +868,7 @@ class TestSchemaValidate(object): ) with pytest.raises(InvalidSchemaValue): - schema.validate(value) + schema.obj_validate(value) @pytest.mark.parametrize('value', [ Model({'a': 1}), @@ -882,7 +882,7 @@ class TestSchemaValidate(object): max_properties=3, ) - result = schema.validate(value) + result = schema.obj_validate(value) assert result == value @@ -890,21 +890,21 @@ class TestSchemaValidate(object): def test_object_additional_propetries(self, value): schema = Schema('object') - schema.validate(value) + schema.obj_validate(value) @pytest.mark.parametrize('value', [Model({'additional': 1}), ]) def test_object_additional_propetries_false(self, value): schema = Schema('object', additional_properties=False) with pytest.raises(UndefinedSchemaProperty): - schema.validate(value) + schema.obj_validate(value) @pytest.mark.parametrize('value', [Model({'additional': 1}), ]) def test_object_additional_propetries_object(self, value): additional_properties = Schema('integer') schema = Schema('object', additional_properties=additional_properties) - schema.validate(value) + schema.obj_validate(value) @pytest.mark.parametrize('value', [[], ]) def test_list_min_items_invalid_schema(self, value): @@ -915,7 +915,7 @@ class TestSchemaValidate(object): ) with pytest.raises(OpenAPISchemaError): - schema.validate(value) + schema.obj_validate(value) @pytest.mark.parametrize('value', [[], [1], [1, 2]]) def test_list_min_items_invalid(self, value): @@ -926,7 +926,7 @@ class TestSchemaValidate(object): ) with pytest.raises(Exception): - schema.validate(value) + schema.obj_validate(value) @pytest.mark.parametrize('value', [[], [1], [1, 2]]) def test_list_min_items(self, value): @@ -936,7 +936,7 @@ class TestSchemaValidate(object): min_items=0, ) - result = schema.validate(value) + result = schema.obj_validate(value) assert result == value @@ -949,7 +949,7 @@ class TestSchemaValidate(object): ) with pytest.raises(OpenAPISchemaError): - schema.validate(value) + schema.obj_validate(value) @pytest.mark.parametrize('value', [[1, 2], [2, 3, 4]]) def test_list_max_items_invalid(self, value): @@ -960,7 +960,7 @@ class TestSchemaValidate(object): ) with pytest.raises(Exception): - schema.validate(value) + schema.obj_validate(value) @pytest.mark.parametrize('value', [[1, 2, 1], [2, 2]]) def test_list_unique_items_invalid(self, value): @@ -971,7 +971,7 @@ class TestSchemaValidate(object): ) with pytest.raises(Exception): - schema.validate(value) + schema.obj_validate(value) @pytest.mark.parametrize('value', [ Model({ @@ -994,7 +994,7 @@ class TestSchemaValidate(object): }, ) - result = schema.validate(value) + result = schema.obj_validate(value) assert result == value @@ -1034,4 +1034,4 @@ class TestSchemaValidate(object): ) with pytest.raises(Exception): - schema.validate(value) + schema.obj_validate(value) From b2410e2f3ad82354c0937347e636dce8132eb080 Mon Sep 17 00:00:00 2001 From: p1c2u Date: Tue, 3 Sep 2019 01:38:19 +0100 Subject: [PATCH 2/4] OAS 3.0 validator --- openapi_core/schema/media_types/models.py | 15 ++++- openapi_core/schema/parameters/models.py | 17 ++++-- openapi_core/schema/schemas/_format.py | 9 +++ openapi_core/schema/schemas/_types.py | 21 +++++++ openapi_core/schema/schemas/_validators.py | 27 +++++++++ openapi_core/schema/schemas/factories.py | 5 +- openapi_core/schema/schemas/models.py | 31 ++++++---- openapi_core/schema/schemas/validators.py | 54 +++++++++++++++++ openapi_core/schema/specs/factories.py | 14 ++++- openapi_core/schema/specs/models.py | 4 +- openapi_core/shortcuts.py | 4 +- openapi_core/validation/request/validators.py | 25 ++++++-- .../validation/response/validators.py | 10 +++- tests/integration/data/v3.0/petstore.yaml | 5 +- tests/integration/test_petstore.py | 33 ++++++----- tests/integration/test_validators.py | 16 ++++- tests/unit/schema/test_media_types.py | 53 +++++++++++++++++ tests/unit/schema/test_parameters.py | 59 +++++++++++++++---- tests/unit/schema/test_schemas.py | 43 +++++++++++--- 19 files changed, 377 insertions(+), 68 deletions(-) create mode 100644 openapi_core/schema/schemas/_format.py create mode 100644 openapi_core/schema/schemas/_types.py create mode 100644 openapi_core/schema/schemas/_validators.py create mode 100644 tests/unit/schema/test_media_types.py diff --git a/openapi_core/schema/media_types/models.py b/openapi_core/schema/media_types/models.py index 5cc278b..dce3737 100644 --- a/openapi_core/schema/media_types/models.py +++ b/openapi_core/schema/media_types/models.py @@ -32,17 +32,26 @@ class MediaType(object): deserializer = self.get_dererializer() return deserializer(value) - def unmarshal(self, value, custom_formatters=None): + def cast(self, value): if not self.schema: return value try: - deserialized = self.deserialize(value) + return self.deserialize(value) except ValueError as exc: raise InvalidMediaTypeValue(exc) + def unmarshal(self, value, custom_formatters=None, resolver=None): + if not self.schema: + return value + try: - unmarshalled = self.schema.unmarshal(deserialized, custom_formatters=custom_formatters) + self.schema.validate(value, resolver=resolver) + except OpenAPISchemaError as exc: + raise InvalidMediaTypeValue(exc) + + try: + unmarshalled = self.schema.unmarshal(value, custom_formatters=custom_formatters) except OpenAPISchemaError as exc: raise InvalidMediaTypeValue(exc) diff --git a/openapi_core/schema/parameters/models.py b/openapi_core/schema/parameters/models.py index f20b4f9..74ef1ae 100644 --- a/openapi_core/schema/parameters/models.py +++ b/openapi_core/schema/parameters/models.py @@ -72,7 +72,7 @@ class Parameter(object): deserializer = self.get_dererializer() return deserializer(value) - def get_value(self, request): + def get_raw_value(self, request): location = request.parameters[self.location.value] if self.name not in location: @@ -89,7 +89,7 @@ class Parameter(object): return location[self.name] - def unmarshal(self, value, custom_formatters=None): + def cast(self, value): if self.deprecated: warnings.warn( "{0} parameter is deprecated".format(self.name), @@ -109,13 +109,22 @@ class Parameter(object): raise InvalidParameterValue(self.name, exc) try: - casted = self.schema.cast(deserialized) + return self.schema.cast(deserialized) + except OpenAPISchemaError as exc: + raise InvalidParameterValue(self.name, exc) + + def unmarshal(self, value, custom_formatters=None, resolver=None): + if not self.schema: + return value + + try: + self.schema.validate(value, resolver=resolver) except OpenAPISchemaError as exc: raise InvalidParameterValue(self.name, exc) try: unmarshalled = self.schema.unmarshal( - casted, + value, custom_formatters=custom_formatters, strict=True, ) diff --git a/openapi_core/schema/schemas/_format.py b/openapi_core/schema/schemas/_format.py new file mode 100644 index 0000000..ec65771 --- /dev/null +++ b/openapi_core/schema/schemas/_format.py @@ -0,0 +1,9 @@ +from jsonschema._format import FormatChecker +from six import binary_type + +oas30_format_checker = FormatChecker() + + +@oas30_format_checker.checks('binary') +def binary(value): + return isinstance(value, binary_type) diff --git a/openapi_core/schema/schemas/_types.py b/openapi_core/schema/schemas/_types.py new file mode 100644 index 0000000..12d1d26 --- /dev/null +++ b/openapi_core/schema/schemas/_types.py @@ -0,0 +1,21 @@ +from jsonschema._types import ( + TypeChecker, is_any, is_array, is_bool, is_integer, + is_object, is_number, +) +from six import text_type, binary_type + + +def is_string(checker, instance): + return isinstance(instance, (text_type, binary_type)) + + +oas30_type_checker = TypeChecker( + { + u"string": is_string, + u"number": is_number, + u"integer": is_integer, + u"boolean": is_bool, + u"array": is_array, + u"object": is_object, + }, +) diff --git a/openapi_core/schema/schemas/_validators.py b/openapi_core/schema/schemas/_validators.py new file mode 100644 index 0000000..19df6d3 --- /dev/null +++ b/openapi_core/schema/schemas/_validators.py @@ -0,0 +1,27 @@ +from jsonschema.exceptions import ValidationError + + +def type(validator, data_type, instance, schema): + if instance is None: + return + + if not validator.is_type(instance, data_type): + yield ValidationError("%r is not of type %s" % (instance, data_type)) + + +def items(validator, items, instance, schema): + if not validator.is_type(instance, "array"): + return + + for index, item in enumerate(instance): + for error in validator.descend(item, items, path=index): + yield error + + +def nullable(validator, is_nullable, instance, schema): + if instance is None and not is_nullable: + yield ValidationError("None for not nullable") + + +def not_implemented(validator, value, instance, schema): + pass diff --git a/openapi_core/schema/schemas/factories.py b/openapi_core/schema/schemas/factories.py index 136fbd0..96229d3 100644 --- a/openapi_core/schema/schemas/factories.py +++ b/openapi_core/schema/schemas/factories.py @@ -50,11 +50,11 @@ class SchemaFactory(object): all_of = [] if all_of_spec: - all_of = map(self.create, all_of_spec) + all_of = list(map(self.create, all_of_spec)) one_of = [] if one_of_spec: - one_of = map(self.create, one_of_spec) + one_of = list(map(self.create, one_of_spec)) items = None if items_spec: @@ -76,6 +76,7 @@ class SchemaFactory(object): exclusive_maximum=exclusive_maximum, exclusive_minimum=exclusive_minimum, min_properties=min_properties, max_properties=max_properties, + _source=schema_deref, ) @property diff --git a/openapi_core/schema/schemas/models.py b/openapi_core/schema/schemas/models.py index 17fa44f..aa38e3d 100644 --- a/openapi_core/schema/schemas/models.py +++ b/openapi_core/schema/schemas/models.py @@ -9,8 +9,10 @@ import re import warnings from six import iteritems, integer_types, binary_type, text_type +from jsonschema.exceptions import ValidationError from openapi_core.extensions.models.factories import ModelFactory +from openapi_core.schema.schemas._format import oas30_format_checker from openapi_core.schema.schemas.enums import SchemaFormat, SchemaType from openapi_core.schema.schemas.exceptions import ( InvalidSchemaValue, UndefinedSchemaProperty, MissingSchemaProperty, @@ -23,7 +25,7 @@ from openapi_core.schema.schemas.util import ( format_number, ) from openapi_core.schema.schemas.validators import ( - TypeValidator, AttributeValidator, + TypeValidator, AttributeValidator, OAS30Validator, ) log = logging.getLogger(__name__) @@ -85,7 +87,7 @@ class Schema(object): min_length=None, max_length=None, pattern=None, unique_items=False, minimum=None, maximum=None, multiple_of=None, exclusive_minimum=False, exclusive_maximum=False, - min_properties=None, max_properties=None): + min_properties=None, max_properties=None, _source=None): self.type = SchemaType(schema_type) self.model = model self.properties = properties and dict(properties) or {} @@ -119,6 +121,8 @@ class Schema(object): self._all_required_properties_cache = None self._all_optional_properties_cache = None + self._source = _source + def __getitem__(self, name): return self.properties[name] @@ -214,6 +218,18 @@ class Schema(object): return defaultdict(lambda: lambda x: x, mapping) + def get_validator(self, resolver=None): + return OAS30Validator( + self._source, resolver=resolver, format_checker=oas30_format_checker) + + def validate(self, value, resolver=None): + validator = self.get_validator(resolver=resolver) + try: + return validator.validate(value) + except ValidationError: + # TODO: pass validation errors + raise InvalidSchemaValue("Value not valid for schema", value, self.type) + def unmarshal(self, value, custom_formatters=None, strict=True): """Unmarshal parameter from the value.""" if self.deprecated: @@ -241,10 +257,7 @@ class Schema(object): "Value {value} is not of type {type}", value, self.type) except ValueError: raise InvalidSchemaValue( - "Failed to cast value {value} to type {type}", value, self.type) - - if unmarshalled is None and not self.required: - return None + "Failed to unmarshal value {value} to type {type}", value, self.type) return unmarshalled @@ -297,8 +310,7 @@ class Schema(object): return unmarshal_callable(value) except UnmarshallerStrictTypeError: continue - # @todo: remove ValueError when validation separated - except (OpenAPISchemaError, TypeError, ValueError): + except (OpenAPISchemaError, TypeError): continue raise NoValidSchema(value) @@ -307,9 +319,6 @@ class Schema(object): if not isinstance(value, (list, tuple)): raise InvalidSchemaValue("Value {value} is not of type {type}", value, self.type) - if self.items is None: - raise UndefinedItemsSchema(self.type) - f = functools.partial( self.items.unmarshal, custom_formatters=custom_formatters, strict=strict, diff --git a/openapi_core/schema/schemas/validators.py b/openapi_core/schema/schemas/validators.py index 2d9ec49..7d7b117 100644 --- a/openapi_core/schema/schemas/validators.py +++ b/openapi_core/schema/schemas/validators.py @@ -1,3 +1,10 @@ +from jsonschema import _legacy_validators, _format, _types, _utils, _validators +from jsonschema.validators import create + +from openapi_core.schema.schemas import _types as oas_types +from openapi_core.schema.schemas import _validators as oas_validators + + class TypeValidator(object): def __init__(self, *types, **options): @@ -24,3 +31,50 @@ class AttributeValidator(object): return False return True + + +OAS30Validator = create( + meta_schema=_utils.load_schema("draft4"), + validators={ + u"multipleOf": _validators.multipleOf, + u"maximum": _legacy_validators.maximum_draft3_draft4, + u"exclusiveMaximum": _validators.exclusiveMaximum, + u"minimum": _legacy_validators.minimum_draft3_draft4, + u"exclusiveMinimum": _validators.exclusiveMinimum, + u"maxLength": _validators.maxLength, + u"minLength": _validators.minLength, + u"pattern": _validators.pattern, + u"maxItems": _validators.maxItems, + u"minItems": _validators.minItems, + u"uniqueItems": _validators.uniqueItems, + u"maxProperties": _validators.maxProperties, + u"minProperties": _validators.minProperties, + u"required": _validators.required, + u"enum": _validators.enum, + # adjusted to OAS + u"type": oas_validators.type, + u"allOf": _validators.allOf, + u"oneOf": _validators.oneOf, + u"anyOf": _validators.anyOf, + u"not": _validators.not_, + u"items": oas_validators.items, + u"properties": _validators.properties, + u"additionalProperties": _validators.additionalProperties, + # TODO: adjust description + u"format": _validators.format, + # TODO: adjust default + u"$ref": _validators.ref, + # fixed OAS fields + u"nullable": oas_validators.nullable, + u"discriminator": oas_validators.not_implemented, + u"readOnly": oas_validators.not_implemented, + u"writeOnly": oas_validators.not_implemented, + u"xml": oas_validators.not_implemented, + u"externalDocs": oas_validators.not_implemented, + u"example": oas_validators.not_implemented, + u"deprecated": oas_validators.not_implemented, + }, + type_checker=oas_types.oas30_type_checker, + version="oas30", + id_of=lambda schema: schema.get(u"id", ""), +) diff --git a/openapi_core/schema/specs/factories.py b/openapi_core/schema/specs/factories.py index 16f736d..0d31dd6 100644 --- a/openapi_core/schema/specs/factories.py +++ b/openapi_core/schema/specs/factories.py @@ -2,6 +2,7 @@ """OpenAPI core specs factories module""" from openapi_spec_validator import openapi_v3_spec_validator +from openapi_spec_validator.validators import Dereferencer from openapi_core.compat import lru_cache from openapi_core.schema.components.factories import ComponentsFactory @@ -14,8 +15,8 @@ from openapi_core.schema.specs.models import Spec class SpecFactory(object): - def __init__(self, dereferencer, config=None): - self.dereferencer = dereferencer + def __init__(self, spec_resolver, config=None): + self.spec_resolver = spec_resolver self.config = config or {} def create(self, spec_dict, spec_url=''): @@ -34,9 +35,16 @@ class SpecFactory(object): paths = self.paths_generator.generate(paths) components = self.components_factory.create(components_spec) spec = Spec( - info, list(paths), servers=list(servers), components=components) + info, list(paths), servers=list(servers), components=components, + _resolver=self.spec_resolver, + ) return spec + @property + @lru_cache() + def dereferencer(self): + return Dereferencer(self.spec_resolver) + @property @lru_cache() def schemas_registry(self): diff --git a/openapi_core/schema/specs/models.py b/openapi_core/schema/specs/models.py index 7e7c4e1..f4a115e 100644 --- a/openapi_core/schema/specs/models.py +++ b/openapi_core/schema/specs/models.py @@ -14,12 +14,14 @@ log = logging.getLogger(__name__) class Spec(object): """Represents an OpenAPI Specification for a service.""" - def __init__(self, info, paths, servers=None, components=None): + def __init__(self, info, paths, servers=None, components=None, _resolver=None): self.info = info self.paths = paths and dict(paths) self.servers = servers or [] self.components = components + self._resolver = _resolver + def __getitem__(self, path_pattern): return self.get_path(path_pattern) diff --git a/openapi_core/shortcuts.py b/openapi_core/shortcuts.py index bcf4d31..02df1c1 100644 --- a/openapi_core/shortcuts.py +++ b/openapi_core/shortcuts.py @@ -1,6 +1,5 @@ """OpenAPI core shortcuts module""" from jsonschema.validators import RefResolver -from openapi_spec_validator.validators import Dereferencer from openapi_spec_validator import default_handlers from openapi_core.schema.media_types.exceptions import OpenAPIMediaTypeError @@ -17,8 +16,7 @@ from openapi_core.validation.response.validators import ResponseValidator def create_spec(spec_dict, spec_url=''): spec_resolver = RefResolver( spec_url, spec_dict, handlers=default_handlers) - dereferencer = Dereferencer(spec_resolver) - spec_factory = SpecFactory(dereferencer) + spec_factory = SpecFactory(spec_resolver) return spec_factory.create(spec_dict, spec_url=spec_url) diff --git a/openapi_core/validation/request/validators.py b/openapi_core/validation/request/validators.py index 734b589..ba4851e 100644 --- a/openapi_core/validation/request/validators.py +++ b/openapi_core/validation/request/validators.py @@ -58,7 +58,7 @@ class RequestValidator(object): continue seen.add((param_name, param.location.value)) try: - raw_value = param.get_value(request) + raw_value = param.get_raw_value(request) except MissingParameter: continue except OpenAPIMappingError as exc: @@ -66,11 +66,20 @@ class RequestValidator(object): continue try: - value = param.unmarshal(raw_value, self.custom_formatters) + casted = param.cast(raw_value) + except OpenAPIMappingError as exc: + errors.append(exc) + continue + + try: + unmarshalled = param.unmarshal( + casted, self.custom_formatters, + resolver=self.spec._resolver, + ) except OpenAPIMappingError as exc: errors.append(exc) else: - parameters[param.location.value][param_name] = value + parameters[param.location.value][param_name] = unmarshalled return parameters, errors @@ -92,8 +101,16 @@ class RequestValidator(object): errors.append(exc) else: try: - body = media_type.unmarshal(raw_body, self.custom_formatters) + casted = media_type.cast(raw_body) except OpenAPIMappingError as exc: errors.append(exc) + else: + try: + body = media_type.unmarshal( + casted, self.custom_formatters, + resolver=self.spec._resolver, + ) + except OpenAPIMappingError as exc: + errors.append(exc) return body, errors diff --git a/openapi_core/validation/response/validators.py b/openapi_core/validation/response/validators.py index 4f9696b..2ed923a 100644 --- a/openapi_core/validation/response/validators.py +++ b/openapi_core/validation/response/validators.py @@ -61,9 +61,17 @@ class ResponseValidator(object): errors.append(exc) else: try: - data = media_type.unmarshal(raw_data, self.custom_formatters) + casted = media_type.cast(raw_data) except OpenAPIMappingError as exc: errors.append(exc) + else: + try: + data = media_type.unmarshal( + casted, self.custom_formatters, + resolver=self.spec._resolver, + ) + except OpenAPIMappingError as exc: + errors.append(exc) return data, errors diff --git a/tests/integration/data/v3.0/petstore.yaml b/tests/integration/data/v3.0/petstore.yaml index efd817d..5f42339 100644 --- a/tests/integration/data/v3.0/petstore.yaml +++ b/tests/integration/data/v3.0/petstore.yaml @@ -317,7 +317,10 @@ components: suberror: $ref: "#/components/schemas/ExtendedError" additionalProperties: - type: string + oneOf: + - type: string + - type: integer + format: int32 responses: ErrorResponse: description: unexpected error diff --git a/tests/integration/test_petstore.py b/tests/integration/test_petstore.py index 4c92c91..5e57c5e 100644 --- a/tests/integration/test_petstore.py +++ b/tests/integration/test_petstore.py @@ -19,9 +19,7 @@ from openapi_core.schema.paths.models import Path from openapi_core.schema.request_bodies.models import RequestBody from openapi_core.schema.responses.models import Response from openapi_core.schema.schemas.enums import SchemaType -from openapi_core.schema.schemas.exceptions import ( - InvalidSchemaProperty, InvalidSchemaValue, -) +from openapi_core.schema.schemas.exceptions import InvalidSchemaValue from openapi_core.schema.schemas.models import Schema from openapi_core.schema.servers.exceptions import InvalidServer from openapi_core.schema.servers.models import Server, ServerVariable @@ -41,13 +39,17 @@ class TestPetstore(object): api_key_bytes_enc = b64encode(api_key_bytes) return text_type(api_key_bytes_enc, 'utf8') + @pytest.fixture + def spec_uri(self): + return "file://tests/integration/data/v3.0/petstore.yaml" + @pytest.fixture def spec_dict(self, factory): return factory.spec_from_file("data/v3.0/petstore.yaml") @pytest.fixture - def spec(self, spec_dict): - return create_spec(spec_dict) + def spec(self, spec_dict, spec_uri): + return create_spec(spec_dict, spec_uri) @pytest.fixture def request_validator(self, spec): @@ -267,6 +269,9 @@ class TestPetstore(object): { 'id': 1, 'name': 'Cat', + 'ears': { + 'healthy': True, + }, } ], } @@ -322,16 +327,10 @@ class TestPetstore(object): assert response_result.errors == [ InvalidMediaTypeValue( - original_exception=InvalidSchemaProperty( - property_name='data', - original_exception=InvalidSchemaProperty( - property_name='name', - original_exception=InvalidSchemaValue( - msg="Value {value} is not of type {type}", - type=SchemaType.STRING, - value={'first_name': 'Cat'}, - ), - ), + original_exception=InvalidSchemaValue( + msg='Value not valid for schema', + type=SchemaType.OBJECT, + value=data_json, ), ), ] @@ -932,6 +931,9 @@ class TestPetstore(object): 'data': { 'id': data_id, 'name': data_name, + 'ears': { + 'healthy': True, + }, }, } data = json.dumps(data_json) @@ -1239,7 +1241,6 @@ class TestPetstore(object): assert response_result.data.rootCause == rootCause assert response_result.data.additionalinfo == additionalinfo - @pytest.mark.xfail(reason='OneOf for string not supported atm') def test_post_tags_created_invalid_type( self, spec, response_validator): host_url = 'http://petstore.swagger.io/v1' diff --git a/tests/integration/test_validators.py b/tests/integration/test_validators.py index 2f26a7b..2d3af8f 100644 --- a/tests/integration/test_validators.py +++ b/tests/integration/test_validators.py @@ -421,6 +421,17 @@ class TestResponseValidator(object): assert result.data is None assert result.headers == {} + def test_invalid_media_type(self, validator): + request = MockRequest(self.host_url, 'get', '/v1/pets') + response = MockResponse("abcde") + + result = validator.validate(request, response) + + assert len(result.errors) == 1 + assert type(result.errors[0]) == InvalidMediaTypeValue + assert result.data is None + assert result.headers == {} + def test_invalid_media_type_value(self, validator): request = MockRequest(self.host_url, 'get', '/v1/pets') response = MockResponse("{}") @@ -458,7 +469,10 @@ class TestResponseValidator(object): 'data': [ { 'id': 1, - 'name': 'Sparky' + 'name': 'Sparky', + 'ears': { + 'healthy': True, + }, }, ], } diff --git a/tests/unit/schema/test_media_types.py b/tests/unit/schema/test_media_types.py new file mode 100644 index 0000000..2d266ac --- /dev/null +++ b/tests/unit/schema/test_media_types.py @@ -0,0 +1,53 @@ +import pytest + +from openapi_core.schema.media_types.exceptions import InvalidMediaTypeValue +from openapi_core.schema.media_types.models import MediaType +from openapi_core.schema.schemas.models import Schema + + +class TestMediaTypeCast(object): + + def test_empty(self): + media_type = MediaType('application/json') + value = '' + + result = media_type.cast(value) + + assert result == value + + +class TestParameterUnmarshal(object): + + def test_empty(self): + media_type = MediaType('application/json') + value = '' + + result = media_type.unmarshal(value) + + assert result == value + + def test_schema_type_invalid(self): + schema = Schema('integer', _source={'type': 'integer'}) + media_type = MediaType('application/json', schema=schema) + value = 'test' + + with pytest.raises(InvalidMediaTypeValue): + media_type.unmarshal(value) + + def test_schema_custom_format_invalid(self): + def custom_formatter(value): + raise ValueError + schema = Schema( + 'string', + schema_format='custom', + _source={'type': 'string', 'format': 'custom'}, + ) + custom_formatters = { + 'custom': custom_formatter, + } + media_type = MediaType('application/json', schema=schema) + value = 'test' + + with pytest.raises(InvalidMediaTypeValue): + media_type.unmarshal( + value, custom_formatters=custom_formatters) diff --git a/tests/unit/schema/test_parameters.py b/tests/unit/schema/test_parameters.py index 952e956..2755dcb 100644 --- a/tests/unit/schema/test_parameters.py +++ b/tests/unit/schema/test_parameters.py @@ -1,8 +1,11 @@ import pytest -from openapi_core.schema.parameters.exceptions import EmptyParameterValue +from openapi_core.schema.parameters.exceptions import ( + EmptyParameterValue, InvalidParameterValue, +) from openapi_core.schema.parameters.enums import ParameterStyle from openapi_core.schema.parameters.models import Parameter +from openapi_core.schema.schemas.models import Schema class TestParameterInit(object): @@ -36,17 +39,35 @@ class TestParameterInit(object): assert param.explode is True -class TestParameterUnmarshal(object): +class TestParameterCast(object): def test_deprecated(self): param = Parameter('param', 'query', deprecated=True) value = 'test' with pytest.warns(DeprecationWarning): - result = param.unmarshal(value) + result = param.cast(value) assert result == value + def test_query_empty(self): + param = Parameter('param', 'query') + value = '' + + with pytest.raises(EmptyParameterValue): + param.cast(value) + + def test_query_valid(self): + param = Parameter('param', 'query') + value = 'test' + + result = param.cast(value) + + assert result == value + + +class TestParameterUnmarshal(object): + def test_query_valid(self): param = Parameter('param', 'query') value = 'test' @@ -55,13 +76,6 @@ class TestParameterUnmarshal(object): assert result == value - def test_query_empty(self): - param = Parameter('param', 'query') - value = '' - - with pytest.raises(EmptyParameterValue): - param.unmarshal(value) - def test_query_allow_empty_value(self): param = Parameter('param', 'query', allow_empty_value=True) value = '' @@ -69,3 +83,28 @@ class TestParameterUnmarshal(object): result = param.unmarshal(value) assert result == value + + def test_query_schema_type_invalid(self): + schema = Schema('integer', _source={'type': 'integer'}) + param = Parameter('param', 'query', schema=schema) + value = 'test' + + with pytest.raises(InvalidParameterValue): + param.unmarshal(value) + + def test_query_schema_custom_format_invalid(self): + def custom_formatter(value): + raise ValueError + schema = Schema( + 'string', + schema_format='custom', + _source={'type': 'string', 'format': 'custom'}, + ) + custom_formatters = { + 'custom': custom_formatter, + } + param = Parameter('param', 'query', schema=schema) + value = 'test' + + with pytest.raises(InvalidParameterValue): + param.unmarshal(value, custom_formatters=custom_formatters) diff --git a/tests/unit/schema/test_schemas.py b/tests/unit/schema/test_schemas.py index 0e7a2fe..4436ff8 100644 --- a/tests/unit/schema/test_schemas.py +++ b/tests/unit/schema/test_schemas.py @@ -42,6 +42,17 @@ class TestSchemaUnmarshal(object): assert result == value + @pytest.mark.parametrize('schema_type', [ + 'boolean', 'array', 'integer', 'number', + ]) + def test_non_string_empty_value(self, schema_type): + schema = Schema(schema_type) + value = '' + + result = schema.unmarshal(value) + + assert result is None + def test_string_valid(self): schema = Schema('string') value = 'test' @@ -121,19 +132,28 @@ class TestSchemaUnmarshal(object): assert result == datetime.datetime(2018, 1, 2, 0, 0) - @pytest.mark.xfail(reason="No custom formats support atm") def test_string_format_custom(self): + def custom_formatter(value): + return 'x-custom' custom_format = 'custom' schema = Schema('string', schema_format=custom_format) value = 'x' - with mock.patch.dict( - Schema.STRING_FORMAT_CAST_CALLABLE_GETTER, - {custom_format: lambda x: x + '-custom'}, - ): - result = schema.unmarshal(value) + result = schema.unmarshal( + value, custom_formatters={custom_format: custom_formatter}) - assert result == 'x-custom' + assert result == custom_formatter(value) + + def test_string_format_custom_value_error(self): + def custom_formatter(value): + raise ValueError + custom_format = 'custom' + schema = Schema('string', schema_format=custom_format) + value = 'x' + + with pytest.raises(InvalidSchemaValue): + schema.unmarshal( + value, custom_formatters={custom_format: custom_formatter}) def test_string_format_unknown(self): unknown_format = 'unknown' @@ -143,7 +163,6 @@ class TestSchemaUnmarshal(object): with pytest.raises(OpenAPISchemaError): schema.unmarshal(value) - @pytest.mark.xfail(reason="No custom formats support atm") def test_string_format_invalid_value(self): custom_format = 'custom' schema = Schema('string', schema_format=custom_format) @@ -351,6 +370,14 @@ class TestSchemaObjValidate(object): assert result is None + def test_string_format_custom_missing(self): + custom_format = 'custom' + schema = Schema('string', schema_format=custom_format) + value = 'x' + + with pytest.raises(OpenAPISchemaError): + schema.obj_validate(value) + @pytest.mark.parametrize('value', [False, True]) def test_boolean(self, value): schema = Schema('boolean') From a2fc5284c6593d2db680bfbedfbb60b32c3316f7 Mon Sep 17 00:00:00 2001 From: p1c2u Date: Wed, 11 Sep 2019 22:47:16 +0100 Subject: [PATCH 3/4] Get rid of object validator --- openapi_core/schema/media_types/models.py | 8 +- openapi_core/schema/parameters/models.py | 8 +- openapi_core/schema/schemas/_format.py | 106 ++++++- openapi_core/schema/schemas/_validators.py | 33 +- openapi_core/schema/schemas/factories.py | 59 ++++ openapi_core/schema/schemas/models.py | 242 +------------- openapi_core/schema/schemas/types.py | 11 + openapi_core/schema/schemas/validators.py | 25 +- tests/unit/schema/test_schemas.py | 349 ++++++++++----------- 9 files changed, 403 insertions(+), 438 deletions(-) create mode 100644 openapi_core/schema/schemas/types.py diff --git a/openapi_core/schema/media_types/models.py b/openapi_core/schema/media_types/models.py index dce3737..61b2e3d 100644 --- a/openapi_core/schema/media_types/models.py +++ b/openapi_core/schema/media_types/models.py @@ -51,12 +51,6 @@ class MediaType(object): raise InvalidMediaTypeValue(exc) try: - unmarshalled = self.schema.unmarshal(value, custom_formatters=custom_formatters) - except OpenAPISchemaError as exc: - raise InvalidMediaTypeValue(exc) - - try: - return self.schema.obj_validate( - unmarshalled, custom_formatters=custom_formatters) + return self.schema.unmarshal(value, custom_formatters=custom_formatters) except OpenAPISchemaError as exc: raise InvalidMediaTypeValue(exc) diff --git a/openapi_core/schema/parameters/models.py b/openapi_core/schema/parameters/models.py index 74ef1ae..612b062 100644 --- a/openapi_core/schema/parameters/models.py +++ b/openapi_core/schema/parameters/models.py @@ -123,16 +123,10 @@ class Parameter(object): raise InvalidParameterValue(self.name, exc) try: - unmarshalled = self.schema.unmarshal( + return self.schema.unmarshal( value, custom_formatters=custom_formatters, strict=True, ) except OpenAPISchemaError as exc: raise InvalidParameterValue(self.name, exc) - - try: - return self.schema.obj_validate( - unmarshalled, custom_formatters=custom_formatters) - except OpenAPISchemaError as exc: - raise InvalidParameterValue(self.name, exc) diff --git a/openapi_core/schema/schemas/_format.py b/openapi_core/schema/schemas/_format.py index ec65771..67d832c 100644 --- a/openapi_core/schema/schemas/_format.py +++ b/openapi_core/schema/schemas/_format.py @@ -1,9 +1,105 @@ -from jsonschema._format import FormatChecker -from six import binary_type +from base64 import b64encode, b64decode +import binascii +from datetime import datetime +from uuid import UUID -oas30_format_checker = FormatChecker() +from jsonschema._format import FormatChecker +from jsonschema.exceptions import FormatError +from six import binary_type, text_type, integer_types + + +class StrictFormatChecker(FormatChecker): + + def check(self, instance, format): + if format not in self.checkers: + raise FormatError( + "Format checker for %r format not found" % (format, )) + return super(StrictFormatChecker, self).check( + instance, format) + + +oas30_format_checker = StrictFormatChecker() + + +@oas30_format_checker.checks('int32') +def is_int32(instance): + return isinstance(instance, integer_types) + + +@oas30_format_checker.checks('int64') +def is_int64(instance): + return isinstance(instance, integer_types) + + +@oas30_format_checker.checks('float') +def is_float(instance): + return isinstance(instance, float) + + +@oas30_format_checker.checks('double') +def is_double(instance): + # float has double precision in Python + # It's double in CPython and Jython + return isinstance(instance, float) @oas30_format_checker.checks('binary') -def binary(value): - return isinstance(value, binary_type) +def is_binary(instance): + return isinstance(instance, binary_type) + + +@oas30_format_checker.checks('byte', raises=(binascii.Error, TypeError)) +def is_byte(instance): + if isinstance(instance, text_type): + instance = instance.encode() + + return b64encode(b64decode(instance)) == instance + + +try: + import strict_rfc3339 +except ImportError: + try: + import isodate + except ImportError: + pass + else: + @oas30_format_checker.checks( + "date-time", raises=(ValueError, isodate.ISO8601Error)) + def is_datetime(instance): + if isinstance(instance, binary_type): + return False + if not isinstance(instance, text_type): + return True + return isodate.parse_datetime(instance) +else: + @oas30_format_checker.checks("date-time") + def is_datetime(instance): + if isinstance(instance, binary_type): + return False + if not isinstance(instance, text_type): + return True + return strict_rfc3339.validate_rfc3339(instance) + + +@oas30_format_checker.checks("date", raises=ValueError) +def is_date(instance): + if isinstance(instance, binary_type): + return False + if not isinstance(instance, text_type): + return True + return datetime.strptime(instance, "%Y-%m-%d") + + +@oas30_format_checker.checks("uuid", raises=AttributeError) +def is_uuid(instance): + if isinstance(instance, binary_type): + return False + if not isinstance(instance, text_type): + return True + try: + uuid_obj = UUID(instance) + except ValueError: + return False + + return text_type(uuid_obj) == instance diff --git a/openapi_core/schema/schemas/_validators.py b/openapi_core/schema/schemas/_validators.py index 19df6d3..fc5a4ba 100644 --- a/openapi_core/schema/schemas/_validators.py +++ b/openapi_core/schema/schemas/_validators.py @@ -1,4 +1,5 @@ -from jsonschema.exceptions import ValidationError +from jsonschema._utils import find_additional_properties, extras_msg +from jsonschema.exceptions import ValidationError, FormatError def type(validator, data_type, instance, schema): @@ -9,6 +10,17 @@ def type(validator, data_type, instance, schema): yield ValidationError("%r is not of type %s" % (instance, data_type)) +def format(validator, format, instance, schema): + if instance is None: + return + + if validator.format_checker is not None: + try: + validator.format_checker.check(instance, format) + except FormatError as error: + yield ValidationError(error.message, cause=error.cause) + + def items(validator, items, instance, schema): if not validator.is_type(instance, "array"): return @@ -23,5 +35,24 @@ def nullable(validator, is_nullable, instance, schema): yield ValidationError("None for not nullable") +def additionalProperties(validator, aP, instance, schema): + if not validator.is_type(instance, "object"): + return + + extras = set(find_additional_properties(instance, schema)) + + if not extras: + return + + if validator.is_type(aP, "object"): + for extra in extras: + for error in validator.descend(instance[extra], aP, path=extra): + yield error + elif validator.is_type(aP, "boolean"): + if not aP: + error = "Additional properties are not allowed (%s %s unexpected)" + yield ValidationError(error % extras_msg(extras)) + + def not_implemented(validator, value, instance, schema): pass diff --git a/openapi_core/schema/schemas/factories.py b/openapi_core/schema/schemas/factories.py index 96229d3..b42fcbb 100644 --- a/openapi_core/schema/schemas/factories.py +++ b/openapi_core/schema/schemas/factories.py @@ -1,9 +1,12 @@ """OpenAPI core schemas factories module""" import logging +from six import iteritems + from openapi_core.compat import lru_cache from openapi_core.schema.properties.generators import PropertiesGenerator from openapi_core.schema.schemas.models import Schema +from openapi_core.schema.schemas.types import Contribution log = logging.getLogger(__name__) @@ -86,3 +89,59 @@ class SchemaFactory(object): def _create_items(self, items_spec): return self.create(items_spec) + + +class SchemaDictFactory(object): + + contributions = ( + Contribution('type', src_prop_attr='value'), + Contribution('format'), + Contribution('properties', is_dict=True, dest_default={}), + Contribution('required', dest_default=[]), + Contribution('default'), + Contribution('nullable', dest_default=False), + Contribution('all_of', dest_prop_name='allOf', is_list=True, dest_default=[]), + Contribution('one_of', dest_prop_name='oneOf', is_list=True, dest_default=[]), + Contribution('additional_properties', dest_prop_name='additionalProperties', dest_default=True), + Contribution('min_items', dest_prop_name='minItems'), + Contribution('max_items', dest_prop_name='maxItems'), + Contribution('min_length', dest_prop_name='minLength'), + Contribution('max_length', dest_prop_name='maxLength'), + Contribution('pattern', src_prop_attr='pattern'), + Contribution('unique_items', dest_prop_name='uniqueItems', dest_default=False), + Contribution('minimum'), + Contribution('maximum'), + Contribution('multiple_of', dest_prop_name='multipleOf'), + Contribution('exclusive_minimum', dest_prop_name='exclusiveMinimum', dest_default=False), + Contribution('exclusive_maximum', dest_prop_name='exclusiveMaximum', dest_default=False), + Contribution('min_properties', dest_prop_name='minProperties'), + Contribution('max_properties', dest_prop_name='maxProperties'), + ) + + def create(self, schema): + schema_dict = {} + for contrib in self.contributions: + self._contribute(schema, schema_dict, contrib) + return schema_dict + + def _contribute(self, schema, schema_dict, contrib): + def src_map(x): + return getattr(x, '__dict__') + src_val = getattr(schema, contrib.src_prop_name) + + if src_val and contrib.src_prop_attr: + src_val = getattr(src_val, contrib.src_prop_attr) + + if contrib.is_list: + src_val = list(map(src_map, src_val)) + if contrib.is_dict: + src_val = dict( + (k, src_map(v)) + for k, v in iteritems(src_val) + ) + + if src_val == contrib.dest_default: + return + + dest_prop_name = contrib.dest_prop_name or contrib.src_prop_name + schema_dict[dest_prop_name] = src_val diff --git a/openapi_core/schema/schemas/models.py b/openapi_core/schema/schemas/models.py index aa38e3d..3c8b412 100644 --- a/openapi_core/schema/schemas/models.py +++ b/openapi_core/schema/schemas/models.py @@ -123,6 +123,14 @@ class Schema(object): self._source = _source + @property + def __dict__(self): + return self._source or self.to_dict() + + def to_dict(self): + from openapi_core.schema.schemas.factories import SchemaDictFactory + return SchemaDictFactory().create(self) + def __getitem__(self, name): return self.properties[name] @@ -220,7 +228,7 @@ class Schema(object): def get_validator(self, resolver=None): return OAS30Validator( - self._source, resolver=resolver, format_checker=oas30_format_checker) + self.__dict__, resolver=resolver, format_checker=oas30_format_checker) def validate(self, value, resolver=None): validator = self.get_validator(resolver=resolver) @@ -396,236 +404,4 @@ class Schema(object): except OpenAPISchemaError as exc: raise InvalidSchemaProperty(prop_name, exc) - self._validate_properties(properties, one_of_schema=one_of_schema, - custom_formatters=custom_formatters) - return properties - - def get_validator_mapping(self): - mapping = { - SchemaType.ARRAY: self._validate_collection, - SchemaType.STRING: self._validate_string, - SchemaType.OBJECT: self._validate_object, - SchemaType.INTEGER: self._validate_number, - SchemaType.NUMBER: self._validate_number, - } - - def default(x, **kw): - return x - - return defaultdict(lambda: default, mapping) - - def obj_validate(self, value, custom_formatters=None): - if value is None: - if not self.nullable: - raise InvalidSchemaValue("Null value for non-nullable schema of type {type}", value, self.type) - return - - # type validation - type_validator_callable = self.TYPE_VALIDATOR_CALLABLE_GETTER[ - self.type] - if not type_validator_callable(value): - raise InvalidSchemaValue( - "Value {value} not valid type {type}", value, self.type.value) - - # structure validation - validator_mapping = self.get_validator_mapping() - validator_callable = validator_mapping[self.type] - validator_callable(value, custom_formatters=custom_formatters) - - return value - - def _validate_collection(self, value, custom_formatters=None): - if self.items is None: - raise UndefinedItemsSchema(self.type) - - if self.min_items is not None: - if self.min_items < 0: - raise OpenAPISchemaError( - "Schema for collection invalid:" - " minItems must be non-negative" - ) - if len(value) < self.min_items: - raise InvalidSchemaValue( - "Value must contain at least {type} item(s)," - " {value} found", len(value), self.min_items) - if self.max_items is not None: - if self.max_items < 0: - raise OpenAPISchemaError( - "Schema for collection invalid:" - " maxItems must be non-negative" - ) - if len(value) > self.max_items: - raise InvalidSchemaValue( - "Value must contain at most {value} item(s)," - " {type} found", len(value), self.max_items) - if self.unique_items and len(set(value)) != len(value): - raise OpenAPISchemaError("Value may not contain duplicate items") - - f = functools.partial(self.items.obj_validate, - custom_formatters=custom_formatters) - return list(map(f, value)) - - def _validate_number(self, value, custom_formatters=None): - if self.minimum is not None: - if self.exclusive_minimum and value <= self.minimum: - raise InvalidSchemaValue( - "Value {value} is not less than or equal to {type}", value, self.minimum) - elif value < self.minimum: - raise InvalidSchemaValue( - "Value {value} is not less than {type}", value, self.minimum) - - if self.maximum is not None: - if self.exclusive_maximum and value >= self.maximum: - raise InvalidSchemaValue( - "Value {value} is not greater than or equal to {type}", value, self.maximum) - elif value > self.maximum: - raise InvalidSchemaValue( - "Value {value} is not greater than {type}", value, self.maximum) - - if self.multiple_of is not None and value % self.multiple_of: - raise InvalidSchemaValue( - "Value {value} is not a multiple of {type}", - value, self.multiple_of) - - def _validate_string(self, value, custom_formatters=None): - try: - schema_format = SchemaFormat(self.format) - except ValueError: - msg = "Unsupported {0} format validation".format(self.format) - if custom_formatters is not None: - formatstring = custom_formatters.get(self.format) - if formatstring is None: - raise OpenAPISchemaError(msg) - else: - raise OpenAPISchemaError(msg) - else: - formatstring =\ - self.STRING_FORMAT_CALLABLE_GETTER[schema_format] - - if not formatstring.validate(value): - raise InvalidSchemaValue( - "Value {value} not valid format {type}", value, self.format) - - if self.min_length is not None: - if self.min_length < 0: - raise OpenAPISchemaError( - "Schema for string invalid:" - " minLength must be non-negative" - ) - if len(value) < self.min_length: - raise InvalidSchemaValue( - "Value is shorter ({value}) than the minimum length of {type}", - len(value), self.min_length - ) - if self.max_length is not None: - if self.max_length < 0: - raise OpenAPISchemaError( - "Schema for string invalid:" - " maxLength must be non-negative" - ) - if len(value) > self.max_length: - raise InvalidSchemaValue( - "Value is longer ({value}) than the maximum length of {type}", - len(value), self.max_length - ) - if self.pattern is not None and not self.pattern.search(value): - raise InvalidSchemaValue( - "Value {value} does not match the pattern {type}", - value, self.pattern.pattern - ) - - return True - - def _validate_object(self, value, custom_formatters=None): - properties = value.__dict__ - - if self.one_of: - valid_one_of_schema = None - for one_of_schema in self.one_of: - try: - self._validate_properties( - properties, one_of_schema, - custom_formatters=custom_formatters) - except OpenAPISchemaError: - pass - else: - if valid_one_of_schema is not None: - raise MultipleOneOfSchema(self.type) - valid_one_of_schema = True - - if valid_one_of_schema is None: - raise NoOneOfSchema(self.type) - - else: - self._validate_properties(properties, - custom_formatters=custom_formatters) - - if self.min_properties is not None: - if self.min_properties < 0: - raise OpenAPISchemaError( - "Schema for object invalid:" - " minProperties must be non-negative" - ) - - if len(properties) < self.min_properties: - raise InvalidSchemaValue( - "Value must contain at least {type} properties," - " {value} found", len(properties), self.min_properties - ) - - if self.max_properties is not None: - if self.max_properties < 0: - raise OpenAPISchemaError( - "Schema for object invalid:" - " maxProperties must be non-negative" - ) - if len(properties) > self.max_properties: - raise InvalidSchemaValue( - "Value must contain at most {type} properties," - " {value} found", len(properties), self.max_properties - ) - - return True - - def _validate_properties(self, value, one_of_schema=None, - custom_formatters=None): - all_props = self.get_all_properties() - all_props_names = self.get_all_properties_names() - all_req_props_names = self.get_all_required_properties_names() - - if one_of_schema is not None: - all_props.update(one_of_schema.get_all_properties()) - all_props_names |= one_of_schema.\ - get_all_properties_names() - all_req_props_names |= one_of_schema.\ - get_all_required_properties_names() - - value_props_names = value.keys() - extra_props = set(value_props_names) - set(all_props_names) - extra_props_allowed = self.are_additional_properties_allowed( - one_of_schema) - if extra_props and not extra_props_allowed: - raise UndefinedSchemaProperty(extra_props) - - if self.additional_properties is not True: - for prop_name in extra_props: - prop_value = value[prop_name] - self.additional_properties.obj_validate( - prop_value, custom_formatters=custom_formatters) - - for prop_name, prop in iteritems(all_props): - try: - prop_value = value[prop_name] - except KeyError: - if prop_name in all_req_props_names: - raise MissingSchemaProperty(prop_name) - if not prop.nullable and not prop.default: - continue - prop_value = prop.default - try: - prop.obj_validate(prop_value, custom_formatters=custom_formatters) - except OpenAPISchemaError as exc: - raise InvalidSchemaProperty(prop_name, original_exception=exc) - - return True diff --git a/openapi_core/schema/schemas/types.py b/openapi_core/schema/schemas/types.py new file mode 100644 index 0000000..acfcae4 --- /dev/null +++ b/openapi_core/schema/schemas/types.py @@ -0,0 +1,11 @@ +import attr + + +@attr.s(hash=True) +class Contribution(object): + src_prop_name = attr.ib() + src_prop_attr = attr.ib(default=None) + dest_prop_name = attr.ib(default=None) + is_list = attr.ib(default=False) + is_dict = attr.ib(default=False) + dest_default = attr.ib(default=None) diff --git a/openapi_core/schema/schemas/validators.py b/openapi_core/schema/schemas/validators.py index 7d7b117..7f1ed3a 100644 --- a/openapi_core/schema/schemas/validators.py +++ b/openapi_core/schema/schemas/validators.py @@ -33,14 +33,14 @@ class AttributeValidator(object): return True -OAS30Validator = create( +BaseOAS30Validator = create( meta_schema=_utils.load_schema("draft4"), validators={ u"multipleOf": _validators.multipleOf, + # exclusiveMaximum supported inside maximum_draft3_draft4 u"maximum": _legacy_validators.maximum_draft3_draft4, - u"exclusiveMaximum": _validators.exclusiveMaximum, + # exclusiveMinimum supported inside minimum_draft3_draft4 u"minimum": _legacy_validators.minimum_draft3_draft4, - u"exclusiveMinimum": _validators.exclusiveMinimum, u"maxLength": _validators.maxLength, u"minLength": _validators.minLength, u"pattern": _validators.pattern, @@ -59,9 +59,9 @@ OAS30Validator = create( u"not": _validators.not_, u"items": oas_validators.items, u"properties": _validators.properties, - u"additionalProperties": _validators.additionalProperties, + u"additionalProperties": oas_validators.additionalProperties, # TODO: adjust description - u"format": _validators.format, + u"format": oas_validators.format, # TODO: adjust default u"$ref": _validators.ref, # fixed OAS fields @@ -78,3 +78,18 @@ OAS30Validator = create( version="oas30", id_of=lambda schema: schema.get(u"id", ""), ) + + +class OAS30Validator(BaseOAS30Validator): + + def iter_errors(self, instance, _schema=None): + if _schema is None: + _schema = self.schema + + # append defaults to trigger validator (i.e. nullable) + if 'nullable' not in _schema: + _schema.update({ + 'nullable': False, + }) + + return super(OAS30Validator, self).iter_errors(instance, _schema) diff --git a/tests/unit/schema/test_schemas.py b/tests/unit/schema/test_schemas.py index 4436ff8..8d43524 100644 --- a/tests/unit/schema/test_schemas.py +++ b/tests/unit/schema/test_schemas.py @@ -8,7 +8,6 @@ from openapi_core.extensions.models.models import Model from openapi_core.schema.schemas.enums import SchemaFormat, SchemaType from openapi_core.schema.schemas.exceptions import ( InvalidSchemaValue, MultipleOneOfSchema, NoOneOfSchema, OpenAPISchemaError, - UndefinedSchemaProperty ) from openapi_core.schema.schemas.models import Schema @@ -347,7 +346,7 @@ class TestSchemaUnmarshal(object): assert schema.unmarshal('string') == 'string' -class TestSchemaObjValidate(object): +class TestSchemaValidate(object): @pytest.mark.parametrize('schema_type', [ 'boolean', 'array', 'integer', 'number', 'string', @@ -357,7 +356,7 @@ class TestSchemaObjValidate(object): value = None with pytest.raises(InvalidSchemaValue): - schema.obj_validate(value) + schema.validate(value) @pytest.mark.parametrize('schema_type', [ 'boolean', 'array', 'integer', 'number', 'string', @@ -366,219 +365,221 @@ class TestSchemaObjValidate(object): schema = Schema(schema_type, nullable=True) value = None - result = schema.obj_validate(value) + result = schema.validate(value) assert result is None + @pytest.mark.xfail( + reason="validation does not care about custom formats atm") def test_string_format_custom_missing(self): custom_format = 'custom' schema = Schema('string', schema_format=custom_format) value = 'x' with pytest.raises(OpenAPISchemaError): - schema.obj_validate(value) + schema.validate(value) @pytest.mark.parametrize('value', [False, True]) def test_boolean(self, value): schema = Schema('boolean') - result = schema.obj_validate(value) + result = schema.validate(value) - assert result == value + assert result is None @pytest.mark.parametrize('value', [1, 3.14, u('true'), [True, False]]) def test_boolean_invalid(self, value): schema = Schema('boolean') with pytest.raises(InvalidSchemaValue): - schema.obj_validate(value) + schema.validate(value) - @pytest.mark.parametrize('value', [[1, 2], (3, 4)]) + @pytest.mark.parametrize('value', [(1, 2)]) def test_array_no_schema(self, value): schema = Schema('array') with pytest.raises(OpenAPISchemaError): - schema.obj_validate(value) + schema.validate(value) - @pytest.mark.parametrize('value', [[1, 2], (3, 4)]) + @pytest.mark.parametrize('value', [[1, 2]]) def test_array(self, value): schema = Schema('array', items=Schema('integer')) - result = schema.obj_validate(value) + result = schema.validate(value) - assert result == value + assert result is None - @pytest.mark.parametrize('value', [False, 1, 3.14, u('true')]) + @pytest.mark.parametrize('value', [False, 1, 3.14, u('true'), (3, 4)]) def test_array_invalid(self, value): schema = Schema('array') with pytest.raises(InvalidSchemaValue): - schema.obj_validate(value) + schema.validate(value) @pytest.mark.parametrize('value', [1, 3]) def test_integer(self, value): schema = Schema('integer') - result = schema.obj_validate(value) + result = schema.validate(value) - assert result == value + assert result is None @pytest.mark.parametrize('value', [False, 3.14, u('true'), [1, 2]]) def test_integer_invalid(self, value): schema = Schema('integer') with pytest.raises(InvalidSchemaValue): - schema.obj_validate(value) + schema.validate(value) @pytest.mark.parametrize('value', [0, 1, 2]) def test_integer_minimum_invalid(self, value): schema = Schema('integer', minimum=3) with pytest.raises(InvalidSchemaValue): - schema.obj_validate(value) + schema.validate(value) @pytest.mark.parametrize('value', [4, 5, 6]) def test_integer_minimum(self, value): schema = Schema('integer', minimum=3) - result = schema.obj_validate(value) + result = schema.validate(value) - assert result == value + assert result is None @pytest.mark.parametrize('value', [4, 5, 6]) def test_integer_maximum_invalid(self, value): schema = Schema('integer', maximum=3) with pytest.raises(InvalidSchemaValue): - schema.obj_validate(value) + schema.validate(value) @pytest.mark.parametrize('value', [0, 1, 2]) def test_integer_maximum(self, value): schema = Schema('integer', maximum=3) - result = schema.obj_validate(value) + result = schema.validate(value) - assert result == value + assert result is None @pytest.mark.parametrize('value', [1, 2, 4]) def test_integer_multiple_of_invalid(self, value): schema = Schema('integer', multiple_of=3) with pytest.raises(InvalidSchemaValue): - schema.obj_validate(value) + schema.validate(value) @pytest.mark.parametrize('value', [3, 6, 18]) def test_integer_multiple_of(self, value): schema = Schema('integer', multiple_of=3) - result = schema.obj_validate(value) + result = schema.validate(value) - assert result == value + assert result is None @pytest.mark.parametrize('value', [1, 3.14]) def test_number(self, value): schema = Schema('number') - result = schema.obj_validate(value) + result = schema.validate(value) - assert result == value + assert result is None @pytest.mark.parametrize('value', [False, 'true', [1, 3]]) def test_number_invalid(self, value): schema = Schema('number') with pytest.raises(InvalidSchemaValue): - schema.obj_validate(value) + schema.validate(value) @pytest.mark.parametrize('value', [0, 1, 2]) def test_number_minimum_invalid(self, value): schema = Schema('number', minimum=3) with pytest.raises(InvalidSchemaValue): - schema.obj_validate(value) + schema.validate(value) @pytest.mark.parametrize('value', [3, 4, 5]) def test_number_minimum(self, value): schema = Schema('number', minimum=3) - result = schema.obj_validate(value) + result = schema.validate(value) - assert result == value + assert result is None @pytest.mark.parametrize('value', [1, 2, 3]) def test_number_exclusive_minimum_invalid(self, value): schema = Schema('number', minimum=3, exclusive_minimum=3) with pytest.raises(InvalidSchemaValue): - schema.obj_validate(value) + schema.validate(value) @pytest.mark.parametrize('value', [4, 5, 6]) def test_number_exclusive_minimum(self, value): schema = Schema('number', minimum=3) - result = schema.obj_validate(value) + result = schema.validate(value) - assert result == value + assert result is None @pytest.mark.parametrize('value', [4, 5, 6]) def test_number_maximum_invalid(self, value): schema = Schema('number', maximum=3) with pytest.raises(InvalidSchemaValue): - schema.obj_validate(value) + schema.validate(value) @pytest.mark.parametrize('value', [1, 2, 3]) def test_number_maximum(self, value): schema = Schema('number', maximum=3) - result = schema.obj_validate(value) + result = schema.validate(value) - assert result == value + assert result is None @pytest.mark.parametrize('value', [3, 4, 5]) def test_number_exclusive_maximum_invalid(self, value): schema = Schema('number', maximum=3, exclusive_maximum=True) with pytest.raises(InvalidSchemaValue): - schema.obj_validate(value) + schema.validate(value) @pytest.mark.parametrize('value', [0, 1, 2]) def test_number_exclusive_maximum(self, value): schema = Schema('number', maximum=3, exclusive_maximum=True) - result = schema.obj_validate(value) + result = schema.validate(value) - assert result == value + assert result is None @pytest.mark.parametrize('value', [1, 2, 4]) def test_number_multiple_of_invalid(self, value): schema = Schema('number', multiple_of=3) with pytest.raises(InvalidSchemaValue): - schema.obj_validate(value) + schema.validate(value) @pytest.mark.parametrize('value', [3, 6, 18]) def test_number_multiple_of(self, value): schema = Schema('number', multiple_of=3) - result = schema.obj_validate(value) + result = schema.validate(value) - assert result == value + assert result is None - @pytest.mark.parametrize('value', [u('true'), ]) + @pytest.mark.parametrize('value', [u('true'), b('test')]) def test_string(self, value): schema = Schema('string') - result = schema.obj_validate(value) + result = schema.validate(value) - assert result == value + assert result is None - @pytest.mark.parametrize('value', [b('test'), False, 1, 3.14, [1, 3]]) + @pytest.mark.parametrize('value', [False, 1, 3.14, [1, 3]]) def test_string_invalid(self, value): schema = Schema('string') with pytest.raises(InvalidSchemaValue): - schema.obj_validate(value) + schema.validate(value) @pytest.mark.parametrize('value', [ b('true'), u('test'), False, 1, 3.14, [1, 3], @@ -588,27 +589,27 @@ class TestSchemaObjValidate(object): schema = Schema('string', schema_format='date') with pytest.raises(InvalidSchemaValue): - schema.obj_validate(value) + schema.validate(value) @pytest.mark.parametrize('value', [ - datetime.date(1989, 1, 2), datetime.date(2018, 1, 2), + u('1989-01-02'), u('2018-01-02'), ]) def test_string_format_date(self, value): schema = Schema('string', schema_format='date') - result = schema.obj_validate(value) + result = schema.validate(value) - assert result == value + assert result is None @pytest.mark.parametrize('value', [ - uuid.UUID('{12345678-1234-5678-1234-567812345678}'), + u('12345678-1234-5678-1234-567812345678'), ]) def test_string_format_uuid(self, value): schema = Schema('string', schema_format='uuid') - result = schema.obj_validate(value) + result = schema.validate(value) - assert result == value + assert result is None @pytest.mark.parametrize('value', [ b('true'), u('true'), False, 1, 3.14, [1, 3], @@ -618,38 +619,38 @@ class TestSchemaObjValidate(object): schema = Schema('string', schema_format='uuid') with pytest.raises(InvalidSchemaValue): - schema.obj_validate(value) + schema.validate(value) @pytest.mark.parametrize('value', [ b('true'), u('true'), False, 1, 3.14, [1, 3], - datetime.date(1989, 1, 2), + u('1989-01-02'), ]) def test_string_format_datetime_invalid(self, value): schema = Schema('string', schema_format='date-time') with pytest.raises(InvalidSchemaValue): - schema.obj_validate(value) + schema.validate(value) @pytest.mark.parametrize('value', [ - datetime.datetime(1989, 1, 2, 0, 0, 0), - datetime.datetime(2018, 1, 2, 23, 59, 59), + u('1989-01-02T00:00:00Z'), + u('2018-01-02T23:59:59Z'), ]) def test_string_format_datetime(self, value): schema = Schema('string', schema_format='date-time') - result = schema.obj_validate(value) + result = schema.validate(value) - assert result == value + assert result is None @pytest.mark.parametrize('value', [ - u('true'), False, 1, 3.14, [1, 3], datetime.date(1989, 1, 2), - datetime.datetime(1989, 1, 2, 0, 0, 0), + u('true'), False, 1, 3.14, [1, 3], u('1989-01-02'), + u('1989-01-02T00:00:00Z'), ]) def test_string_format_binary_invalid(self, value): schema = Schema('string', schema_format='binary') with pytest.raises(InvalidSchemaValue): - schema.obj_validate(value) + schema.validate(value) @pytest.mark.parametrize('value', [ b('stream'), b('text'), @@ -657,28 +658,28 @@ class TestSchemaObjValidate(object): def test_string_format_binary(self, value): schema = Schema('string', schema_format='binary') - result = schema.obj_validate(value) + result = schema.validate(value) - assert result == value + assert result is None @pytest.mark.parametrize('value', [ - b('tsssst'), b('dGVzdA=='), + b('dGVzdA=='), u('dGVzdA=='), + ]) + def test_string_format_byte(self, value): + schema = Schema('string', schema_format='byte') + + result = schema.validate(value) + + assert result is None + + @pytest.mark.parametrize('value', [ + u('tsssst'), b('tsssst'), b('tesddddsdsdst'), ]) def test_string_format_byte_invalid(self, value): schema = Schema('string', schema_format='byte') with pytest.raises(OpenAPISchemaError): - schema.obj_validate(value) - - @pytest.mark.parametrize('value', [ - u('tsssst'), u('dGVzdA=='), - ]) - def test_string_format_byte(self, value): - schema = Schema('string', schema_format='byte') - - result = schema.obj_validate(value) - - assert result == value + schema.validate(value) @pytest.mark.parametrize('value', [ u('test'), b('stream'), datetime.date(1989, 1, 2), @@ -689,73 +690,66 @@ class TestSchemaObjValidate(object): schema = Schema('string', schema_format=unknown_format) with pytest.raises(OpenAPISchemaError): - schema.obj_validate(value) - - @pytest.mark.parametrize('value', [u(""), ]) - def test_string_min_length_invalid_schema(self, value): - schema = Schema('string', min_length=-1) - - with pytest.raises(OpenAPISchemaError): - schema.obj_validate(value) + schema.validate(value) @pytest.mark.parametrize('value', [u(""), u("a"), u("ab")]) def test_string_min_length_invalid(self, value): schema = Schema('string', min_length=3) with pytest.raises(InvalidSchemaValue): - schema.obj_validate(value) + schema.validate(value) @pytest.mark.parametrize('value', [u("abc"), u("abcd")]) def test_string_min_length(self, value): schema = Schema('string', min_length=3) - result = schema.obj_validate(value) + result = schema.validate(value) - assert result == value + assert result is None @pytest.mark.parametrize('value', [u(""), ]) def test_string_max_length_invalid_schema(self, value): schema = Schema('string', max_length=-1) with pytest.raises(OpenAPISchemaError): - schema.obj_validate(value) + schema.validate(value) @pytest.mark.parametrize('value', [u("ab"), u("abc")]) def test_string_max_length_invalid(self, value): schema = Schema('string', max_length=1) with pytest.raises(InvalidSchemaValue): - schema.obj_validate(value) + schema.validate(value) @pytest.mark.parametrize('value', [u(""), u("a")]) def test_string_max_length(self, value): schema = Schema('string', max_length=1) - result = schema.obj_validate(value) + result = schema.validate(value) - assert result == value + assert result is None @pytest.mark.parametrize('value', [u("foo"), u("bar")]) def test_string_pattern_invalid(self, value): schema = Schema('string', pattern='baz') with pytest.raises(InvalidSchemaValue): - schema.obj_validate(value) + schema.validate(value) @pytest.mark.parametrize('value', [u("bar"), u("foobar")]) def test_string_pattern(self, value): schema = Schema('string', pattern='bar') - result = schema.obj_validate(value) + result = schema.validate(value) - assert result == value + assert result is None @pytest.mark.parametrize('value', ['true', False, 1, 3.14, [1, 3]]) def test_object_not_an_object(self, value): schema = Schema('object') with pytest.raises(InvalidSchemaValue): - schema.obj_validate(value) + schema.validate(value) @pytest.mark.parametrize('value', [Model(), ]) def test_object_multiple_one_of(self, value): @@ -764,20 +758,20 @@ class TestSchemaObjValidate(object): ] schema = Schema('object', one_of=one_of) - with pytest.raises(MultipleOneOfSchema): - schema.obj_validate(value) + with pytest.raises(InvalidSchemaValue): + schema.validate(value) - @pytest.mark.parametrize('value', [Model(), ]) + @pytest.mark.parametrize('value', [{}, ]) def test_object_defferent_type_one_of(self, value): one_of = [ Schema('integer'), Schema('string'), ] schema = Schema('object', one_of=one_of) - with pytest.raises(MultipleOneOfSchema): - schema.obj_validate(value) + with pytest.raises(InvalidSchemaValue): + schema.validate(value) - @pytest.mark.parametrize('value', [Model(), ]) + @pytest.mark.parametrize('value', [{}, ]) def test_object_no_one_of(self, value): one_of = [ Schema( @@ -793,17 +787,17 @@ class TestSchemaObjValidate(object): ] schema = Schema('object', one_of=one_of) - with pytest.raises(NoOneOfSchema): - schema.obj_validate(value) + with pytest.raises(InvalidSchemaValue): + schema.validate(value) @pytest.mark.parametrize('value', [ - Model({ + { 'foo': u("FOO"), - }), - Model({ + }, + { 'foo': u("FOO"), 'bar': u("BAR"), - }), + }, ]) def test_unambiguous_one_of(self, value): one_of = [ @@ -827,27 +821,30 @@ class TestSchemaObjValidate(object): ] schema = Schema('object', one_of=one_of) - schema.obj_validate(value) + result = schema.validate(value) - @pytest.mark.parametrize('value', [Model(), ]) + assert result is None + + @pytest.mark.parametrize('value', [{}, ]) def test_object_default_property(self, value): schema = Schema('object', default='value1') - result = schema.obj_validate(value) + result = schema.validate(value) - assert result == value + assert result is None @pytest.mark.parametrize('value', [Model(), ]) def test_object_min_properties_invalid_schema(self, value): schema = Schema('object', min_properties=-1) with pytest.raises(OpenAPISchemaError): - schema.obj_validate(value) + schema.validate(value) @pytest.mark.parametrize('value', [ - Model({'a': 1}), - Model({'a': 1, 'b': 2}), - Model({'a': 1, 'b': 2, 'c': 3})]) + {'a': 1}, + {'a': 1, 'b': 2}, + {'a': 1, 'b': 2, 'c': 3}, + ]) def test_object_min_properties_invalid(self, value): schema = Schema( 'object', @@ -857,12 +854,13 @@ class TestSchemaObjValidate(object): ) with pytest.raises(InvalidSchemaValue): - schema.obj_validate(value) + schema.validate(value) @pytest.mark.parametrize('value', [ - Model({'a': 1}), - Model({'a': 1, 'b': 2}), - Model({'a': 1, 'b': 2, 'c': 3})]) + {'a': 1}, + {'a': 1, 'b': 2}, + {'a': 1, 'b': 2, 'c': 3}, + ]) def test_object_min_properties(self, value): schema = Schema( 'object', @@ -871,21 +869,22 @@ class TestSchemaObjValidate(object): min_properties=1, ) - result = schema.obj_validate(value) + result = schema.validate(value) - assert result == value + assert result is None @pytest.mark.parametrize('value', [Model(), ]) def test_object_max_properties_invalid_schema(self, value): schema = Schema('object', max_properties=-1) with pytest.raises(OpenAPISchemaError): - schema.obj_validate(value) + schema.validate(value) @pytest.mark.parametrize('value', [ - Model({'a': 1}), - Model({'a': 1, 'b': 2}), - Model({'a': 1, 'b': 2, 'c': 3})]) + {'a': 1}, + {'a': 1, 'b': 2}, + {'a': 1, 'b': 2, 'c': 3}, + ]) def test_object_max_properties_invalid(self, value): schema = Schema( 'object', @@ -895,12 +894,13 @@ class TestSchemaObjValidate(object): ) with pytest.raises(InvalidSchemaValue): - schema.obj_validate(value) + schema.validate(value) @pytest.mark.parametrize('value', [ - Model({'a': 1}), - Model({'a': 1, 'b': 2}), - Model({'a': 1, 'b': 2, 'c': 3})]) + {'a': 1}, + {'a': 1, 'b': 2}, + {'a': 1, 'b': 2, 'c': 3}, + ]) def test_object_max_properties(self, value): schema = Schema( 'object', @@ -909,40 +909,33 @@ class TestSchemaObjValidate(object): max_properties=3, ) - result = schema.obj_validate(value) + result = schema.validate(value) - assert result == value + assert result is None - @pytest.mark.parametrize('value', [Model({'additional': 1}), ]) + @pytest.mark.parametrize('value', [{'additional': 1}, ]) def test_object_additional_propetries(self, value): schema = Schema('object') - schema.obj_validate(value) + result = schema.validate(value) - @pytest.mark.parametrize('value', [Model({'additional': 1}), ]) + assert result is None + + @pytest.mark.parametrize('value', [{'additional': 1}, ]) def test_object_additional_propetries_false(self, value): schema = Schema('object', additional_properties=False) - with pytest.raises(UndefinedSchemaProperty): - schema.obj_validate(value) + with pytest.raises(InvalidSchemaValue): + schema.validate(value) - @pytest.mark.parametrize('value', [Model({'additional': 1}), ]) + @pytest.mark.parametrize('value', [{'additional': 1}, ]) def test_object_additional_propetries_object(self, value): additional_properties = Schema('integer') schema = Schema('object', additional_properties=additional_properties) - schema.obj_validate(value) + result = schema.validate(value) - @pytest.mark.parametrize('value', [[], ]) - def test_list_min_items_invalid_schema(self, value): - schema = Schema( - 'array', - items=Schema('number'), - min_items=-1, - ) - - with pytest.raises(OpenAPISchemaError): - schema.obj_validate(value) + assert result is None @pytest.mark.parametrize('value', [[], [1], [1, 2]]) def test_list_min_items_invalid(self, value): @@ -953,7 +946,7 @@ class TestSchemaObjValidate(object): ) with pytest.raises(Exception): - schema.obj_validate(value) + schema.validate(value) @pytest.mark.parametrize('value', [[], [1], [1, 2]]) def test_list_min_items(self, value): @@ -963,9 +956,9 @@ class TestSchemaObjValidate(object): min_items=0, ) - result = schema.obj_validate(value) + result = schema.validate(value) - assert result == value + assert result is None @pytest.mark.parametrize('value', [[], ]) def test_list_max_items_invalid_schema(self, value): @@ -976,7 +969,7 @@ class TestSchemaObjValidate(object): ) with pytest.raises(OpenAPISchemaError): - schema.obj_validate(value) + schema.validate(value) @pytest.mark.parametrize('value', [[1, 2], [2, 3, 4]]) def test_list_max_items_invalid(self, value): @@ -987,7 +980,7 @@ class TestSchemaObjValidate(object): ) with pytest.raises(Exception): - schema.obj_validate(value) + schema.validate(value) @pytest.mark.parametrize('value', [[1, 2, 1], [2, 2]]) def test_list_unique_items_invalid(self, value): @@ -998,19 +991,19 @@ class TestSchemaObjValidate(object): ) with pytest.raises(Exception): - schema.obj_validate(value) + schema.validate(value) @pytest.mark.parametrize('value', [ - Model({ + { 'someint': 123, - }), - Model({ + }, + { 'somestr': u('content'), - }), - Model({ + }, + { 'somestr': u('content'), 'someint': 123, - }), + }, ]) def test_object_with_properties(self, value): schema = Schema( @@ -1021,34 +1014,30 @@ class TestSchemaObjValidate(object): }, ) - result = schema.obj_validate(value) + result = schema.validate(value) - assert result == value + assert result is None @pytest.mark.parametrize('value', [ - Model({ - 'somestr': Model(), - 'someint': 123, - }), - Model({ + { 'somestr': {}, 'someint': 123, - }), - Model({ + }, + { 'somestr': [ 'content1', 'content2' ], 'someint': 123, - }), - Model({ + }, + { 'somestr': 123, 'someint': 123, - }), - Model({ + }, + { 'somestr': 'content', 'someint': 123, 'not_in_scheme_prop': 123, - }), + }, ]) def test_object_with_invalid_properties(self, value): schema = Schema( @@ -1061,4 +1050,4 @@ class TestSchemaObjValidate(object): ) with pytest.raises(Exception): - schema.obj_validate(value) + schema.validate(value) From fc60083e78a7bf7ac03033c5fbda4b5571d18e7a Mon Sep 17 00:00:00 2001 From: p1c2u Date: Thu, 12 Sep 2019 22:39:53 +0100 Subject: [PATCH 4/4] Get rid of schema validation in unmarshal --- openapi_core/schema/schemas/_format.py | 56 ++++++++++-------- openapi_core/schema/schemas/exceptions.py | 16 ----- openapi_core/schema/schemas/models.py | 61 +++++--------------- openapi_core/schema/schemas/unmarshallers.py | 2 +- openapi_core/schema/schemas/validators.py | 28 --------- requirements.txt | 1 + tests/unit/schema/test_schemas.py | 52 ++++++++++------- 7 files changed, 80 insertions(+), 136 deletions(-) diff --git a/openapi_core/schema/schemas/_format.py b/openapi_core/schema/schemas/_format.py index 67d832c..e2af856 100644 --- a/openapi_core/schema/schemas/_format.py +++ b/openapi_core/schema/schemas/_format.py @@ -7,6 +7,26 @@ from jsonschema._format import FormatChecker from jsonschema.exceptions import FormatError from six import binary_type, text_type, integer_types +DATETIME_HAS_STRICT_RFC3339 = False +DATETIME_HAS_ISODATE = False +DATETIME_RAISES = () + +try: + import isodate +except ImportError: + pass +else: + DATETIME_HAS_ISODATE = True + DATETIME_RAISES += (ValueError, isodate.ISO8601Error) + +try: + import strict_rfc3339 +except ImportError: + pass +else: + DATETIME_HAS_STRICT_RFC3339 = True + DATETIME_RAISES += (ValueError, TypeError) + class StrictFormatChecker(FormatChecker): @@ -56,30 +76,20 @@ def is_byte(instance): return b64encode(b64decode(instance)) == instance -try: - import strict_rfc3339 -except ImportError: - try: - import isodate - except ImportError: - pass - else: - @oas30_format_checker.checks( - "date-time", raises=(ValueError, isodate.ISO8601Error)) - def is_datetime(instance): - if isinstance(instance, binary_type): - return False - if not isinstance(instance, text_type): - return True - return isodate.parse_datetime(instance) -else: - @oas30_format_checker.checks("date-time") - def is_datetime(instance): - if isinstance(instance, binary_type): - return False - if not isinstance(instance, text_type): - return True +@oas30_format_checker.checks("date-time", raises=DATETIME_RAISES) +def is_datetime(instance): + if isinstance(instance, binary_type): + return False + if not isinstance(instance, text_type): + return True + + if DATETIME_HAS_STRICT_RFC3339: return strict_rfc3339.validate_rfc3339(instance) + + if DATETIME_HAS_ISODATE: + return isodate.parse_datetime(instance) + + return True @oas30_format_checker.checks("date", raises=ValueError) diff --git a/openapi_core/schema/schemas/exceptions.py b/openapi_core/schema/schemas/exceptions.py index c984578..2adcde5 100644 --- a/openapi_core/schema/schemas/exceptions.py +++ b/openapi_core/schema/schemas/exceptions.py @@ -66,22 +66,6 @@ class MissingSchemaProperty(OpenAPISchemaError): return "Missing schema property: {0}".format(self.property_name) -@attr.s(hash=True) -class NoOneOfSchema(OpenAPISchemaError): - type = attr.ib() - - def __str__(self): - return "Exactly one valid schema type {0} should be valid, None found.".format(self.type) - - -@attr.s(hash=True) -class MultipleOneOfSchema(OpenAPISchemaError): - type = attr.ib() - - def __str__(self): - return "Exactly one schema type {0} should be valid, more than one found".format(self.type) - - class UnmarshallerError(OpenAPIMappingError): pass diff --git a/openapi_core/schema/schemas/models.py b/openapi_core/schema/schemas/models.py index 3c8b412..44ca3e6 100644 --- a/openapi_core/schema/schemas/models.py +++ b/openapi_core/schema/schemas/models.py @@ -16,7 +16,7 @@ from openapi_core.schema.schemas._format import oas30_format_checker from openapi_core.schema.schemas.enums import SchemaFormat, SchemaType from openapi_core.schema.schemas.exceptions import ( InvalidSchemaValue, UndefinedSchemaProperty, MissingSchemaProperty, - OpenAPISchemaError, NoOneOfSchema, MultipleOneOfSchema, NoValidSchema, + OpenAPISchemaError, NoValidSchema, UndefinedItemsSchema, InvalidCustomFormatSchemaValue, InvalidSchemaProperty, UnmarshallerStrictTypeError, ) @@ -24,9 +24,7 @@ from openapi_core.schema.schemas.util import ( forcebool, format_date, format_datetime, format_byte, format_uuid, format_number, ) -from openapi_core.schema.schemas.validators import ( - TypeValidator, AttributeValidator, OAS30Validator, -) +from openapi_core.schema.schemas.validators import OAS30Validator log = logging.getLogger(__name__) @@ -49,36 +47,6 @@ class Schema(object): DEFAULT_UNMARSHAL_CALLABLE_GETTER = { } - STRING_FORMAT_CALLABLE_GETTER = { - SchemaFormat.NONE: Format(text_type, TypeValidator(text_type)), - SchemaFormat.PASSWORD: Format(text_type, TypeValidator(text_type)), - SchemaFormat.DATE: Format( - format_date, TypeValidator(date, exclude=datetime)), - SchemaFormat.DATETIME: Format(format_datetime, TypeValidator(datetime)), - SchemaFormat.BINARY: Format(binary_type, TypeValidator(binary_type)), - SchemaFormat.UUID: Format(format_uuid, TypeValidator(UUID)), - SchemaFormat.BYTE: Format(format_byte, TypeValidator(text_type)), - } - - NUMBER_FORMAT_CALLABLE_GETTER = { - SchemaFormat.NONE: Format(format_number, TypeValidator( - integer_types + (float, ), exclude=bool)), - SchemaFormat.FLOAT: Format(float, TypeValidator(float)), - SchemaFormat.DOUBLE: Format(float, TypeValidator(float)), - } - - TYPE_VALIDATOR_CALLABLE_GETTER = { - SchemaType.ANY: lambda x: True, - SchemaType.BOOLEAN: TypeValidator(bool), - SchemaType.INTEGER: TypeValidator(integer_types, exclude=bool), - SchemaType.NUMBER: TypeValidator( - integer_types + (float, ), exclude=bool), - SchemaType.STRING: TypeValidator( - text_type, date, datetime, binary_type, UUID), - SchemaType.ARRAY: TypeValidator(list, tuple), - SchemaType.OBJECT: AttributeValidator('__dict__'), - } - def __init__( self, schema_type=None, model=None, properties=None, items=None, schema_format=None, required=None, default=None, nullable=False, @@ -304,11 +272,12 @@ class Schema(object): continue else: if result is not None: - raise MultipleOneOfSchema(self.type) + log.warning("multiple valid oneOf schemas found") + continue result = unmarshalled if result is None: - raise NoOneOfSchema(self.type) + log.warning("valid oneOf schema not found") return result else: @@ -321,7 +290,8 @@ class Schema(object): except (OpenAPISchemaError, TypeError): continue - raise NoValidSchema(value) + log.warning("failed to unmarshal any type") + return value def _unmarshal_collection(self, value, custom_formatters=None, strict=True): if not isinstance(value, (list, tuple)): @@ -344,17 +314,18 @@ class Schema(object): properties = None for one_of_schema in self.one_of: try: - found_props = self._unmarshal_properties( + unmarshalled = self._unmarshal_properties( value, one_of_schema, custom_formatters=custom_formatters) except OpenAPISchemaError: pass else: if properties is not None: - raise MultipleOneOfSchema(self.type) - properties = found_props + log.warning("multiple valid oneOf schemas found") + continue + properties = unmarshalled if properties is None: - raise NoOneOfSchema(self.type) + log.warning("valid oneOf schema not found") else: properties = self._unmarshal_properties( @@ -398,10 +369,8 @@ class Schema(object): if not prop.nullable and not prop.default: continue prop_value = prop.default - try: - properties[prop_name] = prop.unmarshal( - prop_value, custom_formatters=custom_formatters) - except OpenAPISchemaError as exc: - raise InvalidSchemaProperty(prop_name, exc) + + properties[prop_name] = prop.unmarshal( + prop_value, custom_formatters=custom_formatters) return properties diff --git a/openapi_core/schema/schemas/unmarshallers.py b/openapi_core/schema/schemas/unmarshallers.py index 86fd46b..31ba5b6 100644 --- a/openapi_core/schema/schemas/unmarshallers.py +++ b/openapi_core/schema/schemas/unmarshallers.py @@ -3,7 +3,7 @@ from six import text_type, binary_type, integer_types from openapi_core.schema.schemas.enums import SchemaFormat, SchemaType from openapi_core.schema.schemas.exceptions import ( InvalidSchemaValue, InvalidCustomFormatSchemaValue, - OpenAPISchemaError, MultipleOneOfSchema, NoOneOfSchema, + OpenAPISchemaError, InvalidSchemaProperty, UnmarshallerStrictTypeError, ) diff --git a/openapi_core/schema/schemas/validators.py b/openapi_core/schema/schemas/validators.py index 7f1ed3a..52cf1dc 100644 --- a/openapi_core/schema/schemas/validators.py +++ b/openapi_core/schema/schemas/validators.py @@ -5,34 +5,6 @@ from openapi_core.schema.schemas import _types as oas_types from openapi_core.schema.schemas import _validators as oas_validators -class TypeValidator(object): - - def __init__(self, *types, **options): - self.types = types - self.exclude = options.get('exclude') - - def __call__(self, value): - if self.exclude is not None and isinstance(value, self.exclude): - return False - - if not isinstance(value, self.types): - return False - - return True - - -class AttributeValidator(object): - - def __init__(self, attribute): - self.attribute = attribute - - def __call__(self, value): - if not hasattr(value, self.attribute): - return False - - return True - - BaseOAS30Validator = create( meta_schema=_utils.load_schema("draft4"), validators={ diff --git a/requirements.txt b/requirements.txt index 1141546..de7efe3 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,4 +2,5 @@ openapi-spec-validator six lazy-object-proxy strict_rfc3339 +isodate attrs diff --git a/tests/unit/schema/test_schemas.py b/tests/unit/schema/test_schemas.py index 8d43524..f8d9a88 100644 --- a/tests/unit/schema/test_schemas.py +++ b/tests/unit/schema/test_schemas.py @@ -7,7 +7,7 @@ import pytest from openapi_core.extensions.models.models import Model from openapi_core.schema.schemas.enums import SchemaFormat, SchemaType from openapi_core.schema.schemas.exceptions import ( - InvalidSchemaValue, MultipleOneOfSchema, NoOneOfSchema, OpenAPISchemaError, + InvalidSchemaValue, OpenAPISchemaError, ) from openapi_core.schema.schemas.models import Schema @@ -167,10 +167,7 @@ class TestSchemaUnmarshal(object): schema = Schema('string', schema_format=custom_format) value = 'x' - with mock.patch.dict( - Schema.STRING_FORMAT_CALLABLE_GETTER, - {custom_format: mock.Mock(side_effect=ValueError())}, - ), pytest.raises( + with pytest.raises( InvalidSchemaValue, message='Failed to format value' ): schema.unmarshal(value) @@ -325,22 +322,6 @@ class TestSchemaUnmarshal(object): ]) assert schema.unmarshal(['hello']) == ['hello'] - def test_schema_any_one_of_mutiple(self): - schema = Schema(one_of=[ - Schema('array', items=Schema('string')), - Schema('array', items=Schema('number')), - ]) - with pytest.raises(MultipleOneOfSchema): - schema.unmarshal([]) - - def test_schema_any_one_of_no_valid(self): - schema = Schema(one_of=[ - Schema('array', items=Schema('string')), - Schema('array', items=Schema('number')), - ]) - with pytest.raises(NoOneOfSchema): - schema.unmarshal({}) - def test_schema_any(self): schema = Schema() assert schema.unmarshal('string') == 'string' @@ -635,7 +616,34 @@ class TestSchemaValidate(object): u('1989-01-02T00:00:00Z'), u('2018-01-02T23:59:59Z'), ]) - def test_string_format_datetime(self, value): + @mock.patch( + 'openapi_core.schema.schemas._format.' + 'DATETIME_HAS_STRICT_RFC3339', True + ) + @mock.patch( + 'openapi_core.schema.schemas._format.' + 'DATETIME_HAS_ISODATE', False + ) + def test_string_format_datetime_strict_rfc3339(self, value): + schema = Schema('string', schema_format='date-time') + + result = schema.validate(value) + + assert result is None + + @pytest.mark.parametrize('value', [ + u('1989-01-02T00:00:00Z'), + u('2018-01-02T23:59:59Z'), + ]) + @mock.patch( + 'openapi_core.schema.schemas._format.' + 'DATETIME_HAS_STRICT_RFC3339', False + ) + @mock.patch( + 'openapi_core.schema.schemas._format.' + 'DATETIME_HAS_ISODATE', True + ) + def test_string_format_datetime_isodate(self, value): schema = Schema('string', schema_format='date-time') result = schema.validate(value)