Add support for additional validation properties

Add support for the following validation properties:
- multipleOf
- maximum
- exclusiveMaximum
- minimum
- exclusiveMinimum
- maxLength
- minLength
- pattern
- maxItems
- minItems
- uniqueItems
- maxProperties
- minProperties

Fixes #49
This commit is contained in:
Correl Roush 2018-09-07 16:40:10 -04:00
parent 24109d9568
commit 8db5c08ed1
3 changed files with 478 additions and 1 deletions

View file

@ -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

View file

@ -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):

View file

@ -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)