Skip to content

Commit 0531956

Browse files
committed
OAS30 read write validators
1 parent 14c6149 commit 0531956

File tree

5 files changed

+226
-29
lines changed

5 files changed

+226
-29
lines changed

Diff for: README.rst

+37-1
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,43 @@ In order to validate OpenAPI 3.0 schema, import and use ``OAS30Validator`` inste
130130
"additionalProperties": False,
131131
}
132132
133-
validate({"name": "John", "age": 23}, schema, cls=OAS30Validator)
133+
validate({"name": "John", "age": None}, schema, cls=OAS30Validator)
134+
135+
In order to validate read/write context in OpenAPI 3.0 schema, import and use ``OAS30ReadValidator`` or ``OAS30WriteValidator``.
136+
137+
.. code-block:: python
138+
139+
from openapi_schema_validator import OAS30WriteValidator
140+
141+
# A sample schema
142+
schema = {
143+
"type": "object",
144+
"required": [
145+
"name"
146+
],
147+
"properties": {
148+
"name": {
149+
"type": "string"
150+
},
151+
"age": {
152+
"type": "integer",
153+
"format": "int32",
154+
"minimum": 0,
155+
"readOnly": True,
156+
},
157+
"birth-date": {
158+
"type": "string",
159+
"format": "date",
160+
}
161+
},
162+
"additionalProperties": False,
163+
}
164+
165+
validate({"name": "John", "age": 23}, schema, cls=OAS30WriteValidator)
166+
167+
Traceback (most recent call last):
168+
...
169+
ValidationError: Tried to write read-only property with 23
134170
135171
Format check
136172
************

Diff for: openapi_schema_validator/__init__.py

+4
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
from openapi_schema_validator._format import oas30_format_checker
22
from openapi_schema_validator._format import oas31_format_checker
33
from openapi_schema_validator.shortcuts import validate
4+
from openapi_schema_validator.validators import OAS30ReadValidator
5+
from openapi_schema_validator.validators import OAS30WriteValidator
46
from openapi_schema_validator.validators import OAS30Validator
57
from openapi_schema_validator.validators import OAS31Validator
68

@@ -12,6 +14,8 @@
1214

