Skip to content

Commit

Permalink
Enable json_schema for other types, add tests for this where not redu…
Browse files Browse the repository at this point in the history
…ndant, update README
  • Loading branch information
d3-steichman committed Aug 14, 2024
1 parent 4c6aa5e commit 80ef751
Show file tree
Hide file tree
Showing 10 changed files with 177 additions and 33 deletions.
41 changes: 21 additions & 20 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -145,26 +145,27 @@ These can be used in tandem to describe a parameter to validate: `parameter_name
### Validation with arguments to Parameter
Validation beyond type-checking can be done by passing arguments into the constructor of the `Parameter` subclass. The arguments available for use on each type hint are:

| Parameter Name | Type of Argument | Effective On Types | Description |
|-------------------|--------------------------------------------------|------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `default` | any | All, except in `Route` | Specifies the default value for the field, makes non-Optional fields not required |
| `min_str_length` | `int` | `str` | Specifies the minimum character length for a string input |
| `max_str_length` | `int` | `str` | Specifies the maximum character length for a string input |
| `min_list_length` | `int` | `list` | Specifies the minimum number of elements in a list |
| `max_list_length` | `int` | `list` | Specifies the maximum number of elements in a list |
| `min_int` | `int` | `int` | Specifies the minimum number for an integer input |
| `max_int` | `int` | `int` | Specifies the maximum number for an integer input |
| `whitelist` | `str` | `str` | A string containing allowed characters for the value |
| `blacklist` | `str` | `str` | A string containing forbidden characters for the value |
| `pattern` | `str` | `str` | A regex pattern to test for string matches |
| `func` | `Callable[Any] -> Union[bool, tuple[bool, str]]` | All | A function containing a fully customized logic to validate the value. See the [custom validation function](#custom-validation-function) below for usage |
| `datetime_format` | `str` | `datetime.datetime` | Python datetime format string datetime format string ([datetime format codes](https://docs.python.org/3/library/datetime.html#strftime-and-strptime-format-codes)) |
| `comment` | `str` | All | A string to display as the argument description in any generated documentation |
| `alias` | `str` | All but `FileStorage` | An expected parameter name to receive instead of the function name. |
| `json_schema` | `dict` | All but `FileStorage` | An expected [JSON Schema](https://json-schema.org) which the input must conform to |
| `content_types` | `list[str]` | `FileStorage` | Allowed `Content-Type`s |
| `min_length` | `int` | `FileStorage` | Minimum `Content-Length` for a file |
| `max_length` | `int` | `FileStorage` | Maximum `Content-Length` for a file |
| Parameter Name | Type of Argument | Effective On Types | Description |
|-------------------|--------------------------------------------------|---------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `default` | any | All, except in `Route` | Specifies the default value for the field, makes non-Optional fields not required |
| `min_str_length` | `int` | `str` | Specifies the minimum character length for a string input |
| `max_str_length` | `int` | `str` | Specifies the maximum character length for a string input |
| `min_list_length` | `int` | `list` | Specifies the minimum number of elements in a list |
| `max_list_length` | `int` | `list` | Specifies the maximum number of elements in a list |
| `min_int` | `int` | `int` | Specifies the minimum number for an integer input |
| `max_int` | `int` | `int` | Specifies the maximum number for an integer input |
| `whitelist` | `str` | `str` | A string containing allowed characters for the value |
| `blacklist` | `str` | `str` | A string containing forbidden characters for the value |
| `pattern` | `str` | `str` | A regex pattern to test for string matches |
| `func` | `Callable[Any] -> Union[bool, tuple[bool, str]]` | All | A function containing a fully customized logic to validate the value. See the [custom validation function](#custom-validation-function) below for usage |
| `datetime_format` | `str` | `datetime.datetime` | Python datetime format string datetime format string ([datetime format codes](https://docs.python.org/3/library/datetime.html#strftime-and-strptime-format-codes)) |
| `comment` | `str` | All | A string to display as the argument description in any generated documentation |
| `alias` | `str` | All but `FileStorage` | An expected parameter name to receive instead of the function name. |
| `json_schema` | `dict` | `str`, `int`, `float`, `dict`, `list`<sub>1</sub> | An expected [JSON Schema](https://json-schema.org) which the input must conform to. See [python-jsonschema docs](https://python-jsonschema.readthedocs.io/en/latest/validate/#validating-formats) for information about string format validation |
| `content_types` | `list[str]` | `FileStorage` | Allowed `Content-Type`s |
| `min_length` | `int` | `FileStorage` | Minimum `Content-Length` for a file |
| `max_length` | `int` | `FileStorage` | Maximum `Content-Length` for a file |
<sub>1</sub> `json_schema` is tested to work with `str`, `int`, `float`, `dict` and `list` - other types may work, but are redundant in use and testing (i.e. JSON Schema provides no further validation on booleans beyond checking that it is a boolean)

These validators are passed into the `Parameter` subclass in the route function, such as:
* `username: str = Json(default="defaultusername", min_length=5)`
Expand Down
20 changes: 7 additions & 13 deletions flask_parameter_validation/parameter_types/parameter.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import dateutil.parser as parser
import jsonschema
from jsonschema.exceptions import ValidationError as JSONSchemaValidationError
from jsonschema.validators import Draft202012Validator


class Parameter:
Expand Down Expand Up @@ -69,6 +70,12 @@ def func_helper(self, v):
# Validator
def validate(self, value):
original_value_type_list = type(value) is list
if self.json_schema is not None:
try:
# Uses JSON Schema 2020-12 as OpenAPI 3.1.0 is fully compatible with this draft
jsonschema.validate(value, self.json_schema, format_checker=Draft202012Validator.FORMAT_CHECKER)
except JSONSchemaValidationError as e:
raise ValueError(f"failed JSON Schema validation: {e.args[0]}")
if type(value) is list:
values = value
# Min list len
Expand All @@ -85,19 +92,6 @@ def validate(self, value):
)
if self.func is not None:
self.func_helper(value)
if self.json_schema is not None:
try:
jsonschema.validate(value, self.json_schema)
except JSONSchemaValidationError as e:
raise ValueError(f"failed JSON Schema validation: {e.args[0]}")
elif type(value) is dict:
# TODO: Make json_schema work for all parameters besides FileStorage and datetime.*? Or maybe even datetime.*?
if self.json_schema is not None:
try:
jsonschema.validate(value, self.json_schema)
except JSONSchemaValidationError as e:
raise ValueError(f"failed JSON Schema validation: {e.args[0]}")
values = [value]
else:
values = [value]

Expand Down
35 changes: 35 additions & 0 deletions flask_parameter_validation/test/test_form_params.py
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,19 @@ def test_str_alias(client):
assert r.json["value"] == "abc"


def test_str_json_schema(client):
url = "/form/str/json_schema"
# Test that input matching schema yields input
r = client.post(url, data={"v": "[email protected]"})
assert "v" in r.json
assert r.json["v"] == "[email protected]"
# Test that input failing schema yields error
r = client.post(url, data={"v": "not an email"})
assert "error" in r.json




# Int Validation
def test_required_int(client):
url = "/form/int/required"
Expand Down Expand Up @@ -258,6 +271,17 @@ def test_int_func(client):
assert "error" in r.json


def test_int_json_schema(client):
url = "/form/int/json_schema"
# Test that input matching schema yields input
r = client.post(url, data={"v": 10})
assert "v" in r.json
assert r.json["v"] == 10
# Test that input failing schema yields error
r = client.post(url, data={"v": 100})
assert "error" in r.json


# Bool Validation
def test_required_bool(client):
url = "/form/bool/required"
Expand Down Expand Up @@ -382,6 +406,17 @@ def test_float_func(client):
assert "error" in r.json


def test_float_json_schema(client):
url = "/form/float/json_schema"
# Test that input matching schema yields input
r = client.post(url, data={"v": 3.14})
assert "v" in r.json
assert r.json["v"] == 3.14
# Test that input failing schema yields error
r = client.post(url, data={"v": 3.141592})
assert "error" in r.json


# datetime Validation
def test_required_datetime(client):
url = "/form/datetime/required"
Expand Down
33 changes: 33 additions & 0 deletions flask_parameter_validation/test/test_json_params.py
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,17 @@ def test_str_alias(client):
assert r.json["value"] == "abc"


def test_str_json_schema(client):
url = "/json/str/json_schema"
# Test that input matching schema yields input
r = client.post(url, json={"v": "[email protected]"})
assert "v" in r.json
assert r.json["v"] == "[email protected]"
# Test that input failing schema yields error
r = client.post(url, json={"v": "not an email"})
assert "error" in r.json


# Int Validation
def test_required_int(client):
url = "/json/int/required"
Expand Down Expand Up @@ -236,6 +247,17 @@ def test_int_func(client):
assert "error" in r.json


def test_int_json_schema(client):
url = "/json/int/json_schema"
# Test that input matching schema yields input
r = client.post(url, json={"v": 10})
assert "v" in r.json
assert r.json["v"] == 10
# Test that input failing schema yields error
r = client.post(url, json={"v": 100})
assert "error" in r.json


# Bool Validation
def test_required_bool(client):
url = "/json/bool/required"
Expand Down Expand Up @@ -360,6 +382,17 @@ def test_float_func(client):
assert "error" in r.json


def test_float_json_schema(client):
url = "/json/float/json_schema"
# Test that input matching schema yields input
r = client.post(url, json={"v": 3.14})
assert "v" in r.json
assert r.json["v"] == 3.14
# Test that input failing schema yields error
r = client.post(url, json={"v": 3.141592})
assert "error" in r.json


# datetime Validation
def test_required_datetime(client):
url = "/json/datetime/required"
Expand Down
32 changes: 32 additions & 0 deletions flask_parameter_validation/test/test_query_params.py
Original file line number Diff line number Diff line change
Expand Up @@ -406,6 +406,17 @@ def test_str_alias_async_decorator(client):
assert r.json["value"] == "abc"


def test_str_json_schema(client):
url = "/query/str/json_schema"
# Test that input matching schema yields input
r = client.get(url, query_string={"v": "[email protected]"})
assert "v" in r.json
assert r.json["v"] == "[email protected]"
# Test that input failing schema yields error
r = client.get(url, query_string={"v": "not an email"})
assert "error" in r.json


# Int Validation
def test_required_int(client):
url = "/query/int/required"
Expand Down Expand Up @@ -524,6 +535,17 @@ def test_int_func(client):
assert "error" in r.json


def test_int_json_schema(client):
url = "/query/int/json_schema"
# Test that input matching schema yields input
r = client.get(url, query_string={"v": 10})
assert "v" in r.json
assert r.json["v"] == 10
# Test that input failing schema yields error
r = client.get(url, query_string={"v": 100})
assert "error" in r.json


# Bool Validation
def test_required_bool(client):
url = "/query/bool/required"
Expand Down Expand Up @@ -736,6 +758,16 @@ def test_float_func(client):
assert "error" in r.json


def test_float_json_schema(client):
url = "/query/float/json_schema"
# Test that input matching schema yields input
r = client.get(url, query_string={"v": 3.14})
assert "v" in r.json
assert r.json["v"] == 3.14
# Test that input failing schema yields error
r = client.get(url, query_string={"v": 3.141592})
assert "error" in r.json

# datetime Validation
def test_required_datetime(client):
url = "/query/datetime/required"
Expand Down
Loading

0 comments on commit 80ef751

Please sign in to comment.