Skip to content

Commit cf5b157

Browse files
authored
Fix validate params (#23)
* fix validate params.
1 parent aa7c0bd commit cf5b157

18 files changed

+538
-241
lines changed

.python-version

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
3.10.6
1+
3.10

CHANGES.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,11 @@
1+
## Version 0.14.1
2+
3+
* Fix README.md.
4+
5+
## Version 0.14.0
6+
7+
* Fix parameters validation.
8+
19
## Version 0.13.2
210

311
* Fix cookie validation.

README.md

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,15 @@ Flask extension for using "specification first" principle.
55
Features:
66

77
* `Application Factory` supported.
8+
* Validating and serializing arguments from `request.headers` to `request.first_headers`.
89
* Validating and serializing arguments from `request.view_args` to `request.first_view_args`.
910
* Validating and serializing arguments from `request.args` to `request.first_args`.
1011
* Validating and serializing arguments from `request.cookies` to `request.first_cookies`.
12+
* Validating and serializing arguments from `request.json` to `request.first_json`.
13+
* Validating headers from request.
14+
* Validating cookies from request.
15+
* Validating path parameters from request.
16+
* Validating parameters from request.
1117
* Validating JSON from request.
1218
* Validating JSON from response.
1319
* Provides a Swagger UI.
@@ -18,7 +24,6 @@ Limitations
1824
Will be added in future releases.
1925

2026
* Full specification in one file.
21-
* Headers not supported.
2227
* Authorization not supported.
2328

2429
## Installing

src/flask_first/__init__.py

Lines changed: 55 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -13,23 +13,16 @@
1313
from flask import send_file
1414
from flask import url_for
1515
from marshmallow.exceptions import ValidationError
16-
from openapi_spec_validator import validate_spec
17-
from openapi_spec_validator.readers import read_from_filename
18-
from openapi_spec_validator.validation.exceptions import OpenAPIValidationError
16+
from werkzeug.datastructures import Headers
1917
from werkzeug.datastructures import MultiDict
2018

21-
from .exceptions import FirstException
22-
from .exceptions import FirstOpenAPIValidation
23-
from .exceptions import FirstRequestArgsValidation
24-
from .exceptions import FirstRequestCookieValidation
25-
from .exceptions import FirstRequestJSONValidation
26-
from .exceptions import FirstRequestPathArgsValidation
27-
from .exceptions import FirstResponseJSONValidation
28-
from .exceptions import FirstValidation
29-
from .schema.tools import convert_schemas
30-
from .schema.tools import resolving_refs
19+
from .first import RequestSerializer
20+
from .first import Specification
21+
from .first.exceptions import FirstException
22+
from .first.exceptions import FirstResponseJSONValidation
23+
from .first.exceptions import FirstValidation
3124

32-
__version__ = '0.13.2'
25+
__version__ = '0.14.1'
3326

3427

3528
class First:
@@ -50,15 +43,7 @@ def __init__(
5043
if self.app is not None:
5144
self.init_app(app)
5245

53-
self.raw_spec, _ = read_from_filename(path_to_spec)
54-
try:
55-
validate_spec(self.raw_spec)
56-
except OpenAPIValidationError as e:
57-
raise FirstOpenAPIValidation(repr(e))
58-
59-
self.resolved_spec = resolving_refs(self.raw_spec)
60-
61-
self.serialized_spec = convert_schemas(self.resolved_spec)
46+
self.spec = Specification(path_to_spec)
6247

6348
self._mapped_routes_from_spec = []
6449

@@ -69,7 +54,7 @@ def route_to_openapi_format(route: str) -> str:
6954
def _route_registration_in_flask(self, func: callable) -> None:
7055
route = method = ''
7156

72-
for path, path_item in self.resolved_spec['paths'].items():
57+
for path, path_item in self.spec.resolved_spec['paths'].items():
7358
for method_name, operation in path_item.items():
7459
if operation.get('operationId') == func.__name__:
7560
route: str = path
@@ -78,7 +63,7 @@ def _route_registration_in_flask(self, func: callable) -> None:
7863
if not route:
7964
raise FirstException(f'Route function <{route}> not found in OpenAPI specification!')
8065

81-
params_schema = self.resolved_spec['paths'][route][method].get('parameters')
66+
params_schema = self.spec.resolved_spec['paths'][route][method].get('parameters')
8267

8368
if params_schema and '{' in route and '}' in route:
8469
path_params = re.findall(r'{(\S*?)}', route)
@@ -98,14 +83,16 @@ def _route_registration_in_flask(self, func: callable) -> None:
9883

9984
def _extract_data_from_request(
10085
self, request_obj: Request
101-
) -> tuple[Any, str | None, dict[str, Any] | None, dict, dict, Any | None]:
86+
) -> tuple[Any, str | None, Headers, dict[str, Any] | None, dict, dict, Any | None]:
10287
method = request_obj.method.lower()
10388

10489
if request_obj.url_rule is not None:
10590
route = request_obj.url_rule.rule
10691
else:
10792
route = request_obj.url_rule
10893

94+
headers = request_obj.headers
95+
10996
view_args = request_obj.view_args
11097

11198
args = self._resolved_params(request_obj.args)
@@ -117,7 +104,7 @@ def _extract_data_from_request(
117104
else:
118105
json = None
119106

120-
return method, route, view_args, args, cookies, json
107+
return method, route, headers, view_args, args, cookies, json
121108

122109
def _resolved_params(self, payload: MultiDict) -> dict:
123110
# payload.to_dict(flat=False) serializing all arguments as list for correct receipt of
@@ -171,69 +158,58 @@ def get_file_spec():
171158
def _register_request_validation(self) -> None:
172159
@self.app.before_request
173160
def add_request_validating() -> None:
174-
method, route, view_args, args, cookie, json = self._extract_data_from_request(request)
175-
176-
if route not in self._mapped_routes_from_spec:
161+
if request.content_type != 'application/json' and request.method not in ('GET',):
177162
return
178163

179-
if method in ('options',):
164+
if request.method in ('OPTIONS',):
180165
return
181166

182-
route_as_in_spec = self.route_to_openapi_format(route)
167+
(
168+
method,
169+
route,
170+
headers,
171+
view_args,
172+
args,
173+
cookies,
174+
json,
175+
) = self._extract_data_from_request(request)
183176

184-
paths_schemas = self.serialized_spec['paths']
185-
method_schema = paths_schemas[route_as_in_spec][method]
186-
187-
request.first_view_args = {}
188-
request.first_args = {}
189-
request.first_cookie = {}
177+
if route not in self._mapped_routes_from_spec:
178+
return
190179

191-
if view_args:
192-
view_args_schema = method_schema['parameters']['view_args']
193-
try:
194-
request.first_view_args = view_args_schema().load(view_args)
195-
except ValidationError as e:
196-
raise FirstRequestPathArgsValidation(str(e))
180+
route_as_in_spec = self.route_to_openapi_format(route)
197181

198-
if args:
199-
args_schema = method_schema['parameters']['args']
200-
try:
182+
params_schemas = self.spec.serialized_spec['paths'][route_as_in_spec][method].get(
183+
'parameters'
184+
)
185+
if params_schemas:
186+
args_schema = params_schemas.get('args')
187+
if args_schema:
201188
schema_fields = args_schema().fields
202-
args_with_args_as_list = self._arg_to_list(args, schema_fields)
203-
request.first_args = args_schema().load(args_with_args_as_list)
204-
except ValidationError as e:
205-
raise FirstRequestArgsValidation(str(e))
206-
207-
if cookie:
208-
if 'parameters' in method_schema:
209-
if 'cookie' in method_schema['parameters']:
210-
cookie_schema = method_schema['parameters']['cookie']
211-
try:
212-
request.first_cookie = cookie_schema().load(cookie)
213-
except ValidationError as e:
214-
raise FirstRequestCookieValidation(str(e))
215-
216-
if json:
217-
content = method_schema['requestBody']['content']
218-
json_schema = content[request.content_type]['schema']
219-
try:
220-
if isinstance(json, list):
221-
request.first_json = json_schema._load(json, None)
222-
elif 'allOf' in json_schema._declared_fields:
223-
request.first_json = json_schema().load({'allOf': json})
224-
elif 'anyOf' in json_schema._declared_fields:
225-
request.first_json = json_schema().load({'anyOf': json})
226-
elif 'oneOf' in json_schema._declared_fields:
227-
request.first_json = json_schema().load({'oneOf': json})
228-
else:
229-
request.first_json = json_schema().load(json)
230-
except ValidationError as e:
231-
raise FirstRequestJSONValidation(str(e))
189+
args = self._arg_to_list(args, schema_fields)
190+
191+
rv = RequestSerializer(
192+
self.spec,
193+
method,
194+
route_as_in_spec,
195+
headers=dict(headers),
196+
cookies=cookies,
197+
path_params=view_args,
198+
params=args,
199+
json=json,
200+
)
201+
rv.validate()
202+
203+
request.first_headers = rv.serialized_headers
204+
request.first_view_args = rv.serialized_path_params
205+
request.first_args = rv.serialized_params
206+
request.first_cookies = rv.serialized_cookies
207+
request.first_json = rv.serialized_json
232208

233209
def _register_response_validation(self) -> None:
234210
@self.app.after_request
235211
def add_response_validating(response: Response) -> Response:
236-
method, route, _, _, _, json = self._extract_data_from_request(request)
212+
method, route, _, _, _, _, json = self._extract_data_from_request(request)
237213
json = response.get_json()
238214

239215
if route not in self._mapped_routes_from_spec:
@@ -242,7 +218,7 @@ def add_response_validating(response: Response) -> Response:
242218
route_as_in_spec = self.route_to_openapi_format(route)
243219

244220
try:
245-
route_schema: dict = self.serialized_spec['paths'][route_as_in_spec]
221+
route_schema: dict = self.spec.serialized_spec['paths'][route_as_in_spec]
246222
except KeyError as e:
247223
raise FirstResponseJSONValidation(
248224
f'Route <{e.args[0]}> not defined in specification.'

src/flask_first/first/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
from .serializers import RequestSerializer
2+
from .specification import Specification
3+
4+
__all__ = ['Specification', 'RequestSerializer']

src/flask_first/exceptions.py renamed to src/flask_first/first/exceptions.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,18 @@ class FirstValidation(FirstException):
1313
"""Exception for request validation error."""
1414

1515

16+
class FirstEndpointValidation(FirstValidation):
17+
"""Exception for endpoint validation error."""
18+
19+
20+
class FirstRequestHeadersValidation(FirstValidation):
21+
"""Exception for headers validation error."""
22+
23+
24+
class FirstRequestCookiesValidation(FirstValidation):
25+
"""Exception for cookies validation error."""
26+
27+
1628
class FirstRequestPathArgsValidation(FirstValidation):
1729
"""Exception for path-parameters validation error."""
1830

0 commit comments

Comments
 (0)