1315
__all__ = [
1416
"validate",
17+
"OAS30ReadValidator",
18+
"OAS30WriteValidator",
1519
"OAS30Validator",
1620
"oas30_format_checker",
1721
"OAS31Validator",

Diff for: openapi_schema_validator/_validators.py

+43-4
Original file line numberDiff line numberDiff line change
@@ -165,15 +165,54 @@ def required(
165165
read_only = prop_schema.get("readOnly", False)
166166
write_only = prop_schema.get("writeOnly", False)
167167
if (
168-
validator.write
168+
getattr(validator, "write", True)
169169
and read_only
170-
or validator.read
170+
or getattr(validator, "read", True)
171171
and write_only
172172
):
173173
continue
174174
yield ValidationError(f"{property!r} is a required property")
175175

176176

177+
def read_required(
178+
validator: Validator,
179+
required: List[str],
180+
instance: Any,
181+
schema: Mapping[Hashable, Any],
182+
) -> Iterator[ValidationError]:
183+
if not validator.is_type(instance, "object"):
184+
return
185+
for property in required:
186+
if property not in instance:
187+
prop_schema = schema.get("properties", {}).get(property)
188+
if prop_schema:
189+
write_only = prop_schema.get("writeOnly", False)
190+
if (
191+
getattr(validator, "read", True)
192+
and write_only
193+
):
194+
continue
195+
yield ValidationError(f"{property!r} is a required property")
196+
197+
198+
def write_required(
199+
validator: Validator,
200+
required: List[str],
201+
instance: Any,
202+
schema: Mapping[Hashable, Any],
203+
) -> Iterator[ValidationError]:
204+
if not validator.is_type(instance, "object"):
205+
return
206+
for property in required:
207+
if property not in instance:
208+
prop_schema = schema.get("properties", {}).get(property)
209+
if prop_schema:
210+
read_only = prop_schema.get("readOnly", False)
211+
if read_only:
212+
continue
213+
yield ValidationError(f"{property!r} is a required property")
214+
215+
177216
def additionalProperties(
178217
validator: Validator,
179218
aP: Union[Mapping[Hashable, Any], bool],
@@ -204,7 +243,7 @@ def readOnly(
204243
instance: Any,
205244
schema: Mapping[Hashable, Any],
206245
) -> Iterator[ValidationError]:
207-
if not validator.write or not ro:
246+
if not getattr(validator, "write", True) or not ro:
208247
return
209248

210249
yield ValidationError(f"Tried to write read-only property with {instance}")
@@ -216,7 +255,7 @@ def writeOnly(
216255
instance: Any,
217256
schema: Mapping[Hashable, Any],
218257
) -> Iterator[ValidationError]:
219-
if not validator.read or not wo:
258+
if not getattr(validator, "read", True) or not wo:
220259
return
221260

222261
yield ValidationError(f"Tried to read write-only property with {instance}")

Diff for: openapi_schema_validator/validators.py

+30
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from typing import Any
22
from typing import Type
3+
import warnings
34

45
from jsonschema import _legacy_validators
56
from jsonschema import _utils
@@ -60,6 +61,23 @@
6061
id_of=lambda schema: schema.get("id", ""),
6162
)
6263

64+
OAS30ReadValidator = extend(
65+
OAS30Validator,
66+
validators={
67+
"required": oas_validators.read_required,
68+
"readOnly": oas_validators.not_implemented,
69+
"writeOnly": oas_validators.writeOnly,
70+
},
71+
)
72+
OAS30WriteValidator = extend(
73+
OAS30Validator,
74+
validators={
75+
"required": oas_validators.write_required,
76+
"readOnly": oas_validators.readOnly,
77+
"writeOnly": oas_validators.not_implemented,
78+
},
79+
)
80+
6381
OAS31Validator = extend(
6482
Draft202012Validator,
6583
{
@@ -89,7 +107,19 @@ def _patch_validator_with_read_write_context(cls: Type[Validator]) -> None:
89107

90108
def __init__(self: Validator, *args: Any, **kwargs: Any) -> None:
91109
self.read = kwargs.pop("read", None)
110+
if self.read is not None:
111+
warnings.warn(
112+
"read property is deprecated. "
113+
"Use OAS30ReadValidator instead.",
114+
DeprecationWarning,
115+
)
92116
self.write = kwargs.pop("write", None)
117+
if self.write is not None:
118+
warnings.warn(
119+
"write property is deprecated. "
120+
"Use OAS30WriteValidator instead.",
121+
DeprecationWarning,
122+
)
93123
original_init(self, *args, **kwargs)
94124

95125
def evolve(self: Validator, **changes: Any) -> Validator:

Diff for: tests/integration/test_validators.py

+112-24
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import pytest
22
from jsonschema import ValidationError
33

4+
from openapi_schema_validator import OAS30ReadValidator
5+
from openapi_schema_validator import OAS30WriteValidator
46
from openapi_schema_validator import OAS30Validator
57
from openapi_schema_validator import OAS31Validator
68
from openapi_schema_validator import oas30_format_checker
@@ -258,16 +260,18 @@ def test_read_only(self):
258260
"properties": {"some_prop": {"type": "string", "readOnly": True}},
259261
}
260262

261-
validator = OAS30Validator(
262-
schema, format_checker=oas30_format_checker, write=True
263-
)
263+
with pytest.warns(DeprecationWarning):
264+
validator = OAS30Validator(
265+
schema, format_checker=oas30_format_checker, write=True
266+
)
264267
with pytest.raises(
265268
ValidationError, match="Tried to write read-only property with hello"
266269
):
267270
validator.validate({"some_prop": "hello"})
268-
validator = OAS30Validator(
269-
schema, format_checker=oas30_format_checker, read=True
270-
)
271+
with pytest.warns(DeprecationWarning):
272+
validator = OAS30Validator(
273+
schema, format_checker=oas30_format_checker, read=True
274+
)
271275
assert validator.validate({"some_prop": "hello"}) is None
272276

273277
def test_write_only(self):
@@ -276,16 +280,18 @@ def test_write_only(self):
276280
"properties": {"some_prop": {"type": "string", "writeOnly": True}},
277281
}
278282

279-
validator = OAS30Validator(
280-
schema, format_checker=oas30_format_checker, read=True
281-
)
283+
with pytest.warns(DeprecationWarning):
284+
validator = OAS30Validator(
285+
schema, format_checker=oas30_format_checker, read=True
286+
)
282287
with pytest.raises(
283288
ValidationError, match="Tried to read write-only property with hello"
284289
):
285290
validator.validate({"some_prop": "hello"})
286-
validator = OAS30Validator(
287-
schema, format_checker=oas30_format_checker, write=True
288-
)
291+
with pytest.warns(DeprecationWarning):
292+
validator = OAS30Validator(
293+
schema, format_checker=oas30_format_checker, write=True
294+
)
289295
assert validator.validate({"some_prop": "hello"}) is None
290296

291297
def test_required_read_only(self):
@@ -295,16 +301,18 @@ def test_required_read_only(self):
295301
"required": ["some_prop"],
296302
}
297303

298-
validator = OAS30Validator(
299-
schema, format_checker=oas30_format_checker, read=True
300-
)
304+
with pytest.warns(DeprecationWarning):
305+
validator = OAS30Validator(
306+
schema, format_checker=oas30_format_checker, read=True
307+
)
301308
with pytest.raises(
302309
ValidationError, match="'some_prop' is a required property"
303310
):
304311
validator.validate({"another_prop": "hello"})
305-
validator = OAS30Validator(
306-
schema, format_checker=oas30_format_checker, write=True
307-
)
312+
with pytest.warns(DeprecationWarning):
313+
validator = OAS30Validator(
314+
schema, format_checker=oas30_format_checker, write=True
315+
)
308316
assert validator.validate({"another_prop": "hello"}) is None
309317

310318
def test_required_write_only(self):
@@ -314,16 +322,18 @@ def test_required_write_only(self):
314322
"required": ["some_prop"],
315323
}
316324

