Current section
Files
Jump to
Current section
Files
src/cowboy_constraints.erl
%% Copyright (c) Loïc Hoguin <essen@ninenines.eu>
%%
%% 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]).
-export([from_fun/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, 2) ->
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.
%% Constraint generator.
-spec from_fun(fun((any()) -> any())) -> fun().
from_fun(Fun) when is_function(Fun, 1) ->
fun
(forward, Value) ->
try
{ok, Fun(Value)}
catch _:_ ->
{error, not_valid}
end;
(reverse, _) ->
{error, not_implemented};
(format_error, {not_valid, Value}) ->
io_lib:format("The value ~0p is not valid.", [Value]);
(format_error, {not_implemented, _}) ->
<<"The reverse operation is not implemented.">>
end.
%% @todo Constraint generators with support for reverse
%% or custom error formatting.
-ifdef(TEST).
from_fun_forward_test() ->
Constraint = from_fun(fun cow_http:ensure_token/1),
%% Value, Result.
Tests = [
{<<>>, error},
{<<"value">>, <<"value">>},
{<<"some-weird-token^">>, <<"some-weird-token^">>},
{123, error},
{<<"invalid\r\ntoken">>, error}
],
[{lists:flatten(io_lib:format("~p", [V])), fun() ->
case R of
error -> {error, {Constraint, not_valid, V}} = validate(V, Constraint);
_ -> {ok, R} = validate(V, Constraint)
end
end} || {V, R} <- Tests].
from_fun_reverse_test() ->
Constraint = from_fun(fun cow_http:ensure_token/1),
{error, {Constraint, not_implemented, <<"value">>}}
= reverse(<<"value">>, [Constraint]),
ok.
from_fun_format_error_test() ->
Constraint = from_fun(fun cow_http:ensure_token/1),
[_|_] = format_error({Constraint, not_valid, <<"value">>}),
<<_/bits>> = format_error({Constraint, not_implemented, <<"value">>}),
ok.
-endif.