Skip to content

Commit 1bea601

Browse files
committed
Property read-only and write-only support
1 parent 975ac0d commit 1bea601

File tree

16 files changed

+233
-166
lines changed

16 files changed

+233
-166
lines changed

openapi_core/schema/parameters/models.py

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,6 @@
99
log = logging.getLogger(__name__)
1010

1111

12-
_CONTEXT = UnmarshalContext.REQUEST
13-
14-
1512
class Parameter(object):
1613
"""Represents an OpenAPI operation Parameter."""
1714

openapi_core/schema/schemas/enums.py

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,3 @@ class SchemaFormat(Enum):
2626
DATETIME = 'date-time'
2727
PASSWORD = 'password'
2828
UUID = 'uuid'
29-
30-
31-
class UnmarshalContext(Enum):
32-
REQUEST = 'request'
33-
RESPONSE = 'response'

openapi_core/schema/schemas/models.py

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -60,9 +60,6 @@ def __init__(
6060
self.read_only = read_only
6161
self.write_only = write_only
6262

63-
if self.read_only and self.write_only:
64-
raise OpenAPISchemaError("Schema cannot be readOnly AND writeOnly")
65-
6663
self.extensions = extensions and dict(extensions) or {}
6764

6865
self._all_required_properties_cache = None

openapi_core/schema/schemas/util.py

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,6 @@
33
from six import string_types
44
from json import dumps
55

6-
from openapi_core.schema.schemas.enums import UnmarshalContext
7-
from openapi_core.schema.schemas.exceptions import UnmarshalContextNotSet
8-
96

107
def forcebool(val):
118
if isinstance(val, string_types):

openapi_core/schema_validator/_validators.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,19 @@ def nullable(validator, is_nullable, instance, schema):
3535
yield ValidationError("None for not nullable")
3636

3737

38+
def required(validator, required, instance, schema):
39+
if not validator.is_type(instance, "object"):
40+
return
41+
for property in required:
42+
if property not in instance:
43+
prop_schema = schema['properties'][property]
44+
read_only = prop_schema.get('readOnly', False)
45+
write_only = prop_schema.get('writeOnly', False)
46+
if validator.write and read_only or validator.read and write_only:
47+
continue
48+
yield ValidationError("%r is a required property" % property)
49+
50+
3851
def additionalProperties(validator, aP, instance, schema):
3952
if not validator.is_type(instance, "object"):
4053
return
@@ -54,5 +67,21 @@ def additionalProperties(validator, aP, instance, schema):
5467
yield ValidationError(error % extras_msg(extras))
5568

5669

70+
def readOnly(validator, ro, instance, schema):
71+
if not validator.write or not ro:
72+
return
73+
74+
yield ValidationError(
75+
"Tried to write read-only proparty with %s" % (instance))
76+
77+
78+
def writeOnly(validator, wo, instance, schema):
79+
if not validator.read or not wo:
80+
return
81+
82+
yield ValidationError(
83+
"Tried to read write-only proparty with %s" % (instance))
84+
85+
5786
def not_implemented(validator, value, instance, schema):
5887
pass

openapi_core/schema_validator/validators.py

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@
2121
u"uniqueItems": _validators.uniqueItems,
2222
u"maxProperties": _validators.maxProperties,
2323
u"minProperties": _validators.minProperties,
24-
u"required": _validators.required,
2524
u"enum": _validators.enum,
2625
# adjusted to OAS
2726
u"type": oas_validators.type,
@@ -31,6 +30,7 @@
3130
u"not": _validators.not_,
3231
u"items": oas_validators.items,
3332
u"properties": _validators.properties,
33+
u"required": oas_validators.required,
3434
u"additionalProperties": oas_validators.additionalProperties,
3535
# TODO: adjust description
3636
u"format": oas_validators.format,
@@ -39,8 +39,8 @@
3939
# fixed OAS fields
4040
u"nullable": oas_validators.nullable,
4141
u"discriminator": oas_validators.not_implemented,
42-
u"readOnly": oas_validators.not_implemented,
43-
u"writeOnly": oas_validators.not_implemented,
42+
u"readOnly": oas_validators.readOnly,
43+
u"writeOnly": oas_validators.writeOnly,
4444
u"xml": oas_validators.not_implemented,
4545
u"externalDocs": oas_validators.not_implemented,
4646
u"example": oas_validators.not_implemented,
@@ -54,6 +54,11 @@
5454

5555
class OAS30Validator(BaseOAS30Validator):
5656

57+
def __init__(self, *args, **kwargs):
58+
self.read = kwargs.pop('read', None)
59+
self.write = kwargs.pop('write', None)
60+
super(OAS30Validator, self).__init__(*args, **kwargs)
61+
5762
def iter_errors(self, instance, _schema=None):
5863
if _schema is None:
5964
_schema = self.schema
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
"""OpenAPI core unmarshalling schemas enums module"""
2+
from enum import Enum
3+
4+
5+
class UnmarshalContext(Enum):
6+
REQUEST = 'request'
7+
RESPONSE = 'response'

openapi_core/unmarshalling/schemas/factories.py

Lines changed: 24 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
from openapi_core.schema.schemas.models import Schema
66
from openapi_core.schema_validator import OAS30Validator
77
from openapi_core.schema_validator import oas30_format_checker
8+
from openapi_core.unmarshalling.schemas.enums import UnmarshalContext
89
from openapi_core.unmarshalling.schemas.exceptions import (
910
FormatterNotFoundError,
1011
)
@@ -29,11 +30,17 @@ class SchemaUnmarshallersFactory(object):
2930
SchemaType.ANY: AnyUnmarshaller,
3031
}
3132