317-
validator = OAS30Validator(
318-
schema, format_checker=oas30_format_checker, write=True
319-
)
325+
with pytest.warns(DeprecationWarning):
326+
validator = OAS30Validator(
327+
schema, format_checker=oas30_format_checker, write=True
328+
)
320329
with pytest.raises(
321330
ValidationError, match="'some_prop' is a required property"
322331
):
323332
validator.validate({"another_prop": "hello"})
324-
validator = OAS30Validator(
325-
schema, format_checker=oas30_format_checker, read=True
326-
)
333+
with pytest.warns(DeprecationWarning):
334+
validator = OAS30Validator(
335+
schema, format_checker=oas30_format_checker, read=True
336+
)
327337
assert validator.validate({"another_prop": "hello"}) is None
328338

329339
def test_oneof_required(self):
@@ -567,6 +577,84 @@ def test_nullable_schema_combos(self, is_nullable, schema_type, not_nullable_reg
567577
validator.validate({"testfield": None})
568578
assert False
569579

580+
581+
class TestOAS30ReadWriteValidatorValidate:
582+
583+
def test_read_only(self):
584+
schema = {
585+
"type": "object",
586+
"properties": {"some_prop": {"type": "string", "readOnly": True}},
587+
}
588+
589+
validator = OAS30WriteValidator(
590+
schema, format_checker=oas30_format_checker,
591+
)
592+
with pytest.raises(
593+
ValidationError, match="Tried to write read-only property with hello"
594+
):
595+
validator.validate({"some_prop": "hello"})
596+
validator = OAS30ReadValidator(
597+
schema, format_checker=oas30_format_checker,
598+
)
599+
assert validator.validate({"some_prop": "hello"}) is None
600+
601+
def test_write_only(self):
602+
schema = {
603+
"type": "object",
604+
"properties": {"some_prop": {"type": "string", "writeOnly": True}},
605+
}
606+
607+
validator = OAS30ReadValidator(
608+
schema, format_checker=oas30_format_checker,
609+
)
610+
with pytest.raises(
611+
ValidationError, match="Tried to read write-only property with hello"
612+
):
613+
validator.validate({"some_prop": "hello"})
614+
validator = OAS30WriteValidator(
615+
schema, format_checker=oas30_format_checker,
616+
)
617+
assert validator.validate({"some_prop": "hello"}) is None
618+
619+
def test_required_read_only(self):
620+
schema = {
621+
"type": "object",
622+
"properties": {"some_prop": {"type": "string", "readOnly": True}},
623+
"required": ["some_prop"],
624+
}
625+
626+
validator = OAS30ReadValidator(
627+
schema, format_checker=oas30_format_checker,
628+
)
629+
with pytest.raises(
630+
ValidationError, match="'some_prop' is a required property"
631+
):
632+
validator.validate({"another_prop": "hello"})
633+
validator = OAS30WriteValidator(
634+
schema, format_checker=oas30_format_checker,
635+
)
636+
assert validator.validate({"another_prop": "hello"}) is None
637+
638+
def test_required_write_only(self):
639+
schema = {
640+
"type": "object",
641+
"properties": {"some_prop": {"type": "string", "writeOnly": True}},
642+
"required": ["some_prop"],
643+
}
644+
645+
validator = OAS30WriteValidator(
646+
schema, format_checker=oas30_format_checker,
647+
)
648+
with pytest.raises(
649+
ValidationError, match="'some_prop' is a required property"
650+
):
651+
validator.validate({"another_prop": "hello"})
652+
validator = OAS30ReadValidator(
653+
schema, format_checker=oas30_format_checker,
654+
)
655+
assert validator.validate({"another_prop": "hello"}) is None
656+
657+
570658
class TestOAS31ValidatorValidate:
571659
@pytest.mark.parametrize(
572660
"schema_type",

0 commit comments

Comments
 (0)