Skip to content

Commit 82f32be

Browse files
committed
String validation
1 parent 101a11e commit 82f32be

File tree

4 files changed

+148
-18
lines changed

4 files changed

+148
-18
lines changed

openapi_core/schema/schemas/models.py

Lines changed: 51 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
"""OpenAPI core schemas models module"""
22
import logging
33
from collections import defaultdict
4+
from datetime import date, datetime
45
import warnings
56

67
from six import iteritems, integer_types, binary_type, text_type
@@ -11,7 +12,9 @@
1112
InvalidSchemaValue, UndefinedSchemaProperty, MissingSchemaProperty,
1213
OpenAPISchemaError, NoOneOfSchema, MultipleOneOfSchema,
1314
)
14-
from openapi_core.schema.schemas.util import forcebool, format_date
15+
from openapi_core.schema.schemas.util import (
16+
forcebool, format_date, format_datetime,
17+
)
1518
from openapi_core.schema.schemas.validators import (
1619
TypeValidator, AttributeValidator,
1720
)
@@ -28,16 +31,27 @@ class Schema(object):
2831
SchemaType.BOOLEAN: forcebool,
2932
}
3033

31-
FORMAT_CALLABLE_GETTER = defaultdict(lambda: lambda x: x, {
32-
SchemaFormat.DATE.value: format_date,
33-
})
34+
STRING_FORMAT_CAST_CALLABLE_GETTER = {
35+
SchemaFormat.NONE: text_type,
36+
SchemaFormat.DATE: format_date,
37+
SchemaFormat.DATETIME: format_datetime,
38+
SchemaFormat.BINARY: binary_type,
39+
}
40+
41+
STRING_FORMAT_VALIDATOR_CALLABLE_GETTER = {
42+
SchemaFormat.NONE: TypeValidator(text_type),
43+
SchemaFormat.DATE: TypeValidator(date, exclude=datetime),
44+
SchemaFormat.DATETIME: TypeValidator(datetime),
45+
SchemaFormat.BINARY: TypeValidator(binary_type),
46+
}
3447

3548
TYPE_VALIDATOR_CALLABLE_GETTER = {
36-
None: lambda x: x,
49+
None: lambda x: True,
3750
SchemaType.BOOLEAN: TypeValidator(bool),
3851
SchemaType.INTEGER: TypeValidator(integer_types, exclude=bool),
3952
SchemaType.NUMBER: TypeValidator(integer_types, float, exclude=bool),
40-
SchemaType.STRING: TypeValidator(binary_type, text_type),
53+
SchemaType.STRING: TypeValidator(
54+
text_type, date, datetime, binary_type),
4155
SchemaType.ARRAY: TypeValidator(list, tuple),
4256
SchemaType.OBJECT: AttributeValidator('__dict__'),
4357
}
@@ -158,7 +172,16 @@ def unmarshal(self, value):
158172
return casted
159173

160174
def _unmarshal_string(self, value):
161-
formatter = self.FORMAT_CALLABLE_GETTER[self.format]
175+
try:
176+
schema_format = SchemaFormat(self.format)
177+
except ValueError:
178+
# @todo: implement custom format unmarshalling support
179+
raise OpenAPISchemaError(
180+
"Unsupported {0} format unmarshalling".format(self.format)
181+
)
182+
else:
183+
formatter = self.STRING_FORMAT_CAST_CALLABLE_GETTER[schema_format]
184+
162185
try:
163186
return formatter(value)
164187
except ValueError:
@@ -244,6 +267,7 @@ def _unmarshal_properties(self, value, one_of_schema=None):
244267
def get_validator_mapping(self):
245268
mapping = {
246269
SchemaType.ARRAY: self._validate_collection,
270+
SchemaType.STRING: self._validate_string,
247271
SchemaType.OBJECT: self._validate_object,
248272
}
249273

@@ -277,6 +301,26 @@ def _validate_collection(self, value):
277301

278302
return list(map(self.items.validate, value))
279303

