Skip to content

Commit b2410e2

Browse files
committed
OAS 3.0 validator
1 parent c4c5163 commit b2410e2

File tree

19 files changed

+375
-66
lines changed

19 files changed

+375
-66
lines changed

openapi_core/schema/media_types/models.py

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,17 +32,26 @@ 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+
48+
try:
49+
self.schema.validate(value, resolver=resolver)
50+
except OpenAPISchemaError as exc:
51+
raise InvalidMediaTypeValue(exc)
52+
4453
try:
45-
unmarshalled = self.schema.unmarshal(deserialized, custom_formatters=custom_formatters)
54+
unmarshalled = self.schema.unmarshal(value, custom_formatters=custom_formatters)
4655
except OpenAPISchemaError as exc:
4756
raise InvalidMediaTypeValue(exc)
4857

openapi_core/schema/parameters/models.py

Lines changed: 13 additions & 4 deletions
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,13 +109,22 @@ 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)
113+
except OpenAPISchemaError as exc:
114+
raise InvalidParameterValue(self.name, exc)
115+
116+
def unmarshal(self, value, custom_formatters=None, resolver=None):
117+
if not self.schema:
118+
return value
119+
120+
try:
121+
self.schema.validate(value, resolver=resolver)
113122
except OpenAPISchemaError as exc:
114123
raise InvalidParameterValue(self.name, exc)
115124

116125
try:
117126
unmarshalled = self.schema.unmarshal(
118-
casted,
127+
value,
119128
custom_formatters=custom_formatters,
120129
strict=True,
121130
)
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
from jsonschema._format import FormatChecker
2+
from six import binary_type
3+
4+
oas30_format_checker = FormatChecker()
5+
6+
7+
@oas30_format_checker.checks('binary')
8+
def binary(value):
9+
return isinstance(value, binary_type)

openapi_core/schema/schemas/_types.py

Lines changed: 21 additions & 0 deletions
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+
)
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
from jsonschema.exceptions import ValidationError
2+
3+
4+
def type(validator, data_type, instance, schema):
5+
if instance is None:
6+
return
7+
8+
if not validator.is_type(instance, data_type):
9+
yield ValidationError("%r is not of type %s" % (instance, data_type))
10+
11+
12+
def items(validator, items, instance, schema):
13+
if not validator.is_type(instance, "array"):
14+
return
15+
16+
for index, item in enumerate(instance):
17+
for error in validator.descend(item, items, path=index):
18+
yield error
19+
20+
21+
def nullable(validator, is_nullable, instance, schema):
22+
if instance is None and not is_nullable:
23+
yield ValidationError("None for not nullable")
24+
25+
26+
def not_implemented(validator, value, instance, schema):
27+
pass

openapi_core/schema/schemas/factories.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,11 +50,11 @@ def create(self, schema_spec):
5050

5151
all_of = []
5252
if all_of_spec:
53-
all_of = map(self.create, all_of_spec)
53+
all_of = list(map(self.create, all_of_spec))
5454

5555
one_of = []
5656
if one_of_spec:
57-
one_of = map(self.create, one_of_spec)
57+
one_of = list(map(self.create, one_of_spec))
5858

5959
items = None
6060
if items_spec:
@@ -76,6 +76,7 @@ def create(self, schema_spec):
7676
exclusive_maximum=exclusive_maximum,
7777
exclusive_minimum=exclusive_minimum,
7878
min_properties=min_properties, max_properties=max_properties,
79+
_source=schema_deref,
7980
)
8081

8182
@property

openapi_core/schema/schemas/models.py

Lines changed: 20 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,10 @@
99
import warnings
1010

1111
from six import iteritems, integer_types, binary_type, text_type
12+
from jsonschema.exceptions import ValidationError
1213

1314
from openapi_core.extensions.models.factories import ModelFactory
15+
from openapi_core.schema.schemas._format import oas30_format_checker
1416
from openapi_core.schema.schemas.enums import SchemaFormat, SchemaType
1517
from openapi_core.schema.schemas.exceptions import (
1618
InvalidSchemaValue, UndefinedSchemaProperty, MissingSchemaProperty,
@@ -23,7 +25,7 @@
2325
format_number,
2426
)
2527
from openapi_core.schema.schemas.validators import (
26-
TypeValidator, AttributeValidator,
28+
TypeValidator, AttributeValidator, OAS30Validator,
2729
)
2830

