Skip to content

OAS30 read write validators #59

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Jan 25, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 37 additions & 1 deletion README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
************
Expand Down
4 changes: 4 additions & 0 deletions openapi_schema_validator/__init__.py
Original file line number Diff line number Diff line change
@@ -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

Expand All @@ -12,6 +14,8 @@

__all__ = [
"validate",
"OAS30ReadValidator",
"OAS30WriteValidator",
"OAS30Validator",
"oas30_format_checker",
"OAS31Validator",
Expand Down
47 changes: 43 additions & 4 deletions openapi_schema_validator/_validators.py
Original file line number Diff line number Diff line change
Expand Up @@ -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],
Expand Down Expand Up @@ -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}")
Expand All @@ -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}")
Expand Down
30 changes: 30 additions & 0 deletions openapi_schema_validator/validators.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from typing import Any
from typing import Type
import warnings

from jsonschema import _legacy_validators
from jsonschema import _utils
Expand Down Expand Up @@ -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,
{
Expand Down Expand Up @@ -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:
Expand Down
136 changes: 112 additions & 24 deletions tests/integration/test_validators.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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):
Expand All @@ -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):
Expand All @@ -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):
Expand All @@ -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):
Expand Down Expand Up @@ -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",
Expand Down