Skip to content

Commit 4690f77

Browse files
authored
Merge pull request #157 from p1c2u/feature/validation-refactor
OAS validation with JSONSchema
2 parents f414b18 + fc60083 commit 4690f77

23 files changed

+736
-518
lines changed

openapi_core/schema/media_types/models.py

+8-5
Original file line numberDiff line numberDiff line change
@@ -32,22 +32,25 @@ def deserialize(self, value):
3232
deserializer = self.get_dererializer()
3333
return deserializer(value)
3434

35-
def unmarshal(self, value, custom_formatters=None):
35+
def cast(self, value):
3636
if not self.schema:
3737
return value
3838

3939
try:
40-
deserialized = self.deserialize(value)
40+
return self.deserialize(value)
4141
except ValueError as exc:
4242
raise InvalidMediaTypeValue(exc)
4343

44+
def unmarshal(self, value, custom_formatters=None, resolver=None):
45+
if not self.schema:
46+
return value
47+
4448
try:
45-
unmarshalled = self.schema.unmarshal(deserialized, custom_formatters=custom_formatters)
49+
self.schema.validate(value, resolver=resolver)
4650
except OpenAPISchemaError as exc:
4751
raise InvalidMediaTypeValue(exc)
4852

4953
try:
50-
return self.schema.validate(
51-
unmarshalled, custom_formatters=custom_formatters)
54+
return self.schema.unmarshal(value, custom_formatters=custom_formatters)
5255
except OpenAPISchemaError as exc:
5356
raise InvalidMediaTypeValue(exc)

openapi_core/schema/parameters/models.py

+13-10
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ def deserialize(self, value):
7272
deserializer = self.get_dererializer()
7373
return deserializer(value)
7474

75-
def get_value(self, request):
75+
def get_raw_value(self, request):
7676
location = request.parameters[self.location.value]
7777

7878
if self.name not in location:
@@ -89,7 +89,7 @@ def get_value(self, request):
8989

9090
return location[self.name]
9191

