diff --git a/.bumpversion.cfg b/.bumpversion.cfg index 626eac4..e700c56 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 0.2.1 +current_version = 0.2.3 tag = True tag_name = {new_version} commit = True diff --git a/openapi_schema_validator/__init__.py b/openapi_schema_validator/__init__.py index c1085a6..bfb16ba 100644 --- a/openapi_schema_validator/__init__.py +++ b/openapi_schema_validator/__init__.py @@ -5,7 +5,7 @@ __author__ = 'Artur Maciag' __email__ = 'maciag.artur@gmail.com' -__version__ = '0.2.1' +__version__ = '0.2.3' __url__ = 'https://github.com/p1c2u/openapi-schema-validator' __license__ = '3-clause BSD License' diff --git a/openapi_schema_validator/_validators.py b/openapi_schema_validator/_validators.py index 2f25660..ec5ab4a 100644 --- a/openapi_schema_validator/_validators.py +++ b/openapi_schema_validator/_validators.py @@ -1,7 +1,73 @@ from jsonschema._utils import find_additional_properties, extras_msg +from jsonschema._validators import oneOf as _oneOf, anyOf as _anyOf, allOf as _allOf + from jsonschema.exceptions import ValidationError, FormatError +def handle_discriminator(validator, _, instance, schema): + """ + Handle presence of discriminator in anyOf, oneOf and allOf. + The behaviour is the same in all 3 cases because at most 1 schema will match. + """ + discriminator = schema['discriminator'] + prop_name = discriminator['propertyName'] + prop_value = instance.get(prop_name) + if not prop_value: + # instance is missing $propertyName + yield ValidationError( + "%r does not contain discriminating property %r" % (instance, prop_name), + context=[], + ) + return + + # Use explicit mapping if available, otherwise try implicit value + ref = discriminator.get('mapping', {}).get(prop_value) or f'#/components/schemas/{prop_value}' + + if not isinstance(ref, str): + # this is a schema error + yield ValidationError( + "%r mapped value for %r should be a string, was %r" % ( + instance, prop_value, ref), + context=[], + ) + return + + try: + validator.resolver.resolve(ref) + except: + yield ValidationError( + "%r reference %r could not be resolved" % ( + instance, ref), + context=[], + ) + return + + yield from validator.descend(instance, { + "$ref": ref + }) + + +def anyOf(validator, anyOf, instance, schema): + if 'discriminator' not in schema: + yield from _anyOf(validator, anyOf, instance, schema) + else: + yield from handle_discriminator(validator, anyOf, instance, schema) + + +def oneOf(validator, oneOf, instance, schema): + if 'discriminator' not in schema: + yield from _oneOf(validator, oneOf, instance, schema) + else: + yield from handle_discriminator(validator, oneOf, instance, schema) + + +def allOf(validator, allOf, instance, schema): + if 'discriminator' not in schema: + yield from _allOf(validator, allOf, instance, schema) + else: + yield from handle_discriminator(validator, allOf, instance, schema) + + def type(validator, data_type, instance, schema): if instance is None: return diff --git a/openapi_schema_validator/validators.py b/openapi_schema_validator/validators.py index 8521c64..4e69c58 100644 --- a/openapi_schema_validator/validators.py +++ b/openapi_schema_validator/validators.py @@ -26,9 +26,9 @@ 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"allOf": oas_validators.allOf, + u"oneOf": oas_validators.oneOf, + u"anyOf": oas_validators.anyOf, u"not": _validators.not_, u"items": oas_validators.items, u"properties": _validators.properties, @@ -63,6 +63,13 @@ def __init__(self, *args, **kwargs): self.write = kwargs.pop('write', None) super(OAS30Validator, self).__init__(*args, **kwargs) + def evolve(self, **kwargs): + # jsonschema4 interface compatibility workaround + validator = super(OAS30Validator, self).evolve(**kwargs) + validator.read = self.read + validator.write = self.write + return validator + def iter_errors(self, instance, _schema=None): if _schema is None: # creates a copy by value from schema to prevent mutation diff --git a/pyproject.toml b/pyproject.toml index 2c3e086..7d8c078 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -11,7 +11,7 @@ output = "reports/coverage.xml" [tool.poetry] name = "openapi-schema-validator" -version = "0.2.1" +version = "0.2.3" description = "OpenAPI schema validation for Python" authors = ["Artur Maciag "] license = "BSD-3-Clause" diff --git a/tests/integration/test_validators.py b/tests/integration/test_validators.py index 9b5e458..5f862c5 100644 --- a/tests/integration/test_validators.py +++ b/tests/integration/test_validators.py @@ -196,6 +196,9 @@ def test_required_read_only(self): validator.validate({"another_prop": "hello"}) validator = OAS30Validator(schema, format_checker=oas30_format_checker, write=True) + with pytest.raises(ValidationError, + match="Tried to write read-only property with hello"): + validator.validate({"some_prop": "hello"}) assert validator.validate({"another_prop": "hello"}) is None def test_required_write_only(self): @@ -217,6 +220,9 @@ def test_required_write_only(self): validator.validate({"another_prop": "hello"}) validator = OAS30Validator(schema, format_checker=oas30_format_checker, read=True) + with pytest.raises(ValidationError, + match="Tried to read write-only property with hello"): + validator.validate({"some_prop": "hello"}) assert validator.validate({"another_prop": "hello"}) is None def test_oneof_required(self): @@ -237,3 +243,116 @@ def test_oneof_required(self): validator = OAS30Validator(schema, format_checker=oas30_format_checker) result = validator.validate(instance) assert result is None + + @pytest.mark.parametrize('schema_type', [ + 'oneOf', 'anyOf', 'allOf', + ]) + def test_oneof_discriminator(self, schema_type): + # We define a few components schemas + components = { + "MountainHiking": { + "type": "object", + "properties": { + "discipline": { + "type": "string", + # we allow both the explicitely matched mountain_hiking discipline + # and the implicitely matched MoutainHiking discipline + "enum": ["mountain_hiking", "MountainHiking"] + }, + "length": { + "type": "integer", + } + }, + "required": ["discipline", "length"] + }, + "AlpineClimbing": { + "type": "object", + "properties": { + "discipline": { + "type": "string", + "enum": ["alpine_climbing"] + }, + "height": { + "type": "integer", + }, + }, + "required": ["discipline", "height"] + }, + "Route": { + # defined later + } + } + components['Route'][schema_type] = [ + {"$ref": "#/components/schemas/MountainHiking"}, + {"$ref": "#/components/schemas/AlpineClimbing"}, + ] + + # Add the compoments in a minimalis schema + schema = { + "$ref": "#/components/schemas/Route", + "components": { + "schemas": components + } + } + + if schema_type != 'allOf': + # use jsonschema validator when no discriminator is defined + validator = OAS30Validator(schema, format_checker=oas30_format_checker) + with pytest.raises(ValidationError, match="is not valid under any of the given schemas"): + validator.validate({ + "something": "matching_none_of_the_schemas" + }) + assert False + + if schema_type == 'anyOf': + # use jsonschema validator when no discriminator is defined + validator = OAS30Validator(schema, format_checker=oas30_format_checker) + with pytest.raises(ValidationError, match="is not valid under any of the given schemas"): + validator.validate({ + "something": "matching_none_of_the_schemas" + }) + assert False + + discriminator = { + "propertyName": "discipline", + "mapping": { + "mountain_hiking": "#/components/schemas/MountainHiking", + "alpine_climbing": "#/components/schemas/AlpineClimbing", + } + } + schema['components']['schemas']['Route']['discriminator'] = discriminator + + # Optional: check we return useful result when the schema is wrong + validator = OAS30Validator(schema, format_checker=oas30_format_checker) + with pytest.raises(ValidationError, match="does not contain discriminating property"): + validator.validate({ + "something": "missing" + }) + assert False + + # Check we get a non-generic, somehow usable, error message when a discriminated schema is failing + with pytest.raises(ValidationError, match="'bad_string' is not of type integer"): + validator.validate({ + "discipline": "mountain_hiking", + "length": "bad_string" + }) + assert False + + # Check explicit MountainHiking resolution + validator.validate({ + "discipline": "mountain_hiking", + "length": 10 + }) + + # Check implicit MountainHiking resolution + validator.validate({ + "discipline": "MountainHiking", + "length": 10 + }) + + # Check non resolvable implicit schema + with pytest.raises(ValidationError, match="reference '#/components/schemas/other' could not be resolved"): + result = validator.validate({ + "discipline": "other" + }) + assert False pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy