From 0531956b642f616781af32407fa0bda4ab326e58 Mon Sep 17 00:00:00 2001 From: p1c2u Date: Wed, 25 Jan 2023 05:34:35 +0000 Subject: [PATCH] OAS30 read write validators --- README.rst | 38 ++++++- openapi_schema_validator/__init__.py | 4 + openapi_schema_validator/_validators.py | 47 +++++++- openapi_schema_validator/validators.py | 30 ++++++ tests/integration/test_validators.py | 136 +++++++++++++++++++----- 5 files changed, 226 insertions(+), 29 deletions(-) diff --git a/README.rst b/README.rst index b82085e..42391e7 100644 --- a/README.rst +++ b/README.rst @@ -130,7 +130,43 @@ In order to validate OpenAPI 3.0 schema, import and use ``OAS30Validator`` inste "additionalProperties": False, } - validate({"name": "John", "age": 23}, schema, cls=OAS30Validator) + validate({"name": "John", "age": None}, schema, cls=OAS30Validator) + +In order to validate read/write context in OpenAPI 3.0 schema, import and use ``OAS30ReadValidator`` or ``OAS30WriteValidator``. + +.. code-block:: python + + from openapi_schema_validator import OAS30WriteValidator + + # A sample schema + schema = { + "type": "object", + "required": [ + "name" + ], + "properties": { + "name": { + "type": "string" + }, + "age": { + "type": "integer", + "format": "int32", + "minimum": 0, + "readOnly": True, + }, + "birth-date": { + "type": "string", + "format": "date", + } + }, + "additionalProperties": False, + } + + validate({"name": "John", "age": 23}, schema, cls=OAS30WriteValidator) + + Traceback (most recent call last): + ... + ValidationError: Tried to write read-only property with 23 Format check ************ diff --git a/openapi_schema_validator/__init__.py b/openapi_schema_validator/__init__.py index 7529f16..ae7f2b9 100644 --- a/openapi_schema_validator/__init__.py +++ b/openapi_schema_validator/__init__.py @@ -1,6 +1,8 @@ from openapi_schema_validator._format import oas30_format_checker from openapi_schema_validator._format import oas31_format_checker from openapi_schema_validator.shortcuts import validate +from openapi_schema_validator.validators import OAS30ReadValidator +from openapi_schema_validator.validators import OAS30WriteValidator from openapi_schema_validator.validators import OAS30Validator from openapi_schema_validator.validators import OAS31Validator @@ -12,6 +14,8 @@ __all__ = [ "validate", + "OAS30ReadValidator", + "OAS30WriteValidator", "OAS30Validator", "oas30_format_checker", "OAS31Validator", diff --git a/openapi_schema_validator/_validators.py b/openapi_schema_validator/_validators.py index f0144ab..680045a 100644 --- a/openapi_schema_validator/_validators.py +++ b/openapi_schema_validator/_validators.py @@ -165,15 +165,54 @@ def required( read_only = prop_schema.get("readOnly", False) write_only = prop_schema.get("writeOnly", False) if ( - validator.write + getattr(validator, "write", True) and read_only - or validator.read + or getattr(validator, "read", True) and write_only ): continue yield ValidationError(f"{property!r} is a required property") +def read_required( + validator: Validator, + required: List[str], + instance: Any, + schema: Mapping[Hashable, Any], +) -> Iterator[ValidationError]: + if not validator.is_type(instance, "object"): + return + for property in required: + if property not in instance: + prop_schema = schema.get("properties", {}).get(property) + if prop_schema: + write_only = prop_schema.get("writeOnly", False) + if ( + getattr(validator, "read", True) + and write_only + ): + continue + yield ValidationError(f"{property!r} is a required property") + + +def write_required( + validator: Validator, + required: List[str], + instance: Any, + schema: Mapping[Hashable, Any], +) -> Iterator[ValidationError]: + if not validator.is_type(instance, "object"): + return + for property in required: + if property not in instance: + prop_schema = schema.get("properties", {}).get(property) + if prop_schema: + read_only = prop_schema.get("readOnly", False) + if read_only: + continue + yield ValidationError(f"{property!r} is a required property") + + def additionalProperties( validator: Validator, aP: Union[Mapping[Hashable, Any], bool], @@ -204,7 +243,7 @@ def readOnly( instance: Any, schema: Mapping[Hashable, Any], ) -> Iterator[ValidationError]: - if not validator.write or not ro: + if not getattr(validator, "write", True) or not ro: return yield ValidationError(f"Tried to write read-only property with {instance}") @@ -216,7 +255,7 @@ def writeOnly( instance: Any, schema: Mapping[Hashable, Any], ) -> Iterator[ValidationError]: - if not validator.read or not wo: + if not getattr(validator, "read", True) or not wo: return yield ValidationError(f"Tried to read write-only property with {instance}") diff --git a/openapi_schema_validator/validators.py b/openapi_schema_validator/validators.py index f4b12ea..7a57d9b 100644 --- a/openapi_schema_validator/validators.py +++ b/openapi_schema_validator/validators.py @@ -1,5 +1,6 @@ from typing import Any from typing import Type +import warnings from jsonschema import _legacy_validators from jsonschema import _utils @@ -60,6 +61,23 @@ id_of=lambda schema: schema.get("id", ""), ) +OAS30ReadValidator = extend( + OAS30Validator, + validators={ + "required": oas_validators.read_required, + "readOnly": oas_validators.not_implemented, + "writeOnly": oas_validators.writeOnly, + }, +) +OAS30WriteValidator = extend( + OAS30Validator, + validators={ + "required": oas_validators.write_required, + "readOnly": oas_validators.readOnly, + "writeOnly": oas_validators.not_implemented, + }, +) + OAS31Validator = extend( Draft202012Validator, { @@ -89,7 +107,19 @@ def _patch_validator_with_read_write_context(cls: Type[Validator]) -> None: def __init__(self: Validator, *args: Any, **kwargs: Any) -> None: self.read = kwargs.pop("read", None) + if self.read is not None: + warnings.warn( + "read property is deprecated. " + "Use OAS30ReadValidator instead.", + DeprecationWarning, + ) self.write = kwargs.pop("write", None) + if self.write is not None: + warnings.warn( + "write property is deprecated. " + "Use OAS30WriteValidator instead.", + DeprecationWarning, + ) original_init(self, *args, **kwargs) def evolve(self: Validator, **changes: Any) -> Validator: diff --git a/tests/integration/test_validators.py b/tests/integration/test_validators.py index 1bbc9f6..df6b160 100644 --- a/tests/integration/test_validators.py +++ b/tests/integration/test_validators.py @@ -1,6 +1,8 @@ import pytest from jsonschema import ValidationError +from openapi_schema_validator import OAS30ReadValidator +from openapi_schema_validator import OAS30WriteValidator from openapi_schema_validator import OAS30Validator from openapi_schema_validator import OAS31Validator from openapi_schema_validator import oas30_format_checker @@ -258,16 +260,18 @@ def test_read_only(self): "properties": {"some_prop": {"type": "string", "readOnly": True}}, } - validator = OAS30Validator( - schema, format_checker=oas30_format_checker, write=True - ) + with pytest.warns(DeprecationWarning): + 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"}) - validator = OAS30Validator( - schema, format_checker=oas30_format_checker, read=True - ) + with pytest.warns(DeprecationWarning): + validator = OAS30Validator( + schema, format_checker=oas30_format_checker, read=True + ) assert validator.validate({"some_prop": "hello"}) is None def test_write_only(self): @@ -276,16 +280,18 @@ def test_write_only(self): "properties": {"some_prop": {"type": "string", "writeOnly": True}}, } - validator = OAS30Validator( - schema, format_checker=oas30_format_checker, read=True - ) + with pytest.warns(DeprecationWarning): + 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"}) - validator = OAS30Validator( - schema, format_checker=oas30_format_checker, write=True - ) + with pytest.warns(DeprecationWarning): + validator = OAS30Validator( + schema, format_checker=oas30_format_checker, write=True + ) assert validator.validate({"some_prop": "hello"}) is None def test_required_read_only(self): @@ -295,16 +301,18 @@ def test_required_read_only(self): "required": ["some_prop"], } - validator = OAS30Validator( - schema, format_checker=oas30_format_checker, read=True - ) + with pytest.warns(DeprecationWarning): + validator = OAS30Validator( + schema, format_checker=oas30_format_checker, read=True + ) with pytest.raises( ValidationError, match="'some_prop' is a required property" ): validator.validate({"another_prop": "hello"}) - validator = OAS30Validator( - schema, format_checker=oas30_format_checker, write=True - ) + with pytest.warns(DeprecationWarning): + validator = OAS30Validator( + schema, format_checker=oas30_format_checker, write=True + ) assert validator.validate({"another_prop": "hello"}) is None def test_required_write_only(self): @@ -314,16 +322,18 @@ def test_required_write_only(self): "required": ["some_prop"], } - validator = OAS30Validator( - schema, format_checker=oas30_format_checker, write=True - ) + with pytest.warns(DeprecationWarning): + validator = OAS30Validator( + schema, format_checker=oas30_format_checker, write=True + ) with pytest.raises( ValidationError, match="'some_prop' is a required property" ): validator.validate({"another_prop": "hello"}) - validator = OAS30Validator( - schema, format_checker=oas30_format_checker, read=True - ) + with pytest.warns(DeprecationWarning): + validator = OAS30Validator( + schema, format_checker=oas30_format_checker, read=True + ) assert validator.validate({"another_prop": "hello"}) is None def test_oneof_required(self): @@ -567,6 +577,84 @@ def test_nullable_schema_combos(self, is_nullable, schema_type, not_nullable_reg validator.validate({"testfield": None}) assert False + +class TestOAS30ReadWriteValidatorValidate: + + def test_read_only(self): + schema = { + "type": "object", + "properties": {"some_prop": {"type": "string", "readOnly": True}}, + } + + validator = OAS30WriteValidator( + schema, format_checker=oas30_format_checker, + ) + with pytest.raises( + ValidationError, match="Tried to write read-only property with hello" + ): + validator.validate({"some_prop": "hello"}) + validator = OAS30ReadValidator( + schema, format_checker=oas30_format_checker, + ) + assert validator.validate({"some_prop": "hello"}) is None + + def test_write_only(self): + schema = { + "type": "object", + "properties": {"some_prop": {"type": "string", "writeOnly": True}}, + } + + validator = OAS30ReadValidator( + schema, format_checker=oas30_format_checker, + ) + with pytest.raises( + ValidationError, match="Tried to read write-only property with hello" + ): + validator.validate({"some_prop": "hello"}) + validator = OAS30WriteValidator( + schema, format_checker=oas30_format_checker, + ) + assert validator.validate({"some_prop": "hello"}) is None + + def test_required_read_only(self): + schema = { + "type": "object", + "properties": {"some_prop": {"type": "string", "readOnly": True}}, + "required": ["some_prop"], + } + + validator = OAS30ReadValidator( + schema, format_checker=oas30_format_checker, + ) + with pytest.raises( + ValidationError, match="'some_prop' is a required property" + ): + validator.validate({"another_prop": "hello"}) + validator = OAS30WriteValidator( + schema, format_checker=oas30_format_checker, + ) + assert validator.validate({"another_prop": "hello"}) is None + + def test_required_write_only(self): + schema = { + "type": "object", + "properties": {"some_prop": {"type": "string", "writeOnly": True}}, + "required": ["some_prop"], + } + + validator = OAS30WriteValidator( + schema, format_checker=oas30_format_checker, + ) + with pytest.raises( + ValidationError, match="'some_prop' is a required property" + ): + validator.validate({"another_prop": "hello"}) + validator = OAS30ReadValidator( + schema, format_checker=oas30_format_checker, + ) + assert validator.validate({"another_prop": "hello"}) is None + + class TestOAS31ValidatorValidate: @pytest.mark.parametrize( "schema_type",