Validation
Why do we need validation
Erlang Web, like any other framework, is a natural front end to existing systems that communicates with human beings. The result is information exchange. All-in-all, we need to know the type of data in the system. Even if we believe it is well formed and comes from a secure source, it needs to be rendered somehow. More importantly, is the validation of the input values coming over HTTP from user.
Expectations
The Set of types of the expected data from the final user is declared in the header file representing custom type. We already know the Validator collects it and sends to the proper functions for validating the basic types. Let’s see it in an example of code that is built around the bookmark links.
Links records
-record(link,{
id,
title,
uri,
text,
category,
new_cat
}).
-record(link_types,{
id = {integer, [{description, "Link ID"},
{private, true},
{primary_key}]},
title = {string, [{description, "Title"},
{min_length, 1}]},
uri = {string, [{description, "URI http://"},
{min_length, 1}]},
text = {text, [{description, "Description"},
{max_length, 255},
{rows, 5},
{cols, 40}]},
category = {enum, [{description, "Category" },
{optional, ""}]},
new_cat = {string, [{description, "New Category"},
{optional, ""}]}
}).
Including records
The simplest form of wtype file below. Obligatory function validate/1
1 -module(wtype_link).
2
3 -export([validate/1, get_record_info/1]).
4
5 -include("link_records.hrl").
6
7 get_record_info(link_types) -> #link_types{};
8 get_record_info(link) -> record_info(fields, link).
9
10 validate(From) ->
11 SuperField = get_record_info(link),
12 SuperType = get_record_info(link_types),
13
14 wpart_valid:validate(SuperField, SuperType, From ++ ["link"]).
String in line 14 is quite important for building default unique names. We use a custom name type to let generic tools recreate them. Function get record_info/1 is a space for dynamic changing types (e.g. on base of authentication).
Validating an integer
Our Link type was analyzed and now the incoming values from the user are sent to validate functions of basic types. They are on the framework side, but in the case of adding a new type, let’s see an example of wtype_integer.
Obligatory function validate/1 has to be exported.
1 validate({Types,Input}) ->
2 case wpart_valid:is_private(Types) of
3 true ->
4 {ok, Input};
5 false ->
6 case catch list_to_integer(Input) of
7 Int when is_integer(Int) ->
8 case check_min(Int, Types) of
9 {ok, Int} ->
10 case check_max(Int, Types) of
11 {ok, Int} -> {ok, Int};
12 ErrorMax -> ErrorMax
13 end;
14 ErrorMin -> ErrorMin
15 end;
16 _ -> {error, {not_integer, Input}}
17 end
18 end.
In line 1, the function takes arguments. They are sent by the validator. It is always a tuple {Types, Input}. Types is a rewritten list from record link types. The last important thing is the specification of ok and the error result. Only format from lines 11 and 16 is valid.
Private field
As a careful reader can see, there is a special case for private field in line 3. Id field in record link is private. It will not be visible or editable by the user and it will not be validated.
Back to controller
Finally, let’s see how to call generic validation from controller. Just the code for the date flow function and the validate function.
1 %...
2 dataflow(create) -> [authenticate, validate, validate_logic];
3 %...
4 validate(create,_) ->
5 validate_tool:validate_cu(link, create);
6
7 %...
8
9 error(create, not_valid) ->
10 Err = wpart:fget("__error"),
11 Message = "ERROR: Incomplete input or wrong type in form!"
12 " Reason: " ++ Err,
13 wpart:fset("error_message",Message),
14
15 Not_validated = wtype_link:prepare_initial(),
16 wpart:fset("__edit", Not_validated),
17 {template, "link/add_link.html"};
18 %...
Any calls for the framework functions is not obligatory (see line 5 validation). We can easily create our own validator and call it. What happens in the above? The dataflow function causes validate to be called [2]. It uses the framework tool to call and analyze a response from wpart_valid [6]. On error, dataflow makes sure that function error/2 is called. It forms error Message and fills the form with initial values (originally input by user). The wrong inputs have CSS class set to form_error - so they could be easily surrounded with e.g. red frame (you must define the proper class in your CSS file).
Special cases
What if we want to use one record definition for several controllers and models, which will interpret data differently? To achieve that we need to export from model (wtype_custom_type) get_parent_info/0 function.
1 -module(wtype_download).
2 -export([get_parent_type/0]).
3 %...some code
4 get_parent_type() -> link.
validate_tool
In the example above we could see validate tool in action. Let’s take a closer look at engine inside this module.
validate_tool was created to cooperate closely with validator. It is connected with data format returned by validator. Below is an example of such a list:
1 {error,[{{ok,[]},"link_new_cat"},
2 {{ok,[]},"link_category"},
3 {{error,{empty_input,undefined}},"link_text"},
4 {{ok,"http://asd"},"link_uri"},
5 {{ok,"VeryInterestingTitle"},"link_title"},
6 {{ok,undefined},"link_id"}]}
Specification:
{GlobalResult, ListResult}
ListResult = [ItemResult]
ItemResult = {Result, UniqueName}
GlobalResult = error | ok
Result = {ok, Input} | {error, ErrorCode}
ErrorCode = atom()
UniqueName = list()The extracting and analyzing process of the data passed with GET/POST is a very laborious activity. Handling it in each controller would be difficult. That is why we need the validate tool. Its main functionality is to build the record and send it to the controller. Next issue is handling error ’branch’. The validate tool inserts all failed long unique names into a request dictionary. They are caught by wpart_derived and then an error frame is built. At the same time, error atoms are collected and an error message string is set in the dictionary. All of this information is handled by the proper clause of local error/2 function inside of the controller.
