tiramisu/doc/validator.md

495 lines
14 KiB
Markdown
Raw Normal View History

2022-11-13 15:04:12 +01:00
# Validator
## What are validator ?
Validator is a functionnality that allow you to call a function to determine if an option is valid.
To define validator we have to use [calculation](calculation.md) object.
The function have to raise a "ValueError" object if the value is not valid. It could emit a warning when raises a "ValueWarning".
## Validator with options
Here an example, where we want to ask a new password to an user. This password should not be weak. The password will be asked twice and must match.
First of all, import necessary object:
```python
from asyncio import run
import warnings
from re import match
from tiramisu import StrOption, IntOption, OptionDescription, Config, \
Calculation, Params, ParamOption, ParamSelfOption, ParamValue
from tiramisu.error import ValueWarning
```
Create a first function to valid that the password is not weak:
```python
def is_password_conform(password):
# password must containe at least a number, a lowercase letter, an uppercase letter and a symbol
if not match(r'(?=.*\d)(?=.*[a-z])(?=.*[A-Z])(?=.*\W)', password):
raise ValueError('please choose a stronger password, try a mix of letters, numbers and symbols')
```
Secondly create a function to valid password length. The password must be longer than the value of "min_len" and should be longer than the value of "recommand_len".
In first case, function raise "ValueError", this value is incorrect.
In second case, function raise "ValueWarning", the value is valid but discouraged:
```python
def password_correct_len(min_len, recommand_len, password):
# password must have at least min_len characters
if len(password) < min_len:
raise ValueError(f'use {min_len} characters or more for your password')
# password should have at least recommand_len characters
if len(password) < recommand_len:
raise ValueWarning(f'it would be better to use more than {recommand_len} characters for your password')
```
Thirdly create a function that verify that the login name is not a part of password (password "foo2aZ$" if not valid for user "foo"):
```python
def user_not_in_password(login, password):
if login in password:
raise ValueError('the login must not be part of the password')
```
Now we can creation an option to ask user login:
```python
login = StrOption('login', 'Login', properties=('mandatory',))
```
Create a calculation to launch "is_password_conform". This function will be use in a new option and must validate this new option. So we use the object "ParamSelfOption" has parameter to retrieve the value of current option:
```python
calc1 = Calculation(is_password_conform,
Params(ParamSelfOption()))
```
Create a second calculation to launch "password_correct_len" function. We want set 8 as "min_len" value and 12 as "recommand_len" value:
```python
calc2 = Calculation(password_correct_len,
Params((ParamValue(8),
ParamValue(12),
ParamSelfOption())))
```
Create a third calculation to launch "user_not_in_password" function. For this function, we use keyword argument. This function normaly raise "ValueError" but in this case we want demoting this error as a simple warning. So we add "warnings_only" parameter:
```python
calc3 = Calculation(user_not_in_password,
Params(kwargs={'login': ParamOption(login),
'password': ParamSelfOption()}),
warnings_only=True)
```
So now we can create first password option that use those calculations:
```python
password1 = StrOption('password1',
'Password',
properties=('mandatory',),
validators=[calc1, calc2, calc3])
```
A new function is created to conform that password1 and password2 match:
```python
def password_match(password1, password2):
if password1 != password2:
raise ValueError("those passwords didn't match, try again")
```
And now we can create second password option that use this function:
```python
password2 = StrOption('password2',
'Confirm',
properties=('mandatory',),
validators=[Calculation(password_match, Params((ParamOption(password1), ParamSelfOption())))])
```
Finally we create optiondescription and config:
```python
od = OptionDescription('password', 'Define your password', [password1, password2])
root = OptionDescription('root', '', [login, od])
async def main():
config = await Config(root)
await config.property.read_write()
run(main())
```
Now we can test this "Config" with a tested password too weak:
```python
async def main():
config = await Config(root)
await config.property.read_write()
await config.option('login').value.set('user')
try:
await config.option('password.password1').value.set('aAbBc')
except ValueError as err:
print(f'Error: {err}')
run(main())
```
returns:
```
Error: "aAbBc" is an invalid string for "Password", please choose a stronger password, try a mix of letters, numbers and symbols
```
The password is part of error message. In this case it's a bad idea. So we have to remove "prefix" to the error message:
```python
async def main():
config = await Config(root)
await config.property.read_write()
await config.option('login').value.set('user')
try:
await config.option('password.password1').value.set('aAbBc')
except ValueError as err:
err.prefix = ''
print(f'Error: {err}')
run(main())
```
Now the error is:
```
Error: please choose a stronger password, try a mix of letters, numbers and symbols
```
Let's try with a password not weak but too short:
```python
async def main():
config = await Config(root)
await config.property.read_write()
await config.option('login').value.set('user')
try:
await config.option('password.password1').value.set('aZ$1')
except ValueError as err:
err.prefix = ''
print(f'Error: {err}')
run(main())
```
The error is:
```
Error: use 8 characters or more for your password
```
Now try a password with 8 characters:
```python
warnings.simplefilter('always', ValueWarning)
async def main():
config = await Config(root)
await config.property.read_write()
await config.option('login').value.set('user')
with warnings.catch_warnings(record=True) as warn:
await config.option('password.password1').value.set('aZ$1bN:2')
if warn:
warn[0].message.prefix = ''
print(f'Warning: {warn[0].message}')
password = await config.option('password.password1').value.get()
print(f'The password is "{password}"')
run(main())
```
Warning is display but password is store:
```
Warning: it would be better to use more than 12 characters for your password
The password is "aZ$1bN:2"
```
Try a password with the login as part of it:
```python
warnings.simplefilter('always', ValueWarning)
async def main():
config = await Config(root)
await config.property.read_write()
await config.option('login').value.set('user')
with warnings.catch_warnings(record=True) as warn:
await config.option('password.password1').value.set('aZ$1bN:2u@1Bjuser')
if warn:
warn[0].message.prefix = ''
print(f'Warning: {warn[0].message}')
password = await config.option('password.password1').value.get()
print(f'The password is "{password}"')
run(main())
```
Warning is display but password is store:
```
Warning: the login must not be part of the password
The password is "aZ$1bN:2u@1Bjuser"
```
Now try with a valid password but that doesn't match:
```python
async def main():
config = await Config(root)
await config.property.read_write()
await config.option('login').value.set('user')
await config.option('password.password1').value.set('aZ$1bN:2u@1Bj')
try:
await config.option('password.password2').value.set('aZ$1aaaa')
except ValueError as err:
err.prefix = ''
print(f'Error: {err}')
run(main())
```
An error is displayed:
```
Error: those passwords didn't match, try again
```
Finally try a valid password:
```python
async def main():
config = await Config(root)
await config.property.read_write()
await config.option('login').value.set('user')
await config.option('login').value.set('user')
await config.option('password.password1').value.set('aZ$1bN:2u@1Bj')
await config.option('password.password2').value.set('aZ$1bN:2u@1Bj')
await config.property.read_only()
user_login = await config.option('login').value.get()
password = await config.option('password.password2').value.get()
print(f'The password for "{user_login}" is "{password}"')
run(main())
```
As expected, we have:
```
The password for "user" is "aZ$1bN:2u@1Bj"
```
## Validator with a multi option
Assume we ask percentage value to an user. The sum of values mustn't be higher than 100% and shouldn't be lower than 100%.
Let's start by importing the objects:
```python
from asyncio import run
import warnings
from tiramisu import IntOption, OptionDescription, Config, \
Calculation, Params, ParamSelfOption
from tiramisu.error import ValueWarning
```
Continue by writing the validation function:
```python
def valid_pourcent(option):
total = sum(option)
if total > 100:
raise ValueError(f'the total {total}% is bigger than 100%')
if total < 100:
raise ValueWarning(f'the total {total}% is lower than 100%')
```
And create a simple option in a single OptionDescription:
```python
percent = IntOption('percent',
'Percent',
multi=True,
validators=[Calculation(valid_pourcent, Params(ParamSelfOption()))])
od = OptionDescription('root', 'root', [percent])
```
Now try with bigger sum:
```python
async def main():
config = await Config(od)
try:
await config.option('percent').value.set([20, 90])
except ValueError as err:
err.prefix = ''
print(f'Error: {err}')
percent_value = await config.option('percent').value.get()
print(f'The value is "{percent_value}"')
run(main())
```
The result is:
```
Error: the total 110% is bigger than 100%
The value is "[]"
```
Let's try with lower sum:
```python
async def main():
config = await Config(od)
warnings.simplefilter('always', ValueWarning)
with warnings.catch_warnings(record=True) as warn:
await config.option('percent').value.set([20, 70])
if warn:
warn[0].message.prefix = ''
print(f'Warning: {warn[0].message}')
percent_value = await config.option('percent').value.get()
print(f'The value is "{percent_value}"')
run(main())
```
The result is:
```
Warning: the total 90% is lower than 100%
The value is "[20, 70]"
```
Finally with correct value:
```python
async def main():
config = await Config(od)
await config.option('percent').value.set([20, 80])
percent_value = await config.option('percent').value.get()
print(f'The value is "{percent_value}"')
run(main())
```
The result is:
```
The value is "[20, 80]"
```
## Validator with a follower option
Assume we want distribute something to differents users. The sum of values mustn't be higher than 100%.
First, import all needed objects:
```python
from asyncio import run
import warnings
from tiramisu import StrOption, IntOption, Leadership, OptionDescription, Config, \
Calculation, Params, ParamSelfOption, ParamIndex
from tiramisu.error import ValueWarning
```
Let's start to write a function with three arguments:
- the first argument will have all values set for the follower
- the second argument will have only last value set for the follower
- the third argument will have the index
```python
def valid_pourcent(option, current_option, index):
if None in option:
return
total = sum(option)
if total > 100:
raise ValueError(f'the value {current_option} (at index {index}) is too big, the total is {total}%')
```
Continue by creating a calculation:
```python
calculation = Calculation(valid_pourcent, Params((ParamSelfOption(whole=True),
ParamSelfOption(),
ParamIndex())))
```
And instanciate differents option:
```python
user = StrOption('user', 'User', multi=True)
percent = IntOption('percent',
'Distribution',
multi=True,
validators=[calculation])
leader = Leadership('percent', 'Percent', [user, percent])
od = OptionDescription('root', 'root', [leader])
```
Add two values to the leader:
```python
async def main():
config = await Config(od)
await config.option('percent.user').value.set(['user1', 'user2'])
run(main())
```
The user user1 will have 20%:
```python
async def main():
config = await Config(od)
await config.option('percent.user').value.set(['user1', 'user2'])
await config.option('percent.percent', 0).value.set(20)
run(main())
```
If we try to set 90% to user2:
```python
async def main():
config = await Config(od)
await config.option('percent.user').value.set(['user1', 'user2'])
await config.option('percent.percent', 0).value.set(20)
try:
await config.option('percent.percent', 1).value.set(90)
except ValueError as err:
err.prefix = ''
print(f'Error: {err}')
run(main())
```
results:
```
Error: the value 90 (at index 1) is too big, the total is 110%
```
No problem with 80%:
```python
async def main():
config = await Config(od)
await config.option('percent.user').value.set(['user1', 'user2'])
await config.option('percent.percent', 0).value.set(20)
await config.option('percent.percent', 1).value.set(80)
run(main())
```