Dev Corner

Friday, January 13, 2006

Yaws and form parameters

The first post!

Well, I'll start with my obsession with erlang. It is my favorite language for server side apps. It is simply the best, in my opinion, concurrent programming language for server applications.
Alot of other people have written the advantages, my hope of this blog is to demonstrate how to use erlang for practical problems.

Enough praising, lets get down to the code.

I started playing with yaws, a erlang-based very fast and cool web server, and created a simple login form, but was used to PHPs QuickForm. So I wanted to create some code to parse and validate parameters.

With QuickForm, you create a form object, add parameter definitions and validation rules are attached to those parameters. QuickForm also supports rendering, but for now I figured I would start on validation only.

Here is how I wanted to define form parameters to process:

Rules = [{"username", [required, {regex, "^test$"}]},
{"password", [required,integer_rule()]},
{"id", [required, integer_rule()]}],

The rules is just a list of tuples that is of the form:
{form_parameter_name, [list_of_rules]}

Now the next step is to develop an api for processing form elements and checking them against the rules.

Lets start with:
validate_page_data(Data, Rules)
Where Data is the parsed GET or POST data from yaws. Rules a list of rules that we have defined previously.

Now lets define the meat of the function:
validate_page_data(Data, Rules) ->
validate_page_data(Data, Rules, []).

validate_page_data(Data, [{Name,Rules} | Rest], Errors) ->
case process_rules(Data, Name, Rules) of
ok ->
validate_page_data(Data, Rest, Errors);
{errors, Reason} ->
validate_page_data(Data, Rest, [Reason|Errors])
end;
validate_page_data(_Data, [], Errors) ->
case is_errors_empty(Errors) of
true ->
ok;
false ->
{errors, Errors}
end.
The purpose of validate_page_data is to loop over all parameter definitions in order to apply rules to each parameter. It also collects all errors for rules that failed rule validation.

What is a rule? Well, here is the example again:
 Rules = [{"username", [required, {regex, "^test$"}]},
{"password", [required, {callback, fun integer_rule_cb/2}]},
{"id", [required, integer_rule()]}],
So there is a required rule here, a regex one and a function called integer_rule, and a callback rule.
The callback rule is very useful because, with it, you can define a whole set of custom rules. integer_rule, as you will see, is actually a callback.
Now onto rule processing, what we want a rule processor to do is to parse the list
of rules for each parameter, and call the rule handler for the data in the form.

Here it is:
%% Data here is the posted data
%% Name is the form parameter name
%% required is the rule atom
process_rule(Data, Name, required) ->
case lists:keymember(Name, 1, Data) of
true ->
ok;
false ->
{error, lists:flatten(io_lib:format("~s is required", [Name]))}
end;
process_rule(Data, Name, {regex, Regex}) ->
case get_value(Name,Data) of
{ok, Value} ->
case regexp:match(Value, Regex) of
{match, _Start, _Length} ->
ok;
nomatch ->
{error, lists:flatten(io_lib:format("~s not valid", [Name]))};
Error ->
{error, lists:flatten(io_lib:format("~s invalid regex ~p", [Name,Error]))}
end;
_Else ->
ok
end;
process_rule(Data, Name, {callback, Fun}) ->
case lists:keysearch(Name, 1, Data) of
{value, {K,V}} ->
Fun(K,V);
false ->
ok
end.

process_rules(Data, Name, Rules) ->
process_rules(Data, Name, Rules, []).

process_rules(Data, Name, [Rule|Rest], Errors) ->
case process_rule(Data, Name, Rule) of
ok ->
process_rules(Data, Name, Rest, Errors);
{error, Reason} ->
process_rules(Data, Name, Rest, [Reason |Errors])
end;
process_rules(_Data, _Name, [], Errors) ->
case is_errors_empty(Errors) of
true ->
ok;
false ->
{errors, Errors}
end.
The required rule is a first class rule instead of being a callback, because it acutally has to run whether or not the argument is present in the form parameters.
If a parameter is not present, in the data, then they are never run for validation. I could have created the regex rule as a callback, but decided to make it first class also. Just a choice, nothing more.