32-
def __init__(self, resolver=None, custom_formatters=None):
33+
CONTEXT_VALIDATION = {
34+
UnmarshalContext.REQUEST: 'write',
35+
UnmarshalContext.RESPONSE: 'read',
36+
}
37+
38+
def __init__(self, resolver=None, custom_formatters=None, context=None):
3339
self.resolver = resolver
3440
if custom_formatters is None:
3541
custom_formatters = {}
3642
self.custom_formatters = custom_formatters
43+
self.context = context
3744

3845
def create(self, schema, type_override=None):
3946
"""Create unmarshaller from the schema."""
@@ -50,7 +57,9 @@ def create(self, schema, type_override=None):
5057
elif schema_type in self.COMPLEX_UNMARSHALLERS:
5158
klass = self.COMPLEX_UNMARSHALLERS[schema_type]
5259
kwargs = dict(
53-
schema=schema, unmarshallers_factory=self)
60+
schema=schema, unmarshallers_factory=self,
61+
context=self.context,
62+
)
5463

5564
formatter = self.get_formatter(klass.FORMATTERS, schema.format)
5665

@@ -70,10 +79,17 @@ def get_formatter(self, default_formatters, type_format=SchemaFormat.NONE):
7079
return default_formatters.get(schema_format)
7180

7281
def get_validator(self, schema):
73-
format_checker = deepcopy(oas30_format_checker)
82+
format_checker = self._get_format_checker()
83+
kwargs = {
84+
'resolver': self.resolver,
85+
'format_checker': format_checker,
86+
}
87+
if self.context is not None:
88+
kwargs[self.CONTEXT_VALIDATION[self.context]] = True
89+
return OAS30Validator(schema.__dict__, **kwargs)
90+
91+
def _get_format_checker(self):
92+
fc = deepcopy(oas30_format_checker)
7493
for name, formatter in self.custom_formatters.items():
75-
format_checker.checks(name)(formatter.validate)
76-
return OAS30Validator(
77-
schema.__dict__,
78-
resolver=self.resolver, format_checker=format_checker,
79-
)
94+
fc.checks(name)(formatter.validate)
95+
return fc

