Merge pull request #152 from phrfpeixoto/read_only_write_only

Yet another readOnly and writeOnly support
This commit is contained in:
A 2020-02-17 12:54:38 +00:00 committed by GitHub
commit 778b4a51a5
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 279 additions and 19 deletions

View file

@ -46,6 +46,8 @@ class SchemaFactory(object):
exclusive_maximum = schema_deref.get('exclusiveMaximum', False) exclusive_maximum = schema_deref.get('exclusiveMaximum', False)
min_properties = schema_deref.get('minProperties', None) min_properties = schema_deref.get('minProperties', None)
max_properties = schema_deref.get('maxProperties', None) max_properties = schema_deref.get('maxProperties', None)
read_only = schema_deref.get('readOnly', False)
write_only = schema_deref.get('writeOnly', False)
extensions = self.extensions_generator.generate(schema_deref) extensions = self.extensions_generator.generate(schema_deref)
@ -81,7 +83,7 @@ class SchemaFactory(object):
exclusive_maximum=exclusive_maximum, exclusive_maximum=exclusive_maximum,
exclusive_minimum=exclusive_minimum, exclusive_minimum=exclusive_minimum,
min_properties=min_properties, max_properties=max_properties, min_properties=min_properties, max_properties=max_properties,
extensions=extensions, read_only=read_only, write_only=write_only, extensions=extensions,
_source=schema_deref, _source=schema_deref,
) )

View file

@ -26,7 +26,8 @@ class Schema(object):
min_length=None, max_length=None, pattern=None, unique_items=False, min_length=None, max_length=None, pattern=None, unique_items=False,
minimum=None, maximum=None, multiple_of=None, minimum=None, maximum=None, multiple_of=None,
exclusive_minimum=False, exclusive_maximum=False, exclusive_minimum=False, exclusive_maximum=False,
min_properties=None, max_properties=None, extensions=None, min_properties=None, max_properties=None,
read_only=False, write_only=False, extensions=None,
_source=None): _source=None):
self.type = SchemaType(schema_type) self.type = SchemaType(schema_type)
self.properties = properties and dict(properties) or {} self.properties = properties and dict(properties) or {}
@ -56,6 +57,8 @@ class Schema(object):
if min_properties is not None else None if min_properties is not None else None
self.max_properties = int(max_properties)\ self.max_properties = int(max_properties)\
if max_properties is not None else None if max_properties is not None else None
self.read_only = read_only
self.write_only = write_only
self.extensions = extensions and dict(extensions) or {} self.extensions = extensions and dict(extensions) or {}

View file

@ -35,6 +35,19 @@ def nullable(validator, is_nullable, instance, schema):
yield ValidationError("None for not nullable") 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): def additionalProperties(validator, aP, instance, schema):
if not validator.is_type(instance, "object"): if not validator.is_type(instance, "object"):
return return
@ -54,5 +67,21 @@ def additionalProperties(validator, aP, instance, schema):
yield ValidationError(error % extras_msg(extras)) 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): def not_implemented(validator, value, instance, schema):
pass pass

View file

