Skip to content

Commit 64a5045

Browse files
authored
Merge pull request #12 from p1c2u/feature/serialized-parameters-support
Serialized parameters support
2 parents 56f6a6d + fd450e6 commit 64a5045

File tree

9 files changed

+326
-79
lines changed

9 files changed

+326
-79
lines changed

openapi_core/enums.py

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
from enum import Enum
2+
3+
4+
class ParameterLocation(Enum):
5+
6+
PATH = 'path'
7+
QUERY = 'query'
8+
HEADER = 'header'
9+
COOKIE = 'cookie'
10+
11+
@classmethod
12+
def has_value(cls, value):
13+
return (any(value == item.value for item in cls))
14+
15+
16+
class ParameterStyle(Enum):
17+
18+
MATRIX = 'matrix'
19+
LABEL = 'label'
20+
FORM = 'form'
21+
SIMPLE = 'simple'
22+
SPACE_DELIMITED = 'spaceDelimited'
23+
PIPE_DELIMITED = 'pipeDelimited'
24+
DEEP_OBJECT = 'deepObject'
25+
26+
27+
class SchemaType(Enum):
28+
29+
INTEGER = 'integer'
30+
NUMBER = 'number'
31+
STRING = 'string'
32+
BOOLEAN = 'boolean'
33+
ARRAY = 'array'
34+
OBJECT = 'object'
35+
36+
37+
class SchemaFormat(Enum):
38+
39+
NONE = None
40+
INT32 = 'int32'
41+
INT64 = 'int64'
42+
FLOAT = 'float'
43+
DOUBLE = 'double'
44+
BYTE = 'byte'
45+
BINARY = 'binary'
46+
DATE = 'date'
47+
DATETIME = 'date-time'
48+
PASSWORD = 'password'

openapi_core/parameters.py

Lines changed: 95 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -2,32 +2,73 @@
22
import logging
33
import warnings
44

5+
from functools import lru_cache
56
from six import iteritems
67

8+
from openapi_core.enums import ParameterLocation, ParameterStyle, SchemaType
79
from openapi_core.exceptions import (
810
EmptyValue, InvalidValueType, InvalidParameterValue,
911
)
1012

1113
log = logging.getLogger(__name__)
1214

15+
PARAMETER_STYLE_DESERIALIZERS = {
16+
ParameterStyle.FORM: lambda x: x.split(','),
17+
ParameterStyle.SIMPLE: lambda x: x.split(','),
18+
ParameterStyle.SPACE_DELIMITED: lambda x: x.split(' '),
19+
ParameterStyle.PIPE_DELIMITED: lambda x: x.split('|'),
20+
}
21+
1322

1423
class Parameter(object):
1524
"""Represents an OpenAPI operation Parameter."""
1625

1726
def __init__(
1827
self, name, location, schema=None, required=False,
1928
deprecated=False, allow_empty_value=False,
20-
items=None, collection_format=None):
29+
items=None, style=None, explode=None):
2130
self.name = name
22-
self.location = location
31+
self.location = ParameterLocation(location)
2332
self.schema = schema
24-
self.required = True if self.location == "path" else required
33+
self.required = (
34+
True if self.location == ParameterLocation.PATH else required
35+
)
2536
self.deprecated = deprecated
2637
self.allow_empty_value = (
27-
allow_empty_value if self.location == "query" else False
38+
allow_empty_value if self.location == ParameterLocation.QUERY
39+
else False
2840
)
2941
self.items = items
30-
self.collection_format = collection_format
42+
self.style = ParameterStyle(style or self.default_style)
43+
self.explode = self.default_explode if explode is None else explode
44+
45+
@property
46+
def aslist(self):
47+
return (
48+
self.schema and
49+
self.schema.type in [SchemaType.ARRAY, SchemaType.OBJECT]
50+
)
51+
52+
@property
53+
def default_style(self):
54+
simple_locations = [ParameterLocation.PATH, ParameterLocation.HEADER]
55+
return (
56+
'simple' if self.location in simple_locations else "form"
57+
)
58+
59+
@property
60+
def default_explode(self):
61+
return self.style == ParameterStyle.FORM
62+
63+
def get_dererializer(self):
64+
return PARAMETER_STYLE_DESERIALIZERS[self.style]
65+
66+
def deserialize(self, value):
67+
if not self.aslist or self.explode:
68+
return value
69+
70+
deserializer = self.get_dererializer()
71+
return deserializer(value)
3172