92-
def unmarshal(self, value, custom_formatters=None):
92+
def cast(self, value):
9393
if self.deprecated:
9494
warnings.warn(
9595
"{0} parameter is deprecated".format(self.name),
@@ -109,21 +109,24 @@ def unmarshal(self, value, custom_formatters=None):
109109
raise InvalidParameterValue(self.name, exc)
110110

111111
try:
112-
casted = self.schema.cast(deserialized)
112+
return self.schema.cast(deserialized)
113113
except OpenAPISchemaError as exc:
114114
raise InvalidParameterValue(self.name, exc)
115115

116+
def unmarshal(self, value, custom_formatters=None, resolver=None):
117+
if not self.schema:
118+
return value
119+
116120
try:
117-
unmarshalled = self.schema.unmarshal(
118-
casted,
119-
custom_formatters=custom_formatters,
120-
strict=True,
121-
)
121+
self.schema.validate(value, resolver=resolver)
122122
except OpenAPISchemaError as exc:
123123
raise InvalidParameterValue(self.name, exc)
124124

125125
try:
126-
return self.schema.validate(
127-
unmarshalled, custom_formatters=custom_formatters)
126+
return self.schema.unmarshal(
127+
value,
128+
custom_formatters=custom_formatters,
129+
strict=True,
130+
)
128131
except OpenAPISchemaError as exc:
129132
raise InvalidParameterValue(self.name, exc)
+115
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
from base64 import b64encode, b64decode
2+
import binascii
3+
from datetime import datetime
4+
from uuid import UUID
5+
6+
from jsonschema._format import FormatChecker
7+
from jsonschema.exceptions import FormatError
8+
from six import binary_type, text_type, integer_types
9+
10+
DATETIME_HAS_STRICT_RFC3339 = False
11+
DATETIME_HAS_ISODATE = False
12+
DATETIME_RAISES = ()
13+
14+
try:
15+
import isodate
16+
except ImportError:
17+
pass
18+
else:
19+
DATETIME_HAS_ISODATE = True
20+
DATETIME_RAISES += (ValueError, isodate.ISO8601Error)
21+
22+
try:
23+
import strict_rfc3339
24+
except ImportError:
25+
pass
26+
else:
27+
DATETIME_HAS_STRICT_RFC3339 = True
28+
DATETIME_RAISES += (ValueError, TypeError)
29+
30+
31+
class StrictFormatChecker(FormatChecker):
32+
33+
def check(self, instance, format):
34+
if format not in self.checkers:
35+
raise FormatError(
36+
"Format checker for %r format not found" % (format, ))
37+
return super(StrictFormatChecker, self).check(
38+
instance, format)
39+
40+
41+
oas30_format_checker = StrictFormatChecker()
42+
43+
44+
@oas30_format_checker.checks('int32')
45+
def is_int32(instance):
46+
return isinstance(instance, integer_types)
47+
48+
49+
@oas30_format_checker.checks('int64')
50+
def is_int64(instance):
51+
return isinstance(instance, integer_types)
52+
53+
54+
@oas30_format_checker.checks('float')
55+
def is_float(instance):
56+
return isinstance(instance, float)
57+
58+
59+
@oas30_format_checker.checks('double')
60+
def is_double(instance):
61+
# float has double precision in Python
62+
# It's double in CPython and Jython
63+
return isinstance(instance, float)
64+
65+
66+
@oas30_format_checker.checks('binary')
67+
def is_binary(instance):
68+
return isinstance(instance, binary_type)
69+
70+
71+
@oas30_format_checker.checks('byte', raises=(binascii.Error, TypeError))
72+
def is_byte(instance):
73+
if isinstance(instance, text_type):
74+
instance = instance.encode()
75+
76+
return b64encode(b64decode(instance)) == instance
77+
78+
79+
@oas30_format_checker.checks("date-time", raises=DATETIME_RAISES)
80+
def is_datetime(instance):
81+
if isinstance(instance, binary_type):
82+
return False
83+
if not isinstance(instance, text_type):
84+
return True
85+
86+
if DATETIME_HAS_STRICT_RFC3339:
87+
return strict_rfc3339.validate_rfc3339(instance)
88+
89+
if DATETIME_HAS_ISODATE:
90+
return isodate.parse_datetime(instance)
91+
92+
return True
93+
94+
95+
@oas30_format_checker.checks("date", raises=ValueError)
96+
def is_date(instance):
97+
if isinstance(instance, binary_type):
98+
return False
99+
if not isinstance(instance, text_type):
100+
return True
101+
return datetime.strptime(instance, "%Y-%m-%d")
102+
103+
104+
@oas30_format_checker.checks("uuid", raises=AttributeError)
105+
def is_uuid(instance):
106+
if isinstance(instance, binary_type):
107+
return False
108+
if not isinstance(instance, text_type):
109+
return True
110+
try:
111+
uuid_obj = UUID(instance)
112+
except ValueError:
113+
return False
114+
115+
return text_type(uuid_obj) == instance

openapi_core/schema/schemas/_types.py

+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
from jsonschema._types import (
2+
TypeChecker, is_any, is_array, is_bool, is_integer,
3+
is_object, is_number,
4+
)
5+
from six import text_type, binary_type
6+
7+
8+
def is_string(checker, instance):
9+
return isinstance(instance, (text_type, binary_type))
10+
11+
12+
oas30_type_checker = TypeChecker(
13+
{
14+
u"string": is_string,
15+
u"number": is_number,
16+
u"integer": is_integer,
17+
u"boolean": is_bool,
18+
u"array": is_array,
19+
u"object": is_object,
20+
},
21+
)
+58
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
from jsonschema._utils import find_additional_properties, extras_msg
2+
from jsonschema.exceptions import ValidationError, FormatError
3+
4+
5+
def type(validator, data_type, instance, schema):
6+
if instance is None:
7+
return
8+
9+
if not validator.is_type(instance, data_type):
10+
yield ValidationError("%r is not of type %s" % (instance, data_type))
11+
12+
13+
def format(validator, format, instance, schema):
14+
if instance is None:
15+
return
16+
17+
if validator.format_checker is not None:
18+
try:
19+
validator.format_checker.check(instance, format)
20+
except FormatError as error:
21+
yield ValidationError(error.message, cause=error.cause)
22+
23+
24+
def items(validator, items, instance, schema):
25+
if not validator.is_type(instance, "array"):
26+
return
27+
28+
for index, item in enumerate(instance):
29+
for error in validator.descend(item, items, path=index):
30+
yield error
31+
32+
33+
def nullable(validator, is_nullable, instance, schema):
34+
if instance is None and not is_nullable:
35+
yield ValidationError("None for not nullable")
36+
37+
38+
def additionalProperties(validator, aP, instance, schema):
39+
if not validator.is_type(instance, "object"):
40+
return
41+
42+
extras = set(find_additional_properties(instance, schema))
43+
44+
if not extras:
45+
return
46+
47+
if validator.is_type(aP, "object"):
48+
for extra in extras:
49+
for error in validator.descend(instance[extra], aP, path=extra):
50+
yield error
51+
elif validator.is_type(aP, "boolean"):
52+
if not aP:
53+
error = "Additional properties are not allowed (%s %s unexpected)"
54+
yield ValidationError(error % extras_msg(extras))
55+
56+
57+
def not_implemented(validator, value, instance, schema):
58+
pass

openapi_core/schema/schemas/exceptions.py

-16
Original file line numberDiff line numberDiff line change
@@ -66,22 +66,6 @@ def __str__(self):
6666
return "Missing schema property: {0}".format(self.property_name)
6767

6868

69-
@attr.s(hash=True)
70-
class NoOneOfSchema(OpenAPISchemaError):
71-
type = attr.ib()
72-
73-
def __str__(self):
74-
return "Exactly one valid schema type {0} should be valid, None found.".format(self.type)
75-
76-
77-
@attr.s(hash=True)
78-
class MultipleOneOfSchema(OpenAPISchemaError):
79-
type = attr.ib()
80-
81-
def __str__(self):
82-
return "Exactly one schema type {0} should be valid, more than one found".format(self.type)
83-
84-
8569
class UnmarshallerError(OpenAPIMappingError):
8670
pass
8771

openapi_core/schema/schemas/factories.py

+62-2
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
"""OpenAPI core schemas factories module"""
22
import logging
33

4+
from six import iteritems
5+
46
from openapi_core.compat import lru_cache
57
from openapi_core.schema.properties.generators import PropertiesGenerator
68
from openapi_core.schema.schemas.models import Schema
9+
from openapi_core.schema.schemas.types import Contribution
710

811
log = logging.getLogger(__name__)
912

@@ -50,11 +53,11 @@ def create(self, schema_spec):
5053

5154
all_of = []
5255
if all_of_spec:
53-
all_of = map(self.create, all_of_spec)
56+
all_of = list(map(self.create, all_of_spec))
5457

5558
one_of = []
5659
if one_of_spec:
57-
one_of = map(self.create, one_of_spec)
60+
one_of = list(map(self.create, one_of_spec))
5861

5962
items = None
6063
if items_spec:
@@ -76,6 +79,7 @@ def create(self, schema_spec):
7679
exclusive_maximum=exclusive_maximum,
7780
exclusive_minimum=exclusive_minimum,
7881
min_properties=min_properties, max_properties=max_properties,
82+
_source=schema_deref,
7983
)
8084