Now onto creating a rule that will be needed for numbers. integer_rule.
We can create a callback for that one. Here is how we would use it:
Rules = [{"id", [required, integer_rule()]}]
We want id to be a required integer. I've created an integer_rule convenience function for syntactic sugar.

Here it is:
integer_rule_cb(Name, Value) ->
case string:to_integer(Value) of
{error, _Reason} ->
{error, lists:flatten(io_lib:format("~s must be numeric", [Name]))};
{_IntValue, []} ->
ok
end.

integer_rule() ->
{callback, fun integer_rule_cb/2}.

regex_rule(Regex) ->
{regex, Regex}.

I also created a regex_rule function for fun. Callbacks, as all rules, must either return ok, or {error, Reason}.

I could also augment each of the rules to add a custom reason string for each parameter, but have left this out on purpose to be breif.

Here are some convenience functions to get at form data, after validation has been done:
get_value(Name,Data) ->
case lists:keysearch(Name, 1, Data) of
{value, {_K,V}} ->
{ok, V};
false ->
{error, notfound}
end.

eget_value_integer(Name,Data) ->
case get_value_integer(Name, Data) of
Result = {ok, _Value} ->
Result;
Error ->
throw(Error)
end.

get_value_integer(Name, Data) ->
case lists:keysearch(Name, 1, Data) of
{value, {_K,V}} ->
case string:to_integer(V) of
{error, Reason} ->
{error, Reason};
{IntValue, _} ->
{ok, IntValue}
end;
false ->
{error, notfound}
end.

%% Here is a test for everything.
test() ->
Data = [{"username","test"},{"password","password"},
{"id", "23"}],
Rules = [{"username", [required, {regex, "^test$"}]},
{"password", [required,integer_rule()]},
{"id", [required, integer_rule()]}],
io:format("~p~n", [validate_page_data(Data, Rules)]),
io:format("~p~n", [get_value_integer("id", Data)]),
io:format("~p~n", [get_value_integer("username", Data)]),
io:format("~p~n", [get_value("username",Data)]).

In the coming week I'll polish up this library and post it somewhere, maybe the yaws mailing list. Hopefully, there is some interest in it.

I recommend anyone programming in erlang to consider yaws for front end management webapps for their erlang servers or even for full-fledged web applications. It simply is very cool and its easy to rpc:call with remote erlang nodes.

Thats it for this post. I will cover alot of topics in coming posts. I wrote a postgresql native erlang access library. I will go over the structure of that in this blog, in many posts of course. I will be posting that to jungerl RSN.

4 Comments:

  • Instead of calling it rules, why don't you call it fields or form ?
    It is inevitable that it will grow a bit till you can automatically generate the XHTML form out of it.
    Keep the good work and blogging! Looking forward for your lib (as a release)!

    By Anonymous Pupeno, at 1:03 AM  

  • As a newcomer to erlang, I want to thank you for joining the small community of erlang bloggers. Its great to see some new content out there!

    With regards to your rule processing library, I think conceptually it would be cleaner if all of the rules were implemented as callbacks (a list of functions). In my opinion, the rule processing logic would be simplified greatly by eliminating the idea of first-class rule types.

    Instead, I think writing a set of convenience functions that return functions or closures is a more elegant approach. I.e., required(), regex(Pattern), etc ...

    By Anonymous petekaz, at 1:44 AM  

  • Thanks for posting this. I was avoiding YAWS because the included web server seemed to work. I'll take another look at YAWS.

    I'm also very interested to see your PostgreSQL library...

    By Blogger Rich, at 10:02 AM  

  • Hi Can you please let me know how to make rpc:call to remote erlang node from .yaws file..

    By Blogger Raga, at 10:04 AM  

Post a Comment

<< Home