@ -21,7 +21,6 @@ BaseOAS30Validator = create(
u"uniqueItems": _validators.uniqueItems, u"uniqueItems": _validators.uniqueItems,
u"maxProperties": _validators.maxProperties, u"maxProperties": _validators.maxProperties,
u"minProperties": _validators.minProperties, u"minProperties": _validators.minProperties,
u"required": _validators.required,
u"enum": _validators.enum, u"enum": _validators.enum,
# adjusted to OAS # adjusted to OAS
u"type": oas_validators.type, u"type": oas_validators.type,
@ -31,6 +30,7 @@ BaseOAS30Validator = create(
u"not": _validators.not_, u"not": _validators.not_,
u"items": oas_validators.items, u"items": oas_validators.items,
u"properties": _validators.properties, u"properties": _validators.properties,
u"required": oas_validators.required,
u"additionalProperties": oas_validators.additionalProperties, u"additionalProperties": oas_validators.additionalProperties,
# TODO: adjust description # TODO: adjust description
u"format": oas_validators.format, u"format": oas_validators.format,
@ -39,8 +39,8 @@ BaseOAS30Validator = create(
# fixed OAS fields # fixed OAS fields
u"nullable": oas_validators.nullable, u"nullable": oas_validators.nullable,
u"discriminator": oas_validators.not_implemented, u"discriminator": oas_validators.not_implemented,
u"readOnly": oas_validators.not_implemented, u"readOnly": oas_validators.readOnly,
u"writeOnly": oas_validators.not_implemented, u"writeOnly": oas_validators.writeOnly,
u"xml": oas_validators.not_implemented, u"xml": oas_validators.not_implemented,
u"externalDocs": oas_validators.not_implemented, u"externalDocs": oas_validators.not_implemented,
u"example": oas_validators.not_implemented, u"example": oas_validators.not_implemented,
@ -54,6 +54,11 @@ BaseOAS30Validator = create(
class OAS30Validator(BaseOAS30Validator): 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): def iter_errors(self, instance, _schema=None):
if _schema is None: if _schema is None:
_schema = self.schema _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.schemas.models import Schema
from openapi_core.schema_validator import OAS30Validator from openapi_core.schema_validator import OAS30Validator
from openapi_core.schema_validator import oas30_format_checker from openapi_core.schema_validator import oas30_format_checker
from openapi_core.unmarshalling.schemas.enums import UnmarshalContext
from openapi_core.unmarshalling.schemas.exceptions import ( from openapi_core.unmarshalling.schemas.exceptions import (
FormatterNotFoundError, FormatterNotFoundError,
) )
@ -29,11 +30,17 @@ class SchemaUnmarshallersFactory(object):
SchemaType.ANY: AnyUnmarshaller, 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 self.resolver = resolver
if custom_formatters is None: if custom_formatters is None:
custom_formatters = {} custom_formatters = {}
self.custom_formatters = custom_formatters self.custom_formatters = custom_formatters
self.context = context
def create(self, schema, type_override=None): def create(self, schema, type_override=None):
"""Create unmarshaller from the schema.""" """Create unmarshaller from the schema."""
@ -50,7 +57,9 @@ class SchemaUnmarshallersFactory(object):
elif schema_type in self.COMPLEX_UNMARSHALLERS: elif schema_type in self.COMPLEX_UNMARSHALLERS:
klass = self.COMPLEX_UNMARSHALLERS[schema_type] klass = self.COMPLEX_UNMARSHALLERS[schema_type]
kwargs = dict( kwargs = dict(
schema=schema, unmarshallers_factory=self) schema=schema, unmarshallers_factory=self,
context=self.context,
)
formatter = self.get_formatter(klass.FORMATTERS, schema.format) formatter = self.get_formatter(klass.FORMATTERS, schema.format)
@ -70,10 +79,17 @@ class SchemaUnmarshallersFactory(object):
return default_formatters.get(schema_format) return default_formatters.get(schema_format)
def get_validator(self, schema): 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(): for name, formatter in self.custom_formatters.items():
format_checker.checks(name)(formatter.validate) fc.checks(name)(formatter.validate)
return OAS30Validator( return fc
schema.__dict__,
resolver=self.resolver, format_checker=format_checker,
)

View file

@ -13,6 +13,7 @@ from openapi_core.schema_validator._types import (
is_object, is_number, is_string, is_object, is_number, is_string,
) )
from openapi_core.schema_validator._format import oas30_format_checker 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 ( from openapi_core.unmarshalling.schemas.exceptions import (
UnmarshalError, ValidateError, InvalidSchemaValue, UnmarshalError, ValidateError, InvalidSchemaValue,
InvalidSchemaFormatValue, InvalidSchemaFormatValue,
@ -120,9 +121,12 @@ class BooleanUnmarshaller(PrimitiveTypeUnmarshaller):
class ComplexUnmarshaller(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) super(ComplexUnmarshaller, self).__init__(formatter, validator, schema)
self.unmarshallers_factory = unmarshallers_factory self.unmarshallers_factory = unmarshallers_factory
self.context = context
class ArrayUnmarshaller(ComplexUnmarshaller): class ArrayUnmarshaller(ComplexUnmarshaller):
@ -206,6 +210,10 @@ class ObjectUnmarshaller(ComplexUnmarshaller):
properties[prop_name] = prop_value properties[prop_name] = prop_value
for prop_name, prop in iteritems(all_props): 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: try:
prop_value = value[prop_name] prop_value = value[prop_name]
except KeyError: except KeyError:

View file

@ -13,6 +13,7 @@ from openapi_core.schema.paths.exceptions import InvalidPath
from openapi_core.schema.request_bodies.exceptions import MissingRequestBody from openapi_core.schema.request_bodies.exceptions import MissingRequestBody
from openapi_core.schema.servers.exceptions import InvalidServer from openapi_core.schema.servers.exceptions import InvalidServer
from openapi_core.security.exceptions import SecurityError from openapi_core.security.exceptions import SecurityError
from openapi_core.unmarshalling.schemas.enums import UnmarshalContext
from openapi_core.unmarshalling.schemas.exceptions import ( from openapi_core.unmarshalling.schemas.exceptions import (
UnmarshalError, ValidateError, UnmarshalError, ValidateError,
) )
@ -256,7 +257,9 @@ class RequestValidator(object):
SchemaUnmarshallersFactory, SchemaUnmarshallersFactory,
) )
unmarshallers_factory = SchemaUnmarshallersFactory( unmarshallers_factory = SchemaUnmarshallersFactory(
self.spec._resolver, self.custom_formatters) self.spec._resolver, self.custom_formatters,
context=UnmarshalContext.REQUEST,
)
unmarshaller = unmarshallers_factory.create( unmarshaller = unmarshallers_factory.create(
param_or_media_type.schema) param_or_media_type.schema)
return unmarshaller(value) return unmarshaller(value)

View file

@ -7,6 +7,7 @@ from openapi_core.schema.responses.exceptions import (
InvalidResponse, MissingResponseContent, InvalidResponse, MissingResponseContent,
) )
from openapi_core.schema.servers.exceptions import InvalidServer from openapi_core.schema.servers.exceptions import InvalidServer
from openapi_core.unmarshalling.schemas.enums import UnmarshalContext
from openapi_core.unmarshalling.schemas.exceptions import ( from openapi_core.unmarshalling.schemas.exceptions import (
UnmarshalError, ValidateError, UnmarshalError, ValidateError,
) )
@ -139,7 +140,9 @@ class ResponseValidator(object):
SchemaUnmarshallersFactory, SchemaUnmarshallersFactory,
) )
unmarshallers_factory = SchemaUnmarshallersFactory( unmarshallers_factory = SchemaUnmarshallersFactory(
self.spec._resolver, self.custom_formatters) self.spec._resolver, self.custom_formatters,
context=UnmarshalContext.RESPONSE,
)
unmarshaller = unmarshallers_factory.create( unmarshaller = unmarshallers_factory.create(
param_or_media_type.schema) param_or_media_type.schema)
return unmarshaller(value) return unmarshaller(value)

