mirror of
https://github.com/correl/openapi-core.git
synced 2024-12-01 03:00:09 +00:00
Merge pull request #96 from correl/min-max
Add support for additional validation properties
This commit is contained in:
commit
9c0c2af97f
3 changed files with 478 additions and 1 deletions
|
@ -29,6 +29,19 @@ class SchemaFactory(object):
|
|||
all_of_spec = schema_deref.get('allOf', None)
|
||||
one_of_spec = schema_deref.get('oneOf', None)
|
||||
additional_properties_spec = schema_deref.get('additionalProperties')
|
||||
min_items = schema_deref.get('minItems', None)
|
||||
max_items = schema_deref.get('maxItems', None)
|
||||
min_length = schema_deref.get('minLength', None)
|
||||
max_length = schema_deref.get('maxLength', None)
|
||||
pattern = schema_deref.get('pattern', None)
|
||||
unique_items = schema_deref.get('uniqueItems', None)
|
||||
minimum = schema_deref.get('minimum', None)
|
||||
maximum = schema_deref.get('maximum', None)
|
||||
multiple_of = schema_deref.get('multipleOf', None)
|
||||
exclusive_minimum = schema_deref.get('exclusiveMinimum', False)
|
||||
exclusive_maximum = schema_deref.get('exclusiveMaximum', False)
|
||||
min_properties = schema_deref.get('minProperties', None)
|
||||
max_properties = schema_deref.get('maxProperties', None)
|
||||
|
||||
properties = None
|
||||
if properties_spec:
|
||||
|
@ -56,6 +69,12 @@ class SchemaFactory(object):
|
|||
default=default, nullable=nullable, enum=enum,
|
||||
deprecated=deprecated, all_of=all_of, one_of=one_of,
|
||||
additional_properties=additional_properties,
|
||||
min_items=min_items, max_items=max_items, min_length=min_length,
|
||||
max_length=max_length, pattern=pattern, unique_items=unique_items,
|
||||
minimum=minimum, maximum=maximum, multiple_of=multiple_of,
|
||||
exclusive_maximum=exclusive_maximum,
|
||||
exclusive_minimum=exclusive_minimum,
|
||||
min_properties=min_properties, max_properties=max_properties,
|
||||
)
|
||||
|
||||
@property
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
import logging
|
||||
from collections import defaultdict
|
||||
from datetime import date, datetime
|
||||
import re
|
||||
import warnings
|
||||
|
||||
from six import iteritems, integer_types, binary_type, text_type
|
||||
|
@ -61,7 +62,11 @@ class Schema(object):
|
|||
self, schema_type=None, model=None, properties=None, items=None,
|
||||
schema_format=None, required=None, default=None, nullable=False,
|
||||
enum=None, deprecated=False, all_of=None, one_of=None,
|
||||
additional_properties=None):
|
||||
additional_properties=None, min_items=None, max_items=None,
|
||||
min_length=None, max_length=None, pattern=None, unique_items=False,
|
||||
minimum=None, maximum=None, multiple_of=None,
|
||||
exclusive_minimum=False, exclusive_maximum=False,
|
||||
min_properties=None, max_properties=None):
|
||||
self.type = SchemaType(schema_type)
|
||||
self.model = model
|
||||
self.properties = properties and dict(properties) or {}
|
||||
|
@ -75,6 +80,22 @@ class Schema(object):
|
|||
self.all_of = all_of and list(all_of) or []
|
||||
self.one_of = one_of and list(one_of) or []
|
||||
self.additional_properties = additional_properties
|
||||
self.min_items = int(min_items) if min_items is not None else None
|
||||
self.max_items = int(max_items) if max_items is not None else None
|
||||
self.min_length = int(min_length) if min_length is not None else None
|
||||
self.max_length = int(max_length) if max_length is not None else None
|
||||
self.pattern = pattern and re.compile(pattern) or None
|
||||
self.unique_items = unique_items
|
||||
self.minimum = int(minimum) if minimum is not None else None
|
||||
self.maximum = int(maximum) if maximum is not None else None
|
||||
self.multiple_of = int(multiple_of)\
|
||||
if multiple_of is not None else None
|
||||
self.exclusive_minimum = exclusive_minimum
|
||||
self.exclusive_maximum = exclusive_maximum
|
||||
self.min_properties = int(min_properties)\
|
||||
if min_properties is not None else None
|
||||
self.max_properties = int(max_properties)\
|
||||
if max_properties is not None else None
|
||||
|
||||
self._all_required_properties_cache = None
|
||||
self._all_optional_properties_cache = None
|
||||
|
@ -288,6 +309,8 @@ class Schema(object):
|
|||
SchemaType.ARRAY: self._validate_collection,
|
||||
SchemaType.STRING: self._validate_string,
|
||||
SchemaType.OBJECT: self._validate_object,
|
||||
SchemaType.INTEGER: self._validate_number,
|
||||
SchemaType.NUMBER: self._validate_number,
|
||||
}
|
||||
|
||||
return defaultdict(lambda: lambda x: x, mapping)
|
||||
|
@ -318,8 +341,66 @@ class Schema(object):
|
|||
if self.items is None:
|
||||
raise OpenAPISchemaError("Schema for collection not defined")
|
||||
|
||||
if self.min_items is not None:
|
||||
if self.min_items < 0:
|
||||
raise OpenAPISchemaError(
|
||||
"Schema for collection invalid:"
|
||||
" minItems must be non-negative"
|
||||
)
|
||||
if len(value) < self.min_items:
|
||||
raise InvalidSchemaValue(
|
||||
"Value must contain at least {0} item(s),"
|
||||
" {1} found".format(
|
||||
self.min_items, len(value))
|
||||
)
|
||||
if self.max_items is not None:
|
||||
if self.max_items < 0:
|
||||
raise OpenAPISchemaError(
|
||||
"Schema for collection invalid:"
|
||||
" maxItems must be non-negative"
|
||||
)
|
||||
if len(value) > self.max_items:
|
||||
raise InvalidSchemaValue(
|
||||
"Value must contain at most {0} item(s),"
|
||||
" {1} found".format(
|
||||
self.max_items, len(value))
|
||||
)
|
||||
if self.unique_items and len(set(value)) != len(value):
|
||||
raise InvalidSchemaValue("Value may not contain duplicate items")
|
||||
|
||||
return list(map(self.items.validate, value))
|
||||
|
||||
def _validate_number(self, value):
|
||||
if self.minimum is not None:
|
||||
if self.exclusive_minimum and value <= self.minimum:
|
||||
raise InvalidSchemaValue(
|
||||
"Value {0} is not less than or equal to {1}".format(
|
||||
value, self.minimum)
|
||||
)
|
||||
elif value < self.minimum:
|
||||
raise InvalidSchemaValue(
|
||||
"Value {0} is not less than {1}".format(
|
||||
value, self.minimum)
|
||||
)
|
||||
|
||||
if self.maximum is not None:
|
||||
if self.exclusive_maximum and value >= self.maximum:
|
||||
raise InvalidSchemaValue(
|
||||
"Value {0} is not greater than or equal to {1}".format(
|
||||
value, self.maximum)
|
||||
)
|
||||
elif value > self.maximum:
|
||||
raise InvalidSchemaValue(
|
||||
"Value {0} is not greater than {1}".format(
|
||||
value, self.maximum)
|
||||
)
|
||||
|
||||
if self.multiple_of is not None and value % self.multiple_of:
|
||||
raise InvalidSchemaValue(
|
||||
"Value {0} is not a multiple of {1}".format(
|
||||
value, self.multiple_of)
|
||||
)
|
||||
|
||||
def _validate_string(self, value):
|
||||
try:
|
||||
schema_format = SchemaFormat(self.format)
|
||||
|
@ -338,6 +419,34 @@ class Schema(object):
|
|||
value, self.format)
|
||||
)
|
||||
|
||||
if self.min_length is not None:
|
||||
if self.min_length < 0:
|
||||
raise OpenAPISchemaError(
|
||||
"Schema for string invalid:"
|
||||
" minLength must be non-negative"
|
||||
)
|
||||
if len(value) < self.min_length:
|
||||
raise InvalidSchemaValue(
|
||||
"Value is shorter than the minimum length of {0}".format(
|
||||
self.min_length)
|
||||
)
|
||||
if self.max_length is not None:
|
||||
if self.max_length < 0:
|
||||
raise OpenAPISchemaError(
|
||||
"Schema for string invalid:"
|
||||
" maxLength must be non-negative"
|
||||
)
|
||||
if len(value) > self.max_length:
|
||||
raise InvalidSchemaValue(
|
||||
"Value is longer than the maximum length of {0}".format(
|
||||
self.max_length)
|
||||
)
|
||||
if self.pattern is not None and not self.pattern.search(value):
|
||||
raise InvalidSchemaValue(
|
||||
"Value {0} does not match the pattern {1}".format(
|
||||
value, self.pattern.pattern)
|
||||
)
|
||||
|
||||
return True
|
||||
|
||||
def _validate_object(self, value):
|
||||
|
@ -364,6 +473,33 @@ class Schema(object):
|
|||
else:
|
||||
self._validate_properties(properties)
|
||||
|
||||
if self.min_properties is not None:
|
||||
if self.min_properties < 0:
|
||||
raise OpenAPISchemaError(
|
||||
"Schema for object invalid:"
|
||||
" minProperties must be non-negative"
|
||||
)
|
||||
|
||||
if len(properties) < self.min_properties:
|
||||
raise InvalidSchemaValue(
|
||||
"Value must contain at least {0} properties,"
|
||||
" {1} found".format(
|
||||
self.min_properties, len(properties))
|
||||
)
|
||||
|
||||
if self.max_properties is not None:
|
||||
if self.max_properties < 0:
|
||||
raise OpenAPISchemaError(
|
||||
"Schema for object invalid:"
|
||||
" maxProperties must be non-negative"
|
||||
)
|
||||
if len(properties) > self.max_properties:
|
||||
raise InvalidSchemaValue(
|
||||
"Value must contain at most {0} properties,"
|
||||
" {1} found".format(
|
||||
self.max_properties, len(properties))
|
||||
)
|
||||
|
||||
return True
|
||||
|
||||
def _validate_properties(self, value, one_of_schema=None):
|
||||
|
|
|
@ -246,6 +246,51 @@ class TestSchemaValidate(object):
|
|||
with pytest.raises(InvalidSchemaValue):
|
||||
schema.validate(value)
|
||||
|
||||
@pytest.mark.parametrize('value', [0, 1, 2])
|
||||
def test_integer_minimum_invalid(self, value):
|
||||
schema = Schema('integer', minimum=3)
|
||||
|
||||
with pytest.raises(InvalidSchemaValue):
|
||||
schema.validate(value)
|
||||
|
||||
@pytest.mark.parametrize('value', [4, 5, 6])
|
||||
def test_integer_minimum(self, value):
|
||||
schema = Schema('integer', minimum=3)
|
||||
|
||||
result = schema.validate(value)
|
||||
|
||||
assert result == value
|
||||
|
||||
@pytest.mark.parametrize('value', [4, 5, 6])
|
||||
def test_integer_maximum_invalid(self, value):
|
||||
schema = Schema('integer', maximum=3)
|
||||
|
||||
with pytest.raises(InvalidSchemaValue):
|
||||
schema.validate(value)
|
||||
|
||||
@pytest.mark.parametrize('value', [0, 1, 2])
|
||||
def test_integer_maximum(self, value):
|
||||
schema = Schema('integer', maximum=3)
|
||||
|
||||
result = schema.validate(value)
|
||||
|
||||
assert result == value
|
||||
|
||||
@pytest.mark.parametrize('value', [1, 2, 4])
|
||||
def test_integer_multiple_of_invalid(self, value):
|
||||
schema = Schema('integer', multiple_of=3)
|
||||
|
||||
with pytest.raises(InvalidSchemaValue):
|
||||
schema.validate(value)
|
||||
|
||||
@pytest.mark.parametrize('value', [3, 6, 18])
|
||||
def test_integer_multiple_of(self, value):
|
||||
schema = Schema('integer', multiple_of=3)
|
||||
|
||||
result = schema.validate(value)
|
||||
|
||||
assert result == value
|
||||
|
||||
@pytest.mark.parametrize('value', [1, 3.14])
|
||||
def test_number(self, value):
|
||||
schema = Schema('number')
|
||||
|
@ -261,6 +306,81 @@ class TestSchemaValidate(object):
|
|||
with pytest.raises(InvalidSchemaValue):
|
||||
schema.validate(value)
|
||||
|
||||
@pytest.mark.parametrize('value', [0, 1, 2])
|
||||
def test_number_minimum_invalid(self, value):
|
||||
schema = Schema('number', minimum=3)
|
||||
|
||||
with pytest.raises(InvalidSchemaValue):
|
||||
schema.validate(value)
|
||||
|
||||
@pytest.mark.parametrize('value', [3, 4, 5])
|
||||
def test_number_minimum(self, value):
|
||||
schema = Schema('number', minimum=3)
|
||||
|
||||
result = schema.validate(value)
|
||||
|
||||
assert result == value
|
||||
|
||||
@pytest.mark.parametrize('value', [1, 2, 3])
|
||||
def test_number_exclusive_minimum_invalid(self, value):
|
||||
schema = Schema('number', minimum=3, exclusive_minimum=3)
|
||||
|
||||
with pytest.raises(InvalidSchemaValue):
|
||||
schema.validate(value)
|
||||
|
||||
@pytest.mark.parametrize('value', [4, 5, 6])
|
||||
def test_number_exclusive_minimum(self, value):
|
||||
schema = Schema('number', minimum=3)
|
||||
|
||||
result = schema.validate(value)
|
||||
|
||||
assert result == value
|
||||
|
||||
@pytest.mark.parametrize('value', [4, 5, 6])
|
||||
def test_number_maximum_invalid(self, value):
|
||||
schema = Schema('number', maximum=3)
|
||||
|
||||
with pytest.raises(InvalidSchemaValue):
|
||||
schema.validate(value)
|
||||
|
||||
@pytest.mark.parametrize('value', [1, 2, 3])
|
||||
def test_number_maximum(self, value):
|
||||
schema = Schema('number', maximum=3)
|
||||
|
||||
result = schema.validate(value)
|
||||
|
||||
assert result == value
|
||||
|
||||
@pytest.mark.parametrize('value', [3, 4, 5])
|
||||
def test_number_exclusive_maximum_invalid(self, value):
|
||||
schema = Schema('number', maximum=3, exclusive_maximum=True)
|
||||
|
||||
with pytest.raises(InvalidSchemaValue):
|
||||
schema.validate(value)
|
||||
|
||||
@pytest.mark.parametrize('value', [0, 1, 2])
|
||||
def test_number_exclusive_maximum(self, value):
|
||||
schema = Schema('number', maximum=3, exclusive_maximum=True)
|
||||
|
||||
result = schema.validate(value)
|
||||
|
||||
assert result == value
|
||||
|
||||
@pytest.mark.parametrize('value', [1, 2, 4])
|
||||
def test_number_multiple_of_invalid(self, value):
|
||||
schema = Schema('number', multiple_of=3)
|
||||
|
||||
with pytest.raises(InvalidSchemaValue):
|
||||
schema.validate(value)
|
||||
|
||||
@pytest.mark.parametrize('value', [3, 6, 18])
|
||||
def test_number_multiple_of(self, value):
|
||||
schema = Schema('number', multiple_of=3)
|
||||
|
||||
result = schema.validate(value)
|
||||
|
||||
assert result == value
|
||||
|
||||
@pytest.mark.parametrize('value', [u('true'), ])
|
||||
def test_string(self, value):
|
||||
schema = Schema('string')
|
||||
|
@ -348,6 +468,65 @@ class TestSchemaValidate(object):
|
|||
with pytest.raises(OpenAPISchemaError):
|
||||
schema.validate(value)
|
||||
|
||||
@pytest.mark.parametrize('value', [u(""), ])
|
||||
def test_string_min_length_invalid_schema(self, value):
|
||||
schema = Schema('string', min_length=-1)
|
||||
|
||||
with pytest.raises(OpenAPISchemaError):
|
||||
schema.validate(value)
|
||||
|
||||
@pytest.mark.parametrize('value', [u(""), u("a"), u("ab")])
|
||||
def test_string_min_length_invalid(self, value):
|
||||
schema = Schema('string', min_length=3)
|
||||
|
||||
with pytest.raises(InvalidSchemaValue):
|
||||
schema.validate(value)
|
||||
|
||||
@pytest.mark.parametrize('value', [u("abc"), u("abcd")])
|
||||
def test_string_min_length(self, value):
|
||||
schema = Schema('string', min_length=3)
|
||||
|
||||
result = schema.validate(value)
|
||||
|
||||
assert result == value
|
||||
|
||||
@pytest.mark.parametrize('value', [u(""), ])
|
||||
def test_string_max_length_invalid_schema(self, value):
|
||||
schema = Schema('string', max_length=-1)
|
||||
|
||||
with pytest.raises(OpenAPISchemaError):
|
||||
schema.validate(value)
|
||||
|
||||
@pytest.mark.parametrize('value', [u("ab"), u("abc")])
|
||||
def test_string_max_length_invalid(self, value):
|
||||
schema = Schema('string', max_length=1)
|
||||
|
||||
with pytest.raises(InvalidSchemaValue):
|
||||
schema.validate(value)
|
||||
|
||||
@pytest.mark.parametrize('value', [u(""), u("a")])
|
||||
def test_string_max_length(self, value):
|
||||
schema = Schema('string', max_length=1)
|
||||
|
||||
result = schema.validate(value)
|
||||
|
||||
assert result == value
|
||||
|
||||
@pytest.mark.parametrize('value', [u("foo"), u("bar")])
|
||||
def test_string_pattern_invalid(self, value):
|
||||
schema = Schema('string', pattern='baz')
|
||||
|
||||
with pytest.raises(InvalidSchemaValue):
|
||||
schema.validate(value)
|
||||
|
||||
@pytest.mark.parametrize('value', [u("bar"), u("foobar")])
|
||||
def test_string_pattern(self, value):
|
||||
schema = Schema('string', pattern='bar')
|
||||
|
||||
result = schema.validate(value)
|
||||
|
||||
assert result == value
|
||||
|
||||
@pytest.mark.parametrize('value', ['true', False, 1, 3.14, [1, 3]])
|
||||
def test_object_not_an_object(self, value):
|
||||
schema = Schema('object')
|
||||
|
@ -401,3 +580,146 @@ class TestSchemaValidate(object):
|
|||
result = schema.validate(value)
|
||||
|
||||
assert result == value
|
||||
|
||||
@pytest.mark.parametrize('value', [Model(), ])
|
||||
def test_object_min_properties_invalid_schema(self, value):
|
||||
schema = Schema('object', min_properties=-1)
|
||||
|
||||
with pytest.raises(OpenAPISchemaError):
|
||||
schema.validate(value)
|
||||
|
||||
@pytest.mark.parametrize('value', [
|
||||
Model({'a': 1}),
|
||||
Model({'a': 1, 'b': 2}),
|
||||
Model({'a': 1, 'b': 2, 'c': 3})])
|
||||
def test_object_min_properties_invalid(self, value):
|
||||
schema = Schema(
|
||||
'object',
|
||||
properties={k: Schema('number')
|
||||
for k in ['a', 'b', 'c']},
|
||||
min_properties=4,
|
||||
)
|
||||
|
||||
with pytest.raises(InvalidSchemaValue):
|
||||
schema.validate(value)
|
||||
|
||||
@pytest.mark.parametrize('value', [
|
||||
Model({'a': 1}),
|
||||
Model({'a': 1, 'b': 2}),
|
||||
Model({'a': 1, 'b': 2, 'c': 3})])
|
||||
def test_object_min_properties(self, value):
|
||||
schema = Schema(
|
||||
'object',
|
||||
properties={k: Schema('number')
|
||||
for k in ['a', 'b', 'c']},
|
||||
min_properties=1,
|
||||
)
|
||||
|
||||
result = schema.validate(value)
|
||||
|
||||
assert result == value
|
||||
|
||||
@pytest.mark.parametrize('value', [Model(), ])
|
||||
def test_object_max_properties_invalid_schema(self, value):
|
||||
schema = Schema('object', max_properties=-1)
|
||||
|
||||
with pytest.raises(OpenAPISchemaError):
|
||||
schema.validate(value)
|
||||
|
||||
@pytest.mark.parametrize('value', [
|
||||
Model({'a': 1}),
|
||||
Model({'a': 1, 'b': 2}),
|
||||
Model({'a': 1, 'b': 2, 'c': 3})])
|
||||
def test_object_max_properties_invalid(self, value):
|
||||
schema = Schema(
|
||||
'object',
|
||||
properties={k: Schema('number')
|
||||
for k in ['a', 'b', 'c']},
|
||||
max_properties=0,
|
||||
)
|
||||
|
||||
with pytest.raises(InvalidSchemaValue):
|
||||
schema.validate(value)
|
||||
|
||||
@pytest.mark.parametrize('value', [
|
||||
Model({'a': 1}),
|
||||
Model({'a': 1, 'b': 2}),
|
||||
Model({'a': 1, 'b': 2, 'c': 3})])
|
||||
def test_object_max_properties(self, value):
|
||||
schema = Schema(
|
||||
'object',
|
||||
properties={k: Schema('number')
|
||||
for k in ['a', 'b', 'c']},
|
||||
max_properties=3,
|
||||
)
|
||||
|
||||
result = schema.validate(value)
|
||||
|
||||
assert result == value
|
||||
|
||||
@pytest.mark.parametrize('value', [[], ])
|
||||
def test_list_min_items_invalid_schema(self, value):
|
||||
schema = Schema(
|
||||
'array',
|
||||
items=Schema('number'),
|
||||
min_items=-1,
|
||||
)
|
||||
|
||||
with pytest.raises(OpenAPISchemaError):
|
||||
schema.validate(value)
|
||||
|
||||
@pytest.mark.parametrize('value', [[], [1], [1, 2]])
|
||||
def test_list_min_items_invalid(self, value):
|
||||
schema = Schema(
|
||||
'array',
|
||||
items=Schema('number'),
|
||||
min_items=3,
|
||||
)
|
||||
|
||||
with pytest.raises(Exception):
|
||||
schema.validate(value)
|
||||
|
||||
@pytest.mark.parametrize('value', [[], [1], [1, 2]])
|
||||
def test_list_min_items(self, value):
|
||||
schema = Schema(
|
||||
'array',
|
||||
items=Schema('number'),
|
||||
min_items=0,
|
||||
)
|
||||
|
||||
result = schema.validate(value)
|
||||
|
||||
assert result == value
|
||||
|
||||
@pytest.mark.parametrize('value', [[], ])
|
||||
def test_list_max_items_invalid_schema(self, value):
|
||||
schema = Schema(
|
||||
'array',
|
||||
items=Schema('number'),
|
||||
max_items=-1,
|
||||
)
|
||||
|
||||
with pytest.raises(OpenAPISchemaError):
|
||||
schema.validate(value)
|
||||
|
||||
@pytest.mark.parametrize('value', [[1, 2], [2, 3, 4]])
|
||||
def test_list_max_items_invalid(self, value):
|
||||
schema = Schema(
|
||||
'array',
|
||||
items=Schema('number'),
|
||||
max_items=1,
|
||||
)
|
||||
|
||||
with pytest.raises(Exception):
|
||||
schema.validate(value)
|
||||
|
||||
@pytest.mark.parametrize('value', [[1, 2, 1], [2, 2]])
|
||||
def test_list_unique_items_invalid(self, value):
|
||||
schema = Schema(
|
||||
'array',
|
||||
items=Schema('number'),
|
||||
unique_items=True,
|
||||
)
|
||||
|
||||
with pytest.raises(Exception):
|
||||
schema.validate(value)
|
||||
|
|
Loading…
Reference in a new issue