Created
July 15, 2011 18:20
-
-
Save teburd/1085216 to your computer and use it in GitHub Desktop.
Erlang Data Validation
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
%% @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