View file

@ -0,0 +1,39 @@
openapi: "3.0.0"
info:
title: Specification Containing readOnly
version: "0.1"
paths:
/users:
post:
operationId: createUser
requestBody:
description: Post data for creating a user
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/User'
responses:
default:
description: Create a user
content:
application/json:
schema:
$ref: '#/components/schemas/User'
components:
schemas:
User:
type: object
required:
- id
- name
properties:
id:
type: integer
format: int32
readOnly: true
name:
type: string
hidden:
type: boolean
writeOnly: true

View file

@ -268,5 +268,9 @@ class TestPetstore(object):
if not spec.components: if not spec.components:
return return
for _, schema in iteritems(spec.components.schemas): for schema_name, schema in iteritems(spec.components.schemas):
assert type(schema) == Schema 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

@ -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.enums import SchemaType
from openapi_core.schema.schemas.models import Schema from openapi_core.schema.schemas.models import Schema
from openapi_core.schema.schemas.types import NoValue from openapi_core.schema.schemas.types import NoValue
from openapi_core.unmarshalling.schemas.enums import UnmarshalContext
from openapi_core.unmarshalling.schemas.exceptions import ( from openapi_core.unmarshalling.schemas.exceptions import (
InvalidSchemaFormatValue, InvalidSchemaValue, UnmarshalError, InvalidSchemaFormatValue, InvalidSchemaValue, UnmarshalError,
FormatterNotFoundError, FormatterNotFoundError,
@ -20,9 +21,10 @@ from openapi_core.unmarshalling.schemas.formatters import Formatter
@pytest.fixture @pytest.fixture
def unmarshaller_factory(): def unmarshaller_factory():
def create_unmarshaller(schema, custom_formatters=None): def create_unmarshaller(schema, custom_formatters=None, context=None):
return SchemaUnmarshallersFactory( return SchemaUnmarshallersFactory(
custom_formatters=custom_formatters).create(schema) custom_formatters=custom_formatters, context=context).create(
schema)
return create_unmarshaller return create_unmarshaller
@ -429,3 +431,45 @@ class TestSchemaUnmarshallerCall(object):
result = unmarshaller_factory(schema)(value) result = unmarshaller_factory(schema)(value)
assert result == 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 == {}