3273
def unmarshal(self, value):
3374
if self.deprecated:
@@ -36,69 +77,73 @@ def unmarshal(self, value):
3677
DeprecationWarning,
3778
)
3879

39-
if (self.location == "query" and value == "" and
80+
if (self.location == ParameterLocation.QUERY and value == "" and
4081
not self.allow_empty_value):
4182
raise EmptyValue(
4283
"Value of {0} parameter cannot be empty".format(self.name))
4384

4485
if not self.schema:
4586
return value
4687

88+
deserialized = self.deserialize(value)
89+
4790
try:
48-
return self.schema.unmarshal(value)
91+
return self.schema.unmarshal(deserialized)
4992
except InvalidValueType as exc:
5093
raise InvalidParameterValue(str(exc))
5194

5295

96+
class ParameterFactory(object):
97+
98+
def __init__(self, dereferencer, schemas_registry):
99+
self.dereferencer = dereferencer
100+
self.schemas_registry = schemas_registry
101+
102+
def create(self, parameter_spec, parameter_name=None):
103+
parameter_deref = self.dereferencer.dereference(parameter_spec)
104+
105+
parameter_name = parameter_name or parameter_deref['name']
106+
parameter_in = parameter_deref.get('in', 'header')
107+
108+
allow_empty_value = parameter_deref.get('allowEmptyValue')
109+
required = parameter_deref.get('required', False)
110+
111+
style = parameter_deref.get('style')
112+
explode = parameter_deref.get('explode')
113+
114+
schema_spec = parameter_deref.get('schema', None)
115+
schema = None
116+
if schema_spec:
117+
schema, _ = self.schemas_registry.get_or_create(schema_spec)
118+
119+
return Parameter(
120+
parameter_name, parameter_in,
121+
schema=schema, required=required,
122+
allow_empty_value=allow_empty_value,
123+
style=style, explode=explode,
124+
)
125+
126+
53127
class ParametersGenerator(object):
54128

55129
def __init__(self, dereferencer, schemas_registry):
56130
self.dereferencer = dereferencer
57131
self.schemas_registry = schemas_registry
58132

59133
def generate(self, parameters):
60-
for parameter_name, parameter in iteritems(parameters):
61-
parameter_deref = self.dereferencer.dereference(parameter)
62-
63-
parameter_in = parameter_deref.get('in', 'header')
64-
65-
allow_empty_value = parameter_deref.get('allowEmptyValue')
66-
required = parameter_deref.get('required', False)
67-
68-
schema_spec = parameter_deref.get('schema', None)
69-
schema = None
70-
if schema_spec:
71-
schema, _ = self.schemas_registry.get_or_create(schema_spec)
72-
73-
yield (
74-
parameter_name,
75-
Parameter(
76-
parameter_name, parameter_in,
77-
schema=schema, required=required,
78-
allow_empty_value=allow_empty_value,
79-
),
80-
)
134+
for parameter_name, parameter_spec in iteritems(parameters):
135+
parameter = self.parameter_factory.create(
136+
parameter_spec, parameter_name=parameter_name)
137+
138+
yield (parameter_name, parameter)
81139

82140
def generate_from_list(self, parameters_list):
83-
for parameter in parameters_list:
84-
parameter_deref = self.dereferencer.dereference(parameter)
85-
86-
parameter_name = parameter_deref['name']
87-
parameter_in = parameter_deref.get('in', 'header')
88-
89-
allow_empty_value = parameter_deref.get('allowEmptyValue')
90-
required = parameter_deref.get('required', False)
91-
92-
schema_spec = parameter_deref.get('schema', None)
93-
schema = None
94-
if schema_spec:
95-
schema, _ = self.schemas_registry.get_or_create(schema_spec)
96-
97-
yield (
98-
parameter_name,
99-
Parameter(
100-
parameter_name, parameter_in,
101-
schema=schema, required=required,
102-
allow_empty_value=allow_empty_value,
103-
),
104-
)
141+
for parameter_spec in parameters_list:
142+
parameter = self.parameter_factory.create(parameter_spec)
143+
144+
yield (parameter.name, parameter)
145+
146+
@property
147+
@lru_cache()
148+
def parameter_factory(self):
149+
return ParameterFactory(self.dereferencer, self.schemas_registry)

openapi_core/schemas.py

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
from json import loads
1010
from six import iteritems
1111

12+
from openapi_core.enums import SchemaType, SchemaFormat
1213
from openapi_core.exceptions import (
1314
InvalidValueType, UndefinedSchemaProperty, MissingProperty, InvalidValue,
1415
)
@@ -17,9 +18,9 @@
1718
log = logging.getLogger(__name__)
1819

1920
DEFAULT_CAST_CALLABLE_GETTER = {
20-
'integer': int,
21-
'number': float,
22-
'boolean': lambda x: bool(strtobool(x)),
21+
SchemaType.INTEGER: int,
22+
SchemaType.NUMBER: float,
23+
SchemaType.BOOLEAN: lambda x: bool(strtobool(x)),
2324
}
2425

2526

@@ -28,13 +29,13 @@ class Schema(object):
2829

2930
def __init__(
3031
self, schema_type, model=None, properties=None, items=None,
31-
spec_format=None, required=None, default=None, nullable=False,
32+
schema_format=None, required=None, default=None, nullable=False,
3233
enum=None, deprecated=False, all_of=None):
33-
self.type = schema_type
34+
self.type = SchemaType(schema_type)
3435
self.model = model
3536
self.properties = properties and dict(properties) or {}
3637
self.items = items
37-
self.format = spec_format
38+
self.format = SchemaFormat(schema_format)
3839
self.required = required or []
3940
self.default = default
4041
self.nullable = nullable
@@ -57,8 +58,8 @@ def get_all_properties(self):
5758
def get_cast_mapping(self):
5859
mapping = DEFAULT_CAST_CALLABLE_GETTER.copy()
5960
mapping.update({
60-
'array': self._unmarshal_collection,
61-
'object': self._unmarshal_object,
61+
SchemaType.ARRAY: self._unmarshal_collection,
62+
SchemaType.OBJECT: self._unmarshal_object,
6263
})
6364

6465
return defaultdict(lambda: lambda x: x, mapping)
@@ -159,6 +160,7 @@ def create(self, schema_spec):
159160
schema_deref = self.dereferencer.dereference(schema_spec)
160161

161162
schema_type = schema_deref['type']
163+
schema_format = schema_deref.get('format')
162164
model = schema_deref.get('x-model', None)
163165
required = schema_deref.get('required', False)
164166
default = schema_deref.get('default', None)
@@ -183,8 +185,8 @@ def create(self, schema_spec):
183185

184186
return Schema(
185187
schema_type, model=model, properties=properties, items=items,
186-
required=required, default=default, nullable=nullable, enum=enum,
187-
deprecated=deprecated, all_of=all_of,
188+
schema_format=schema_format, required=required, default=default,
189+
nullable=nullable, enum=enum, deprecated=deprecated, all_of=all_of,
188190
)
189191

190192
@property

openapi_core/validators.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ def validate(self, request):
9595
except OpenAPIMappingError as exc:
9696
errors.append(exc)
9797
else:
98-
parameters[param.location][param_name] = value
98+
parameters[param.location.value][param_name] = value
9999

100100
if operation.request_body is not None:
101101
try:
@@ -117,12 +117,19 @@ def validate(self, request):
117117
return RequestValidationResult(errors, body, parameters)
118118

119119
def _get_raw_value(self, request, param):
120+
location = request.parameters[param.location.value]
121+
120122
try:
121-
return request.parameters[param.location][param.name]
123+
raw = request.parameters[param.location.value][param.name]
122124
except KeyError:
123125
raise MissingParameter(
124126
"Missing required `{0}` parameter".format(param.name))
125127

128+
if param.aslist and param.explode:
129+
return location.getlist(param.name)
130+
131+
return raw
132+
126133
def _get_raw_body(self, request):
127134
if not request.body:
128135
raise MissingBody("Missing required request body")

openapi_core/wrappers.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import warnings
33

44
from six.moves.urllib.parse import urljoin
5+
from werkzeug.datastructures import ImmutableMultiDict
56

67

78
class BaseOpenAPIRequest(object):
@@ -54,9 +55,9 @@ def __init__(
5455

5556
self.parameters = {
5657
'path': view_args or {},
57-
'query': args or {},
58-
'headers': headers or {},
59-
'cookies': cookies or {},
58+
'query': ImmutableMultiDict(args or []),
59+
'header': headers or {},
60+
'cookie': cookies or {},
6061
}
6162

6263
self.body = data or ''

0 commit comments

Comments
 (0)