Skip to content

Commit 6b75b98

Browse files
committed
Fix declarative
1 parent c94b7ca commit 6b75b98

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

53 files changed

+3551
-421
lines changed

AGENTS.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,8 @@ poetry add flask-inputfilter
2222
Create validation schemas by inheriting from `InputFilter`:
2323

2424
```python
25-
from flask_inputfilter import InputFilter, field
25+
from flask_inputfilter import InputFilter
26+
from flask_inputfilter.declarative import field, global_filter
2627
from flask_inputfilter.filters import ToIntegerFilter, StringTrimFilter
2728
from flask_inputfilter.validators import IsIntegerValidator, LengthValidator
2829

@@ -41,8 +42,7 @@ class UserInputFilter(InputFilter):
4142
)
4243

4344
# Global filters/validators apply to all fields
44-
_global_filters = [StringTrimFilter()]
45-
_global_validators = []
45+
global_filter(StringTrimFilter())
4646
```
4747

4848
### 2. Usage in Flask Routes

README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ A more detailed guide can be found [in the docs](https://leandercs.github.io/fla
6868

6969
```python
7070
from flask_inputfilter import InputFilter
71-
from flask_inputfilter.declarative import field
71+
from flask_inputfilter.declarative import condition, field
7272
from flask_inputfilter.conditions import ExactlyOneOfCondition
7373
from flask_inputfilter.enums import RegexEnum
7474
from flask_inputfilter.filters import StringTrimFilter, ToIntegerFilter, ToNullFilter
@@ -101,9 +101,9 @@ class UpdateZipcodeInputFilter(InputFilter):
101101
]
102102
)
103103

104-
_conditions = [
104+
condition(
105105
ExactlyOneOfCondition(['zipcode', 'city'])
106-
]
106+
)
107107
```
108108

109109

docs/source/changelog.rst

Lines changed: 55 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,53 @@ Changelog
44
All notable changes to this project will be documented in this file.
55

66

7+
[0.7.2] - 2025-09-28
8+
--------------------
9+
10+
Changed
11+
^^^^^^^
12+
- Changed the way how to use the new decorator ``_condition``, ``_global_filter``, ``_global_validator`` and ``_model``.
13+
They should no longer be assigned to a variable, but should be set with the corresponding declarative method.
14+
15+
``_condition = [Example()]`` => ``condition(Example())``
16+
17+
``_global_filter = [Example()]`` => ``global_filter(Example())``
18+
19+
``_global_validator = [Example()]`` => ``global_validator(Example())``
20+
21+
``_model = Example`` => ``model(Example)``
22+
23+
The previous way is still supported, but is not recommended because it is not streightforward and could lead to confusion.
24+
The new methods also support multiple calls and also mass assignment.
25+
26+
Both of the following examples are valid and have the same effect:
27+
28+
.. code-block:: python
29+
30+
class ExampleInputFilter(InputFilter):
31+
field1: str = field()
32+
field2: str = field()
33+
condition(ExactlyOneOfCondition(['field1', 'field2']))
34+
35+
field3: str = field()
36+
field4: str = field()
37+
condition(AtLeastOneOfCondition(['field3', 'field4']))
38+
39+
40+
.. code-block:: python
41+
42+
class ExampleInputFilter(InputFilter):
43+
field1: str = field()
44+
field2: str = field()
45+
field3: str = field()
46+
field4: str = field()
47+
48+
condition(
49+
ExactlyOneOfCondition(['field1', 'field2']),
50+
AtLeastOneOfCondition(['field3', 'field4'])
51+
)
52+
53+
754
[0.7.1] - 2025-09-27
855
--------------------
956

@@ -24,13 +71,13 @@ Added
2471

2572
``self.add`` => ``field``
2673

27-
``self.add_condition`` => ``_conditions``
74+
``self.add_condition`` => ``condition``
2875

29-
``self.add_global_filter`` => ``_global_filters``
76+
``self.add_global_filter`` => ``global_filter``
3077

31-
``self.add_global_validator`` => ``_global_validators``
78+
``self.add_global_validator`` => ``global_validator``
3279

33-
``self.add_model`` => ``_model``
80+
``self.add_model`` => ``model``
3481

3582
**Before**:
3683
.. code-block:: python
@@ -65,13 +112,13 @@ Added
65112
validators=[IsIntegerValidator()]
66113
)
67114
68-
_conditions = [ExactlyOneOfCondition(['zipcode', 'city'])]
115+
condition(ExactlyOneOfCondition(['zipcode', 'city']))
69116
70-
_global_filters = [StringTrimFilter()]
117+
global_filter(StringTrimFilter())
71118
72-
_global_validators = [IsStringValidator()]
119+
global_validator(IsStringValidator())
73120
74-
_model = UserModel
121+
model(UserModel)
75122
76123
The Change is fully backward compatible, but the new way is more readable
77124
and maintainable.

