Property read-only and write-only support

This commit is contained in:
Artur Maciag 2020-02-17 12:40:32 +00:00
parent 975ac0d7c9
commit 1bea6013c3
16 changed files with 233 additions and 166 deletions

View file

@ -9,9 +9,6 @@ from openapi_core.schema.schemas.enums import SchemaType
log = logging.getLogger(__name__)
_CONTEXT = UnmarshalContext.REQUEST
class Parameter(object):
"""Represents an OpenAPI operation Parameter."""

View file

@ -26,8 +26,3 @@ class SchemaFormat(Enum):
DATETIME = 'date-time'
PASSWORD = 'password'
UUID = 'uuid'
class UnmarshalContext(Enum):
REQUEST = 'request'
RESPONSE = 'response'

View file

@ -60,9 +60,6 @@ class Schema(object):
self.read_only = read_only
self.write_only = write_only
if self.read_only and self.write_only:
raise OpenAPISchemaError("Schema cannot be readOnly AND writeOnly")
self.extensions = extensions and dict(extensions) or {}
self._all_required_properties_cache = None

View file

@ -3,9 +3,6 @@ from distutils.util import strtobool
from six import string_types
from json import dumps
from openapi_core.schema.schemas.enums import UnmarshalContext
from openapi_core.schema.schemas.exceptions import UnmarshalContextNotSet
def forcebool(val):
if isinstance(val, string_types):

View file

@ -35,6 +35,19 @@ def nullable(validator, is_nullable, instance, schema):
yield ValidationError("None for not nullable")
def required(validator, required, instance, schema):
if not validator.is_type(instance, "object"):
return
for property in required:
if property not in instance:
prop_schema = schema['properties'][property]
read_only = prop_schema.get('readOnly', False)
write_only = prop_schema.get('writeOnly', False)
if validator.write and read_only or validator.read and write_only:
continue
yield ValidationError("%r is a required property" % property)
def additionalProperties(validator, aP, instance, schema):
if not validator.is_type(instance, "object"):
return
@ -54,5 +67,21 @@ def additionalProperties(validator, aP, instance, schema):
yield ValidationError(error % extras_msg(extras))
def readOnly(validator, ro, instance, schema):
if not validator.write or not ro:
return
yield ValidationError(
"Tried to write read-only proparty with %s" % (instance))
def writeOnly(validator, wo, instance, schema):
if not validator.read or not wo:
return
yield ValidationError(
"Tried to read write-only proparty with %s" % (instance))
def not_implemented(validator, value, instance, schema):
pass

View file

