diff --git a/.github/workflows/publish-to-pypi.yml b/.github/workflows/publish-to-pypi.yaml similarity index 100% rename from .github/workflows/publish-to-pypi.yml rename to .github/workflows/publish-to-pypi.yaml diff --git a/CHAGELOG.md b/CHAGELOG.md index 5a50090..c3d17be 100644 --- a/CHAGELOG.md +++ b/CHAGELOG.md @@ -3,43 +3,46 @@ All notable changes to this project will be documented in this file. -## [0.0.7] - 2025-01-14 +# [0.0.7] - 2025-01-14 -### Added +## Added - Workflow to run tests on all supported python versions. [Check it out](.github/workflows/test_env.yaml) - Added more test coverage for validators and filters. -- Added tracking of coverage in tests. +- Added tracking of coverage in tests. [Check it out](https://coveralls.io/github/LeanderCS/flask-inputfilter) +- New functionality for global filters and validators in InputFilters. +- New functionality to define custom supported methods. -### Changed +### Validator -- Updated root README.md to include badges. +- New `NotInArrayValidator` to check if a value is not in a list. [Check it out](flask_inputfilter/Validator/NotInArrayValidator.py) +- New `NotValidator` to invert the result of another validator. [Check it out](flask_inputfilter/Validator/NotValidator.py) -## [0.0.6] - 2025-01-12 +# [0.0.6] - 2025-01-12 -### Added +## Added - New date validators and filters. -### Changed +## Removed - Dropped support for Python 3.6. -## [0.0.5] - 2025-01-12 +# [0.0.5] - 2025-01-12 -### Added +## Added - New condition functionality between fields. [Check it out](flask_inputfilter/Condition/README.md) -### Changed +## Changed - Switched external_api config from dict to class. [Check it out](flask_inputfilter/Model/ExternalApiConfig.py) -## [0.0.4] - 2025-01-09 +# [0.0.4] - 2025-01-09 -### Added +## Added - New external api functionality. [Check it out](EXTERNAL_API.md) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..6a10d35 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,9 @@ +# Contributing + +Thank you for considering contributing to the project. To make the process as easy and as effective as possible, +please follow the guidelines below. + +## Reporting Issues + +If you find a bug or have a feature request, please open an issue on the project's GitHub repository. +Before submitting an issue, please search the existing issues to see if your issue has already been reported. diff --git a/README.rst b/README.rst index fddbfe1..5234e91 100644 --- a/README.rst +++ b/README.rst @@ -118,11 +118,11 @@ Options The `add` method supports several options: - `Required`_ -- `Filter` (see `Filter` documentation in :file:`flask_inputfilter/Filter/README.md`) -- `Validator` (see `Validator` documentation in :file:`flask_inputfilter/Validator/README.md`) +- `Filter `_ +- `Validator `_ - `Default`_ - `Fallback`_ -- `ExternalApi` (see :file:`EXTERNAL_API.md`) +- `ExternalApi `_ Required -------- diff --git a/flask_inputfilter/Condition/README.md b/flask_inputfilter/Condition/README.md index 16aa7e3..86911c5 100644 --- a/flask_inputfilter/Condition/README.md +++ b/flask_inputfilter/Condition/README.md @@ -40,18 +40,20 @@ class TestInputFilter(InputFilter): The following conditions are available in the `Condition` module: -1. [`CustomCondition`](CustomCondition.py) - A custom condition that can be used to validate the input data. -2. [`EqualCondition`](EqualCondition.py) - Validates that the input is equal to the given value. -3. [`ExactlyNOfCondition`](ExactlyNOfCondition.py) - Validates that exactly `n` of the given conditions are true. -4. [`ExactlyNOfMatchesCondition`](ExactlyNOfMatchesCondition.py) - Validates that exactly `n` of the given matches are true. -5. [`ExactlyOneOfCondition`](ExactlyOneOfCondition.py) - Validates that exactly one of the given conditions is true. -6. [`ExactlyOneOfMatchesCondition`](ExactlyOneOfMatchesCondition.py) - Validates that exactly one of the given matches is true. -7. [`IntegerBiggerThanCondition`](IntegerBiggerThanCondition.py) - Validates that the integer is bigger than the given value. -8. [`NOfCondition`](NOfCondition.py) - Validates that at least `n` of the given conditions are true. -9. [`NOfMatchesCondition`](NOfMatchesCondition.py) - Validates that at least `n` of the given matches are true. -10. [`NotEqualCondition`](NotEqualCondition.py) - Validates that the input is not equal to the given value. -11. [`OneOfCondition`](OneOfCondition.py) - Validates that at least one of the given conditions is true. -12. [`OneOfMatchesCondition`](OneOfMatchesCondition.py) - Validates that at least one of the given matches is true. -13. [`RequiredIfCondition`](RequiredIfCondition.py) - Validates that the input is required if the given condition is true. -14. [`StringLongerThanCondition`](StringLongerThanCondition.py) - Validates that the string is longer than the given value. -15. [`TemporalOrderCondition`](TemporalOrderCondition.py) - Validates that the input is in correct temporal order. +1. [`ArrayLengthEqualCondition`](ArrayLengthEqualCondition.py) - Validates that the length of the array is equal to the given value. +2. [`ArrayLongerThanCondition`](ArrayLongerThanCondition.py) - Validates that the length of the array is longer than the given value. +3. [`CustomCondition`](CustomCondition.py) - A custom condition that can be used to validate the input data. +4. [`EqualCondition`](EqualCondition.py) - Validates that the input is equal to the given value. +5. [`ExactlyNOfCondition`](ExactlyNOfCondition.py) - Validates that exactly `n` of the given conditions are true. +6. [`ExactlyNOfMatchesCondition`](ExactlyNOfMatchesCondition.py) - Validates that exactly `n` of the given matches are true. +7. [`ExactlyOneOfCondition`](ExactlyOneOfCondition.py) - Validates that exactly one of the given conditions is true. +8. [`ExactlyOneOfMatchesCondition`](ExactlyOneOfMatchesCondition.py) - Validates that exactly one of the given matches is true. +9. [`IntegerBiggerThanCondition`](IntegerBiggerThanCondition.py) - Validates that the integer is bigger than the given value. +10. [`NOfCondition`](NOfCondition.py) - Validates that at least `n` of the given conditions are true. +11. [`NOfMatchesCondition`](NOfMatchesCondition.py) - Validates that at least `n` of the given matches are true. +12. [`NotEqualCondition`](NotEqualCondition.py) - Validates that the input is not equal to the given value. +13. [`OneOfCondition`](OneOfCondition.py) - Validates that at least one of the given conditions is true. +14. [`OneOfMatchesCondition`](OneOfMatchesCondition.py) - Validates that at least one of the given matches is true. +15. [`RequiredIfCondition`](RequiredIfCondition.py) - Validates that the input is required if the given field has a specific value. +16. [`StringLongerThanCondition`](StringLongerThanCondition.py) - Validates that the string is longer than the given value. +17. [`TemporalOrderCondition`](TemporalOrderCondition.py) - Validates that the input is in correct temporal order. diff --git a/flask_inputfilter/Condition/RequiredIfCondition.py b/flask_inputfilter/Condition/RequiredIfCondition.py index 650d969..c04a299 100644 --- a/flask_inputfilter/Condition/RequiredIfCondition.py +++ b/flask_inputfilter/Condition/RequiredIfCondition.py @@ -1,4 +1,4 @@ -from typing import Any, Dict +from typing import Any, Dict, List, Optional, Union from .BaseCondition import BaseCondition @@ -10,14 +10,28 @@ class RequiredIfCondition(BaseCondition): """ def __init__( - self, condition_field: str, value: Any, required_field: str + self, + condition_field: str, + value: Optional[Union[Any, List[Any]]], + required_field: str, ) -> None: self.condition_field = condition_field self.value = value self.required_field = required_field def check(self, data: Dict[str, Any]) -> bool: - return ( - data.get(self.condition_field) != self.value - or data.get(self.required_field) is not None - ) + condition_value = data.get(self.condition_field) + + if self.value is not None: + if isinstance(self.value, list): + if condition_value in self.value: + return data.get(self.required_field) is not None + else: + if condition_value == self.value: + return data.get(self.required_field) is not None + + else: + if condition_value is not None: + return data.get(self.required_field) is not None + + return True diff --git a/flask_inputfilter/Filter/README.md b/flask_inputfilter/Filter/README.md index 80b8a10..5cd0862 100644 --- a/flask_inputfilter/Filter/README.md +++ b/flask_inputfilter/Filter/README.md @@ -7,23 +7,26 @@ The `Filter` module contains the filters that can be used to filter the input da The following filters are available in the `Filter` module: 1. [`ArrayExplodeFilter`](ArrayExplodeFilter.py) - Explodes the input string into an array. -2. [`RemoveEmojisFilter`](RemoveEmojisFilter.py) - Removes the emojis from the string. -3. [`SlugifyFilter`](SlugifyFilter.py) - Converts the string to a slug. -4. [`StringTrimFilter`](StringTrimFilter.py) - Trims the whitespace from the beginning and end of the string. -5. [`ToAlphaNumericFilter`](ToAlphaNumericFilter.py) - Converts the string to an alphanumeric string. -6. [`ToBooleanFilter`](ToBooleanFilter.py) - Converts the string to a boolean value. -7. [`ToCamelCaseFilter`](ToCamelCaseFilter.py) - Converts the string to camel case. -8. [`ToDateFilter`](ToDateFilter.py) - Converts a string to a date value. -9. [`ToDateTimeFilter`](ToDateTimeFilter.py) - Converts a string to a datetime value. -10. [`ToEnumFilter`](ToEnumFilter.py) - Converts a string or integer to an enum value. -11. [`ToFloatFilter`](ToFloatFilter.py) - Converts a string to a float value. -12. [`ToIntegerFilter`](ToIntegerFilter.py) - Converts a string to an integer value. -13. [`ToIsoFilter`](ToIsoFilter.py) - Converts a string to an ISO8601 date time value. -14. [`ToLowerFilter`](ToLowerFilter.py) - Converts a string to lowercase. -15. [`ToNormalizedUnicodeFilter`](ToNormalizedUnicodeFilter.py) - Normalizes a unicode string. -16. [`ToNullFilter`](ToNullFilter.py) - Converts the string to `None` if it is already `None` or `''` (empty string). -17. [`ToPascaleCaseFilter`](ToPascaleCaseFilter.py) - Converts the string to pascal case. -18. [`ToSnakeCaseFilter`](ToSnakeCaseFilter.py) - Converts the string to snake case. -19. [`ToStringFilter`](ToStringFilter.py) - Converts the input to a string value. -20. [`ToUpperFilter`](ToUpperFilter.py) - Converts the string to uppercase. -21. [`WhitespaceCollapseFilter`](WhitespaceCollapseFilter.py) - Collapses the whitespace in the string. +2. [`BlacklistFilter`](BlacklistFilter.py) - Filters the string based on the blacklist. +3. [`RemoveEmojisFilter`](RemoveEmojisFilter.py) - Removes the emojis from the string. +4. [`SlugifyFilter`](SlugifyFilter.py) - Converts the string to a slug. +5. [`StringTrimFilter`](StringTrimFilter.py) - Trims the whitespace from the beginning and end of the string. +6. [`ToAlphaNumericFilter`](ToAlphaNumericFilter.py) - Converts the string to an alphanumeric string. +7. [`ToBooleanFilter`](ToBooleanFilter.py) - Converts the string to a boolean value. +8. [`ToCamelCaseFilter`](ToCamelCaseFilter.py) - Converts the string to camel case. +9. [`ToDateFilter`](ToDateFilter.py) - Converts a string to a date value. +10. [`ToDateTimeFilter`](ToDateTimeFilter.py) - Converts a string to a datetime value. +11. [`ToEnumFilter`](ToEnumFilter.py) - Converts a string or integer to an enum value. +12. [`ToFloatFilter`](ToFloatFilter.py) - Converts a string to a float value. +13. [`ToIntegerFilter`](ToIntegerFilter.py) - Converts a string to an integer value. +14. [`ToIsoFilter`](ToIsoFilter.py) - Converts a string to an ISO8601 date time value. +15. [`ToLowerFilter`](ToLowerFilter.py) - Converts a string to lowercase. +16. [`ToNormalizedUnicodeFilter`](ToNormalizedUnicodeFilter.py) - Normalizes a unicode string. +17. [`ToNullFilter`](ToNullFilter.py) - Converts the string to `None` if it is already `None` or `''` (empty string). +18. [`ToPascaleCaseFilter`](ToPascaleCaseFilter.py) - Converts the string to pascal case. +19. [`ToSnakeCaseFilter`](ToSnakeCaseFilter.py) - Converts the string to snake case. +20. [`ToStringFilter`](ToStringFilter.py) - Converts the input to a string value. +21. [`ToUpperFilter`](ToUpperFilter.py) - Converts the string to uppercase. +22. [`TruncateFilter`](TruncateFilter.py) - Truncates the string to the specified length. +23. [`WhitelistFilter`](WhitelistFilter.py) - Filters the string based on the whitelist. +24. [`WhitespaceCollapseFilter`](WhitespaceCollapseFilter.py) - Collapses the whitespace in the string. diff --git a/flask_inputfilter/InputFilter.py b/flask_inputfilter/InputFilter.py index 5c04c3a..4e75498 100644 --- a/flask_inputfilter/InputFilter.py +++ b/flask_inputfilter/InputFilter.py @@ -16,9 +16,12 @@ class InputFilter: Base class for input filters. """ - def __init__(self) -> None: + def __init__(self, methods: Optional[List[str]] = None) -> None: + self.methods = methods or ["GET", "POST", "PATCH", "PUT", "DELETE"] self.fields = {} self.conditions = [] + self.global_filters = [] + self.global_validators = [] def add( self, @@ -36,7 +39,8 @@ def add( :param name: The name of the field. :param required: Whether the field is required. :param default: The default value of the field. - :param fallback: The fallback value of the field. + :param fallback: The fallback value of the field, if validations fails + or field None, although it is required . :param filters: The filters to apply to the field value. :param validators: The validators to apply to the field value. :param external_api: Configuration for an external API call. @@ -57,15 +61,27 @@ def addCondition(self, condition: BaseCondition) -> None: """ self.conditions.append(condition) + def addGlobalFilter(self, filter_: BaseFilter) -> None: + """ + Add a global filter to be applied to all fields. + """ + self.global_filters.append(filter_) + + def addGlobalValidator(self, validator: BaseValidator) -> None: + """ + Add a global validator to be applied to all fields. + """ + self.global_validators.append(validator) + def _applyFilters(self, field_name: str, value: Any) -> Any: """ Apply filters to the field value. """ - field = self.fields.get(field_name) + for filter_ in self.global_filters: + value = filter_.apply(value) - if not field: - return value + field = self.fields.get(field_name) for filter_ in field["filters"]: value = filter_.apply(value) @@ -77,10 +93,10 @@ def _validateField(self, field_name: str, value: Any) -> None: Validate the field value. """ - field = self.fields.get(field_name) + for validator in self.global_validators: + validator.validate(value) - if not field: - return + field = self.fields.get(field_name) for validator in field["validators"]: validator.validate(value) @@ -133,38 +149,31 @@ def _callExternalApi( return result @staticmethod - def __replacePlaceholders(url: str, validated_data: dict) -> str: + def __replacePlaceholders(value: str, validated_data: dict) -> str: """ - Ersetzt alle Platzhalter in der URL, die mit {{}} definiert sind, - durch die entsprechenden Werte aus den Parametern. + Replace all placeholders, marked with '{{ }}' in value + with the corresponding values from validated_data. """ return re.sub( r"{{(.*?)}}", lambda match: str(validated_data.get(match.group(1))), - url, + value, ) - @staticmethod def __replacePlaceholdersInParams( - params: dict, validated_data: dict + self, params: dict, validated_data: dict ) -> dict: """ Replace all placeholders in params with the corresponding values from validated_data. """ - replaced_params = {} - for key, value in params.items(): - if isinstance(value, str): - replaced_value = re.sub( - r"{{(.*?)}}", - lambda match: str(validated_data.get(match.group(1), "")), - value, - ) - replaced_params[key] = replaced_value - else: - replaced_params[key] = value - return replaced_params + return { + key: self.__replacePlaceholders(value, validated_data) + if isinstance(value, str) + else value + for key, value in params.items() + } def validateData( self, data: Dict[str, Any], kwargs: Dict[str, Any] = None @@ -221,7 +230,7 @@ def validateData( external_api_config, validated_data ) - except ValidationError: + except Exception: if field_info.get("fallback") is None: raise ValidationError( f"External API call failed for field " @@ -274,25 +283,13 @@ def decorator( def wrapper( *args, **kwargs ) -> Union[Response, Tuple[Any, Dict[str, Any]]]: - if request.method == "GET": - data = request.args - - elif request.method in ["POST", "PUT", "DELETE"]: - if not request.is_json: - data = request.args - - else: - data = request.json - - else: - return Response( - status=415, response="Unsupported method Type" - ) + if request.method not in cls().methods: + return Response(status=405, response="Method Not Allowed") - inputFilter = cls() + data = request.json if request.is_json else request.args try: - g.validated_data = inputFilter.validateData(data, kwargs) + g.validated_data = cls().validateData(data, kwargs) except ValidationError as e: return Response(status=400, response=str(e)) diff --git a/flask_inputfilter/Validator/ArrayElementValidator.py b/flask_inputfilter/Validator/ArrayElementValidator.py index d430b0b..e4d75be 100644 --- a/flask_inputfilter/Validator/ArrayElementValidator.py +++ b/flask_inputfilter/Validator/ArrayElementValidator.py @@ -1,4 +1,4 @@ -from typing import TYPE_CHECKING, Any +from typing import TYPE_CHECKING, Any, Optional from ..Exception import ValidationError from .BaseValidator import BaseValidator @@ -15,14 +15,14 @@ class ArrayElementValidator(BaseValidator): def __init__( self, elementFilter: "InputFilter", - error_message: str = "Value '{}' is not in '{}'", + error_message: Optional[str] = None, ) -> None: self.elementFilter = elementFilter self.error_message = error_message def validate(self, value: Any) -> None: if not isinstance(value, list): - raise ValidationError("Value is not an array") + raise ValidationError(f"Value '{value}' is not an array") for i, element in enumerate(value): try: @@ -30,9 +30,7 @@ def validate(self, value: Any) -> None: value[i] = validated_element except ValidationError: - if "{}" in self.error_message: - raise ValidationError( - self.error_message.format(element, self.elementFilter) - ) - - raise ValidationError(self.error_message) + raise ValidationError( + self.error_message + or f"Value '{element}' is not in '{self.elementFilter}'" + ) diff --git a/flask_inputfilter/Validator/ArrayLengthValidator.py b/flask_inputfilter/Validator/ArrayLengthValidator.py index cf8b281..06fdebd 100644 --- a/flask_inputfilter/Validator/ArrayLengthValidator.py +++ b/flask_inputfilter/Validator/ArrayLengthValidator.py @@ -1,4 +1,4 @@ -from typing import Any +from typing import Any, Optional from ..Exception import ValidationError from .BaseValidator import BaseValidator @@ -14,7 +14,7 @@ def __init__( self, min_length: int = 0, max_length: int = float("inf"), - error_message: str = "Array length must be between {} and {}.", + error_message: Optional[str] = None, ) -> None: self.min_length = min_length self.max_length = max_length @@ -22,14 +22,13 @@ def __init__( def validate(self, value: Any) -> None: if not isinstance(value, list): - raise ValidationError("Value must be a list.") + raise ValidationError(f"Value '{value}' must be a list.") array_length = len(value) if not (self.min_length <= array_length <= self.max_length): - if "{}" in self.error_message: - raise ValidationError( - self.error_message.format(self.min_length, self.max_length) - ) - - raise ValidationError(self.error_message) + raise ValidationError( + self.error_message + or f"Array length must be between '{self.min_length}' " + f"and '{self.max_length}'." + ) diff --git a/flask_inputfilter/Validator/DateAfterValidator.py b/flask_inputfilter/Validator/DateAfterValidator.py index 54b1055..3acb0da 100644 --- a/flask_inputfilter/Validator/DateAfterValidator.py +++ b/flask_inputfilter/Validator/DateAfterValidator.py @@ -1,5 +1,5 @@ from datetime import date, datetime -from typing import Any, Union +from typing import Any, Optional, Union from ..Exception import ValidationError from .BaseValidator import BaseValidator @@ -14,7 +14,7 @@ class DateAfterValidator(BaseValidator): def __init__( self, reference_date: Union[str, date, datetime], - error_message: str = "Date '{}' is not after '{}'.", + error_message: Optional[str] = None, ) -> None: self.reference_date = reference_date self.error_message = error_message @@ -24,12 +24,10 @@ def validate(self, value: Any) -> None: value_datetime = self._parse_date(value) if value_datetime <= value_reference_date: - if "{}" in self.error_message: - raise ValidationError( - self.error_message.format(value, value_reference_date) - ) - - raise ValidationError(self.error_message) + raise ValidationError( + self.error_message + or f"Date '{value}' is not after '{value_reference_date}'." + ) def _parse_date(self, value: Any) -> datetime: if isinstance(value, datetime): @@ -43,9 +41,9 @@ def _parse_date(self, value: Any) -> datetime: return datetime.fromisoformat(value) except ValueError: - raise ValidationError(f"Invalid ISO 8601 format: {value}") + raise ValidationError(f"Invalid ISO 8601 format '{value}'.") else: raise ValidationError( - f"Unsupported type for date comparison: {type(value)}" + f"Unsupported type for date comparison '{type(value)}'." ) diff --git a/flask_inputfilter/Validator/DateBeforeValidator.py b/flask_inputfilter/Validator/DateBeforeValidator.py index dda4fd9..e8088ec 100644 --- a/flask_inputfilter/Validator/DateBeforeValidator.py +++ b/flask_inputfilter/Validator/DateBeforeValidator.py @@ -1,5 +1,5 @@ from datetime import date, datetime -from typing import Any, Union +from typing import Any, Optional, Union from ..Exception import ValidationError from .BaseValidator import BaseValidator @@ -14,7 +14,7 @@ class DateBeforeValidator(BaseValidator): def __init__( self, reference_date: Union[str, date, datetime], - error_message: str = "Date '{}' is not before '{}'.", + error_message: Optional[str] = None, ) -> None: self.reference_date = reference_date self.error_message = error_message @@ -24,12 +24,10 @@ def validate(self, value: Any) -> None: value_datetime = self._parse_date(value) if value_datetime >= value_reference_date: - if "{}" in self.error_message: - raise ValidationError( - self.error_message.format(value, value_reference_date) - ) - - raise ValidationError(self.error_message) + raise ValidationError( + self.error_message + or f"Date '{value}' is not before '{value_reference_date}'." + ) def _parse_date(self, value: Any) -> datetime: if isinstance(value, datetime): @@ -43,9 +41,9 @@ def _parse_date(self, value: Any) -> datetime: return datetime.fromisoformat(value) except ValueError: - raise ValidationError(f"Invalid ISO 8601 format: {value}") + raise ValidationError(f"Invalid ISO 8601 format '{value}'.") else: raise ValidationError( - f"Unsupported type for date comparison: {type(value)}" + f"Unsupported type for date comparison '{type(value)}'." ) diff --git a/flask_inputfilter/Validator/DateRangeValidator.py b/flask_inputfilter/Validator/DateRangeValidator.py index 4a0b550..2308712 100644 --- a/flask_inputfilter/Validator/DateRangeValidator.py +++ b/flask_inputfilter/Validator/DateRangeValidator.py @@ -14,7 +14,7 @@ def __init__( self, min_date: Optional[Union[str, date, datetime]] = None, max_date: Optional[Union[str, date, datetime]] = None, - error_message="Date '{}' is not in the range from '{}' to '{}'.", + error_message: Optional[str] = None, ) -> None: self.min_date = min_date self.max_date = max_date @@ -28,14 +28,11 @@ def validate(self, value: Any) -> None: if (min_date and value_date < min_date) or ( max_date and value_date > max_date ): - if "{}" in self.error_message: - raise ValidationError( - self.error_message.format( - value, self.min_date, self.max_date - ) - ) - - raise ValidationError(self.error_message) + raise ValidationError( + self.error_message + or f"Date '{value}' is not in the range from " + f"'{self.min_date}' to '{self.max_date}'." + ) def _parse_date(self, value: Any) -> datetime: """ @@ -51,11 +48,11 @@ def _parse_date(self, value: Any) -> datetime: return datetime.fromisoformat(value) except ValueError: - raise ValidationError(f"Invalid ISO 8601 format: {value}") + raise ValidationError(f"Invalid ISO 8601 format '{value}'.") elif isinstance(value, date): return datetime.combine(value, datetime.min.time()) raise ValidationError( - f"Unsupported type for past date validation: {type(value)}" + f"Unsupported type for past date validation '{type(value)}'." ) diff --git a/flask_inputfilter/Validator/FloatPrecisionValidator.py b/flask_inputfilter/Validator/FloatPrecisionValidator.py index 05dc6ab..867afa5 100644 --- a/flask_inputfilter/Validator/FloatPrecisionValidator.py +++ b/flask_inputfilter/Validator/FloatPrecisionValidator.py @@ -1,5 +1,5 @@ import re -from typing import Any +from typing import Any, Optional from ..Exception import ValidationError from .BaseValidator import BaseValidator @@ -14,8 +14,7 @@ def __init__( self, precision: int, scale: int, - error_message: str = "Value '{}' has more than {} digits in total or " - "{} digits after the decimal point.", + error_message: Optional[str] = None, ) -> None: self.precision = precision self.scale = scale @@ -23,23 +22,23 @@ def __init__( def validate(self, value: Any) -> None: if not isinstance(value, (float, int)): - raise ValidationError("Value must be a float or an integer") + raise ValidationError( + f"Value '{value}' must be a float or an integer." + ) value_str = str(value) match = re.match(r"^-?(\d+)(\.(\d+))?$", value_str) if not match: - raise ValidationError("Value is not a valid float") + raise ValidationError(f"Value '{value}' is not a valid float.") digits_before = len(match.group(1)) digits_after = len(match.group(3)) if match.group(3) else 0 total_digits = digits_before + digits_after if total_digits > self.precision or digits_after > self.scale: - if "{}" in self.error_message: - raise ValidationError( - self.error_message.format( - value, self.precision, self.scale - ) - ) - - raise ValidationError(self.error_message) + raise ValidationError( + self.error_message + or f"Value '{value}' has more than {self.precision} digits " + f"in total or '{self.scale}' digits after the " + f"decimal point." + ) diff --git a/flask_inputfilter/Validator/InArrayValidator.py b/flask_inputfilter/Validator/InArrayValidator.py index bf1cee3..9f3437f 100644 --- a/flask_inputfilter/Validator/InArrayValidator.py +++ b/flask_inputfilter/Validator/InArrayValidator.py @@ -1,4 +1,4 @@ -from typing import Any, List +from typing import Any, List, Optional from ..Exception import ValidationError from .BaseValidator import BaseValidator @@ -13,7 +13,7 @@ def __init__( self, haystack: List[Any], strict: bool = False, - error_message: str = "Value '{}' is not in the allowed values '{}'.", + error_message: Optional[str] = None, ) -> None: self.haystack = haystack self.strict = strict @@ -32,9 +32,8 @@ def validate(self, value: Any) -> None: raise ValidationError except Exception: - if "{}" in self.error_message: - raise ValidationError( - self.error_message.format(value, self.haystack) - ) - - raise ValidationError(self.error_message) + raise ValidationError( + self.error_message + or f"Value '{value}' is not in the allowed " + f"values '{self.haystack}'." + ) diff --git a/flask_inputfilter/Validator/InEnumValidator.py b/flask_inputfilter/Validator/InEnumValidator.py index a6fba7a..cd71f04 100644 --- a/flask_inputfilter/Validator/InEnumValidator.py +++ b/flask_inputfilter/Validator/InEnumValidator.py @@ -1,5 +1,5 @@ from enum import Enum -from typing import Any, Type +from typing import Any, Optional, Type from ..Exception import ValidationError from .BaseValidator import BaseValidator @@ -13,7 +13,7 @@ class InEnumValidator(BaseValidator): def __init__( self, enumClass: Type[Enum], - error_message: str = "Value '{}' is not an value of '{}'", + error_message: Optional[str] = None, ) -> None: self.enumClass = enumClass self.error_message = error_message @@ -22,9 +22,7 @@ def validate(self, value: Any) -> None: if not any( value.lower() == item.name.lower() for item in self.enumClass ): - if "{}" in self.error_message: - raise ValidationError( - self.error_message.format(value, self.enumClass) - ) - - raise ValidationError(self.error_message) + raise ValidationError( + self.error_message + or f"Value '{value}' is not an value of '{self.enumClass}'" + ) diff --git a/flask_inputfilter/Validator/IsArrayValidator.py b/flask_inputfilter/Validator/IsArrayValidator.py index 5524131..47c47e6 100644 --- a/flask_inputfilter/Validator/IsArrayValidator.py +++ b/flask_inputfilter/Validator/IsArrayValidator.py @@ -1,4 +1,4 @@ -from typing import Any +from typing import Any, Optional from ..Exception import ValidationError from .BaseValidator import BaseValidator @@ -9,14 +9,11 @@ class IsArrayValidator(BaseValidator): Validator that checks if a value is an array. """ - def __init__( - self, error_message: str = "Value '{}' is not an array." - ) -> None: + def __init__(self, error_message: Optional[str] = None) -> None: self.error_message = error_message def validate(self, value: Any) -> None: if not isinstance(value, list): - if "{}" in self.error_message: - raise ValidationError(self.error_message.format(value)) - - raise ValidationError(self.error_message) + raise ValidationError( + self.error_message or f"Value '{value}' is not an array." + ) diff --git a/flask_inputfilter/Validator/IsBase64ImageCorrectSizeValidator.py b/flask_inputfilter/Validator/IsBase64ImageCorrectSizeValidator.py index 5a6f904..a95a725 100644 --- a/flask_inputfilter/Validator/IsBase64ImageCorrectSizeValidator.py +++ b/flask_inputfilter/Validator/IsBase64ImageCorrectSizeValidator.py @@ -1,5 +1,5 @@ import base64 -from typing import Any +from typing import Any, Optional from ..Exception import ValidationError from .BaseValidator import BaseValidator @@ -15,8 +15,7 @@ def __init__( self, minSize: int = 1, maxSize: int = 4 * 1024 * 1024, - error_message: str = "The image is invalid or does not " - "have an allowed size.", + error_message: Optional[str] = None, ) -> None: self.min_size = minSize self.max_size = maxSize @@ -30,4 +29,7 @@ def validate(self, value: Any) -> None: raise ValidationError except Exception: - raise ValidationError(self.error_message) + raise ValidationError( + self.error_message + or "The image is invalid or does not have an allowed size." + ) diff --git a/flask_inputfilter/Validator/IsBase64ImageValidator.py b/flask_inputfilter/Validator/IsBase64ImageValidator.py index c89d642..41cbeaf 100644 --- a/flask_inputfilter/Validator/IsBase64ImageValidator.py +++ b/flask_inputfilter/Validator/IsBase64ImageValidator.py @@ -1,6 +1,6 @@ import base64 import io -from typing import Any +from typing import Any, Optional from PIL import Image @@ -15,8 +15,7 @@ class IsBase64ImageValidator(BaseValidator): def __init__( self, - error_message: str = "The image is invalid or does not " - "have an allowed size.", + error_message: Optional[str] = None, ) -> None: self.error_message = error_message @@ -27,4 +26,7 @@ def validate(self, value: Any) -> None: image.verify() except Exception: - raise ValidationError(self.error_message) + raise ValidationError( + self.error_message + or "The image is invalid or does not have an allowed size." + ) diff --git a/flask_inputfilter/Validator/IsBooleanValidator.py b/flask_inputfilter/Validator/IsBooleanValidator.py index 3f87b53..7892ffd 100644 --- a/flask_inputfilter/Validator/IsBooleanValidator.py +++ b/flask_inputfilter/Validator/IsBooleanValidator.py @@ -1,4 +1,4 @@ -from typing import Any +from typing import Any, Optional from ..Exception import ValidationError from .BaseValidator import BaseValidator @@ -9,14 +9,11 @@ class IsBooleanValidator(BaseValidator): Validator that checks if a value is a bool. """ - def __init__( - self, error_message: str = "Value '{}' is not a bool." - ) -> None: + def __init__(self, error_message: Optional[str] = None) -> None: self.error_message = error_message def validate(self, value: Any) -> None: if not isinstance(value, bool): - if "{}" in self.error_message: - raise ValidationError(self.error_message.format(value)) - - raise ValidationError(self.error_message) + raise ValidationError( + self.error_message or f"Value '{value}' is not a boolean." + ) diff --git a/flask_inputfilter/Validator/IsFloatValidator.py b/flask_inputfilter/Validator/IsFloatValidator.py index 11fd99a..b769536 100644 --- a/flask_inputfilter/Validator/IsFloatValidator.py +++ b/flask_inputfilter/Validator/IsFloatValidator.py @@ -1,4 +1,4 @@ -from typing import Any +from typing import Any, Optional from ..Exception import ValidationError from .BaseValidator import BaseValidator @@ -9,14 +9,11 @@ class IsFloatValidator(BaseValidator): Validator that checks if a value is a float. """ - def __init__( - self, error_message: str = "Value '{}' is not a float." - ) -> None: + def __init__(self, error_message: Optional[str] = None) -> None: self.error_message = error_message def validate(self, value: Any) -> None: if not isinstance(value, float): - if "{}" in self.error_message: - raise ValidationError(self.error_message.format(value)) - - raise ValidationError(self.error_message) + raise ValidationError( + self.error_message or f"Value '{value}' is not a float." + ) diff --git a/flask_inputfilter/Validator/IsFutureDateValidator.py b/flask_inputfilter/Validator/IsFutureDateValidator.py index 30124da..f9e5bb5 100644 --- a/flask_inputfilter/Validator/IsFutureDateValidator.py +++ b/flask_inputfilter/Validator/IsFutureDateValidator.py @@ -1,5 +1,5 @@ from datetime import date, datetime -from typing import Any +from typing import Any, Optional from ..Exception import ValidationError from .BaseValidator import BaseValidator @@ -10,19 +10,16 @@ class IsFutureDateValidator(BaseValidator): Validator that checks if a date is in the future. """ - def __init__( - self, error_message: str = "Date '{}' is not in the future." - ) -> None: + def __init__(self, error_message: Optional[str] = None) -> None: self.error_message = error_message def validate(self, value: Any) -> None: value_date = self._parse_date(value) if value_date <= datetime.now(): - if "{}" in self.error_message: - raise ValidationError(self.error_message.format(value)) - - raise ValidationError(self.error_message) + raise ValidationError( + self.error_message or f"Date '{value}' is not in the future." + ) def _parse_date(self, value: Any) -> datetime: """ @@ -41,8 +38,8 @@ def _parse_date(self, value: Any) -> datetime: return datetime.fromisoformat(value) except ValueError: - raise ValidationError(f"Invalid ISO 8601 format: {value}") + raise ValidationError(f"Invalid ISO 8601 format '{value}'.") raise ValidationError( - f"Unsupported type for past date validation: {type(value)}" + f"Unsupported type for past date validation '{type(value)}'." ) diff --git a/flask_inputfilter/Validator/IsHexadecimalValidator.py b/flask_inputfilter/Validator/IsHexadecimalValidator.py index 9170469..ff0e1bd 100644 --- a/flask_inputfilter/Validator/IsHexadecimalValidator.py +++ b/flask_inputfilter/Validator/IsHexadecimalValidator.py @@ -1,4 +1,4 @@ -from typing import Any +from typing import Any, Optional from ..Exception import ValidationError from .BaseValidator import BaseValidator @@ -11,7 +11,7 @@ class IsHexadecimalValidator(BaseValidator): def __init__( self, - error_message: str = "Value '{}' is not a valid hexadecimal string.", + error_message: Optional[str] = None, ) -> None: self.error_message = error_message @@ -23,4 +23,7 @@ def validate(self, value: Any) -> None: int(value, 16) except ValueError: - raise ValidationError(self.error_message.format(value)) + raise ValidationError( + self.error_message + or f"Value '{value}' is not a valid hexadecimal string." + ) diff --git a/flask_inputfilter/Validator/IsInstanceValidator.py b/flask_inputfilter/Validator/IsInstanceValidator.py index 78f4b7e..fbf2da4 100644 --- a/flask_inputfilter/Validator/IsInstanceValidator.py +++ b/flask_inputfilter/Validator/IsInstanceValidator.py @@ -1,4 +1,4 @@ -from typing import Any, Type +from typing import Any, Optional, Type from ..Exception import ValidationError from .BaseValidator import BaseValidator @@ -12,16 +12,14 @@ class IsInstanceValidator(BaseValidator): def __init__( self, classType: Type[Any], - error_message: str = "Value '{}' is not an instance of '{}'.", + error_message: Optional[str] = None, ) -> None: self.classType = classType self.error_message = error_message def validate(self, value: Any) -> None: if not isinstance(value, self.classType): - if "{}" in self.error_message: - raise ValidationError( - self.error_message.format(value, self.classType) - ) - - raise ValidationError(self.error_message) + raise ValidationError( + self.error_message + or f"Value '{value}' is not an instance of '{self.classType}'." + ) diff --git a/flask_inputfilter/Validator/IsIntegerValidator.py b/flask_inputfilter/Validator/IsIntegerValidator.py index d9d4f16..306066f 100644 --- a/flask_inputfilter/Validator/IsIntegerValidator.py +++ b/flask_inputfilter/Validator/IsIntegerValidator.py @@ -1,4 +1,4 @@ -from typing import Any +from typing import Any, Optional from ..Exception import ValidationError from .BaseValidator import BaseValidator @@ -9,14 +9,11 @@ class IsIntegerValidator(BaseValidator): Validator that checks if a value is an integer. """ - def __init__( - self, error_message: str = "Value '{}' is not an integer." - ) -> None: + def __init__(self, error_message: Optional[str] = None) -> None: self.error_message = error_message def validate(self, value: Any) -> None: if not isinstance(value, int): - if "{}" in self.error_message: - raise ValidationError(self.error_message.format(value)) - - raise ValidationError(self.error_message) + raise ValidationError( + self.error_message, f"Value '{value}' is not an integer." + ) diff --git a/flask_inputfilter/Validator/IsJsonValidator.py b/flask_inputfilter/Validator/IsJsonValidator.py index 12ecaec..76b6cd8 100644 --- a/flask_inputfilter/Validator/IsJsonValidator.py +++ b/flask_inputfilter/Validator/IsJsonValidator.py @@ -1,5 +1,5 @@ import json -from typing import Any +from typing import Any, Optional from ..Exception import ValidationError from .BaseValidator import BaseValidator @@ -10,9 +10,7 @@ class IsJsonValidator(BaseValidator): Validator that checks if a value is a valid JSON string. """ - def __init__( - self, error_message: str = "Value '{}' is not a valid JSON string." - ) -> None: + def __init__(self, error_message: Optional[str] = None) -> None: self.error_message = error_message def validate(self, value: Any) -> None: @@ -20,7 +18,7 @@ def validate(self, value: Any) -> None: json.loads(value) except (TypeError, ValueError): - if "{}" in self.error_message: - raise ValidationError(self.error_message.format(value)) - - raise ValidationError(self.error_message) + raise ValidationError( + self.error_message + or f"Value '{value}' is not a valid JSON string." + ) diff --git a/flask_inputfilter/Validator/IsPastDateValidator.py b/flask_inputfilter/Validator/IsPastDateValidator.py index 700d43a..9ee4e49 100644 --- a/flask_inputfilter/Validator/IsPastDateValidator.py +++ b/flask_inputfilter/Validator/IsPastDateValidator.py @@ -1,5 +1,5 @@ from datetime import date, datetime -from typing import Any +from typing import Any, Optional from ..Exception import ValidationError from .BaseValidator import BaseValidator @@ -10,19 +10,16 @@ class IsPastDateValidator(BaseValidator): Validator that checks if a date is in the past. """ - def __init__( - self, error_message: str = "Date '{}' is not in the past." - ) -> None: + def __init__(self, error_message: Optional[str] = None) -> None: self.error_message = error_message def validate(self, value: Any) -> None: value_datetime = self._parse_date(value) if value_datetime >= datetime.now(): - if "{}" in self.error_message: - raise ValidationError(self.error_message.format(value)) - - raise ValidationError(self.error_message) + raise ValidationError( + self.error_message or f"Date '{value}' is not in the past." + ) def _parse_date(self, value: Any) -> datetime: """ @@ -38,12 +35,12 @@ def _parse_date(self, value: Any) -> datetime: return datetime.fromisoformat(value) except ValueError: - raise ValidationError(f"Invalid ISO 8601 format: {value}") + raise ValidationError(f"Invalid ISO 8601 format '{value}'.") elif isinstance(value, date): return datetime.combine(value, datetime.min.time()) else: raise ValidationError( - f"Unsupported type for past date validation: {type(value)}" + f"Unsupported type for past date validation '{type(value)}'." ) diff --git a/flask_inputfilter/Validator/IsStringValidator.py b/flask_inputfilter/Validator/IsStringValidator.py index 3c775a0..813ae50 100644 --- a/flask_inputfilter/Validator/IsStringValidator.py +++ b/flask_inputfilter/Validator/IsStringValidator.py @@ -1,4 +1,4 @@ -from typing import Any +from typing import Any, Optional from ..Exception import ValidationError from .BaseValidator import BaseValidator @@ -9,14 +9,11 @@ class IsStringValidator(BaseValidator): Validator that checks if a value is a string. """ - def __init__( - self, error_message: str = "Value '{}' is not a string." - ) -> None: + def __init__(self, error_message: Optional[str] = None) -> None: self.error_message = error_message def validate(self, value: Any) -> None: if not isinstance(value, str): - if "{}" in self.error_message: - raise ValidationError(self.error_message.format(value)) - - raise ValidationError(self.error_message) + raise ValidationError( + self.error_message or f"Value '{value}' is not a string." + ) diff --git a/flask_inputfilter/Validator/IsUUIDValidator.py b/flask_inputfilter/Validator/IsUUIDValidator.py index a212a14..51b8396 100644 --- a/flask_inputfilter/Validator/IsUUIDValidator.py +++ b/flask_inputfilter/Validator/IsUUIDValidator.py @@ -1,5 +1,5 @@ import uuid -from typing import Any +from typing import Any, Optional from ..Exception import ValidationError from .BaseValidator import BaseValidator @@ -10,9 +10,7 @@ class IsUUIDValidator(BaseValidator): Validator that checks if a value is a valid UUID string. """ - def __init__( - self, error_message: str = "Value '{}' is not a valid UUID." - ) -> None: + def __init__(self, error_message: Optional[str] = None) -> None: self.error_message = error_message def validate(self, value: Any) -> None: @@ -23,4 +21,6 @@ def validate(self, value: Any) -> None: uuid.UUID(value) except ValueError: - raise ValidationError(self.error_message.format(value)) + raise ValidationError( + self.error_message or f"Value '{value}' is not a valid UUID." + ) diff --git a/flask_inputfilter/Validator/IsWeekdayValidator.py b/flask_inputfilter/Validator/IsWeekdayValidator.py index 26a01dc..fd0e7ca 100644 --- a/flask_inputfilter/Validator/IsWeekdayValidator.py +++ b/flask_inputfilter/Validator/IsWeekdayValidator.py @@ -1,5 +1,5 @@ from datetime import date, datetime -from typing import Any +from typing import Any, Optional from ..Exception import ValidationError from .BaseValidator import BaseValidator @@ -11,19 +11,16 @@ class IsWeekdayValidator(BaseValidator): Supports datetime and ISO 8601 formatted strings. """ - def __init__( - self, error_message: str = "Date '{}' is not a weekday." - ) -> None: + def __init__(self, error_message: Optional[str] = None) -> None: self.error_message = error_message def validate(self, value: Any) -> None: value_datetime = self._parse_date(value) if value_datetime.weekday() in (5, 6): - if "{}" in self.error_message: - raise ValidationError(self.error_message.format(value)) - - raise ValidationError(self.error_message) + raise ValidationError( + self.error_message or f"Date '{value}' is not a weekday." + ) def _parse_date(self, value: Any) -> datetime: if isinstance(value, datetime): @@ -37,9 +34,9 @@ def _parse_date(self, value: Any) -> datetime: return datetime.fromisoformat(value) except ValueError: - raise ValidationError(f"Invalid ISO 8601 format: {value}") + raise ValidationError(f"Invalid ISO 8601 format '{value}'.") else: raise ValidationError( - f"Unsupported type for weekday validation: {type(value)}" + f"Unsupported type for weekday validation '{type(value)}'." ) diff --git a/flask_inputfilter/Validator/IsWeekendValidator.py b/flask_inputfilter/Validator/IsWeekendValidator.py index 1162dfa..f9845e9 100644 --- a/flask_inputfilter/Validator/IsWeekendValidator.py +++ b/flask_inputfilter/Validator/IsWeekendValidator.py @@ -1,5 +1,5 @@ from datetime import date, datetime -from typing import Any +from typing import Any, Optional from ..Exception import ValidationError from .BaseValidator import BaseValidator @@ -11,19 +11,16 @@ class IsWeekendValidator(BaseValidator): Supports datetime and ISO 8601 formatted strings. """ - def __init__( - self, error_message: str = "Date '{}' is not on a weekend." - ) -> None: + def __init__(self, error_message: Optional[str] = None) -> None: self.error_message = error_message def validate(self, value: Any) -> None: value_datetime = self._parse_date(value) if value_datetime.weekday() not in (5, 6): - if "{}" in self.error_message: - raise ValidationError(self.error_message.format(value)) - - raise ValidationError(self.error_message) + raise ValidationError( + self.error_message or f"Date '{value}' is not on a weekend." + ) def _parse_date(self, value: Any) -> datetime: if isinstance(value, datetime): diff --git a/flask_inputfilter/Validator/LengthValidator.py b/flask_inputfilter/Validator/LengthValidator.py index b77eda8..8fc65a6 100644 --- a/flask_inputfilter/Validator/LengthValidator.py +++ b/flask_inputfilter/Validator/LengthValidator.py @@ -1,5 +1,5 @@ from enum import Enum -from typing import Any +from typing import Any, Optional from ..Exception import ValidationError from .BaseValidator import BaseValidator @@ -11,7 +11,6 @@ class LengthEnum(Enum): """ LEAST = "least" - MOST = "most" @@ -22,31 +21,20 @@ class LengthValidator(BaseValidator): def __init__( self, - min_length: int = 0, - max_length: int = None, - error_message: str = "Value '{}' must be at {} '{}' characters long.", + min_length: Optional[int] = None, + max_length: Optional[int] = None, + error_message: Optional[str] = None, ) -> None: self.min_length = min_length self.max_length = max_length self.error_message = error_message def validate(self, value: Any) -> None: - if len(value) < self.min_length: - if "{}" in self.error_message: - raise ValidationError( - self.error_message.format( - value, LengthEnum.LEAST.value, self.min_length - ) - ) - - raise ValidationError(self.error_message) - - if self.max_length is not None and len(value) > self.max_length: - if "{}" in self.error_message: - raise ValidationError( - self.error_message.format( - value, LengthEnum.MOST.value, self.max_length - ) - ) - - raise ValidationError(self.error_message) + if (self.max_length is not None and len(value) < self.min_length) or ( + self.max_length is not None and len(value) > self.max_length + ): + raise ValidationError( + self.error_message + or f"Value '{value}' is not within the range of " + f"'{self.min_length}' to '{self.max_length}'." + ) diff --git a/flask_inputfilter/Validator/NotInArrayValidator.py b/flask_inputfilter/Validator/NotInArrayValidator.py new file mode 100644 index 0000000..828e936 --- /dev/null +++ b/flask_inputfilter/Validator/NotInArrayValidator.py @@ -0,0 +1,33 @@ +from typing import Any, List, Optional + +from ..Exception import ValidationError +from .BaseValidator import BaseValidator + + +class NotInArrayValidator(BaseValidator): + """ + Validator that checks if a value is in a given list of disallowed values. + """ + + def __init__( + self, + haystack: List[Any], + strict: bool = False, + error_message: Optional[str] = None, + ) -> None: + self.haystack = haystack + self.strict = strict + self.error_message = error_message + + def validate(self, value: Any) -> None: + is_disallowed = value in self.haystack + is_type_mismatch = self.strict and not any( + isinstance(value, type(item)) for item in self.haystack + ) + + if is_disallowed or is_type_mismatch: + raise ValidationError( + self.error_message + or f"Value '{value}' is in the disallowed values " + f"'{self.haystack}'." + ) diff --git a/flask_inputfilter/Validator/NotValidator.py b/flask_inputfilter/Validator/NotValidator.py new file mode 100644 index 0000000..d63f2fe --- /dev/null +++ b/flask_inputfilter/Validator/NotValidator.py @@ -0,0 +1,31 @@ +from typing import Any, Optional + +from ..Exception import ValidationError +from .BaseValidator import BaseValidator + + +class NotValidator(BaseValidator): + """ + Validator that inverts another validator. + """ + + def __init__( + self, + validator: BaseValidator, + error_message: Optional[str] = None, + ) -> None: + self.validator = validator + self.error_message = error_message + + def validate(self, value: Any) -> None: + try: + self.validator.validate(value) + except ValidationError: + return + + raise ValidationError( + self.error_message + or f"Validation of '{value}' in " + f"'{self.validator.__class__.__name__}' where " + f"successful but should have failed" + ) diff --git a/flask_inputfilter/Validator/README.md b/flask_inputfilter/Validator/README.md index 54783df..74f5f48 100644 --- a/flask_inputfilter/Validator/README.md +++ b/flask_inputfilter/Validator/README.md @@ -8,25 +8,29 @@ The following validators are available in the `Validator` module: 1. [`ArrayElementValidator`](ArrayElementValidator.py) - Validates each element of an array with its own defined InputFilter. 2. [`ArrayLengthValidator`](ArrayLengthValidator.py) - Validates the length of an array. -3. [`DateRangeValidator`](DateRangeValidator.py) - Validates that the date is within a specified range. -4. [`FloatPrecisionValidator`](FloatPrecisionValidator.py) - Validates the precision of a float. -5. [`InArrayValidator`](InArrayValidator.py) - Validates that the value is in the given array. -6. [`InEnumValidator`](InEnumValidator.py) - Validates that the value is in the given enum. -7. [`IsArrayValidator`](IsArrayValidator.py) - Validates that the value is an array. -8. [`IsBase64ImageCorrectSizeValidator`](IsBase64ImageCorrectSizeValidator.py) - Validates that the value is a base64 encoded string. -9. [`IsBase64ImageValidator`](IsBase64ImageValidator.py) - Validates that the value is a base64 encoded string. -10. [`IsBooleanValidator`](IsBooleanValidator.py) - Validates that the value is a boolean. -11. [`IsFloatValidator`](IsFloatValidator.py) - Validates that the value is a float. -12. [`IsFutureDateValidator`](IsFutureDateValidator.py) - Validates that the value is a future date. -13. [`IsHexadecimalValidator`](IsHexadecimalValidator.py) - Validates that the value is a hexadecimal string. -14. [`IsInstanceValidator`](IsInstanceValidator.py) - Validates that the value is an instance of a class. -15. [`IsIntegerValidator`](IsIntegerValidator.py) - Validates that the value is an integer. -16. [`IsJsonValidator`](IsJsonValidator.py) - Validates that the value is a json string. -17. [`IsPastDateValidator`](IsPastDateValidator.py) - Validates that the value is a past date. -18. [`IsStringValidator`](IsStringValidator.py) - Validates that the value is a string. -19. [`IsUUIDValidator`](IsUUIDValidator.py) - Validates that the value is a UUID. -20. [`IsWeekdayValidator`](IsWeekdayValidator.py) - Validates that the value is a weekday. -21. [`IsWeekendValidator`](IsWeekendValidator.py) - Validates that the value is a weekend. -22. [`LengthValidator`](LengthValidator.py) - Validates the length of the value. -23. [`RangeValidator`](RangeValidator.py) - Validates that the value is within a specified range. -24. [`RegexValidator`](RegexValidator.py) - Validates that the value matches a regex pattern. +3. [`DateAfterValidator`](DateAfterValidator.py) - Validates that the date is after a specified date. +4. [`DateBeforeValidator`](DateBeforeValidator.py) - Validates that the date is before a specified date. +5. [`DateRangeValidator`](DateRangeValidator.py) - Validates that the date is within a specified range. +6. [`FloatPrecisionValidator`](FloatPrecisionValidator.py) - Validates the precision of a float. +7. [`InArrayValidator`](InArrayValidator.py) - Validates that the value is in the given array. +8. [`InEnumValidator`](InEnumValidator.py) - Validates that the value is in the given enum. +9. [`IsArrayValidator`](IsArrayValidator.py) - Validates that the value is an array. +10. [`IsBase64ImageCorrectSizeValidator`](IsBase64ImageCorrectSizeValidator.py) - Validates that the value is a base64 encoded string. +11. [`IsBase64ImageValidator`](IsBase64ImageValidator.py) - Validates that the value is a base64 encoded string. +12. [`IsBooleanValidator`](IsBooleanValidator.py) - Validates that the value is a boolean. +13. [`IsFloatValidator`](IsFloatValidator.py) - Validates that the value is a float. +14. [`IsFutureDateValidator`](IsFutureDateValidator.py) - Validates that the value is a future date. +15. [`IsHexadecimalValidator`](IsHexadecimalValidator.py) - Validates that the value is a hexadecimal string. +16. [`IsInstanceValidator`](IsInstanceValidator.py) - Validates that the value is an instance of a class. +17. [`IsIntegerValidator`](IsIntegerValidator.py) - Validates that the value is an integer. +18. [`IsJsonValidator`](IsJsonValidator.py) - Validates that the value is a json string. +19. [`IsPastDateValidator`](IsPastDateValidator.py) - Validates that the value is a past date. +20. [`IsStringValidator`](IsStringValidator.py) - Validates that the value is a string. +21. [`IsUUIDValidator`](IsUUIDValidator.py) - Validates that the value is a UUID. +22. [`IsWeekdayValidator`](IsWeekdayValidator.py) - Validates that the value is a weekday. +23. [`IsWeekendValidator`](IsWeekendValidator.py) - Validates that the value is a weekend. +24. [`LengthValidator`](LengthValidator.py) - Validates the length of the value. +25. [`NotInArrayValidator`](NotInArrayValidator.py) - Validates that the value is not in the given array. +26. [`NotValidator`](NotValidator.py) - Validates that inverts the result of another validator. +26. [`RangeValidator`](RangeValidator.py) - Validates that the value is within a specified range. +27. [`RegexValidator`](RegexValidator.py) - Validates that the value matches a regex pattern. diff --git a/flask_inputfilter/Validator/RangeValidator.py b/flask_inputfilter/Validator/RangeValidator.py index 87419c0..77d78cd 100644 --- a/flask_inputfilter/Validator/RangeValidator.py +++ b/flask_inputfilter/Validator/RangeValidator.py @@ -1,4 +1,4 @@ -from typing import Any +from typing import Any, Optional from ..Exception import ValidationError from .BaseValidator import BaseValidator @@ -11,9 +11,9 @@ class RangeValidator(BaseValidator): def __init__( self, - min_value: float = None, - max_value: float = None, - error_message: str = "Value '{}' is not within the range {} to {}.", + min_value: Optional[float] = None, + max_value: Optional[float] = None, + error_message: Optional[str] = None, ) -> None: self.min_value = min_value self.max_value = max_value @@ -23,11 +23,8 @@ def validate(self, value: Any) -> None: if (self.min_value is not None and value < self.min_value) or ( self.max_value is not None and value > self.max_value ): - if "{}" in self.error_message: - raise ValidationError( - self.error_message.format( - value, self.min_value, self.max_value - ) - ) - - raise ValidationError(self.error_message) + raise ValidationError( + self.error_message + or f"Value '{value}' is not within the range of " + f"'{self.min_value}' to '{self.max_value}'." + ) diff --git a/flask_inputfilter/Validator/RegexValidator.py b/flask_inputfilter/Validator/RegexValidator.py index 99c4bdd..fd9741d 100644 --- a/flask_inputfilter/Validator/RegexValidator.py +++ b/flask_inputfilter/Validator/RegexValidator.py @@ -1,4 +1,5 @@ import re +from typing import Optional from ..Exception import ValidationError from .BaseValidator import BaseValidator @@ -13,17 +14,15 @@ class RegexValidator(BaseValidator): def __init__( self, pattern: str, - error_message: str = "Value '{}' does not match the " - "required pattern '{}'.", + error_message: Optional[str] = None, ) -> None: self.pattern = pattern self.error_message = error_message def validate(self, value: str) -> None: if not re.match(self.pattern, value): - if "{}" in self.error_message: - raise ValidationError( - self.error_message.format(value, self.pattern) - ) - - raise ValidationError(self.error_message) + raise ValidationError( + self.error_message + or f"Value '{value}' does not match the required " + f"pattern '{self.pattern}'." + ) diff --git a/flask_inputfilter/Validator/__init__.py b/flask_inputfilter/Validator/__init__.py index 62f12d6..436f222 100644 --- a/flask_inputfilter/Validator/__init__.py +++ b/flask_inputfilter/Validator/__init__.py @@ -25,5 +25,7 @@ from .IsWeekdayValidator import IsWeekdayValidator from .IsWeekendValidator import IsWeekendValidator from .LengthValidator import LengthValidator +from .NotInArrayValidator import NotInArrayValidator +from .NotValidator import NotValidator from .RangeValidator import RangeValidator from .RegexValidator import RegexValidator diff --git a/test/test_condition.py b/test/test_condition.py index 6d7007b..16f80ce 100644 --- a/test/test_condition.py +++ b/test/test_condition.py @@ -358,16 +358,50 @@ def test_required_if_condition(self) -> None: self.inputFilter.add("field1") self.inputFilter.add("field2") + # Case 1: value is a single value self.inputFilter.addCondition( RequiredIfCondition("field1", "value", "field2") ) + self.inputFilter.validateData({"field1": "not value"}) self.inputFilter.validateData({"field2": "value"}) - self.inputFilter.validateData({"field1": "value", "field2": "value"}) + self.inputFilter.validateData( + {"field1": "value", "field2": "other value"} + ) with self.assertRaises(ValidationError): self.inputFilter.validateData({"field1": "value"}) + # Case 2: value is a list + self.inputFilter.add("field3") + self.inputFilter.add("field4") + + self.inputFilter.addCondition( + RequiredIfCondition("field3", ["value1", "value2"], "field4") + ) + + self.inputFilter.validateData({"field4": "value2"}) + self.inputFilter.validateData({"field3": "value1", "field4": "value"}) + self.inputFilter.validateData({"field3": "value2", "field4": "value"}) + + with self.assertRaises(ValidationError): + self.inputFilter.validateData({"field3": "value1"}) + + # Case 3: value is None + self.inputFilter.add("field5") + self.inputFilter.add("field6") + self.inputFilter.addCondition( + RequiredIfCondition("field5", None, "field6") + ) + + self.inputFilter.validateData({"field6": "value"}) + self.inputFilter.validateData( + {"field5": "any_value", "field6": "value"} + ) + + with self.assertRaises(ValidationError): + self.inputFilter.validateData({"field5": "any_value"}) + def test_string_longer_than_condition(self) -> None: """ Test StringLongerThanCondition. diff --git a/test/test_input_filter.py b/test/test_input_filter.py index 3890466..c1a1660 100644 --- a/test/test_input_filter.py +++ b/test/test_input_filter.py @@ -1,10 +1,20 @@ import unittest from unittest.mock import Mock, patch +from flask import Flask, g, jsonify + from flask_inputfilter import InputFilter +from flask_inputfilter.Condition import BaseCondition from flask_inputfilter.Exception import ValidationError +from flask_inputfilter.Filter import ToUpperFilter from flask_inputfilter.Model import ExternalApiConfig -from flask_inputfilter.Validator import InArrayValidator +from flask_inputfilter.Validator import ( + InArrayValidator, + IsIntegerValidator, + IsStringValidator, + LengthValidator, + RegexValidator, +) class TestInputFilter(unittest.TestCase): @@ -15,6 +25,147 @@ def setUp(self) -> None: self.inputFilter = InputFilter() + @patch.object(InputFilter, "validateData") + def test_validate_decorator(self, mock_validateData) -> None: + mock_validateData.return_value = {"username": "test_user", "age": 25} + + app = Flask(__name__) + + class MyInputFilter(InputFilter): + def __init__(self): + super().__init__() + self.add( + name="username", + required=True, + filters=[], + validators=[], + ) + self.add( + name="age", + required=False, + default=18, + filters=[], + validators=[IsIntegerValidator()], + ) + + @app.route("/test", methods=["GET", "POST"]) + @MyInputFilter.validate() + def test_route(): + validated_data = g.validated_data + return jsonify(validated_data) + + with app.test_client() as client: + response = client.get( + "/test", query_string={"username": "test_user"} + ) + self.assertEqual(response.status_code, 200) + self.assertEqual( + response.json, {"username": "test_user", "age": 25} + ) + + response = client.post("/test", json={"username": "test_user"}) + self.assertEqual(response.status_code, 200) + self.assertEqual( + response.json, {"username": "test_user", "age": 25} + ) + + app = Flask(__name__) + + @app.route("/test-delete/", methods=["DELETE"]) + @MyInputFilter.validate() + def test_delete_route(username): + validated_data = g.validated_data + return jsonify(validated_data) + + with app.test_client() as client: + response = client.delete("/test-delete/test_user") + self.assertEqual(response.status_code, 200) + self.assertEqual( + response.json, {"username": "test_user", "age": 25} + ) + + app = Flask(__name__) + + @app.route("/test-unsupported", methods=["NOVALIDMETHOD"]) + @MyInputFilter.validate() + def test_unsupported_route(): + validated_data = g.validated_data + return jsonify(validated_data) + + with app.test_client() as client: + response = client.get("/test-unsupported") + self.assertEqual(response.status_code, 405) + + @patch.object(InputFilter, "validateData") + def test_validation_error_response(self, mock_validateData): + mock_validateData.side_effect = ValidationError("Invalid data") + + class MyInputFilter(InputFilter): + def __init__(self): + super().__init__() + self.add( + name="age", + required=False, + default=18, + filters=[], + validators=[IsIntegerValidator()], + ) + + app = Flask(__name__) + + @app.route("/test", methods=["GET"]) + @MyInputFilter.validate() + def test_route(): + return "Success" + + with app.test_client() as client: + response = client.get("/test", query_string={"age": "not_an_int"}) + + self.assertEqual(response.status_code, 400) + self.assertEqual(response.data.decode(), "Invalid data") + + @patch.object(InputFilter, "validateData") + def test_custom_supported_methods(self, mock_validateData): + mock_validateData.return_value = {"username": "test_user", "age": 25} + + class MyInputFilter(InputFilter): + def __init__(self): + super().__init__(methods=["GET"]) + + self.add( + name="username", + required=True, + filters=[], + validators=[], + ) + self.add( + name="age", + required=False, + default=18, + filters=[], + validators=[IsIntegerValidator()], + ) + + app = Flask(__name__) + + @app.route("/test", methods=["GET", "POST"]) + @MyInputFilter.validate() + def test_route(): + validated_data = g.validated_data + return jsonify(validated_data) + + with app.test_client() as client: + response = client.get( + "/test", query_string={"username": "test_user"} + ) + self.assertEqual(response.status_code, 200) + self.assertEqual( + response.json, {"username": "test_user", "age": 25} + ) + + response = client.post("/test", json={"username": "test_user"}) + self.assertEqual(response.status_code, 405) + def test_optional(self) -> None: """ Test that optional field validation works. @@ -32,7 +183,7 @@ def test_default(self) -> None: Test that default field works. """ - self.inputFilter.add("available", required=False, default=True) + self.inputFilter.add("available", default=True) # Default case triggert validated_data = self.inputFilter.validateData({}) @@ -52,18 +203,16 @@ def test_fallback(self) -> None: self.inputFilter.add("available", required=True, fallback=True) self.inputFilter.add( "color", - required=False, + required=True, fallback="red", validators=[InArrayValidator(["red", "green", "blue"])], ) - # Fallback case triggert validated_data = self.inputFilter.validateData({"color": "yellow"}) self.assertEqual(validated_data["available"], True) self.assertEqual(validated_data["color"], "red") - # Override fallback case validated_data = self.inputFilter.validateData( {"available": False, "color": "green"} ) @@ -71,6 +220,30 @@ def test_fallback(self) -> None: self.assertEqual(validated_data["available"], False) self.assertEqual(validated_data["color"], "green") + def test_fallback_with_default(self) -> None: + """ + Test that fallback field works. + """ + + self.inputFilter.add( + "available", required=True, default=True, fallback=False + ) + self.inputFilter.add( + "color", + default="red", + fallback="blue", + validators=[InArrayValidator(["red", "green", "blue"])], + ) + + validated_data = self.inputFilter.validateData({}) + + self.assertEqual(validated_data["available"], True) + self.assertEqual(validated_data["color"], "red") + + validated_data = self.inputFilter.validateData({"available": False}) + + self.assertEqual(validated_data["available"], False) + @patch("requests.request") def test_external_api(self, mock_request: Mock) -> None: """ @@ -83,7 +256,7 @@ def test_external_api(self, mock_request: Mock) -> None: mock_request.return_value = mock_response # Add a field where the external API receives its value - self.inputFilter.add("name", required=False, default="test_user") + self.inputFilter.add("name", default="test_user") # Add a field with external API configuration self.inputFilter.add( @@ -120,26 +293,23 @@ def test_external_api_params(self, mock_request: Mock) -> None: mock_response.json.return_value = {"is_valid": True} mock_request.return_value = mock_response - # Add fields where the external API receives its values - self.inputFilter.add("name", required=False) + self.inputFilter.add("name") - self.inputFilter.add("hash", required=False) + self.inputFilter.add("hash") - # Add a field with external API configuration self.inputFilter.add( "is_valid", required=True, external_api=ExternalApiConfig( url="https://api.example.com/validate_user/{{name}}", method="GET", - params={"hash": "{{hash}}"}, + params={"hash": "{{hash}}", "id": 123}, data_key="is_valid", headers={"custom_header": "value"}, api_key="1234", ), ) - # API returns valid result validated_data = self.inputFilter.validateData( {"name": "test_user", "hash": "1234"} ) @@ -150,10 +320,9 @@ def test_external_api_params(self, mock_request: Mock) -> None: headers={"Authorization": "Bearer 1234", "custom_header": "value"}, method="GET", url=expected_url, - params={"hash": "1234"}, + params={"hash": "1234", "id": 123}, ) - # API returns invalid status code mock_response.status_code = 500 mock_response.json.return_value = {"is_valid": False} with self.assertRaises(ValidationError): @@ -161,7 +330,6 @@ def test_external_api_params(self, mock_request: Mock) -> None: {"name": "invalid_user", "hash": "1234"} ) - # API returns invalid result mock_response.json.return_value = {} with self.assertRaises(ValidationError): self.inputFilter.validateData( @@ -175,10 +343,62 @@ def test_external_api_fallback(self, mock_request: Mock) -> None: mock_response.json.return_value = {"name": True} mock_request.return_value = mock_response + self.inputFilter.add( + "username_with_fallback", + required=True, + fallback="fallback_user", + external_api=ExternalApiConfig( + url="https://api.example.com/validate_user", + method="GET", + params={"user": "{{value}}"}, + data_key="name", + ), + ) + + validated_data = self.inputFilter.validateData( + {"username_with_fallback": None} + ) + self.assertEqual( + validated_data["username_with_fallback"], "fallback_user" + ) + + @patch("requests.request") + def test_external_api_default(self, mock_request: Mock) -> None: + mock_response = Mock() + mock_response.status_code = 200 + mock_response.json.return_value = {} + mock_request.return_value = mock_response + # API call with fallback + self.inputFilter.add( + "username_with_default", + default="default_user", + external_api=ExternalApiConfig( + url="https://api.example.com/validate_user", + method="GET", + params={"user": "{{value}}"}, + data_key="name", + ), + ) + + validated_data = self.inputFilter.validateData({}) + self.assertEqual( + validated_data["username_with_default"], "default_user" + ) + + @patch("requests.request") + def test_external_api_fallback_with_default( + self, mock_request: Mock + ) -> None: + mock_response = Mock() + mock_response.status_code = 400 + mock_response.json.return_value = {"name": True} + mock_request.return_value = mock_response + self.inputFilter.add( "username_with_fallback", required=True, + default="default_user", fallback="fallback_user", external_api=ExternalApiConfig( url="https://api.example.com/validate_user", @@ -195,6 +415,129 @@ def test_external_api_fallback(self, mock_request: Mock) -> None: validated_data["username_with_fallback"], "fallback_user" ) + @patch("requests.request") + def test_external_invalid_api_response(self, mock_request: Mock) -> None: + """ + Test that a non-JSON API response raises a ValidationError. + """ + mock_response = Mock() + mock_response.status_code = 200 + mock_response.json.side_effect = ValueError("Invalid JSON") + mock_request.return_value = mock_response + + self.inputFilter.add( + "is_valid", + external_api=ExternalApiConfig( + url="https://api.example.com/validate", + method="GET", + ), + ) + + with self.assertRaises(ValidationError): + self.inputFilter.validateData({}) + + @patch("requests.request") + def test_external_api_response_with_no_data_key( + self, mock_request: Mock + ) -> None: + """ + Test that an API response with no data key raises a ValidationError. + """ + mock_response = Mock() + mock_response.status_code = 400 + mock_response.json.return_value = {} + mock_request.return_value = mock_response + + self.inputFilter.add( + "is_valid", + external_api=ExternalApiConfig( + url="https://api.example.com/validate", + method="GET", + ), + ) + + with self.assertRaises(ValidationError): + self.inputFilter.validateData({}) + + def test_multiple_validators(self) -> None: + """ + Test that multiple validators are applied correctly. + """ + self.inputFilter.add( + "username", + required=True, + validators=[ + RegexValidator(r"^[a-zA-Z0-9_]+$"), + LengthValidator(min_length=3, max_length=15), + ], + ) + + validated_data = self.inputFilter.validateData( + {"username": "valid_user"} + ) + self.assertEqual(validated_data["username"], "valid_user") + + with self.assertRaises(ValidationError): + self.inputFilter.validateData({"username": "no"}) + + def test_conditions(self) -> None: + """ + Test that conditions are checked correctly. + """ + + class MockCondition(BaseCondition): + def check(self, data: dict) -> bool: + return data.get("age") > 18 + + self.inputFilter.add("age", required=True) + self.inputFilter.addCondition(MockCondition()) + + validated_data = self.inputFilter.validateData({"age": 20}) + self.assertEqual(validated_data["age"], 20) + + with self.assertRaises(ValidationError): + self.inputFilter.validateData({"age": 17}) + + def test_global_filter_applied_to_all_fields(self) -> None: + self.inputFilter.add("field1") + self.inputFilter.add("field2") + + self.inputFilter.addGlobalFilter(ToUpperFilter()) + + validated_data = self.inputFilter.validateData( + {"field1": "test", "field2": "example"} + ) + + self.assertEqual(validated_data["field1"], "TEST") + self.assertEqual(validated_data["field2"], "EXAMPLE") + + def test_global_filter_with_no_fields(self) -> None: + self.inputFilter.addGlobalFilter(ToUpperFilter()) + + validated_data = self.inputFilter.validateData({}) + self.assertEqual(validated_data, {}) + + def test_global_validator_applied_to_all_fields(self) -> None: + self.inputFilter.add("field1") + self.inputFilter.add("field2") + self.inputFilter.addGlobalValidator(IsStringValidator()) + + with self.assertRaises(ValidationError): + self.inputFilter.validateData({"field1": 345, "field2": "example"}) + + validated_data = self.inputFilter.validateData( + {"field1": "test", "field2": "example"} + ) + + self.assertEqual(validated_data["field1"], "test") + self.assertEqual(validated_data["field2"], "example") + + def test_global_validator_with_no_fields(self) -> None: + self.inputFilter.addGlobalValidator(IsIntegerValidator()) + + validated_data = self.inputFilter.validateData({}) + self.assertEqual(validated_data, {}) + if __name__ == "__main__": unittest.main() diff --git a/test/test_validator.py b/test/test_validator.py index dc06575..78acbdc 100644 --- a/test/test_validator.py +++ b/test/test_validator.py @@ -31,6 +31,8 @@ IsWeekdayValidator, IsWeekendValidator, LengthValidator, + NotInArrayValidator, + NotValidator, RangeValidator, RegexValidator, ) @@ -877,6 +879,75 @@ def test_length_validator(self) -> None: with self.assertRaises(ValidationError): self.inputFilter.validateData({"name": "a"}) + def test_not_in_array_validator(self) -> None: + """ + Test NotInArrayValidator. + """ + + self.inputFilter.add( + "color", + validators=[NotInArrayValidator(["red", "green", "blue"])], + ) + + self.inputFilter.validateData({"color": "yellow"}) + + with self.assertRaises(ValidationError): + self.inputFilter.validateData({"color": "red"}) + + self.inputFilter.add( + "color_strict", + validators=[NotInArrayValidator(["red", "green", "blue"], True)], + ) + + self.inputFilter.validateData({"color_strict": "yellow"}) + + with self.assertRaises(ValidationError): + self.inputFilter.validateData({"color_strict": "red"}) + + self.inputFilter.add( + "custom_error", + validators=[ + NotInArrayValidator( + ["red", "green", "blue"], + error_message="Custom error message", + ) + ], + ) + + self.inputFilter.validateData({"custom_error": "yellow"}) + + with self.assertRaises(ValidationError): + self.inputFilter.validateData({"custom_error": "red"}) + + def test_not_validator(self) -> None: + """ + Test NotValidator that inverts another validator. + """ + + self.inputFilter.add( + "age", + validators=[NotValidator(IsIntegerValidator())], + ) + + self.inputFilter.validateData({"age": "not an integer"}) + + with self.assertRaises(ValidationError): + self.inputFilter.validateData({"age": 25}) + + self.inputFilter.add( + "age", + validators=[ + NotValidator( + IsIntegerValidator(), error_message="Custom error message" + ) + ], + ) + + self.inputFilter.validateData({"age": "not an integer"}) + + with self.assertRaises(ValidationError): + self.inputFilter.validateData({"age": 25}) + def test_range_validator(self) -> None: """ Test that RangeValidator validates numeric values