175 lines
4.8 KiB
Erlang
175 lines
4.8 KiB
Erlang
%% Copyright (c) 2014-2024, 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]).
|
|
|
|
-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.
|