[info]en_dmitriid


Tigers, and lions, and bears, oh my!


Validation for erlyweb
happy
[info]dmitriid wrote in [info]en_dmitriid
I've written a validation function for Erlyweb. First I'll tell you how it works and then, perhaps, more thoughts on its internals.

Create a form with four fields:
- login
- password
- pasword_repeat
- email

This is a pretty standard registration form. Naturally we'd have to validate input coming from this form:
- login has to be 4-16 symbols in length
- password has to be 4-16 symbols in length
- password has to be the same as password_repeat
- email has to be a valid email address

Erlyweb's validation functions cant cope with this. My function can :)

Suppose you have a function called process_signup, which accepts the yaws_arg record. Then the validation will look like so:

process_signup(A) ->
	F = fun(A, Field) ->
		{ok, Val} = yaws_api:postvar(A, Field),
		L = string:len(Val),
		if
			L < 4 orelse L > 16 ->
				{Field, length};
			true ->
				{}
			end
	end,
	EmailCheck = fun(Args, Field2) ->
		{ok, Email} = yaws_api:postvar(Args, Field2),
		Match = regexp:match(Email, "^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]+"),
		Match /= nomatch
	end,

	%% magic is here:
	buktu_form:validate(A, [
		{login, F},
		{email, EmailCheck},
		{password, [{'=', password_repeat}, F]}
	]).


If we don't input any field at all, we'll get back the following list:
[{login,invalid_field},
 {email,invalid_field},
 {password,[{invalid_fields,[password,password_repeat]},
            invalid_field]}]


If we input values that don't match our criteria, we'll get:
[{login,length},
 {email,invalid_value},
 {password,[{not_equal,password_repeat},
            length]}]


  • The callback function that you can pass can return the following:

    • a tuple {FieldName, Error}

    • true if the field is validated and false otherwise (then the validation function will return {FieldName, invalid_value})

    • any value Value which will be transformed into {FieldName, Value}


  • if any of the fields in the rule don't exist or are empty, for each such rule the validation function will return:

    • invalid_field if the field is compared against a value

    • {invalid_fields, [field1, field2]} if the field is compared against another field


  • If you need to bind several rules to a field, pass a list of rules. If you just need to check if a field exists, pass in a tuple containing the field's name:

    • Does the field exist?
      {field_name}

    • Compare a field field_name to field field2_name (you can use '=', '/=', '<', '=<', '>', '>=' )
      {field_name, {'=', field2_name}}

    • Compare a field to any value:
      {field_name, {'=', Value}}

    • Use a callback function (function/2, first parameter is yaws_arg, the second is the field's name). The function can be a lambda or any function of any module in the form of module:function/2 or {module, function}
      {field_name, F}

    • Use a callback function with an additional value (function/3,first parameter is yaws_arg, the second is the field's name, the third is the value). The function can be a lambda or any function of any module in the form of module:function/3 or {module, function}
      {field_name, {F, Value}}


  • The validation function returns a proplist:
    [{FieldName(), Errors()}]

    where
    FieldName() = atom()
    Errors() = Error() | [Error()]
    Error() = user_defined_values | absent | invalid_value | 
              invalid_field | {invalid_fields, [FieldName(), FieldName()]} |
              ComparisonError()
    ComparisonError() = {not_equal, value_or_field} | {equal, value_or_field} |
                        {not_greater, value_or_field} | {not_less, value_or_field} |
                        {not_greater_or_equal, value_or_field} | {not_less_or_equal, value_or_field} |
    

Musings
happy
[info]dmitriid wrote in [info]en_dmitriid
As I promised here are my thoughts:

1. First of all, pattern matching lets you describe what yo eed with little or no effort. For example, consider these rules:

{field}
{field, Func}
{field, {'=', field2}}


Frankly, I have a very vague idea how these could be matched using ifs only. I guess you would create a Rule class and inherit a slew of classes from it. Something like RuleField, RuleFunc, RuleOperator and so on. If you use pattern matching, however, the rules are easily parsed::

%%{field}
validate_rule({FieldName}) ->
    ok.

%%{field, Func}
validate_rule({FieldName, Func}) when is_function(Func) ->
    ok.

%%{field, {'=', field2}}
validate_rule({FieldName, {Operator, FieldName2}}) ->
    ok.


Such ease, however, may bring (and it does bring me) to my second thought:

2. WTF-ish code. The thought that you can easily slice through complex constructs makes you write before you think.

In my case I knew what I wanted to pass to the function, but I had no idea what I expected the function to return. Ok, I've parsed the rules, what now?

As a result, the first version of the function would return a deeply nested list of lists that looked something like this:


[[Field, [Error1, Error2]], [Field2, [Error3, Error4]]]


It took me two additional refactorings to make it return a properproplist.

This same "wow, look at how I handle things!" approach resulted in an ugly preprocessing of results before I return these to the user:

lists:filter(
    fun(Elem) -> 
        case 
            Elem of {} -> false; 
            {_, []} -> false;
            _ -> true 
        end 
    end, 
    lists:flatten(validate1(A, ValidationRules, [])))


Yup. Get rid of those unwanted elements before the user sees them. Are you scared? I am. I m saddened as well :(


Here's where my train of thoughts stops...
Tags:

Home