Skip to content

Commit 64628d1

Browse files
committed
Sketch out custom formatters design
1 parent 032aecd commit 64628d1

File tree

7 files changed

+52
-38
lines changed

7 files changed

+52
-38
lines changed

openapi_core/schema/media_types/models.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ def deserialize(self, value):
3232
deserializer = self.get_dererializer()
3333
return deserializer(value)
3434

35-
def unmarshal(self, value):
35+
def unmarshal(self, value, custom_formatters=None):
3636
if not self.schema:
3737
return value
3838

@@ -42,7 +42,7 @@ def unmarshal(self, value):
4242
raise InvalidMediaTypeValue(str(exc))
4343

4444
try:
45-
unmarshalled = self.schema.unmarshal(deserialized)
45+
unmarshalled = self.schema.unmarshal(deserialized, custom_formatters)
4646
except InvalidSchemaValue as exc:
4747
raise InvalidMediaTypeValue(str(exc))
4848

openapi_core/schema/parameters/models.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ def get_value(self, request):
9191

9292
return location[self.name]
9393

94-
def unmarshal(self, value):
94+
def unmarshal(self, value, custom_formatters=None):
9595
if self.deprecated:
9696
warnings.warn(
9797
"{0} parameter is deprecated".format(self.name),
@@ -112,7 +112,7 @@ def unmarshal(self, value):
112112
raise InvalidParameterValue(str(exc))
113113

114114
try:
115-
unmarshalled = self.schema.unmarshal(deserialized)
115+
unmarshalled = self.schema.unmarshal(deserialized, custom_formatters)
116116
except InvalidSchemaValue as exc:
117117
raise InvalidParameterValue(str(exc))
118118

openapi_core/schema/schemas/models.py

Lines changed: 39 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
"""OpenAPI core schemas models module"""
2+
import attr
3+
import functools
24
import logging
35
from collections import defaultdict
46
from datetime import date, datetime
@@ -23,6 +25,11 @@
2325

2426
log = logging.getLogger(__name__)
2527

28+
@attr.s
29+
class StringFormat(object):
30+
format = attr.ib()
31+
validate = attr.ib()
32+
2633

2734
class Schema(object):
2835
"""Represents an OpenAPI Schema."""
@@ -33,18 +40,11 @@ class Schema(object):
3340
SchemaType.BOOLEAN: forcebool,
3441
}
3542

