Skip to content

OAS validation with JSONSchema #157

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Sep 13, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 8 additions & 5 deletions openapi_core/schema/media_types/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,22 +32,25 @@ def deserialize(self, value):
deserializer = self.get_dererializer()
return deserializer(value)

def unmarshal(self, value, custom_formatters=None):
def cast(self, value):
if not self.schema:
return value

try:
deserialized = self.deserialize(value)
return self.deserialize(value)
except ValueError as exc:
raise InvalidMediaTypeValue(exc)

def unmarshal(self, value, custom_formatters=None, resolver=None):
if not self.schema:
return value

try:
unmarshalled = self.schema.unmarshal(deserialized, custom_formatters=custom_formatters)
self.schema.validate(value, resolver=resolver)
except OpenAPISchemaError as exc:
raise InvalidMediaTypeValue(exc)

try:
return self.schema.validate(
unmarshalled, custom_formatters=custom_formatters)
return self.schema.unmarshal(value, custom_formatters=custom_formatters)
except OpenAPISchemaError as exc:
raise InvalidMediaTypeValue(exc)
23 changes: 13 additions & 10 deletions openapi_core/schema/parameters/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ def deserialize(self, value):
deserializer = self.get_dererializer()
return deserializer(value)

def get_value(self, request):
def get_raw_value(self, request):
location = request.parameters[self.location.value]

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

return location[self.name]

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

try:
casted = self.schema.cast(deserialized)
return self.schema.cast(deserialized)
except OpenAPISchemaError as exc:
raise InvalidParameterValue(self.name, exc)

def unmarshal(self, value, custom_formatters=None, resolver=None):
if not self.schema:
return value

try:
unmarshalled = self.schema.unmarshal(
casted,
custom_formatters=custom_formatters,
strict=True,
)
self.schema.validate(value, resolver=resolver)
except OpenAPISchemaError as exc:
raise InvalidParameterValue(self.name, exc)

try:
return self.schema.validate(
unmarshalled, custom_formatters=custom_formatters)
return self.schema.unmarshal(
value,
custom_formatters=custom_formatters,
strict=True,
)
except OpenAPISchemaError as exc:
raise InvalidParameterValue(self.name, exc)
115 changes: 115 additions & 0 deletions openapi_core/schema/schemas/_format.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
from base64 import b64encode, b64decode
import binascii
from datetime import datetime
from uuid import UUID

from jsonschema._format import FormatChecker
from jsonschema.exceptions import FormatError
from six import binary_type, text_type, integer_types

DATETIME_HAS_STRICT_RFC3339 = False
DATETIME_HAS_ISODATE = False
DATETIME_RAISES = ()

try:
import isodate
except ImportError:
pass
else:
DATETIME_HAS_ISODATE = True
DATETIME_RAISES += (ValueError, isodate.ISO8601Error)

try:
import strict_rfc3339
except ImportError:
pass
else:
DATETIME_HAS_STRICT_RFC3339 = True
DATETIME_RAISES += (ValueError, TypeError)


class StrictFormatChecker(FormatChecker):

def check(self, instance, format):
if format not in self.checkers:
raise FormatError(
"Format checker for %r format not found" % (format, ))
return super(StrictFormatChecker, self).check(
instance, format)


oas30_format_checker = StrictFormatChecker()


@oas30_format_checker.checks('int32')
def is_int32(instance):
return isinstance(instance, integer_types)


@oas30_format_checker.checks('int64')
def is_int64(instance):
return isinstance(instance, integer_types)


@oas30_format_checker.checks('float')
def is_float(instance):
return isinstance(instance, float)


@oas30_format_checker.checks('double')
def is_double(instance):
# float has double precision in Python
# It's double in CPython and Jython
return isinstance(instance, float)


@oas30_format_checker.checks('binary')
def is_binary(instance):
return isinstance(instance, binary_type)


@oas30_format_checker.checks('byte', raises=(binascii.Error, TypeError))
def is_byte(instance):
if isinstance(instance, text_type):
instance = instance.encode()

return b64encode(b64decode(instance)) == instance


@oas30_format_checker.checks("date-time", raises=DATETIME_RAISES)
def is_datetime(instance):
if isinstance(instance, binary_type):
return False
if not isinstance(instance, text_type):
return True

if DATETIME_HAS_STRICT_RFC3339:
return strict_rfc3339.validate_rfc3339(instance)

if DATETIME_HAS_ISODATE:
return isodate.parse_datetime(instance)

return True


@oas30_format_checker.checks("date", raises=ValueError)
def is_date(instance):
if isinstance(instance, binary_type):
return False
if not isinstance(instance, text_type):
return True
return datetime.strptime(instance, "%Y-%m-%d")


@oas30_format_checker.checks("uuid", raises=AttributeError)
def is_uuid(instance):
if isinstance(instance, binary_type):
return False
if not isinstance(instance, text_type):
return True
try:
uuid_obj = UUID(instance)
except ValueError:
return False

return text_type(uuid_obj) == instance
21 changes: 21 additions & 0 deletions openapi_core/schema/schemas/_types.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
from jsonschema._types import (
TypeChecker, is_any, is_array, is_bool, is_integer,
is_object, is_number,
)
from six import text_type, binary_type


def is_string(checker, instance):
return isinstance(instance, (text_type, binary_type))


