mirror of
https://github.com/correl/openapi-core.git
synced 2024-11-22 03:00:10 +00:00
Merge pull request #152 from phrfpeixoto/read_only_write_only
Yet another readOnly and writeOnly support
This commit is contained in:
commit
778b4a51a5
13 changed files with 279 additions and 19 deletions
|
@ -46,6 +46,8 @@ class SchemaFactory(object):
|
|||
exclusive_maximum = schema_deref.get('exclusiveMaximum', False)
|
||||
min_properties = schema_deref.get('minProperties', 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)
|
||||
|
||||
|
@ -81,7 +83,7 @@ class SchemaFactory(object):
|
|||
exclusive_maximum=exclusive_maximum,
|
||||
exclusive_minimum=exclusive_minimum,
|
||||
min_properties=min_properties, max_properties=max_properties,
|
||||
extensions=extensions,
|
||||
read_only=read_only, write_only=write_only, extensions=extensions,
|
||||
_source=schema_deref,
|
||||
)
|
||||
|
||||
|
|
|
@ -26,7 +26,8 @@ class Schema(object):
|
|||
min_length=None, max_length=None, pattern=None, unique_items=False,
|
||||
minimum=None, maximum=None, multiple_of=None,
|
||||
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):
|
||||
self.type = SchemaType(schema_type)
|
||||
self.properties = properties and dict(properties) or {}
|
||||
|
@ -56,6 +57,8 @@ class Schema(object):
|
|||
if min_properties is not None else None
|
||||
self.max_properties = int(max_properties)\
|
||||
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 {}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
7
openapi_core/unmarshalling/schemas/enums.py
Normal file
7
openapi_core/unmarshalling/schemas/enums.py
Normal file
|
@ -0,0 +1,7 @@
|
|||
"""OpenAPI core unmarshalling schemas enums module"""
|
||||
from enum import Enum
|
||||
|
||||
|
||||
class UnmarshalContext(Enum):
|
||||
REQUEST = 'request'
|
||||
RESPONSE = 'response'
|
|
@ -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
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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.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,
|
||||
)
|
||||
|
@ -256,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)
|
||||
|
|
|
@ -7,6 +7,7 @@ from openapi_core.schema.responses.exceptions import (
|
|||
InvalidResponse, MissingResponseContent,
|
||||
)
|
||||
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,
|
||||
)
|
||||
|
@ -139,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)
|
||||
|
|
39
tests/integration/data/v3.0/read_only_write_only.yaml
Normal file
39
tests/integration/data/v3.0/read_only_write_only.yaml
Normal 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
|
|
@ -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)
|
||||
|
|
97
tests/integration/validation/test_read_only_write_only.py
Normal file
97
tests/integration/validation/test_read_only_write_only.py
Normal 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
|
|
@ -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 == {}
|
||||
|
|
Loading…
Reference in a new issue