36-
STRING_FORMAT_CAST_CALLABLE_GETTER = {
37-
SchemaFormat.NONE: text_type,
38-
SchemaFormat.DATE: format_date,
39-
SchemaFormat.DATETIME: format_datetime,
40-
SchemaFormat.BINARY: binary_type,
41-
}
42-
43-
STRING_FORMAT_VALIDATOR_CALLABLE_GETTER = {
44-
SchemaFormat.NONE: TypeValidator(text_type),
45-
SchemaFormat.DATE: TypeValidator(date, exclude=datetime),
46-
SchemaFormat.DATETIME: TypeValidator(datetime),
47-
SchemaFormat.BINARY: TypeValidator(binary_type),
43+
STRING_FORMAT_CALLABLE_GETTER = {
44+
SchemaFormat.NONE: StringFormat(text_type, TypeValidator(text_type)),
45+
SchemaFormat.DATE: StringFormat(format_date, TypeValidator(date, exclude=datetime)),
46+
SchemaFormat.DATETIME: StringFormat(format_datetime, TypeValidator(datetime)),
47+
SchemaFormat.BINARY: StringFormat(binary_type, TypeValidator(binary_type)),
4848
}
4949

5050
TYPE_VALIDATOR_CALLABLE_GETTER = {
@@ -99,6 +99,7 @@ def __init__(
9999

100100
self._all_required_properties_cache = None
101101
self._all_optional_properties_cache = None
102+
self.custom_formatters = None
102103

103104
def __getitem__(self, name):
104105
return self.properties[name]
@@ -173,11 +174,13 @@ def cast(self, value):
173174
"Failed to cast value of {0} to {1}".format(value, self.type)
174175
)
175176

176-
def unmarshal(self, value):
177+
def unmarshal(self, value, custom_formatters=None):
177178
"""Unmarshal parameter from the value."""
178179
if self.deprecated:
179180
warnings.warn("The schema is deprecated", DeprecationWarning)
180181

182+
self.custom_formatters = custom_formatters
183+
181184
casted = self.cast(value)
182185

183186
if casted is None and not self.required:
@@ -195,15 +198,18 @@ def _unmarshal_string(self, value):
195198
try:
196199
schema_format = SchemaFormat(self.format)
197200
except ValueError:
198-
# @todo: implement custom format unmarshalling support
199-
raise OpenAPISchemaError(
200-
"Unsupported {0} format unmarshalling".format(self.format)
201-
)
201+
msg = "Unsupported {0} format unmarshalling".format(self.format)
202+
if self.custom_formatters is not None:
203+
formatstring = self.custom_formatters.get(self.format)
204+
if formatstring is None:
205+
raise OpenAPISchemaError(msg)
206+
else:
207+
raise OpenAPISchemaError(msg)
202208
else:
203-
formatter = self.STRING_FORMAT_CAST_CALLABLE_GETTER[schema_format]
209+
formatstring = self.STRING_FORMAT_CALLABLE_GETTER[schema_format]
204210

205211
try:
206-
return formatter(value)
212+
return formatstring.format(value)
207213
except ValueError:
208214
raise InvalidSchemaValue(
209215
"Failed to format value of {0} to {1}".format(
@@ -231,7 +237,8 @@ def _unmarshal_collection(self, value):
231237
if self.items is None:
232238
raise UndefinedItemsSchema("Undefined items' schema")
233239

234-
return list(map(self.items.unmarshal, value))
240+
f = functools.partial(self.items.unmarshal, custom_formatters=self.custom_formatters)
241+
return list(map(f, value))
235242

236243
def _unmarshal_object(self, value, model_factory=None):
237244
if not isinstance(value, (dict, )):
@@ -286,7 +293,7 @@ def _unmarshal_properties(self, value, one_of_schema=None):
286293
for prop_name in extra_props:
287294
prop_value = value[prop_name]
288295
properties[prop_name] = self.additional_properties.unmarshal(
289-
prop_value)
296+
prop_value, self.custom_formatters)
290297

291298
for prop_name, prop in iteritems(all_props):
292299
try:
@@ -298,7 +305,7 @@ def _unmarshal_properties(self, value, one_of_schema=None):
298305
if not prop.nullable and not prop.default:
299306
continue
300307
prop_value = prop.default
301-
properties[prop_name] = prop.unmarshal(prop_value)
308+
properties[prop_name] = prop.unmarshal(prop_value, self.custom_formatters)
302309

303310
self._validate_properties(properties, one_of_schema=one_of_schema)
304311

@@ -405,15 +412,18 @@ def _validate_string(self, value):
405412
try:
406413
schema_format = SchemaFormat(self.format)
407414
except ValueError:
408-
# @todo: implement custom format validation support
409-
raise OpenAPISchemaError(
410-
"Unsupported {0} format validation".format(self.format)
411-
)
415+
msg = "Unsupported {0} format validation".format(self.format)
416+
if self.custom_formatters is not None:
417+
formatstring = self.custom_formatters.get(self.format)
418+
if formatstring is None:
419+
raise OpenAPISchemaError(msg)
420+
else:
421+
raise OpenAPISchemaError(msg)
412422
else:
413-
format_validator_callable =\
414-
self.STRING_FORMAT_VALIDATOR_CALLABLE_GETTER[schema_format]
423+
formatstring =\
424+
self.STRING_FORMAT_CALLABLE_GETTER[schema_format]
415425

416-
if not format_validator_callable(value):
426+
if not formatstring.validate(value):
417427
raise InvalidSchemaValue(
418428
"Value of {0} not valid format of {1}".format(
419429
value, self.format)

openapi_core/validation/request/validators.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,9 @@
1111

1212
class RequestValidator(object):
1313

14-
def __init__(self, spec):
14+
def __init__(self, spec, custom_formatters=None):
1515
self.spec = spec
16+
self.custom_formatters = custom_formatters
1617

1718
def validate(self, request):
1819
try:
@@ -52,7 +53,7 @@ def _get_parameters(self, request, operation):
5253
continue
5354

5455
try:
55-
value = param.unmarshal(raw_value)
56+
value = param.unmarshal(raw_value, self.custom_formatters)
5657
except OpenAPIMappingError as exc:
5758
errors.append(exc)
5859
else:
@@ -78,7 +79,7 @@ def _get_body(self, request, operation):
7879
errors.append(exc)
7980
else:
8081
try:
81-
body = media_type.unmarshal(raw_body)
82+
body = media_type.unmarshal(raw_body, self.custom_formatters)
8283
except OpenAPIMappingError as exc:
8384
errors.append(exc)
8485

openapi_core/validation/response/validators.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,9 @@
66

77
class ResponseValidator(object):
88

9-
def __init__(self, spec):
9+
def __init__(self, spec, custom_formatters=None):
1010
self.spec = spec
11+
self.custom_formatters = custom_formatters
1112

1213
def validate(self, request, response):
1314
try:
@@ -60,7 +61,7 @@ def _get_data(self, response, operation_response):
6061
errors.append(exc)
6162
else:
6263
try:
63-
data = media_type.unmarshal(raw_data)
64+
data = media_type.unmarshal(raw_data, self.custom_formatters)
6465
except OpenAPIMappingError as exc:
6566
errors.append(exc)
6667

requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
openapi-spec-validator
22
six
33
lazy-object-proxy
4+
attrs

requirements_2.7.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,4 @@ lazy-object-proxy
44
backports.functools-lru-cache
55
backports.functools-partialmethod
66
enum34
7+
attrs

0 commit comments

Comments
 (0)