oas30_type_checker = TypeChecker(
{
u"string": is_string,
u"number": is_number,
u"integer": is_integer,
u"boolean": is_bool,
u"array": is_array,
u"object": is_object,
},
)
58 changes: 58 additions & 0 deletions openapi_core/schema/schemas/_validators.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
from jsonschema._utils import find_additional_properties, extras_msg
from jsonschema.exceptions import ValidationError, FormatError


def type(validator, data_type, instance, schema):
if instance is None:
return

if not validator.is_type(instance, data_type):
yield ValidationError("%r is not of type %s" % (instance, data_type))


def format(validator, format, instance, schema):
if instance is None:
return

if validator.format_checker is not None:
try:
validator.format_checker.check(instance, format)
except FormatError as error:
yield ValidationError(error.message, cause=error.cause)


def items(validator, items, instance, schema):
if not validator.is_type(instance, "array"):
return

for index, item in enumerate(instance):
for error in validator.descend(item, items, path=index):
yield error


def nullable(validator, is_nullable, instance, schema):
if instance is None and not is_nullable:
yield ValidationError("None for not nullable")


def additionalProperties(validator, aP, instance, schema):
if not validator.is_type(instance, "object"):
return

extras = set(find_additional_properties(instance, schema))

if not extras:
return

if validator.is_type(aP, "object"):
for extra in extras:
for error in validator.descend(instance[extra], aP, path=extra):
yield error
elif validator.is_type(aP, "boolean"):
if not aP:
error = "Additional properties are not allowed (%s %s unexpected)"
yield ValidationError(error % extras_msg(extras))


def not_implemented(validator, value, instance, schema):
pass
16 changes: 0 additions & 16 deletions openapi_core/schema/schemas/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,22 +66,6 @@ def __str__(self):
return "Missing schema property: {0}".format(self.property_name)


@attr.s(hash=True)
class NoOneOfSchema(OpenAPISchemaError):
type = attr.ib()

def __str__(self):
return "Exactly one valid schema type {0} should be valid, None found.".format(self.type)


@attr.s(hash=True)
class MultipleOneOfSchema(OpenAPISchemaError):
type = attr.ib()

def __str__(self):
return "Exactly one schema type {0} should be valid, more than one found".format(self.type)


class UnmarshallerError(OpenAPIMappingError):
pass

Expand Down
64 changes: 62 additions & 2 deletions openapi_core/schema/schemas/factories.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
"""OpenAPI core schemas factories module"""
import logging

from six import iteritems

from openapi_core.compat import lru_cache
from openapi_core.schema.properties.generators import PropertiesGenerator
from openapi_core.schema.schemas.models import Schema
from openapi_core.schema.schemas.types import Contribution

log = logging.getLogger(__name__)

Expand Down Expand Up @@ -50,11 +53,11 @@ def create(self, schema_spec):

all_of = []
if all_of_spec:
all_of = map(self.create, all_of_spec)
all_of = list(map(self.create, all_of_spec))

one_of = []
if one_of_spec:
one_of = map(self.create, one_of_spec)
one_of = list(map(self.create, one_of_spec))

items = None
if items_spec:
Expand All @@ -76,6 +79,7 @@ def create(self, schema_spec):
exclusive_maximum=exclusive_maximum,
exclusive_minimum=exclusive_minimum,
min_properties=min_properties, max_properties=max_properties,
_source=schema_deref,
)

@property
Expand All @@ -85,3 +89,59 @@ def properties_generator(self):

def _create_items(self, items_spec):
return self.create(items_spec)


class SchemaDictFactory(object):

contributions = (
Contribution('type', src_prop_attr='value'),
Contribution('format'),
Contribution('properties', is_dict=True, dest_default={}),
Contribution('required', dest_default=[]),
Contribution('default'),
Contribution('nullable', dest_default=False),
Contribution('all_of', dest_prop_name='allOf', is_list=True, dest_default=[]),
Contribution('one_of', dest_prop_name='oneOf', is_list=True, dest_default=[]),
Contribution('additional_properties', dest_prop_name='additionalProperties', dest_default=True),
Contribution('min_items', dest_prop_name='minItems'),
Contribution('max_items', dest_prop_name='maxItems'),
Contribution('min_length', dest_prop_name='minLength'),
Contribution('max_length', dest_prop_name='maxLength'),
Contribution('pattern', src_prop_attr='pattern'),
Contribution('unique_items', dest_prop_name='uniqueItems', dest_default=False),
Contribution('minimum'),
Contribution('maximum'),
Contribution('multiple_of', dest_prop_name='multipleOf'),
Contribution('exclusive_minimum', dest_prop_name='exclusiveMinimum', dest_default=False),
Contribution('exclusive_maximum', dest_prop_name='exclusiveMaximum', dest_default=False),
Contribution('min_properties', dest_prop_name='minProperties'),
Contribution('max_properties', dest_prop_name='maxProperties'),
)

def create(self, schema):
schema_dict = {}
for contrib in self.contributions:
self._contribute(schema, schema_dict, contrib)
return schema_dict

def _contribute(self, schema, schema_dict, contrib):
def src_map(x):
return getattr(x, '__dict__')
src_val = getattr(schema, contrib.src_prop_name)

if src_val and contrib.src_prop_attr:
src_val = getattr(src_val, contrib.src_prop_attr)

if contrib.is_list:
src_val = list(map(src_map, src_val))
if contrib.is_dict:
src_val = dict(
(k, src_map(v))
for k, v in iteritems(src_val)
)

if src_val == contrib.dest_default:
return

dest_prop_name = contrib.dest_prop_name or contrib.src_prop_name
schema_dict[dest_prop_name] = src_val
Loading