Skip to content

Commit 778b4a5

Browse files
authored
Merge pull request #152 from phrfpeixoto/read_only_write_only
Yet another readOnly and writeOnly support
2 parents b6fdd64 + 1bea601 commit 778b4a5

File tree

13 files changed

+279
-19
lines changed

13 files changed

+279
-19
lines changed

openapi_core/schema/schemas/factories.py

+3-1
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,8 @@ def create(self, schema_spec):
4646
exclusive_maximum = schema_deref.get('exclusiveMaximum', False)
4747
min_properties = schema_deref.get('minProperties', None)
4848
max_properties = schema_deref.get('maxProperties', None)
49+
read_only = schema_deref.get('readOnly', False)
50+
write_only = schema_deref.get('writeOnly', False)
4951

5052
extensions = self.extensions_generator.generate(schema_deref)
5153

@@ -81,7 +83,7 @@ def create(self, schema_spec):
8183
exclusive_maximum=exclusive_maximum,
8284
exclusive_minimum=exclusive_minimum,
8385
min_properties=min_properties, max_properties=max_properties,
84-
extensions=extensions,
86+
read_only=read_only, write_only=write_only, extensions=extensions,
8587
_source=schema_deref,
8688
)
8789

openapi_core/schema/schemas/models.py

+4-1
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,8 @@ def __init__(
2626
min_length=None, max_length=None, pattern=None, unique_items=False,
2727
minimum=None, maximum=None, multiple_of=None,
2828
exclusive_minimum=False, exclusive_maximum=False,
29-
min_properties=None, max_properties=None, extensions=None,
29+
min_properties=None, max_properties=None,
30+
read_only=False, write_only=False, extensions=None,
3031
_source=None):
3132
self.type = SchemaType(schema_type)
3233
self.properties = properties and dict(properties) or {}
@@ -56,6 +57,8 @@ def __init__(
5657
if min_properties is not None else None
5758
self.max_properties = int(max_properties)\
5859
if max_properties is not None else None
60+
self.read_only = read_only
61+
self.write_only = write_only
5962

6063
self.extensions = extensions and dict(extensions) or {}
6164

openapi_core/schema_validator/_validators.py

+29
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

+8-3
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
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

+24-8
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

+9-1
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

+4-1
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
from openapi_core.schema.request_bodies.exceptions import MissingRequestBody
1414
from openapi_core.schema.servers.exceptions import InvalidServer
1515
from openapi_core.security.exceptions import SecurityError
16+
from openapi_core.unmarshalling.schemas.enums import UnmarshalContext
1617
from openapi_core.unmarshalling.schemas.exceptions import (
1718
UnmarshalError, ValidateError,
1819
)
@@ -256,7 +257,9 @@ def _unmarshal(self, param_or_media_type, value):
256257
SchemaUnmarshallersFactory,
257258
)
258259
unmarshallers_factory = SchemaUnmarshallersFactory(
259-
self.spec._resolver, self.custom_formatters)
260+
self.spec._resolver, self.custom_formatters,
261+
context=UnmarshalContext.REQUEST,
262+
)
260263
unmarshaller = unmarshallers_factory.create(
261264
param_or_media_type.schema)
262265
return unmarshaller(value)

openapi_core/validation/response/validators.py

+4-1
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
InvalidResponse, MissingResponseContent,
88
)
99
from openapi_core.schema.servers.exceptions import InvalidServer
10+
from openapi_core.unmarshalling.schemas.enums import UnmarshalContext
1011
from openapi_core.unmarshalling.schemas.exceptions import (
1112
UnmarshalError, ValidateError,
1213
)
@@ -139,7 +140,9 @@ def _unmarshal(self, param_or_media_type, value):
139140
SchemaUnmarshallersFactory,
140141
)
141142
unmarshallers_factory = SchemaUnmarshallersFactory(
142-
self.spec._resolver, self.custom_formatters)
143+
self.spec._resolver, self.custom_formatters,
144+
context=UnmarshalContext.RESPONSE,
145+
)
143146
unmarshaller = unmarshallers_factory.create(
144147
param_or_media_type.schema)
145148
return unmarshaller(value)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
openapi: "3.0.0"
2+
info:
3+
title: Specification Containing readOnly
4+
version: "0.1"
5+
paths:
6+
/users:
7+
post:
8+
operationId: createUser
9+
requestBody:
10+
description: Post data for creating a user
11+
required: true
12+
content:
13+
application/json:
14+
schema:
15+
$ref: '#/components/schemas/User'
16+
responses:
17+
default:
18+
description: Create a user
19+
content:
20+
application/json:
21+
schema:
22+
$ref: '#/components/schemas/User'
23+
components:
24+
schemas:
25+
User:
26+
type: object
27+
required:
28+
- id
29+
- name
30+
properties:
31+
id:
32+
type: integer
33+
format: int32
34+
readOnly: true
35+
name:
36+
type: string
37+
hidden:
38+
type: boolean
39+
writeOnly: true

tests/integration/schema/test_spec.py

+5-1
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)

0 commit comments

Comments
 (0)