2931
log = logging.getLogger(__name__)
@@ -85,7 +87,7 @@ def __init__(
8587
min_length=None, max_length=None, pattern=None, unique_items=False,
8688
minimum=None, maximum=None, multiple_of=None,
8789
exclusive_minimum=False, exclusive_maximum=False,
88-
min_properties=None, max_properties=None):
90+
min_properties=None, max_properties=None, _source=None):
8991
self.type = SchemaType(schema_type)
9092
self.model = model
9193
self.properties = properties and dict(properties) or {}
@@ -119,6 +121,8 @@ def __init__(
119121
self._all_required_properties_cache = None
120122
self._all_optional_properties_cache = None
121123

124+
self._source = _source
125+
122126
def __getitem__(self, name):
123127
return self.properties[name]
124128

@@ -214,6 +218,18 @@ def get_unmarshal_mapping(self, custom_formatters=None, strict=True):
214218

215219
return defaultdict(lambda: lambda x: x, mapping)
216220

221+
def get_validator(self, resolver=None):
222+
return OAS30Validator(
223+
self._source, resolver=resolver, format_checker=oas30_format_checker)
224+
225+
def validate(self, value, resolver=None):
226+
validator = self.get_validator(resolver=resolver)
227+
try:
228+
return validator.validate(value)
229+
except ValidationError:
230+
# TODO: pass validation errors
231+
raise InvalidSchemaValue("Value not valid for schema", value, self.type)
232+
217233
def unmarshal(self, value, custom_formatters=None, strict=True):
218234
"""Unmarshal parameter from the value."""
219235
if self.deprecated:
@@ -241,10 +257,7 @@ def unmarshal(self, value, custom_formatters=None, strict=True):
241257
"Value {value} is not of type {type}", value, self.type)
242258
except ValueError:
243259
raise InvalidSchemaValue(
244-
"Failed to cast value {value} to type {type}", value, self.type)
245-
246-
if unmarshalled is None and not self.required:
247-
return None
260+
"Failed to unmarshal value {value} to type {type}", value, self.type)
248261

249262
return unmarshalled
250263

@@ -297,8 +310,7 @@ def _unmarshal_any(self, value, custom_formatters=None, strict=True):
297310
return unmarshal_callable(value)
298311
except UnmarshallerStrictTypeError:
299312
continue
300-
# @todo: remove ValueError when validation separated
301-
except (OpenAPISchemaError, TypeError, ValueError):
313+
except (OpenAPISchemaError, TypeError):
302314
continue
303315

304316
raise NoValidSchema(value)
@@ -307,9 +319,6 @@ def _unmarshal_collection(self, value, custom_formatters=None, strict=True):
307319
if not isinstance(value, (list, tuple)):
308320
raise InvalidSchemaValue("Value {value} is not of type {type}", value, self.type)
309321

