%% Copyright (c) 2014-2017, Loïc Hoguin <[email protected]>
%%
%% Permission to use, copy, modify, and/or distribute this software for any
%% purpose with or without fee is hereby granted, provided that the above
%% copyright notice and this permission notice appear in all copies.
%%
%% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
%% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
%% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
%% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
%% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
%% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
%% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
-module(cowboy_constraints).
-export([validate/2]).
-export([reverse/2]).
-export([format_error/1]).
-type constraint() :: int | nonempty | fun().
-export_type([constraint/0]).
-type reason() :: {constraint(), any(), any()}.
-export_type([reason/0]).
-spec validate(binary(), constraint() | [constraint()])
-> {ok, any()} | {error, reason()}.
validate(Value, Constraints) when is_list(Constraints) ->
apply_list(forward, Value, Constraints);
validate(Value, Constraint) ->
apply_list(forward, Value, [Constraint]).
-spec reverse(any(), constraint() | [constraint()])
-> {ok, binary()} | {error, reason()}.
reverse(Value, Constraints) when is_list(Constraints) ->
apply_list(reverse, Value, Constraints);
reverse(Value, Constraint) ->
apply_list(reverse, Value, [Constraint]).
-spec format_error(reason()) -> iodata().
format_error({Constraint, Reason, Value}) ->
apply_constraint(format_error, {Reason, Value}, Constraint).
apply_list(_, Value, []) ->
{ok, Value};
apply_list(Type, Value0, [Constraint|Tail]) ->
case apply_constraint(Type, Value0, Constraint) of
{ok, Value} ->
apply_list(Type, Value, Tail);
{error, Reason} ->
{error, {Constraint, Reason, Value0}}
end.
%% @todo {int, From, To}, etc.
apply_constraint(Type, Value, int) ->
int(Type, Value);
apply_constraint(Type, Value, nonempty) ->
nonempty(Type, Value);
apply_constraint(Type, Value, F) when is_function(F) ->
F(Type, Value).
%% Constraint functions.
int(forward, Value) ->
try
{ok, binary_to_integer(Value)}
catch _:_ ->
{error, not_an_integer}
end;
int(reverse, Value) ->
try
{ok, integer_to_binary(Value)}
catch _:_ ->
{error, not_an_integer}
end;
int(format_error, {not_an_integer, Value}) ->
io_lib:format("The value ~p is not an integer.", [Value]).
nonempty(Type, <<>>) when Type =/= format_error ->
{error, empty};
nonempty(Type, Value) when Type =/= format_error, is_binary(Value) ->
{ok, Value};
nonempty(format_error, {empty, Value}) ->
io_lib:format("The value ~p is empty.", [Value]).
-ifdef(TEST).
validate_test() ->
F = fun(_, Value) ->
try
{ok, binary_to_atom(Value, latin1)}
catch _:_ ->
{error, not_a_binary}
end
end,
%% Value, Constraints, Result.
Tests = [
{<<>>, [], <<>>},
{<<"123">>, int, 123},
{<<"123">>, [int], 123},
{<<"123">>, [nonempty, int], 123},
{<<"123">>, [int, nonempty], 123},
{<<>>, nonempty, error},
{<<>>, [nonempty], error},
{<<"hello">>, F, hello},
{<<"hello">>, [F], hello},
{<<"123">>, [F, int], error},
{<<"123">>, [int, F], error},
{<<"hello">>, [nonempty, F], hello},
{<<"hello">>, [F, nonempty], hello}
],
[{lists:flatten(io_lib:format("~p, ~p", [V, C])), fun() ->
case R of
error -> {error, _} = validate(V, C);
_ -> {ok, R} = validate(V, C)
end
end} || {V, C, R} <- Tests].
reverse_test() ->
F = fun(_, Value) ->
try
{ok, atom_to_binary(Value, latin1)}
catch _:_ ->
{error, not_an_atom}
end
end,
%% Value, Constraints, Result.
Tests = [
{<<>>, [], <<>>},
{123, int, <<"123">>},
{123, [int], <<"123">>},
{123, [nonempty, int], <<"123">>},
{123, [int, nonempty], <<"123">>},
{<<>>, nonempty, error},
{<<>>, [nonempty], error},
{hello, F, <<"hello">>},
{hello, [F], <<"hello">>},
{123, [F, int], error},
{123, [int, F], error},
{hello, [nonempty, F], <<"hello">>},
{hello, [F, nonempty], <<"hello">>}
],
[{lists:flatten(io_lib:format("~p, ~p", [V, C])), fun() ->
case R of
error -> {error, _} = reverse(V, C);
_ -> {ok, R} = reverse(V, C)
end
end} || {V, C, R} <- Tests].
int_format_error_test() ->
{error, Reason} = validate(<<"string">>, int),
Bin = iolist_to_binary(format_error(Reason)),
true = is_binary(Bin),
ok.
nonempty_format_error_test() ->
{error, Reason} = validate(<<>>, nonempty),
Bin = iolist_to_binary(format_error(Reason)),
true = is_binary(Bin),
ok.
fun_format_error_test() ->
F = fun
(format_error, {test, <<"value">>}) ->
formatted;
(_, _) ->
{error, test}
end,
{error, Reason} = validate(<<"value">>, F),
formatted = format_error(Reason),
ok.
-endif.