docs/source/index.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ Definition
7777
validators=[IsStringValidator()]
7878
)
7979
80-
_conditions = [ExactlyOneOfCondition(['zipcode', 'city'])]
80+
condition(ExactlyOneOfCondition(['zipcode', 'city']))
8181
8282
Usage
8383
^^^^^

docs/source/options/condition.rst

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ Condition
66
Overview
77
--------
88

9-
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.
9+
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.
1010

1111
Example
1212
-------
@@ -24,7 +24,7 @@ Example
2424
validators=[IsStringValidator()]
2525
)
2626
27-
_conditions = [OneOfCondition(['id', 'name'])]
27+
condition(OneOfCondition(['id', 'name']))
2828
2929
Available Conditions
3030
--------------------
Lines changed: 227 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,227 @@
1+
Declarative API
2+
===============
3+
4+
Overview
5+
--------
6+
7+
The Declarative API is the modern, recommended way to define InputFilters in flask-inputfilter.
8+
It uses Python decorators and class-level declarations to create clean, readable, and maintainable
9+
input validation definitions.
10+
11+
Key Features
12+
------------
13+
14+
- **Clean Syntax**: Define fields, conditions, and global components directly in class definition
15+
- **Type Safety**: Integrates well with type hints and IDEs
16+
- **Inheritance Support**: Full support for class inheritance and MRO
17+
- **Model Integration**: Automatic serialization to dataclasses, Pydantic models, and more
18+
- **Multiple Element Support**: Register multiple components at once for concise definitions
19+
20+
Core Components
21+
---------------
22+
23+
The Declarative API consists of four main decorators:
24+
25+
+----------------------+--------------------------------------------+
26+
| Decorator | Purpose |
27+
+======================+============================================+
28+
| ``field()`` | Define individual input fields |
29+
+----------------------+--------------------------------------------+
30+
| ``condition()`` | Add cross-field validation conditions |
31+
+----------------------+--------------------------------------------+
32+
| ``global_filter()`` | Add filters applied to all fields |
33+
+----------------------+--------------------------------------------+
34+
| ``global_validator()``| Add validators applied to all fields |
35+
+----------------------+--------------------------------------------+
36+
| ``model()`` | Associate with a model class |
37+
+----------------------+--------------------------------------------+
38+
39+
Quick Example
40+
-------------
41+
42+
.. code-block:: python
43+
44+
from flask_inputfilter import InputFilter
45+
from flask_inputfilter.declarative import field, condition, global_filter, global_validator, model
46+
from flask_inputfilter.filters import StringTrimFilter, ToLowerFilter
47+
from flask_inputfilter.validators import IsStringValidator, LengthValidator, EmailValidator
48+
from flask_inputfilter.conditions import EqualCondition
49+
from dataclasses import dataclass
50+
51+
@dataclass
52+
class User:
53+
username: str
54+
email: str
55+
password: str
56+
57+
class UserRegistrationFilter(InputFilter):
58+
# Field definitions with individual configuration
59+
username = field(
60+
required=True,
61+
validators=[LengthValidator(min_length=3, max_length=20)]
62+
)
63+
64+
email = field(
65+
required=True,
66+
validators=[EmailValidator()]
67+
)
68+
69+
password = field(required=True, validators=[LengthValidator(min_length=8)])
70+
password_confirmation = field(required=True)
71+
72+
# Global components - applied to all fields
73+
global_filter(StringTrimFilter(), ToLowerFilter())
74+
global_validator(IsStringValidator())
75+
76+
# Cross-field validation
77+
condition(EqualCondition('password', 'password_confirmation'))
78+
79+
# Model association
80+
model(User)
81+
82+
Declarative API
83+
----------------
84+
85+
.. code-block:: python
86+
87+
class MyFilter(InputFilter):
88+
name = field(required=True, validators=[IsStringValidator()])
89+
email = field(required=True, validators=[EmailValidator()])
90+
91+
global_filter(StringTrimFilter())
92+
condition(RequiredCondition('name'))
93+
94+
Inheritance and MRO
95+
-------------------
96+
97+
The Declarative API fully supports Python's inheritance and Method Resolution Order (MRO):
98+
99+
.. code-block:: python
100+
101+
class BaseUserFilter(InputFilter):
102+
# Base fields
103+
name = field(required=True, validators=[IsStringValidator()])
104+
105+
# Base global components
106+
global_filter(StringTrimFilter())
107+
108+
class ExtendedUserFilter(BaseUserFilter):
109+
# Additional fields
110+
email = field(required=True, validators=[EmailValidator()])
111+
age = field(required=False, validators=[IsIntegerValidator()])
112+
113+
# Additional global components (inherited ones are preserved)
114+
global_validator(LengthValidator(min_length=1))
115+
116+
# Conditions
117+
condition(RequiredCondition('email'))
118+
119+
Field Override
120+
~~~~~~~~~~~~~~
121+
122+
You can override fields from parent classes:
123+
124+
.. code-block:: python
125+
126+
class BaseFilter(InputFilter):
127+
name = field(required=False) # Optional in base
128+
129+
class StrictFilter(BaseFilter):
130+
name = field(required=True, validators=[LengthValidator(min_length=2)]) # Override
131+
132+
Multiple Element Registration
133+
-----------------------------
134+
135+
You can register multiple components at once for cleaner definitions:
136+
137+
.. code-block:: python
138+
139+
class CompactFilter(InputFilter):
140+
name = field(required=True)
141+
email = field(required=True)
142+
143+
# Multiple global filters
144+
global_filter(StringTrimFilter(), ToLowerFilter(), RemoveExtraSpacesFilter())
145+
146+
# Multiple global validators
147+
global_validator(IsStringValidator(), NotEmptyValidator())
148+
149+
# Multiple conditions
150+
condition(
151+
RequiredCondition('name'),
152+
RequiredCondition('email'),
153+
EqualCondition('password', 'password_confirmation')
154+
)
155+
156+
Model Integration
157+
-----------------
158+
159+
The Declarative API seamlessly integrates with various model types:
160+
161+
Dataclasses
162+
~~~~~~~~~~~
163+
164+
.. code-block:: python
165+
166+
from dataclasses import dataclass
167+
168+
@dataclass
169+
class User:
170+
name: str
171+
email: str
172+
173+
class UserFilter(InputFilter):
174+
name = field(required=True, validators=[IsStringValidator()])
175+
email = field(required=True, validators=[EmailValidator()])
176+
177+
model(User)
178+
179+
# Usage
180+
filter_instance = UserFilter()
181+
user = filter_instance.validate_data({'name': 'John', 'email': '[email protected]'})
182+
# user is a User dataclass instance
183+
184+
Pydantic Models
185+
~~~~~~~~~~~~~~~
186+
187+
.. code-block:: python
188+
189+
from pydantic import BaseModel
190+
191+
class User(BaseModel):
192+
name: str
193+
email: str
194+
195+
class UserFilter(InputFilter):
196+
name = field(required=True, validators=[IsStringValidator()])
197+
email = field(required=True, validators=[EmailValidator()])
198+
199+
model(User)
200+
201+
TypedDict
202+
~~~~~~~~~
203+
204+
.. code-block:: python
205+
206+
from typing import TypedDict
207+
208+
class UserDict(TypedDict):
209+
name: str
210+
email: str
211+
212+
class UserFilter(InputFilter):
213+
name = field(required=True, validators=[IsStringValidator()])
214+
email = field(required=True, validators=[EmailValidator()])
215+
216+
model(UserDict)
217+
218+
Next Steps
219+
----------
220+
221+
For detailed information about each component, see:
222+
223+
- :doc:`Field Decorator <field_decorator>` - Complete field configuration options
224+
- :doc:`Global Decorators <global_decorators>` - Global filters, validators, and conditions
225+
- :doc:`Conditions <condition>` - Cross-field validation conditions
226+
- :doc:`Filters <filter>` - Available filters and custom filter creation
227+
- :doc:`Validators <validator>` - Available validators and custom validator creation

0 commit comments

Comments
 (0)