@ -21,7 +21,6 @@ BaseOAS30Validator = create(
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,
@ -31,6 +30,7 @@ BaseOAS30Validator = create(
u"not": _validators.not_,
u"items": oas_validators.items,
u"properties": _validators.properties,
u"required": oas_validators.required,
u"additionalProperties": oas_validators.additionalProperties,
# TODO: adjust description
u"format": oas_validators.format,
@ -39,8 +39,8 @@ BaseOAS30Validator = create(
# 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"readOnly": oas_validators.readOnly,
u"writeOnly": oas_validators.writeOnly,
u"xml": oas_validators.not_implemented,
u"externalDocs": oas_validators.not_implemented,
u"example": oas_validators.not_implemented,
@ -54,6 +54,11 @@ BaseOAS30Validator = create(
class OAS30Validator(BaseOAS30Validator):
def __init__(self, *args, **kwargs):
self.read = kwargs.pop('read', None)
self.write = kwargs.pop('write', None)
super(OAS30Validator, self).__init__(*args, **kwargs)
def iter_errors(self, instance, _schema=None):
if _schema is None:
_schema = self.schema

View file

@ -0,0 +1,7 @@
"""OpenAPI core unmarshalling schemas enums module"""
from enum import Enum
class UnmarshalContext(Enum):
REQUEST = 'request'
RESPONSE = 'response'

View file

@ -5,6 +5,7 @@ from openapi_core.schema.schemas.enums import SchemaType, SchemaFormat
from openapi_core.schema.schemas.models import Schema
from openapi_core.schema_validator import OAS30Validator
from openapi_core.schema_validator import oas30_format_checker
from openapi_core.unmarshalling.schemas.enums import UnmarshalContext
from openapi_core.unmarshalling.schemas.exceptions import (
FormatterNotFoundError,
)
@ -29,11 +30,17 @@ class SchemaUnmarshallersFactory(object):
SchemaType.ANY: AnyUnmarshaller,
}
def __init__(self, resolver=None, custom_formatters=None):
CONTEXT_VALIDATION = {
UnmarshalContext.REQUEST: 'write',
UnmarshalContext.RESPONSE: 'read',
}
def __init__(self, resolver=None, custom_formatters=None, context=None):
self.resolver = resolver
if custom_formatters is None:
custom_formatters = {}
self.custom_formatters = custom_formatters
self.context = context
def create(self, schema, type_override=None):
"""Create unmarshaller from the schema."""
@ -50,7 +57,9 @@ class SchemaUnmarshallersFactory(object):
elif schema_type in self.COMPLEX_UNMARSHALLERS:
klass = self.COMPLEX_UNMARSHALLERS[schema_type]
kwargs = dict(
schema=schema, unmarshallers_factory=self)
schema=schema, unmarshallers_factory=self,
context=self.context,
)
formatter = self.get_formatter(klass.FORMATTERS, schema.format)
@ -70,10 +79,17 @@ class SchemaUnmarshallersFactory(object):
return default_formatters.get(schema_format)
def get_validator(self, schema):
format_checker = deepcopy(oas30_format_checker)
format_checker = self._get_format_checker()
kwargs = {
'resolver': self.resolver,
'format_checker': format_checker,
}
if self.context is not None:
kwargs[self.CONTEXT_VALIDATION[self.context]] = True
return OAS30Validator(schema.__dict__, **kwargs)
def _get_format_checker(self):
fc = deepcopy(oas30_format_checker)
for name, formatter in self.custom_formatters.items():
format_checker.checks(name)(formatter.validate)
return OAS30Validator(
schema.__dict__,
resolver=self.resolver, format_checker=format_checker,
)
fc.checks(name)(formatter.validate)
return fc

View file

@ -13,6 +13,7 @@ from openapi_core.schema_validator._types import (
is_object, is_number, is_string,
)
from openapi_core.schema_validator._format import oas30_format_checker
from openapi_core.unmarshalling.schemas.enums import UnmarshalContext
from openapi_core.unmarshalling.schemas.exceptions import (
UnmarshalError, ValidateError, InvalidSchemaValue,
InvalidSchemaFormatValue,
@ -120,9 +121,12 @@ class BooleanUnmarshaller(PrimitiveTypeUnmarshaller):
class ComplexUnmarshaller(PrimitiveTypeUnmarshaller):
def __init__(self, formatter, validator, schema, unmarshallers_factory):
def __init__(
self, formatter, validator, schema, unmarshallers_factory,
context=None):
super(ComplexUnmarshaller, self).__init__(formatter, validator, schema)
self.unmarshallers_factory = unmarshallers_factory
self.context = context
class ArrayUnmarshaller(ComplexUnmarshaller):
@ -206,6 +210,10 @@ class ObjectUnmarshaller(ComplexUnmarshaller):
properties[prop_name] = prop_value
for prop_name, prop in iteritems(all_props):
if self.context == UnmarshalContext.REQUEST and prop.read_only:
continue
if self.context == UnmarshalContext.RESPONSE and prop.write_only:
continue
try:
prop_value = value[prop_name]
except KeyError:

View file

@ -11,9 +11,9 @@ from openapi_core.schema.parameters.exceptions import (
)
from openapi_core.schema.paths.exceptions import InvalidPath
from openapi_core.schema.request_bodies.exceptions import MissingRequestBody
from openapi_core.schema.schemas.enums import UnmarshalContext
from openapi_core.schema.servers.exceptions import InvalidServer
from openapi_core.security.exceptions import SecurityError
from openapi_core.unmarshalling.schemas.enums import UnmarshalContext
from openapi_core.unmarshalling.schemas.exceptions import (
UnmarshalError, ValidateError,
)
@ -24,8 +24,6 @@ from openapi_core.validation.request.datatypes import (
from openapi_core.validation.util import get_operation_pattern
_CONTEXT = UnmarshalContext.REQUEST
class RequestValidator(object):
def __init__(
@ -259,7 +257,9 @@ class RequestValidator(object):
SchemaUnmarshallersFactory,
)
unmarshallers_factory = SchemaUnmarshallersFactory(
self.spec._resolver, self.custom_formatters)
self.spec._resolver, self.custom_formatters,
context=UnmarshalContext.REQUEST,
)
unmarshaller = unmarshallers_factory.create(
param_or_media_type.schema)
return unmarshaller(value)

View file

@ -6,16 +6,14 @@ from openapi_core.schema.media_types.exceptions import InvalidContentType
from openapi_core.schema.responses.exceptions import (
InvalidResponse, MissingResponseContent,
)
from openapi_core.schema.schemas.enums import UnmarshalContext
from openapi_core.schema.servers.exceptions import InvalidServer
from openapi_core.unmarshalling.schemas.enums import UnmarshalContext
from openapi_core.unmarshalling.schemas.exceptions import (
UnmarshalError, ValidateError,
)
from openapi_core.validation.response.datatypes import ResponseValidationResult
from openapi_core.validation.util import get_operation_pattern
_CONTEXT = UnmarshalContext.RESPONSE
class ResponseValidator(object):
@ -76,7 +74,6 @@ class ResponseValidator(object):
try:
media_type = operation_response[response.mimetype]
except InvalidContentType as exc:
return None, [exc, ]
@ -143,7 +140,9 @@ class ResponseValidator(object):
SchemaUnmarshallersFactory,
)
unmarshallers_factory = SchemaUnmarshallersFactory(
self.spec._resolver, self.custom_formatters)
self.spec._resolver, self.custom_formatters,
context=UnmarshalContext.RESPONSE,
)
unmarshaller = unmarshallers_factory.create(
param_or_media_type.schema)
return unmarshaller(value)

View file

@ -268,5 +268,9 @@ class TestPetstore(object):
if not spec.components:
return
for _, schema in iteritems(spec.components.schemas):
for schema_name, schema in iteritems(spec.components.schemas):
assert type(schema) == Schema
schema_spec = spec_dict['components']['schemas'][schema_name]
assert schema.read_only == schema_spec.get('readOnly', False)
assert schema.write_only == schema_spec.get('writeOnly', False)

View file

@ -1,65 +0,0 @@
import json
import pytest
from openapi_core.schema.media_types.exceptions import InvalidMediaTypeValue
from openapi_core.schema.schemas.enums import UnmarshalContext
from openapi_core.schema.schemas.exceptions import InvalidSchemaProperty
from openapi_core.shortcuts import create_spec
from openapi_core.validation.response.validators import ResponseValidator
from openapi_core.validation.request.validators import RequestValidator
from openapi_core.wrappers.mock import MockRequest, MockResponse
@pytest.fixture
def response_validator(spec):
return ResponseValidator(spec)
@pytest.fixture
def request_validator(spec):
return RequestValidator(spec)
@pytest.fixture('class')
def spec(factory):
spec_dict = factory.spec_from_file("data/v3.0/read_only_write_only.yaml")
return create_spec(spec_dict)
class TestReadOnly(object):
def test_write_a_read_only_property(self, request_validator):
data = json.dumps({
'id': 10,
'name': "Pedro"
})
request = MockRequest(host_url='', method='POST',
path='/users', data=data)
with pytest.raises(InvalidMediaTypeValue) as ex:
request_validator.validate(request).raise_for_errors()
assert isinstance(ex.value.original_exception, InvalidSchemaProperty)
ex = ex.value.original_exception
assert ex.property_name == 'id'
assert UnmarshalContext.REQUEST.value in str(ex.original_exception)
def test_read_only_property_response(self, response_validator):
data = json.dumps({
'id': 10,
'name': "Pedro"
})
request = MockRequest(host_url='', method='POST',
path='/users')
response = MockResponse(data)
is_valid = response_validator.validate(request, response)
is_valid.raise_for_errors()
assert len(is_valid.errors) == 0
assert is_valid.data.id == 10
assert is_valid.data.name == "Pedro"

View file

@ -1,63 +0,0 @@
import json
import pytest
from openapi_core.schema.media_types.exceptions import InvalidMediaTypeValue
from openapi_core.schema.schemas.enums import UnmarshalContext
from openapi_core.schema.schemas.exceptions import InvalidSchemaProperty
from openapi_core.shortcuts import create_spec
from openapi_core.validation.response.validators import ResponseValidator
from openapi_core.validation.request.validators import RequestValidator
from openapi_core.wrappers.mock import MockRequest, MockResponse
@pytest.fixture
def response_validator(spec):
return ResponseValidator(spec)
@pytest.fixture
def request_validator(spec):
return RequestValidator(spec)
@pytest.fixture('class')
def spec(factory):
spec_dict = factory.spec_from_file("data/v3.0/read_only_write_only.yaml")
return create_spec(spec_dict)
class TestWriteOnly(object):
def test_write_only_property(self, request_validator):
data = json.dumps({
'name': "Pedro",
'hidden': False
})
request = MockRequest(host_url='', method='POST',
path='/users', data=data)
is_valid = request_validator.validate(request)
is_valid.raise_for_errors()
assert is_valid.body.name == "Pedro"
assert is_valid.body.hidden is False
def test_read_a_write_only_property(self, response_validator):
data = json.dumps({
'id': 10,
'name': "Pedro",
'hidden': True
})
request = MockRequest(host_url='', method='POST',
path='/users')
response = MockResponse(data)
with pytest.raises(InvalidMediaTypeValue) as ex:
response_validator.validate(request, response).raise_for_errors()
assert isinstance(ex.value.original_exception, InvalidSchemaProperty)
ex = ex.value.original_exception
assert ex.property_name == 'hidden'
assert UnmarshalContext.RESPONSE.value in str(ex.original_exception)

View file

@ -0,0 +1,97 @@
import json
import pytest
from openapi_core.shortcuts import create_spec
from openapi_core.unmarshalling.schemas.exceptions import InvalidSchemaValue
from openapi_core.validation.response.validators import ResponseValidator
from openapi_core.validation.request.validators import RequestValidator
from openapi_core.testing import MockRequest, MockResponse
@pytest.fixture
def response_validator(spec):
return ResponseValidator(spec)
@pytest.fixture
def request_validator(spec):
return RequestValidator(spec)
@pytest.fixture('class')
def spec(factory):
spec_dict = factory.spec_from_file("data/v3.0/read_only_write_only.yaml")
return create_spec(spec_dict)
class TestReadOnly(object):
def test_write_a_read_only_property(self, request_validator):
data = json.dumps({
'id': 10,
'name': "Pedro",
})
request = MockRequest(host_url='', method='POST',
path='/users', data=data)
result = request_validator.validate(request)
assert type(result.errors[0]) == InvalidSchemaValue
assert result.body is None
def test_read_only_property_response(self, response_validator):
data = json.dumps({
'id': 10,
'name': "Pedro",
})
request = MockRequest(host_url='', method='POST',
path='/users')
response = MockResponse(data)
result = response_validator.validate(request, response)
assert not result.errors
assert result.data == {
'id': 10,
'name': "Pedro",
}
class TestWriteOnly(object):
def test_write_only_property(self, request_validator):
data = json.dumps({
'name': "Pedro",
'hidden': False,
})
request = MockRequest(host_url='', method='POST',
path='/users', data=data)
result = request_validator.validate(request)
assert not result.errors
assert result.body == {
'name': "Pedro",
'hidden': False,
}
def test_read_a_write_only_property(self, response_validator):
data = json.dumps({
'id': 10,
'name': "Pedro",
'hidden': True,
})
request = MockRequest(host_url='', method='POST',
path='/users')
response = MockResponse(data)
result = response_validator.validate(request, response)
assert type(result.errors[0]) == InvalidSchemaValue
assert result.data is None

View file

@ -8,6 +8,7 @@ from openapi_core.schema.parameters.models import Parameter
from openapi_core.schema.schemas.enums import SchemaType
from openapi_core.schema.schemas.models import Schema
from openapi_core.schema.schemas.types import NoValue
from openapi_core.unmarshalling.schemas.enums import UnmarshalContext
from openapi_core.unmarshalling.schemas.exceptions import (
InvalidSchemaFormatValue, InvalidSchemaValue, UnmarshalError,
FormatterNotFoundError,
@ -20,9 +21,10 @@ from openapi_core.unmarshalling.schemas.formatters import Formatter
@pytest.fixture
def unmarshaller_factory():
def create_unmarshaller(schema, custom_formatters=None):
def create_unmarshaller(schema, custom_formatters=None, context=None):
return SchemaUnmarshallersFactory(
custom_formatters=custom_formatters).create(schema)
custom_formatters=custom_formatters, context=context).create(
schema)
return create_unmarshaller
@ -429,3 +431,45 @@ class TestSchemaUnmarshallerCall(object):
result = unmarshaller_factory(schema)(value)
assert result == value
def test_read_only_properties(self, unmarshaller_factory):
id_property = Schema('integer', read_only=True)
def properties():
yield ('id', id_property)
obj_schema = Schema('object', properties=properties(), required=['id'])
# readOnly properties may be admitted in a Response context
result = unmarshaller_factory(
obj_schema, context=UnmarshalContext.RESPONSE)({"id": 10})
assert result == {
'id': 10,
}
# readOnly properties are not admitted on a Request context
result = unmarshaller_factory(
obj_schema, context=UnmarshalContext.REQUEST)({"id": 10})
assert result == {}
def test_write_only_properties(self, unmarshaller_factory):
id_property = Schema('integer', write_only=True)
def properties():
yield ('id', id_property)
obj_schema = Schema('object', properties=properties(), required=['id'])
# readOnly properties may be admitted in a Response context
result = unmarshaller_factory(
obj_schema, context=UnmarshalContext.REQUEST)({"id": 10})
assert result == {
'id': 10,
}
# readOnly properties are not admitted on a Request context
result = unmarshaller_factory(
obj_schema, context=UnmarshalContext.RESPONSE)({"id": 10})
assert result == {}