304+
def _validate_string(self, value):
305+
try:
306+
schema_format = SchemaFormat(self.format)
307+
except ValueError:
308+
# @todo: implement custom format validation support
309+
raise OpenAPISchemaError(
310+
"Unsupported {0} format validation".format(self.format)
311+
)
312+
else:
313+
format_validator_callable =\
314+
self.STRING_FORMAT_VALIDATOR_CALLABLE_GETTER[schema_format]
315+
316+
if not format_validator_callable(value):
317+
raise InvalidSchemaValue(
318+
"Value of {0} not valid format of {1}".format(
319+
value, self.format)
320+
)
321+
322+
return True
323+
280324
def _validate_object(self, value):
281325
properties = value.__dict__
282326

openapi_core/schema/schemas/util.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,3 +18,7 @@ def dicthash(d):
1818

1919
def format_date(value):
2020
return datetime.datetime.strptime(value, '%Y-%m-%d').date()
21+
22+
23+
def format_datetime(value):
24+
return datetime.datetime.strptime(value, '%Y-%m-%dT%H:%M:%S')

tests/integration/data/v3.0/petstore.yaml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -286,7 +286,6 @@ components:
286286
properties:
287287
name:
288288
type: string
289-
format: custom
290289
TagList:
291290
type: array
292291
items:

tests/unit/schema/test_schemas.py

Lines changed: 93 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99
)
1010
from openapi_core.schema.schemas.models import Schema
1111

12+
from six import b, u
13+
1214

1315
class TestSchemaIteritems(object):
1416

@@ -77,13 +79,22 @@ def test_string_format_date(self):
7779

7880
assert result == datetime.date(2018, 1, 2)
7981

82+
def test_string_format_datetime(self):
83+
schema = Schema('string', schema_format='date-time')
84+
value = '2018-01-02T00:00:00'
85+
86+
result = schema.unmarshal(value)
87+
88+
assert result == datetime.datetime(2018, 1, 2, 0, 0, 0)
89+
90+
@pytest.mark.xfail(reason="No custom formats support atm")
8091
def test_string_format_custom(self):
8192
custom_format = 'custom'
8293
schema = Schema('string', schema_format=custom_format)
8394
value = 'x'
8495

8596
with mock.patch.dict(
86-
Schema.FORMAT_CALLABLE_GETTER,
97+
Schema.STRING_FORMAT_CAST_CALLABLE_GETTER,
8798
{custom_format: lambda x: x + '-custom'},
8899
):
89100
result = schema.unmarshal(value)
@@ -95,17 +106,17 @@ def test_string_format_unknown(self):
95106
schema = Schema('string', schema_format=unknown_format)
96107
value = 'x'
97108

98-
result = schema.unmarshal(value)
99-
100-
assert result == 'x'
109+
with pytest.raises(OpenAPISchemaError):
110+
schema.unmarshal(value)
101111

112+
@pytest.mark.xfail(reason="No custom formats support atm")
102113
def test_string_format_invalid_value(self):
103114
custom_format = 'custom'
104115
schema = Schema('string', schema_format=custom_format)
105116
value = 'x'
106117