310-
if self.items is None:
311-
raise UndefinedItemsSchema(self.type)
312-
313322
f = functools.partial(
314323
self.items.unmarshal,
315324
custom_formatters=custom_formatters, strict=strict,

openapi_core/schema/schemas/validators.py

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,10 @@
1+
from jsonschema import _legacy_validators, _format, _types, _utils, _validators
2+
from jsonschema.validators import create
3+
4+
from openapi_core.schema.schemas import _types as oas_types
5+
from openapi_core.schema.schemas import _validators as oas_validators
6+
7+
18
class TypeValidator(object):
29

310
def __init__(self, *types, **options):
@@ -24,3 +31,50 @@ def __call__(self, value):
2431
return False
2532

2633
return True
34+
35+
36+
OAS30Validator = create(
37+
meta_schema=_utils.load_schema("draft4"),
38+
validators={
39+
u"multipleOf": _validators.multipleOf,
40+
u"maximum": _legacy_validators.maximum_draft3_draft4,
41+
u"exclusiveMaximum": _validators.exclusiveMaximum,
42+
u"minimum": _legacy_validators.minimum_draft3_draft4,
43+
u"exclusiveMinimum": _validators.exclusiveMinimum,
44+
u"maxLength": _validators.maxLength,
45+
u"minLength": _validators.minLength,
46+
u"pattern": _validators.pattern,
47+
u"maxItems": _validators.maxItems,
48+
u"minItems": _validators.minItems,
49+
u"uniqueItems": _validators.uniqueItems,
50+
u"maxProperties": _validators.maxProperties,
51+
u"minProperties": _validators.minProperties,
52+
u"required": _validators.required,
53+
u"enum": _validators.enum,
54+
# adjusted to OAS
55+
u"type": oas_validators.type,
56+
u"allOf": _validators.allOf,
57+
u"oneOf": _validators.oneOf,
58+
u"anyOf": _validators.anyOf,
59+
u"not": _validators.not_,
60+
u"items": oas_validators.items,
61+
u"properties": _validators.properties,
62+
u"additionalProperties": _validators.additionalProperties,
63+
# TODO: adjust description
64+
u"format": _validators.format,
65+
# TODO: adjust default
66+
u"$ref": _validators.ref,
67+
# fixed OAS fields
68+
u"nullable": oas_validators.nullable,
69+
u"discriminator": oas_validators.not_implemented,
70+
u"readOnly": oas_validators.not_implemented,
71+
u"writeOnly": oas_validators.not_implemented,
72+
u"xml": oas_validators.not_implemented,
73+
u"externalDocs": oas_validators.not_implemented,
74+
u"example": oas_validators.not_implemented,
75+
u"deprecated": oas_validators.not_implemented,
76+
},
77+
type_checker=oas_types.oas30_type_checker,
78+
version="oas30",
79+
id_of=lambda schema: schema.get(u"id", ""),
80+
)

openapi_core/schema/specs/factories.py

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
"""OpenAPI core specs factories module"""
33

44
from openapi_spec_validator import openapi_v3_spec_validator
5+
from openapi_spec_validator.validators import Dereferencer
56

67
from openapi_core.compat import lru_cache
78
from openapi_core.schema.components.factories import ComponentsFactory
@@ -14,8 +15,8 @@
1415

1516
class SpecFactory(object):
1617

17-
def __init__(self, dereferencer, config=None):
18-
self.dereferencer = dereferencer
18+
def __init__(self, spec_resolver, config=None):
19+
self.spec_resolver = spec_resolver
1920
self.config = config or {}
2021

2122
def create(self, spec_dict, spec_url=''):
@@ -34,9 +35,16 @@ def create(self, spec_dict, spec_url=''):
3435
paths = self.paths_generator.generate(paths)
3536
components = self.components_factory.create(components_spec)
3637
spec = Spec(
37-
info, list(paths), servers=list(servers), components=components)
38+
info, list(paths), servers=list(servers), components=components,
39+
_resolver=self.spec_resolver,
40+
)
3841
return spec
3942

43+
@property
44+
@lru_cache()
45+
def dereferencer(self):
46+
return Dereferencer(self.spec_resolver)
47+
4048
@property
4149
@lru_cache()
4250
def schemas_registry(self):

openapi_core/schema/specs/models.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,14 @@
1414
class Spec(object):
1515
"""Represents an OpenAPI Specification for a service."""
1616

17-
def __init__(self, info, paths, servers=None, components=None):
17+
def __init__(self, info, paths, servers=None, components=None, _resolver=None):
1818
self.info = info
1919
self.paths = paths and dict(paths)
2020
self.servers = servers or []
2121
self.components = components
2222

23+
self._resolver = _resolver
24+
2325
def __getitem__(self, path_pattern):
2426
return self.get_path(path_pattern)
2527

openapi_core/shortcuts.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
"""OpenAPI core shortcuts module"""
22
from jsonschema.validators import RefResolver
3-
from openapi_spec_validator.validators import Dereferencer
43
from openapi_spec_validator import default_handlers
54

65
from openapi_core.schema.media_types.exceptions import OpenAPIMediaTypeError
@@ -17,8 +16,7 @@
1716
def create_spec(spec_dict, spec_url=''):
1817
spec_resolver = RefResolver(
1918
spec_url, spec_dict, handlers=default_handlers)
20-
dereferencer = Dereferencer(spec_resolver)
21-
spec_factory = SpecFactory(dereferencer)
19+
spec_factory = SpecFactory(spec_resolver)
2220
return spec_factory.create(spec_dict, spec_url=spec_url)
2321

2422

0 commit comments

Comments
 (0)