openapi_core/unmarshalling/schemas/unmarshallers.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
is_object, is_number, is_string,
1414
)
1515
from openapi_core.schema_validator._format import oas30_format_checker
16+
from openapi_core.unmarshalling.schemas.enums import UnmarshalContext
1617
from openapi_core.unmarshalling.schemas.exceptions import (
1718
UnmarshalError, ValidateError, InvalidSchemaValue,
1819
InvalidSchemaFormatValue,
@@ -120,9 +121,12 @@ class BooleanUnmarshaller(PrimitiveTypeUnmarshaller):
120121

121122
class ComplexUnmarshaller(PrimitiveTypeUnmarshaller):
122123

123-
def __init__(self, formatter, validator, schema, unmarshallers_factory):
124+
def __init__(
125+
self, formatter, validator, schema, unmarshallers_factory,
126+
context=None):
124127
super(ComplexUnmarshaller, self).__init__(formatter, validator, schema)
125128
self.unmarshallers_factory = unmarshallers_factory
129+
self.context = context
126130

127131

128132
class ArrayUnmarshaller(ComplexUnmarshaller):
@@ -206,6 +210,10 @@ def _unmarshal_properties(self, value=NoValue, one_of_schema=None):
206210
properties[prop_name] = prop_value
207211

208212
for prop_name, prop in iteritems(all_props):
213+
if self.context == UnmarshalContext.REQUEST and prop.read_only:
214+
continue
215+
if self.context == UnmarshalContext.RESPONSE and prop.write_only:
216+
continue
209217
try:
210218
prop_value = value[prop_name]
211219
except KeyError:

openapi_core/validation/request/validators.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,9 @@
1111
)
1212
from openapi_core.schema.paths.exceptions import InvalidPath
1313
from openapi_core.schema.request_bodies.exceptions import MissingRequestBody
14-
from openapi_core.schema.schemas.enums import UnmarshalContext
1514
from openapi_core.schema.servers.exceptions import InvalidServer
1615
from openapi_core.security.exceptions import SecurityError
16+
from openapi_core.unmarshalling.schemas.enums import UnmarshalContext
1717
from openapi_core.unmarshalling.schemas.exceptions import (
1818
UnmarshalError, ValidateError,
1919
)
@@ -24,8 +24,6 @@
2424
from openapi_core.validation.util import get_operation_pattern
2525

2626

27-
_CONTEXT = UnmarshalContext.REQUEST
28-
2927
class RequestValidator(object):
3028

3129
def __init__(
@@ -259,7 +257,9 @@ def _unmarshal(self, param_or_media_type, value):
259257
SchemaUnmarshallersFactory,
260258
)
261259
unmarshallers_factory = SchemaUnmarshallersFactory(
262-
self.spec._resolver, self.custom_formatters)
260+
self.spec._resolver, self.custom_formatters,
261+
context=UnmarshalContext.REQUEST,
262+
)
263263
unmarshaller = unmarshallers_factory.create(
264264
param_or_media_type.schema)
265265
return unmarshaller(value)

openapi_core/validation/response/validators.py

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,16 +6,14 @@
66
from openapi_core.schema.responses.exceptions import (
77
InvalidResponse, MissingResponseContent,
88
)
9-
from openapi_core.schema.schemas.enums import UnmarshalContext
109
from openapi_core.schema.servers.exceptions import InvalidServer
10+
from openapi_core.unmarshalling.schemas.enums import UnmarshalContext
1111
from openapi_core.unmarshalling.schemas.exceptions import (
1212
UnmarshalError, ValidateError,
1313
)
1414
from openapi_core.validation.response.datatypes import ResponseValidationResult
1515
from openapi_core.validation.util import get_operation_pattern
1616

17-
_CONTEXT = UnmarshalContext.RESPONSE
18-
1917

2018
class ResponseValidator(object):
2119

@@ -76,7 +74,6 @@ def _get_data(self, response, operation_response):
7674

7775
try:
7876
media_type = operation_response[response.mimetype]
79-
8077
except InvalidContentType as exc:
8178
return None, [exc, ]
8279

@@ -143,7 +140,9 @@ def _unmarshal(self, param_or_media_type, value):
143140
SchemaUnmarshallersFactory,
144141
)
145142
unmarshallers_factory = SchemaUnmarshallersFactory(
146-
self.spec._resolver, self.custom_formatters)
143+
self.spec._resolver, self.custom_formatters,
144+
context=UnmarshalContext.RESPONSE,
145+
)
147146
unmarshaller = unmarshallers_factory.create(
148147
param_or_media_type.schema)
149148
return unmarshaller(value)

tests/integration/schema/test_spec.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -268,5 +268,9 @@ def test_spec(self, spec, spec_dict):
268268
if not spec.components:
269269
return
270270

271-
for _, schema in iteritems(spec.components.schemas):
271+
for schema_name, schema in iteritems(spec.components.schemas):
272272
assert type(schema) == Schema
273+
274+
schema_spec = spec_dict['components']['schemas'][schema_name]
275+
assert schema.read_only == schema_spec.get('readOnly', False)
276+
assert schema.write_only == schema_spec.get('writeOnly', False)

tests/integration/test_read_only.py

Lines changed: 0 additions & 65 deletions
This file was deleted.

0 commit comments

Comments
 (0)