# 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()) ```