Skip to content

Instantly share code, notes, and snippets.

@teburd
Created July 15, 2011 18:20
Show Gist options
  • Save teburd/1085216 to your computer and use it in GitHub Desktop.
Save teburd/1085216 to your computer and use it in GitHub Desktop.
Erlang Data Validation
%% @author Tom Burdick <tburdick@wrightwoodtech.com>
%% @doc Data Validation.
-module(data_validation).
%% api
-export([parse/1, validate/2, has_errors/1, errors/1, values/1]).
-ifdef(TEST).
-include_lib("eunit/include/eunit.hrl").
-endif.
%% types
-type field_type() :: string | integer | float.
-type key() :: binary().
-type value() :: binary() | number().
-type field_rule() :: {type, field_type()} | {optional, boolean()}
| {lt, number()} | {lt_eq, number()} | {gt, number()}
| {gt_eq, number()} | {size, pos_integer()} | {regexp, binary()}.
-type field_spec() :: {binary(), list(field_rule())}.
-export_types([field_type/0, field_spec/0, field_rule/0]).
%% ------------------------------------------------------------------
%% api
%% ------------------------------------------------------------------
%% @doc Parse query string encoded form input. Converts url encoded special
%% characters back to regular characters.
%% @end
-spec parse(list({binary(), binary()})) -> list({binary(), binary()}).
parse(FormData) ->
lists:map(fun({Key, Value}) -> {Key, quoted:from_url(Value)} end, FormData).
%% @doc Generate a form based on a list of rules containing types and input
%% limitations.
%% @end
-spec validate(list(field_spec()), list({binary(), binary()})) ->
list({key(), value(), list(field_rule())}).
validate(Specs, Data) ->
lists:map(fun(Spec) -> validate_optional(Spec, Data) end, Specs).
%% @doc Check if a validation result set has errors.
-spec has_errors(list({key(), value(), list(field_rule())})) -> boolean().
has_errors([]) ->
false;
has_errors([{_, _, []} | Results]) ->
has_errors(Results);
has_errors([{_, _, _} | _Results]) ->
true.
%% @doc From a result set return a list mapping keys to errors.
-spec errors(list({key(), value(), list(field_rule())})) ->
list({key(), list(field_rule())}).
errors(Results) ->
lists:map(fun({Key, _, Errors}) -> {Key, Errors} end, Results).
%% @doc From a result set return a list mapping keys to values.
-spec values(list({key(), value(), list(field_rule())})) ->
list({key(), value()}).
values(Results) ->
lists:map(fun({Key, Value, _}) -> {Key, Value} end, Results).
%% ------------------------------------------------------------------
%% private api
%% ------------------------------------------------------------------
%% @doc Perform lists:keytake with a default return value.
-spec keytake(term(), pos_integer(), list(), term()) -> {term(), list()}.
keytake(Key, Position, List, Default) when is_integer(Position), is_list(List) ->
case lists:keytake(Key, 1, List) of
false -> {Default, List};
{value, Value, List2} -> {Value, List2}
end.
%% @doc Validate a field specification.
-spec validate_optional(field_spec(), list({binary(), binary()})) ->
{binary(), list(field_rule())}.
validate_optional({Key, Rules}, Data) ->
{{optional, Optional}, Rules2} = keytake(optional, 1, Rules,
{optional, false}),
case lists:keyfind(Key, 1, Data) of
false when Optional == true ->
{Key, undefined, []};
false when Optional == false ->
{Key, undefined, [{optional, false}]};
{Key, Value} ->
validate_type({Key, Value}, Rules2)
end.
%% @doc Validate a field type.
-spec validate_type({binary(), binary()}, list(field_rule())) -> {{binary(), term()}, list(field_rule())}.
validate_type({Key, Value}, Rules) ->
{{type, Type}, Rules2} = keytake(type, 1, Rules, {type, string}),
case convert_type(Value, Type) of
error ->
{Key, Value, [{type, Type}]};
CValue ->
validate_rules({Key, CValue}, Rules2)
end.
%% @doc Convert a form element value in binary to the desired type.
-spec convert_type(binary(), field_type()) -> binary() | integer() | float() | error.
convert_type(Value, string) ->
Value;
convert_type(Value, integer) ->
try
list_to_integer(binary_to_list(Value))
catch
_:_ -> error
end;
convert_type(Value, float) ->
try
list_to_float(binary_to_list(Value))
catch
_:_ -> error
end;
convert_type(Value, Type) ->
error_logger:error_msg("Conversion of Value ~p to Type ~p failed~n", [Value, Type]),
error.
%% @doc Validate field rules.
-spec validate_rules({binary(), value()}, list(field_rule())) -> {binary(), list(field_rule())}.
validate_rules({Key, Value}, Rules) ->
Failed = validate_rule(Value, [], Rules),
{Key, Value, Failed}.
validate_rule(_Value, Failed, []) ->
Failed;
validate_rule(Value, Failed, [Rule | Rules]) ->
case validate_rule(Rule, Value) of
ok ->
validate_rule(Value, Failed, Rules);
Rule ->
validate_rule(Value, [Rule | Failed], Rules)
end.
%% @doc Validate a field rule against a converted type. Returns the rule upon
%% failure.
%% @end
-spec validate_rule(field_rule(), binary() | number() | boolean()) -> ok | field_rule().
validate_rule({lt, Max}, Value) when is_number(Max), is_number(Value), Value < Max ->
ok;
validate_rule({lt_eq, Max}, Value) when is_number(Max), is_number(Value), Value =< Max ->
ok;
validate_rule({gt, Min}, Value) when is_number(Min), is_number(Value), Value > Min ->
ok;
validate_rule({gt_eq, Min}, Value) when is_number(Min), is_number(Value), Value >= Min ->
ok;
validate_rule({size, Size}, Value) when is_integer(Size), is_binary(Value), Size >= 0, byte_size(Value) =< Size ->
ok;
validate_rule({regexp, Regexp}, Value) when is_binary(Value), is_binary(Regexp) ->
RegexpStr = binary_to_list(Regexp),
ValueStr = binary_to_list(Value),
case re:run(ValueStr, RegexpStr, []) of
{match, _} ->
ok;
nomatch ->
{regexp, Regexp}
end;
validate_rule(Rule, Value) ->
error_logger:info_msg("Validating Rule ~p against Value ~p failed~n", [Rule, Value]),
Rule.
%% ------------------------------------------------------------------
%% unit tests
%% ------------------------------------------------------------------
-ifdef(TEST).
lt_rule_test_() ->
[?_assertEqual(ok, validate_rule({lt, 10}, 9)),
?_assertEqual({lt, 10}, validate_rule({lt, 10}, 11))].
lt_eq_rule_test_() ->
[?_assertEqual(ok, validate_rule({lt_eq, 10}, 9)),
?_assertEqual(ok, validate_rule({lt_eq, 10}, 10)),
?_assertEqual(ok, validate_rule({lt_eq, 10.0}, 9.2)),
?_assertEqual({lt_eq, 10}, validate_rule({lt_eq, 10}, 11))].
gt_rule_test_() ->
[?_assertEqual(ok, validate_rule({gt, 10}, 11)),
?_assertEqual({gt, 10}, validate_rule({gt, 10}, 9))].
gt_eq_rule_test_() ->
[?_assertEqual(ok, validate_rule({gt_eq, 10}, 11)),
?_assertEqual(ok, validate_rule({gt_eq, 10}, 10)),
?_assertEqual(ok, validate_rule({gt_eq, 10.0}, 10.1)),
?_assertEqual({gt_eq, 10}, validate_rule({gt_eq, 10}, 9))].
size_rule_test_() ->
[?_assertEqual(ok, validate_rule({size, 2}, <<"a">>)),
?_assertEqual(ok, validate_rule({size, 2}, <<"ab">>)),
?_assertEqual({size, 2}, validate_rule({size, 2}, <<"abc">>))].
regexp_rule_test_() ->
[?_assertEqual(ok, validate_rule({regexp, <<"^a">>}, <<"a">>)),
?_assertEqual({regexp, <<"^a">>}, validate_rule({regexp, <<"^a">>}, <<"b">>))].
convert_integer_test_() ->
[?_assertEqual(1, convert_type(<<"1">>, integer)),
?_assertEqual(error, convert_type(<<"a">>, integer))].
convert_float_test_() ->
[?_assertEqual(1.2, convert_type(<<"1.2">>, float)),
?_assertEqual(error, convert_type(<<"a">>, float))].
convert_string_test_() ->
[?_assertEqual(<<"1.2">>, convert_type(<<"1.2">>, string)),
?_assertEqual(<<"a">>, convert_type(<<"a">>, string))].
validate_test_() ->
Specs = [{<<"email">>, [{type, string}, {optional, false},
{regexp, <<"^.*[@].*">>}, {size, 256}]},
{<<"age">>, [{type, integer}, {optional, true},
{gt, 0}, {lt, 200}]}],
?_assertEqual([{<<"email">>, <<"tburdick@wrightwoodtech.com">>, []},
{<<"age">>, undefined, []}],
validate(Specs,
[{<<"email">>, <<"tburdick@wrightwoodtech.com">>}])),
?_assertEqual([{<<"email">>, undefined, [{optional, false}]},
{<<"age">>, undefined, []}],
validate(Specs,
[])).
-endif.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment