Skip to content

Commit 754d482

Browse files
committed
Implement JSON Schema validation for dict and list types
2 parents 71ff420 + a71de22 commit 754d482

31 files changed

+3941
-38
lines changed

.github/workflows/python-test.yml

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
# This workflow will install Python dependencies, run tests and lint with a single version of Python
2+
# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python
3+
4+
name: Flask-Parameter-Validation Unit Tests
5+
6+
on:
7+
push:
8+
branches: [ "master", "github-ci" ]
9+
pull_request:
10+
branches: [ "master" ]
11+
12+
permissions:
13+
contents: read
14+
15+
jobs:
16+
build:
17+
18+
runs-on: ubuntu-latest
19+
20+
steps:
21+
- uses: actions/checkout@v3
22+
- name: Set up Python 3
23+
uses: actions/setup-python@v3
24+
with:
25+
python-version: "3.x"
26+
- name: Install dependencies
27+
run: |
28+
python -m pip install --upgrade pip
29+
pip install pytest
30+
cd flask_parameter_validation/test
31+
if [ -f requirements.txt ]; then pip install -r requirements.txt; fi
32+
- name: Test with Pytest
33+
run: |
34+
cd flask_parameter_validation
35+
pytest

flask_parameter_validation/parameter_types/file.py

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,10 @@
33
- Would originally be in Flask's request.file
44
- Value will be a FileStorage object
55
"""
6+
import io
7+
8+
from werkzeug.datastructures import FileStorage
9+
610
from .parameter import Parameter
711

812

@@ -21,7 +25,7 @@ def __init__(
2125
self.min_length = min_length
2226
self.max_length = max_length
2327

24-
def validate(self, value):
28+
def validate(self, value: FileStorage):
2529
# Content type validation
2630
if self.content_types is not None:
2731
# We check mimetype, as it strips charset etc.
@@ -31,15 +35,19 @@ def validate(self, value):
3135

3236
# Min content length validation
3337
if self.min_length is not None:
34-
if value.content_length < self.min_length:
38+
origin = value.stream.tell()
39+
if value.stream.seek(0, io.SEEK_END) < self.min_length:
3540
raise ValueError(
3641
f"must have a content-length at least {self.min_length}."
3742
)
43+
value.stream.seek(origin)
3844

3945
# Max content length validation
4046
if self.max_length is not None:
41-
if value.content_length > self.max_length:
47+
origin = value.stream.tell()
48+
if value.stream.seek(0, io.SEEK_END) > self.max_length:
4249
raise ValueError(
4350
f"must have a content-length at most {self.max_length}."
4451
)
52+
value.stream.seek(origin)
4553
return True

flask_parameter_validation/parameter_types/parameter.py

Lines changed: 27 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -48,9 +48,28 @@ def __init__(
4848
self.alias = alias
4949
self.json_schema = json_schema
5050

51+
def func_helper(self, v):
52+
func_result = self.func(v)
53+
if type(func_result) is bool:
54+
if not func_result:
55+
raise ValueError(
56+
"value does not match the validator function."
57+
)
58+
elif type(func_result) is tuple:
59+
if len(func_result) == 2 and type(func_result[0]) is bool and type(func_result[1]) is str:
60+
if not func_result[0]:
61+
raise ValueError(
62+
func_result[1]
63+
)
64+
else:
65+
raise ValueError(
66+
f"validator function returned incorrect type: {str(type(func_result))}, should return bool or (bool, str)"
67+
)
68+
5169
# Validator
5270
def validate(self, value):
5371
print(f"Parameter#validate received {type(value)}")
72+
original_value_type_list = type(value) is list
5473
if type(value) is list:
5574
values = value
5675
# Min list len
@@ -65,6 +84,8 @@ def validate(self, value):
6584
raise ValueError(
6685
f"must have have a maximum of {self.max_list_length} items."
6786
)
87+
if self.func is not None:
88+
self.func_helper(value)
6889
if self.json_schema is not None:
6990
try:
7091
jsonschema.validate(value, self.json_schema)
@@ -87,7 +108,7 @@ def validate(self, value):
87108
for value in values:
88109
# Min length
89110
if self.min_str_length is not None:
90-
if hasattr(value, "len") and len(value) < self.min_str_length:
111+
if len(value) < self.min_str_length:
91112
raise ValueError(
92113
f"must have at least {self.min_str_length} characters."
93114
)
@@ -131,24 +152,11 @@ def validate(self, value):
131152
f"pattern does not match: {self.pattern}."
132153
)
133154

134-
# Callable
135-
if self.func is not None:
136-
func_result = self.func(value)
137-
if type(func_result) is bool:
138-
if not func_result:
139-
raise ValueError(
140-
"value does not match the validator function."
141-
)
142-
elif type(func_result) is tuple:
143-
if len(func_result) == 2 and type(func_result[0]) is bool and type(func_result[1]) is str:
144-
if not func_result[0]:
145-
raise ValueError(
146-
func_result[1]
147-
)
148-
else:
149-
raise ValueError(
150-
f"validator function returned incorrect type: {str(type(func_result))}, should return bool or (bool, str)"
151-
)
155+
# Callable (non-list)
156+
if self.func is not None and not original_value_type_list:
157+
self.func_helper(value)
158+
159+
152160

153161
return True
154162

flask_parameter_validation/parameter_types/query.py

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -31,11 +31,13 @@ def convert(self, value, allowed_types):
3131
pass
3232
# bool conversion
3333
if bool in allowed_types:
34-
if value.lower() == "true":
35-
value = True
36-
elif value.lower() == "false":
37-
value = False
38-
# dict conversion
34+
try:
35+
if value.lower() == "true":
36+
value = True
37+
elif value.lower() == "false":
38+
value = False
39+
except AttributeError:
40+
pass
3941
if dict in allowed_types:
4042
try:
4143
value = json.loads(value)

flask_parameter_validation/parameter_types/route.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,30 @@ class Route(Parameter):
1010

1111
def __init__(self, default=None, **kwargs):
1212
super().__init__(default, **kwargs)
13+
14+
def convert(self, value, allowed_types):
15+
"""Convert query parameters to corresponding types."""
16+
if type(value) is str:
17+
# int conversion
18+
if int in allowed_types:
19+
try:
20+
value = int(value)
21+
except ValueError:
22+
pass
23+
# float conversion
24+
if float in allowed_types:
25+
try:
26+
value = float(value)
27+
except ValueError:
28+
pass
29+
# bool conversion
30+
if bool in allowed_types:
31+
try:
32+
if value.lower() == "true":
33+
value = True
34+
elif value.lower() == "false":
35+
value = False
36+
except AttributeError:
37+
pass
38+
39+
return super().convert(value, allowed_types)

flask_parameter_validation/parameter_validation.py

Lines changed: 6 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -236,19 +236,14 @@ def validate(self, expected_input, all_request_inputs):
236236
original_expected_input_type,
237237
)
238238

239-
print(json.dumps(user_input))
240-
241-
if expected_input_type_str.startswith("typing.List"):
242-
# Validate parameter-specific requirements are met
243-
try:
239+
# Validate parameter-specific requirements are met
240+
try:
241+
if type(user_input) is list:
244242
expected_delivery_type.validate(user_input)
245-
except ValueError as e:
246-
raise ValidationError(str(e), expected_name, expected_input_type)
247-
else:
248-
try:
243+
else:
249244
expected_delivery_type.validate(user_inputs[0])
250-
except ValueError as e:
251-
raise ValidationError(str(e), expected_name, expected_input_type)
245+
except ValueError as e:
246+
raise ValidationError(str(e), expected_name, expected_input_type)
252247

253248
# Return input back to parent function
254249
if expected_input_type_str.startswith("typing.List"):

flask_parameter_validation/test/__init__.py

Whitespace-only changes.
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import pytest
2+
from .testing_application import create_app
3+
4+
5+
@pytest.fixture(scope="session")
6+
def app():
7+
app = create_app()
8+
app.config.update({"TESTING": True})
9+
yield app
10+
11+
12+
@pytest.fixture()
13+
def client(app):
14+
return app.test_client()
15+
16+
17+
@pytest.fixture()
18+
def runner(app):
19+
return app.test_cli_runner()
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Flask==3.0.2
2+
../../
3+
requests
Loading

0 commit comments

Comments
 (0)