Packages

Data Validation in Erlang

Current section

Files

Jump to
baleen src baleen.erl
Raw

src/baleen.erl

%% @author Ángel Herranz, Manuel Cherep, Miguel Emilio Ruiz
%%
%% @copyright 2017 Coowry Ltd.
%%
%% @doc Data validation in Erlang.
%%
%% Validators of type {@type validator(A,B)} are functions that accept
%% terms of type {@type A} and returns a validation result of type
%% {@type result(B)}.
%%
-module(baleen).
%% API exports
%% Types
-export_type([validator/2, result/1, predicate/1, str/0, val_map_validator/3, val_map_result/2]).
%% Main validation function
-export([validate/2]).
%% Functions for user validator injections
-export([validator/1, predicate/1]).
%% Validator composition
-export([compose/2, compose/1, any/1]).
%% Validator constructors
%% Basic validators
-export([invalid/0, valid/0]).
-export([member/1]).
-export([literal/1]).
-export([regex/1]).
-export([max_length/1]).
-export([transform/1]).
%% Type casting validators
-export([to_integer/0]).
-export([to_atom/0]).
-export([to_float/0]).
-export([to_binary/0]).
-export([to_string/0]).
%% Compare validators
-export([between/2]).
-export([between_open_start/2]).
-export([between_open_end/2]).
-export([between_open/2]).
%% Validator "lifters"
-export([list_of/1]).
-export([map_of/2]).
-export([tuple_of/1]).
%% Reqopt
-export([val_map/2]).
%%====================================================================
%% Types
%%====================================================================
-type result(R) :: {ok, R} | {error, binary()}.
-type predicate(A) :: fun((A) -> boolean()).
-type str() :: string() | binary().
-opaque validator(A,B) :: fun((A) -> result(B)).
-type val_map_validator(K, A, B) :: #{K => {optional | required, validator(A, B)}}.
-type val_map_result(K, B) :: #{valid => #{K => B},
nonvalid => #{K => binary()},
unexpected => [K],
missing => [K]}.
%%====================================================================
%% Error messages
%%====================================================================
-define(INVALID_FLOAT(V), format("\"~p\" is not a float", [V])).
%%====================================================================
%% API functions
%%====================================================================
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
-spec validate(validator(A,B), A) -> result(B).
%% @doc Validates data with a validator. `X' is the term to be
%% validated with validator `V'.
validate(V, X) -> V(X).
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
-spec predicate(predicate(A)) -> validator(A,A).
%% @doc Returns a validator given a predicate. When validating `X'
%% with a predicate `P', if `P(X)' holds then `{ok, X}' is
%% returned. Otherwise, `{error, <<"Improper term X">>}' is
%% returned.
predicate(P) ->
fun(X) ->
case P(X) of
true ->
{ok, X};
false ->
{error, format("Improper term ~p", [X])}
end
end.
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
-spec validator(fun((A) -> result(B))) -> validator(A,B).
%% @doc Returns a validator given a user defined function that
%% validates.
validator(V) -> V.
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
-spec invalid() -> validator(_,_).
%% @doc Returns a validator that always fails.
invalid() -> fun(X) ->
{error, format("Invalid term ~p", [X])}
end.
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
-spec valid() -> validator(_,_).
%% @doc Returns a validator that always validates.
valid() ->
fun(X) -> {ok, X} end.
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
-spec to_integer() -> validator(str(), integer()).
%% @doc Returns a validator that takes a `Value' and tries to
%% cast to integer. If the cast success, `{ok, Integer}' is returned,
%% otherwise, `{error, <<"Value is not an integer">>}' is returned.
to_integer() ->
fun(Value) when is_binary(Value)->
try erlang:binary_to_integer(Value) of
Integer -> {ok, Integer}
catch
_:_ -> {error, format("~p is not an integer", [Value])}
end;
(Value) ->
case io_lib:fread("~d",Value) of
{ok, [Integer], []} -> {ok, Integer};
_ -> {error, format("~p is not an integer", [Value])}
end
end.
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
-spec compose(validator(A, B), validator(B, C)) -> validator(A, C).
%% @doc Returns a validator that is a composition of two validators.
compose(V1, V2) ->
fun (X1) ->
case validate(V1, X1) of
{ok, X2} ->
validate(V2, X2);
Error ->
Error
end
end.
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
-spec compose(nonempty_list(validator(A,A))) -> validator(A,A).
%% @doc Returns a validator that is a composition of a list of validators.
compose(Validators) -> lists:foldr(fun compose/2, valid(), Validators).
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
-spec any(list(validator(A, B))) -> validator(A, B).
%% @doc Returns only one validator of a list of validators that matches.
any([]) -> invalid();
any([V|Vs]) ->
fun(T) ->
case validate(V,T) of
{ok, X1} ->
{ok, X1};
{error, _Error1} ->
case validate(any(Vs), T) of
{ok, X2} ->
{ok, X2};
{error, _Error2} ->
{error, format("There isn't any valid", [])}
end
end
end.
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
-spec member(list(A)) -> validator(A, A).
%% @doc Returns a validator that matches only if the input is member
%% of `L'.
member(L) ->
fun(T) ->
case lists:member(T, L) of
true ->
{ok, T};
false ->
{error, format("~p is not member of ~p", [T, L])}
end
end.
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
-spec literal(A) -> validator(A, A).
%% @doc Returns a validator that matches only if the input is equals
%% to `Term'.
literal(Term) ->
fun(T) ->
case Term =:= T of
true -> {ok, Term};
false -> {error, format("~p and ~p do not match", [Term, T])}
end
end.
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
-spec regex(String) -> validator(String, String) when String :: str().
%% @doc Returns a validator that validates and `String' if matches
%% a regular expression given.
regex(RegularExpression) ->
%% Let's start with the compilation of the regular expression
case re:compile(RegularExpression) of
{ok, MP} ->
fun(T) ->
case re:run(T, MP) of
{match, [{0,0}]} ->
{error, format("~p is not matching the regular expression ~p", [T, RegularExpression])};
{match, _Captured} ->
{ok, T};
nomatch ->
{error, format("~p is not matching the regular expression ~p", [T, RegularExpression])}
end
end;
{error, _ErrSpec} ->
throw(badarg)
end.
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
-spec max_length(non_neg_integer()) -> validator(S, S) when S :: iodata().
%% @doc Returns a validator that validates an input only if its length
%% is less or equal than the integer is specified.
max_length(I) ->
fun(S) ->
case iolist_size(S) > I of
true -> {error, format("The size of ~p is longer than ~p", [S, I])};
false -> {ok, S}
end
end.
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
-spec to_atom() -> validator(str(), atom()).
%% @doc Returns a validator that takes a `T' and tries to
%% cast to atom. If the cast success, `{ok, Atom}' is returned,
%% otherwise, `{error, <<"T is not a valid binary">>}' or
%% `{error, <<"T is not a valid string">>}' is returned.
to_atom() ->
fun(T) when is_binary(T) ->
try binary_to_atom(T, utf8) of
Atom -> {ok, Atom}
catch
_:_ -> {error, format("\"~p\" is not a valid binary", [T])}
end;
(T) ->
try list_to_existing_atom(T) of
Atom -> {ok, Atom}
catch
_:_ -> {error, format("~p is not a valid string", [T])}
end
end.
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
-spec list_of(validator(A,B)) -> validator(list(A), list(B)).
%% @doc Returns a validator that matches a list.
list_of(V) ->
fun(L) ->
Results = [{Term, validate(V,Term)} || Term <- L],
Errors = [{Term, Msg} || {Term, {error, Msg}} <- Results],
case Errors of
[] -> {ok , [Value || {_, {ok, Value}} <- Results]};
[ {Term, Msg} | _] -> {error, format("Error in element ~p: ~s", [Term, Msg])}
end
end.
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
-spec map_of(validator(K1, K2), validator(V1, V2))
-> validator(#{K1 => V1}, #{K2 => V2}).
%% @doc Returns a validator that validates a map, whose Keys are
%% validated by `VK' and Values are validated by `VV'.
map_of(VK, VV) ->
fun(Map) ->
Keys = maps:keys(Map),
Values = maps:values(Map),
Results = [{{K,V},{validate(VK, K), validate(VV, V)}} || K <- Keys, V <- Values, maps:get(K, Map) =:= V],
KeysErrors = [{K, Msg} || {{K, _},{{error, Msg}, _}} <- Results],
ValuesErrors = [{V, Msg} || {{_, V}, {_, {error, Msg}}} <- Results],
case KeysErrors of
[] ->
case ValuesErrors of
[] ->
{ok, maps:from_list([{ValueK, ValueV} || {{_,_}, {{ok, ValueK}, {ok, ValueV}}} <- Results])};
[{V, Msg} | _] -> {error, format("Error in value ~p: ~s", [V, Msg])}
end;
[{K, Msg} | _] -> {error, format("Error in key ~p: ~s", [K, Msg])}
end
end.
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
-spec tuple_of(validator(A, B)) -> validator({A}, {B}).
%% @doc Returns a validator that matches a tuple.
tuple_of(V) ->
fun(Tuple) ->
TupleList = tuple_to_list(Tuple),
Results = [{T, validate(V, T)} || T <- TupleList],
Errors = [{T, Msg} || {T, {error, Msg}} <- Results],
case Errors of
[] -> {ok, list_to_tuple([Value || {_,{ok, Value}} <- Results])};
[{T, Msg} | _] -> {error, format("Error in ~p: ~s", [T, Msg])}
end
end.
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
-spec transform(fun((A) -> B)) -> validator(A,B).
%% @doc Returns a validator that always success and applies `F'.
transform(F) ->
fun(T) ->
{ok, F(T)}
end.
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
-spec to_float() -> validator(str(), float()).
%% @doc Returns a validator that takes a `Value' and tries to
%% cast to float. If the cast success, `{ok, Float}' is returned,
%% otherwise, `{error, <<"Value is not a float">>}' is returned.
to_float() ->
fun(Value) when is_binary(Value) ->
try erlang:binary_to_float(Value) of
Float -> {ok, Float}
catch
_:_ -> {error, ?INVALID_FLOAT(Value)}
end;
(Value) ->
case io_lib:fread("~f", Value) of
{ok, [Float], []} -> {ok, Float};
_ -> {error, ?INVALID_FLOAT(Value)}
end
end.
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
-spec val_map(val_map_validator(K, A, B),
#{K => A}) ->
val_map_result(K, B).
%% @doc Returns a map of the keys that matches, the keys that doesn't,
%% the keys that are missing and the unexpected keys, and their
%% corresponding values.
val_map(Validator, Map) ->
KeysMap = maps:keys(Map),
Unexpected = KeysMap -- maps:keys(Validator),
Missing = [K || {K, {OptReq, _}} <- maps:to_list(Validator), OptReq =:= required] -- KeysMap,
ToValidate = KeysMap -- Unexpected,
MapToValidate = lists:foldl(fun(K, AccMap) -> AccMap #{K => maps:get(K, Map)} end,
#{},
ToValidate),
{Valids, Invalids} = maps:fold(fun(K, V, {AccValids, AccInvalids}) ->
{_, Val} = maps:get(K, Validator),
case validate(Val, V) of
{ok, R} -> {AccValids #{K => R}, AccInvalids}; % Add to Valid map
{error, Msg} -> {AccValids, AccInvalids #{K => Msg}} % Add to Invalid map
end
end, {#{},#{}}, MapToValidate),
#{valid => Valids,
nonvalid => Invalids,
missing => Missing,
unexpected => Unexpected}.
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
-spec to_binary() -> validator(str(), binary()).
%% @doc Returns a validator that takes a `Value' and tries to
%% cast to binary. If the cast success, `{ok, Binary}' is returned,
%% otherwise, `{error, <<"Value is not an binary">>}' is returned.
to_binary() ->
fun(Value) when is_binary(Value)->
{ok, Value};
(Value) ->
try list_to_binary(Value) of
Binary -> {ok, Binary}
catch
_:_ -> {error, format("~p is not a string nor a binary", [Value])}
end
end.
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
-spec to_string() -> validator(str(), string()).
%% @doc Returns a validator that takes a `Value' and tries to
%% cast to str. If the cast success, `{ok, Str}' is returned,
%% otherwise, `{error, <<"Value is not an str">>}' is returned.
to_string() ->
fun(Value) when is_binary(Value)->
try binary_to_list(Value) of
String -> {ok, String}
catch
_:_ -> {error, format("~p is not a string nor a binary", [Value])}
end;
(Value) ->
try io_lib:fread("~s", Value) of
{ok, [String], []} -> {ok, String}
catch
_:_ -> {error, format("~p is not a string nor a binary", [Value])}
end
end.
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
-spec between(integer(), integer()) -> validator(integer(), integer()).
%% @doc Returns a validator that takes two `Value' and compare the
%% number betwen the (Min,Max). If the cast success, `{ok, integer}' is returned,
%% otherwise, `{error, <<"Value is not in range (Min,Max)">>}' is returned.
between(Min, Max) ->
fun(T) when is_integer(T) ->
if T > Min andalso T < Max -> {ok, T};
true -> {error, format("~p is not in range between (~p, ~p)", [T, Min, Max])}
end;
(T) -> {error, format("~p is not an integer", [T])}
end.
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
-spec between_open_start(integer(), integer()) -> validator(integer(), integer()).
%% @doc Returns a validator that takes two `Value' and compare the
%% number betwen the [Min,Max). If the cast success, `{ok, integer}' is returned,
%% otherwise, `{error, <<"Value is not in range [Min,Max)">>}' is returned.
between_open_start(Min, Max) ->
fun(T) when is_integer(T) ->
if T >= Min andalso T < Max -> {ok, T};
true -> {error, format("~p is not in range between [~p, ~p)", [T, Min, Max])}
end;
(T) -> {error, format("~p is not an integer", [T])}
end.
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
-spec between_open_end(integer(), integer()) -> validator(integer(), integer()).
%% @doc Returns a validator that takes two `Value' and compare the
%% number betwen the (Min,Max]. If the cast success, `{ok, integer}' is returned,
%% otherwise, `{error, <<"Value is not in range (Min,Max]">>}' is returned.
between_open_end(Min, Max) ->
fun(T) when is_integer(T) ->
if T > Min andalso T =< Max -> {ok, T};
true -> {error, format("~p is not in range between (~p, ~p]", [T, Min, Max])}
end;
(T) -> {error, format("~p is not an integer", [T])}
end.
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
-spec between_open(integer(), integer()) -> validator(integer(), integer()).
%% @doc Returns a validator that takes two `Value' and compare the
%% number betwen the [Min,Max]. If the cast success, `{ok, integer}' is returned,
%% otherwise, `{error, <<"Value is not in range [Min,Max]">>}' is returned.
between_open(Min, Max) ->
fun(T) when is_integer(T) ->
if T >= Min andalso T =< Max -> {ok, T};
true -> {error, format("~p is not in range between [~p, ~p]", [T, Min, Max])}
end;
(T) -> {error, format("~p is not an integer", [T])}
end.
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%%====================================================================
%% Internal functions
%%====================================================================
-spec format(io:format(), [term()]) -> binary().
format(Format, Terms) ->
Message = io_lib:format(Format, Terms),
unicode:characters_to_binary(Message).