Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@ poetry add flask-inputfilter
Create validation schemas by inheriting from `InputFilter`:

```python
from flask_inputfilter import InputFilter, field
from flask_inputfilter import InputFilter
from flask_inputfilter.declarative import field, global_filter
from flask_inputfilter.filters import ToIntegerFilter, StringTrimFilter
from flask_inputfilter.validators import IsIntegerValidator, LengthValidator

Expand All @@ -41,8 +42,7 @@ class UserInputFilter(InputFilter):
)

# Global filters/validators apply to all fields
_global_filters = [StringTrimFilter()]
_global_validators = []
global_filter(StringTrimFilter())
```

### 2. Usage in Flask Routes
Expand Down
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ A more detailed guide can be found [in the docs](https://leandercs.github.io/fla

```python
from flask_inputfilter import InputFilter
from flask_inputfilter.declarative import field
from flask_inputfilter.declarative import condition, field
from flask_inputfilter.conditions import ExactlyOneOfCondition
from flask_inputfilter.enums import RegexEnum
from flask_inputfilter.filters import StringTrimFilter, ToIntegerFilter, ToNullFilter
Expand Down Expand Up @@ -101,9 +101,9 @@ class UpdateZipcodeInputFilter(InputFilter):
]
)

_conditions = [
condition(
ExactlyOneOfCondition(['zipcode', 'city'])
]
)
```


Expand Down
63 changes: 55 additions & 8 deletions docs/source/changelog.rst
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,53 @@ Changelog
All notable changes to this project will be documented in this file.


[0.7.2] - 2025-09-28
--------------------

Changed
^^^^^^^
- Changed the way how to use the new decorator ``_condition``, ``_global_filter``, ``_global_validator`` and ``_model``.
They should no longer be assigned to a variable, but should be set with the corresponding declarative method.

``_condition = [Example()]`` => ``condition(Example())``

``_global_filter = [Example()]`` => ``global_filter(Example())``

``_global_validator = [Example()]`` => ``global_validator(Example())``

``_model = Example`` => ``model(Example)``

The previous way is still supported, but is not recommended because it is not streightforward and could lead to confusion.
The new methods also support multiple calls and also mass assignment.

Both of the following examples are valid and have the same effect:

.. code-block:: python

class ExampleInputFilter(InputFilter):
field1: str = field()
field2: str = field()
condition(ExactlyOneOfCondition(['field1', 'field2']))

field3: str = field()
field4: str = field()
condition(AtLeastOneOfCondition(['field3', 'field4']))


.. code-block:: python

class ExampleInputFilter(InputFilter):
field1: str = field()
field2: str = field()
field3: str = field()
field4: str = field()

condition(
ExactlyOneOfCondition(['field1', 'field2']),
AtLeastOneOfCondition(['field3', 'field4'])
)


[0.7.1] - 2025-09-27
--------------------

Expand All @@ -24,13 +71,13 @@ Added

``self.add`` => ``field``

``self.add_condition`` => ``_conditions``
``self.add_condition`` => ``condition``

``self.add_global_filter`` => ``_global_filters``
``self.add_global_filter`` => ``global_filter``

``self.add_global_validator`` => ``_global_validators``
``self.add_global_validator`` => ``global_validator``

``self.add_model`` => ``_model``
``self.add_model`` => ``model``

**Before**:
.. code-block:: python
Expand Down Expand Up @@ -65,13 +112,13 @@ Added
validators=[IsIntegerValidator()]
)

_conditions = [ExactlyOneOfCondition(['zipcode', 'city'])]
condition(ExactlyOneOfCondition(['zipcode', 'city']))

_global_filters = [StringTrimFilter()]
global_filter(StringTrimFilter())

_global_validators = [IsStringValidator()]
global_validator(IsStringValidator())

_model = UserModel
model(UserModel)

The Change is fully backward compatible, but the new way is more readable
and maintainable.
Expand Down
2 changes: 1 addition & 1 deletion docs/source/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ Definition
validators=[IsStringValidator()]
)

_conditions = [ExactlyOneOfCondition(['zipcode', 'city'])]
condition(ExactlyOneOfCondition(['zipcode', 'city']))

Usage
^^^^^
Expand Down
4 changes: 2 additions & 2 deletions docs/source/options/condition.rst
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ Condition
Overview
--------

Conditions are added using the ``_conditions`` class attribute. They evaluate the combined input data, ensuring that inter-field dependencies and relationships (such as equality, ordering, or presence) meet predefined rules.
Conditions are added using the ``condition()`` declarative. They evaluate the combined input data, ensuring that inter-field dependencies and relationships (such as equality, ordering, or presence) meet predefined rules.

Example
-------
Expand All @@ -24,7 +24,7 @@ Example
validators=[IsStringValidator()]
)

_conditions = [OneOfCondition(['id', 'name'])]
condition(OneOfCondition(['id', 'name']))

Available Conditions
--------------------
Expand Down
227 changes: 227 additions & 0 deletions docs/source/options/declarative_api.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,227 @@
Declarative API
===============

Overview
--------

The Declarative API is the modern, recommended way to define InputFilters in flask-inputfilter.
It uses Python decorators and class-level declarations to create clean, readable, and maintainable
input validation definitions.

Key Features
------------

