From 3eeb7718657a36ba04a1281c8d849db8ae7c722a Mon Sep 17 00:00:00 2001 From: A Date: Thu, 27 Jan 2022 04:28:20 +0000 Subject: [PATCH 1/4] Merge pull request #30 from gberaudo/support_oneof_discriminator Validate oneOf, anyOf and allOf with discriminator --- openapi_schema_validator/_validators.py | 66 ++++++++++++++ openapi_schema_validator/validators.py | 6 +- tests/integration/test_validators.py | 113 ++++++++++++++++++++++++ 3 files changed, 182 insertions(+), 3 deletions(-) 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..1d95e0e 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, diff --git a/tests/integration/test_validators.py b/tests/integration/test_validators.py index 9b5e458..f6f27cf 100644 --- a/tests/integration/test_validators.py +++ b/tests/integration/test_validators.py @@ -237,3 +237,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 From 92d952f716d5dd642a9a14ff69f44afb829215a2 Mon Sep 17 00:00:00 2001 From: p1c2u Date: Thu, 27 Jan 2022 07:49:46 +0000 Subject: [PATCH 2/4] Version 0.2.2 --- .bumpversion.cfg | 2 +- openapi_schema_validator/__init__.py | 2 +- pyproject.toml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.bumpversion.cfg b/.bumpversion.cfg index 626eac4..e7f583a 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 0.2.1 +current_version = 0.2.2 tag = True tag_name = {new_version} commit = True diff --git a/openapi_schema_validator/__init__.py b/openapi_schema_validator/__init__.py index c1085a6..8673df7 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.2' __url__ = 'https://github.com/p1c2u/openapi-schema-validator' __license__ = '3-clause BSD License' diff --git a/pyproject.toml b/pyproject.toml index 2c3e086..5afbe68 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.2" description = "OpenAPI schema validation for Python" authors = ["Artur Maciag "] license = "BSD-3-Clause" From e3f59bfc69a17e431b91ee0bf096709e8fa61ad5 Mon Sep 17 00:00:00 2001 From: p1c2u Date: Sat, 29 Jan 2022 05:31:20 +0000 Subject: [PATCH 3/4] readOnly and writeOnly on jsonschema4 --- openapi_schema_validator/validators.py | 7 +++++++ tests/integration/test_validators.py | 6 ++++++ 2 files changed, 13 insertions(+) diff --git a/openapi_schema_validator/validators.py b/openapi_schema_validator/validators.py index 1d95e0e..4e69c58 100644 --- a/openapi_schema_validator/validators.py +++ b/openapi_schema_validator/validators.py @@ -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/tests/integration/test_validators.py b/tests/integration/test_validators.py index f6f27cf..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): From 241ec1ca8527d24e0cd0f8ea2fb88a69cdcbc3bd Mon Sep 17 00:00:00 2001 From: p1c2u Date: Sat, 29 Jan 2022 05:49:22 +0000 Subject: [PATCH 4/4] Version 0.2.3 --- .bumpversion.cfg | 2 +- openapi_schema_validator/__init__.py | 2 +- pyproject.toml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.bumpversion.cfg b/.bumpversion.cfg index e7f583a..e700c56 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 0.2.2 +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 8673df7..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.2' +__version__ = '0.2.3' __url__ = 'https://github.com/p1c2u/openapi-schema-validator' __license__ = '3-clause BSD License' diff --git a/pyproject.toml b/pyproject.toml index 5afbe68..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.2" +version = "0.2.3" description = "OpenAPI schema validation for Python" authors = ["Artur Maciag "] license = "BSD-3-Clause" 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