8185
@property
@@ -85,3 +89,59 @@ def properties_generator(self):
8589

8690
def _create_items(self, items_spec):
8791
return self.create(items_spec)
92+
93+
94+
class SchemaDictFactory(object):
95+
96+
contributions = (
97+
Contribution('type', src_prop_attr='value'),
98+
Contribution('format'),
99+
Contribution('properties', is_dict=True, dest_default={}),
100+
Contribution('required', dest_default=[]),
101+
Contribution('default'),
102+
Contribution('nullable', dest_default=False),
103+
Contribution('all_of', dest_prop_name='allOf', is_list=True, dest_default=[]),
104+
Contribution('one_of', dest_prop_name='oneOf', is_list=True, dest_default=[]),
105+
Contribution('additional_properties', dest_prop_name='additionalProperties', dest_default=True),
106+
Contribution('min_items', dest_prop_name='minItems'),
107+
Contribution('max_items', dest_prop_name='maxItems'),
108+
Contribution('min_length', dest_prop_name='minLength'),
109+
Contribution('max_length', dest_prop_name='maxLength'),
110+
Contribution('pattern', src_prop_attr='pattern'),
111+
Contribution('unique_items', dest_prop_name='uniqueItems', dest_default=False),
112+
Contribution('minimum'),
113+
Contribution('maximum'),
114+
Contribution('multiple_of', dest_prop_name='multipleOf'),
115+
Contribution('exclusive_minimum', dest_prop_name='exclusiveMinimum', dest_default=False),
116+
Contribution('exclusive_maximum', dest_prop_name='exclusiveMaximum', dest_default=False),
117+
Contribution('min_properties', dest_prop_name='minProperties'),
118+
Contribution('max_properties', dest_prop_name='maxProperties'),
119+
)
120+
121+
def create(self, schema):
122+
schema_dict = {}
123+
for contrib in self.contributions:
124+
self._contribute(schema, schema_dict, contrib)
125+
return schema_dict
126+
127+
def _contribute(self, schema, schema_dict, contrib):
128+
def src_map(x):
129+
return getattr(x, '__dict__')
130+
src_val = getattr(schema, contrib.src_prop_name)
131+
132+
if src_val and contrib.src_prop_attr:
133+
src_val = getattr(src_val, contrib.src_prop_attr)
134+
135+
if contrib.is_list:
136+
src_val = list(map(src_map, src_val))
137+
if contrib.is_dict:
138+
src_val = dict(
139+
(k, src_map(v))
140+
for k, v in iteritems(src_val)
141+
)
142+
143+
if src_val == contrib.dest_default:
144+
return
145+
146+
dest_prop_name = contrib.dest_prop_name or contrib.src_prop_name
147+
schema_dict[dest_prop_name] = src_val

0 commit comments

Comments
 (0)