- **Clean Syntax**: Define fields, conditions, and global components directly in class definition
- **Type Safety**: Integrates well with type hints and IDEs
- **Inheritance Support**: Full support for class inheritance and MRO
- **Model Integration**: Automatic serialization to dataclasses, Pydantic models, and more
- **Multiple Element Support**: Register multiple components at once for concise definitions

Core Components
---------------

The Declarative API consists of four main decorators:

+----------------------+--------------------------------------------+
| Decorator | Purpose |
+======================+============================================+
| ``field()`` | Define individual input fields |
+----------------------+--------------------------------------------+
| ``condition()`` | Add cross-field validation conditions |
+----------------------+--------------------------------------------+
| ``global_filter()`` | Add filters applied to all fields |
+----------------------+--------------------------------------------+
| ``global_validator()``| Add validators applied to all fields |
+----------------------+--------------------------------------------+
| ``model()`` | Associate with a model class |
+----------------------+--------------------------------------------+

Quick Example
-------------

.. code-block:: python

from flask_inputfilter import InputFilter
from flask_inputfilter.declarative import field, condition, global_filter, global_validator, model
from flask_inputfilter.filters import StringTrimFilter, ToLowerFilter
from flask_inputfilter.validators import IsStringValidator, LengthValidator, EmailValidator
from flask_inputfilter.conditions import EqualCondition
from dataclasses import dataclass

@dataclass
class User:
username: str
email: str
password: str

class UserRegistrationFilter(InputFilter):
# Field definitions with individual configuration
username = field(
required=True,
validators=[LengthValidator(min_length=3, max_length=20)]
)

email = field(
required=True,
validators=[EmailValidator()]
)

password = field(required=True, validators=[LengthValidator(min_length=8)])
password_confirmation = field(required=True)

# Global components - applied to all fields
global_filter(StringTrimFilter(), ToLowerFilter())
global_validator(IsStringValidator())

# Cross-field validation
condition(EqualCondition('password', 'password_confirmation'))

# Model association
model(User)

Declarative API
----------------

.. code-block:: python

class MyFilter(InputFilter):
name = field(required=True, validators=[IsStringValidator()])
email = field(required=True, validators=[EmailValidator()])

global_filter(StringTrimFilter())
condition(RequiredCondition('name'))

Inheritance and MRO
-------------------

The Declarative API fully supports Python's inheritance and Method Resolution Order (MRO):

.. code-block:: python

class BaseUserFilter(InputFilter):
# Base fields
name = field(required=True, validators=[IsStringValidator()])

# Base global components
global_filter(StringTrimFilter())

class ExtendedUserFilter(BaseUserFilter):
# Additional fields
email = field(required=True, validators=[EmailValidator()])
age = field(required=False, validators=[IsIntegerValidator()])

# Additional global components (inherited ones are preserved)
global_validator(LengthValidator(min_length=1))

# Conditions
condition(RequiredCondition('email'))

Field Override
~~~~~~~~~~~~~~

You can override fields from parent classes:

.. code-block:: python

class BaseFilter(InputFilter):
name = field(required=False) # Optional in base

class StrictFilter(BaseFilter):
name = field(required=True, validators=[LengthValidator(min_length=2)]) # Override

Multiple Element Registration
-----------------------------

You can register multiple components at once for cleaner definitions:

.. code-block:: python

class CompactFilter(InputFilter):
name = field(required=True)
email = field(required=True)

# Multiple global filters
global_filter(StringTrimFilter(), ToLowerFilter(), RemoveExtraSpacesFilter())

# Multiple global validators
global_validator(IsStringValidator(), NotEmptyValidator())

# Multiple conditions
condition(
RequiredCondition('name'),
RequiredCondition('email'),
EqualCondition('password', 'password_confirmation')
)

Model Integration
-----------------

The Declarative API seamlessly integrates with various model types:

Dataclasses
~~~~~~~~~~~

.. code-block:: python

from dataclasses import dataclass

@dataclass
class User:
name: str
email: str

class UserFilter(InputFilter):
name = field(required=True, validators=[IsStringValidator()])
email = field(required=True, validators=[EmailValidator()])

model(User)

# Usage
filter_instance = UserFilter()
user = filter_instance.validate_data({'name': 'John', 'email': '[email protected]'})
# user is a User dataclass instance

Pydantic Models
~~~~~~~~~~~~~~~

.. code-block:: python

from pydantic import BaseModel

class User(BaseModel):
name: str
email: str

class UserFilter(InputFilter):
name = field(required=True, validators=[IsStringValidator()])
email = field(required=True, validators=[EmailValidator()])

model(User)

TypedDict
~~~~~~~~~

.. code-block:: python

from typing import TypedDict

class UserDict(TypedDict):
name: str
email: str

class UserFilter(InputFilter):
name = field(required=True, validators=[IsStringValidator()])
email = field(required=True, validators=[EmailValidator()])

model(UserDict)

Next Steps
----------

For detailed information about each component, see:

- :doc:`Field Decorator <field_decorator>` - Complete field configuration options
- :doc:`Global Decorators <global_decorators>` - Global filters, validators, and conditions
- :doc:`Conditions <condition>` - Cross-field validation conditions
- :doc:`Filters <filter>` - Available filters and custom filter creation
- :doc:`Validators <validator>` - Available validators and custom validator creation
Loading