107118
with mock.patch.dict(
108-
Schema.FORMAT_CALLABLE_GETTER,
119+
Schema.STRING_FORMAT_CAST_CALLABLE_GETTER,
109120
{custom_format: mock.Mock(side_effect=ValueError())},
110121
), pytest.raises(
111122
InvalidSchemaValue, message='Failed to format value'
@@ -191,7 +202,7 @@ def test_boolean(self, value):
191202

192203
assert result == value
193204

194-
@pytest.mark.parametrize('value', [1, 3.14, 'true', [True, False]])
205+
@pytest.mark.parametrize('value', [1, 3.14, u('true'), [True, False]])
195206
def test_boolean_invalid(self, value):
196207
schema = Schema('boolean')
197208

@@ -213,7 +224,7 @@ def test_array(self, value):
213224

214225
assert result == value
215226

216-
@pytest.mark.parametrize('value', [False, 1, 3.14, 'true'])
227+
@pytest.mark.parametrize('value', [False, 1, 3.14, u('true')])
217228
def test_array_invalid(self, value):
218229
schema = Schema('array')
219230

@@ -228,7 +239,7 @@ def test_integer(self, value):
228239

229240
assert result == value
230241

231-
@pytest.mark.parametrize('value', [False, 3.14, 'true', [1, 2]])
242+
@pytest.mark.parametrize('value', [False, 3.14, u('true'), [1, 2]])
232243
def test_integer_invalid(self, value):
233244
schema = Schema('integer')
234245

@@ -250,21 +261,93 @@ def test_number_invalid(self, value):
250261
with pytest.raises(InvalidSchemaValue):
251262
schema.validate(value)
252263

253-
@pytest.mark.parametrize('value', ['true', b'true'])
264+
@pytest.mark.parametrize('value', [u('true'), ])
254265
def test_string(self, value):
255266
schema = Schema('string')
256267

257268
result = schema.validate(value)
258269

259270
assert result == value
260271

261-
@pytest.mark.parametrize('value', [False, 1, 3.14, [1, 3]])
272+
@pytest.mark.parametrize('value', [b('test'), False, 1, 3.14, [1, 3]])
262273
def test_string_invalid(self, value):
263274
schema = Schema('string')
264275

265276
with pytest.raises(InvalidSchemaValue):
266277
schema.validate(value)
267278

279+
@pytest.mark.parametrize('value', [
280+
b('true'), u('test'), False, 1, 3.14, [1, 3],
281+
datetime.datetime(1989, 1, 2),
282+
])
283+
def test_string_format_date_invalid(self, value):
284+
schema = Schema('string', schema_format='date')
285+
286+
with pytest.raises(InvalidSchemaValue):
287+
schema.validate(value)
288+
289+
@pytest.mark.parametrize('value', [
290+
datetime.date(1989, 1, 2), datetime.date(2018, 1, 2),
291+
])
292+
def test_string_format_date(self, value):
293+
schema = Schema('string', schema_format='date')
294+
295+
result = schema.validate(value)
296+
297+
assert result == value
298+
299+
@pytest.mark.parametrize('value', [
300+
b('true'), u('true'), False, 1, 3.14, [1, 3],
301+
datetime.date(1989, 1, 2),
302+
])
303+
def test_string_format_datetime_invalid(self, value):
304+
schema = Schema('string', schema_format='date-time')
305+
306+
with pytest.raises(InvalidSchemaValue):
307+
schema.validate(value)
308+
309+
@pytest.mark.parametrize('value', [
310+
datetime.datetime(1989, 1, 2, 0, 0, 0),
311+
datetime.datetime(2018, 1, 2, 23, 59, 59),
312+
])
313+
def test_string_format_datetime(self, value):
314+
schema = Schema('string', schema_format='date-time')
315+
316+
result = schema.validate(value)
317+
318+
assert result == value
319+
320+
@pytest.mark.parametrize('value', [
321+
u('true'), False, 1, 3.14, [1, 3], datetime.date(1989, 1, 2),
322+
datetime.datetime(1989, 1, 2, 0, 0, 0),
323+
])
324+
def test_string_format_binary_invalid(self, value):
325+
schema = Schema('string', schema_format='binary')
326+
327+
with pytest.raises(InvalidSchemaValue):
328+
schema.validate(value)
329+
330+
@pytest.mark.parametrize('value', [
331+
b('stream'), b('text'),
332+
])
333+
def test_string_format_binary(self, value):
334+
schema = Schema('string', schema_format='binary')
335+
336+
result = schema.validate(value)
337+
338+
assert result == value
339+
340+
@pytest.mark.parametrize('value', [
341+
u('test'), b('stream'), datetime.date(1989, 1, 2),
342+
datetime.datetime(1989, 1, 2, 0, 0, 0),
343+
])
344+
def test_string_format_unknown(self, value):
345+
unknown_format = 'unknown'
346+
schema = Schema('string', schema_format=unknown_format)
347+
348+
with pytest.raises(OpenAPISchemaError):
349+
schema.validate(value)
350+
268351
@pytest.mark.parametrize('value', ['true', False, 1, 3.14, [1, 3]])
269352
def test_object_not_an_object(self, value):
270353
schema = Schema('object')

0 commit comments

Comments
 (0)