% Copyright (c) 2014 Jihyun Yu % All rights reserved. % % Redistribution and use in source and binary forms, with or without % modification, are permitted provided that the following conditions % are met: % % 1. Redistributions of source code must retain the above copyright % notice, this list of conditions and the following disclaimer. % 2. Redistributions in binary form must reproduce the above copyright % notice, this list of conditions and the following disclaimer in % the documentation and/or other materials provided with the % distribution. % % THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS % "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT % LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS % FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE % COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, % INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, % BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; % LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER % CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT % LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN % ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE % POSSIBILITY OF SUCH DAMAGE. -module(sanitizer). -include_lib("sanitizer/include/sanitizer.hrl"). -export([sanitize/2]). validate_type(bool, true) -> true; validate_type(bool, <<"true">>) -> true; validate_type(bool, false) -> false; validate_type(bool, <<"false">>) -> false; validate_type(bool, 0) -> false; validate_type(bool, Int) when is_integer(Int) -> true; validate_type(bool, _Other) -> throw({badarg, {shouldbool, _Other}}); validate_type(int, Given) when is_integer(Given) -> Given; validate_type(int, Given) when is_list(Given) -> try list_to_integer(Given) of Out -> Out catch error:badarg -> throw({badarg, {shouldint, Given}}) end; validate_type(int, Given) when is_binary(Given) -> try binary_to_integer(Given) of Out -> Out catch error:badarg -> throw({badarg, {shouldint, Given}}) end; validate_type(float, Given) when is_number(Given) -> Given; validate_type(float, Given) when is_list(Given) -> validate_type(float, list_to_binary(Given)); validate_type(float, Given) when is_binary(Given) -> try binary_to_integer(Given) of Out -> Out catch error:badarg -> try binary_to_float(Given) of Out -> Out catch error:badarg -> throw({badarg, {shouldfloat, Given}}) end end; validate_type(atom, Given) when is_atom(Given) -> Given; validate_type(atom, Given) when is_binary(Given) -> try binary_to_existing_atom(Given, utf8) of Atom -> Atom catch error:badarg -> throw({badarg, {shouldatom, Given}}) end; validate_type(binary, Given) when is_binary(Given) -> Given; validate_type(binary, Given) when is_atom(Given) -> atom_to_binary(Given, utf8); validate_type(binary, Given) when is_list(Given) -> list_to_binary(Given); validate_type(binary, Given) when is_integer(Given) -> integer_to_binary(Given); validate_type({map, _KeySpec, _ValueSpec} = Spec, Given) when is_map(Given) -> validate_type(Spec, maps:to_list(Given)); validate_type({map, KeySpec, ValueSpec}, Given) when is_list(Given) -> ValidateFunc = fun({Key, Value}) -> {validate_constraint(KeySpec, Key), validate_constraint(ValueSpec, Value)} end, maps:from_list(lists:map(ValidateFunc, Given)); validate_type({list, Spec}, Given) when is_list(Given) -> ValidateFunc = fun(Item) -> validate_type(Spec, Item) end, lists:map(ValidateFunc, Given); validate_type({list, Spec}, Given) -> validate_type({list, Spec}, [Given]); validate_type(any, Given) -> Given; validate_type(_Type, _Spec) -> throw(badspec). validate_constraint(#{type := Type} = Spec, Value) -> ListSpec = [{type, Type} | maps:to_list(maps:without([type, optional, default], Spec))], validate_constraint(ListSpec, Value); validate_constraint([{type, TypeDesc} | T], Value) -> validate_constraint(T, validate_type(TypeDesc, Value)); validate_constraint([{lt, MinVal} | T], Value) -> if Value < MinVal -> validate_constraint(T, Value); true -> throw({badarg, {constraint, '<', MinVal}}) end; validate_constraint([{lte, MinVal} | T], Value) -> if Value =< MinVal -> validate_constraint(T, Value); true -> throw({badarg, {constraint, '=<', MinVal}}) end; validate_constraint([{gt, MaxVal} | T], Value) -> if Value > MaxVal -> validate_constraint(T, Value); true -> throw({badarg, {constraint, '>', MaxVal}}) end; validate_constraint([{gte, MaxVal} | T], Value) -> if Value >= MaxVal -> validate_constraint(T, Value); true -> throw({badarg, {constraint, '>=', MaxVal}}) end; validate_constraint([{oneof, List} | T], Value) -> case lists:member(Value, List) of true -> validate_constraint(T, Value); false -> throw({badarg, {constraint, 'oneof', List}}) end; validate_constraint([], Value) -> Value; %% Shortcut: TypeSpec -> [{type, TypeSpec}] validate_constraint(TypeSpec, Value) when is_atom(TypeSpec) orelse is_tuple(TypeSpec) -> validate_type(TypeSpec, Value); validate_constraint(_Spec, _Value) -> throw(badspec). validate_key(Key) when is_atom(Key) -> Key; validate_key(Key) when is_binary(Key) -> try binary_to_existing_atom(Key, utf8) of Atom -> Atom catch error:badarg -> throw({badarg, {badkey, Key}}) end; validate_key(Key) -> throw({badarg, {badkey, Key}}). -spec sanitize(specs(), any()) -> #{atom() => any()}. sanitize(Spec, Args) -> AtomArgs = [{validate_key(Key), Val} || {Key, Val} <- maps:to_list(Args)], SanitizeKey = fun({Key, DescSpec0}) -> DescSpec = if is_map(DescSpec0) -> DescSpec0; is_list(DescSpec0) -> maps:from_list(DescSpec0); true -> #{type => DescSpec0} end, Optional = maps:get(optional, DescSpec, false), Value = case lists:keyfind(Key, 1, AtomArgs) of {Key, Val} -> Val; false -> maps:get(default, DescSpec, undefined) end, case {Optional, Value} of {true, undefined} -> {Key, undefined}; {true, <<>>} -> {Key, undefined}; {false, undefined} -> throw({badarg, {badkey, Key}}); {_, _} -> try {Key, validate_constraint(DescSpec, Value)} of Sanitized -> Sanitized catch throw:{badarg, Reason} -> throw({badarg, list_to_tuple([Key | tuple_to_list(Reason)])}) end end end, Out = lists:map(SanitizeKey, maps:to_list(Spec)), maps:from_list(Out). -ifdef(TEST). -include_lib("eunit/include/eunit.hrl"). -define(badarg, {badarg, _}). constraint_test_() -> F = fun validate_constraint/2, [ ?_assertEqual(1, F(#{type => int, gte => 1}, 1)), ?_assertEqual(1, F([{type, int}, {gt, 0}], 1)), ?_assertEqual(1, F([{type, int}, {lte, 1}], 1)), ?_assertEqual(1, F([{type, int}, {lte, 2}], 1)), ?_assertThrow(?badarg, F([{type, int}, {lte, 0}], 1)), ?_assertThrow(?badarg, F([{type, int}, {lt, 1}], 1)), ?_assertThrow(?badarg, F([{type, int}, {gte, 2}], 1)), ?_assertThrow(?badarg, F([{type, int}, {gt, 1}], 1)), ?_assertThrow(?badarg, F([{type, int}, {gt, 0}], <<"-1">>)), ?_assertThrow(badspec, F([{type, int}, {gt, 0, 1}], 1)) ]. constraint_oneof_test_() -> F = fun validate_constraint/2, [ ?_assertEqual(<<"foo">>, F(#{type => binary, oneof => [<<"foo">>, <<"bar">>]}, <<"foo">>)), ?_assertEqual(<<"bar">>, F(#{type => binary, oneof => [<<"foo">>, <<"bar">>]}, <<"bar">>)), ?_assertThrow(?badarg, F([{type, int}, {oneof, [<<"foo">>, <<"bar">>]}], <<"baz">>)) ]. -define(SANITIZE_TEST_INPUTS(Expected, Spec, Inputs), [?_assertEqual(Expected, sanitize(Spec, Input)) || Input <- Inputs]). sanitize_test_() -> [ ?_assertThrow(?badarg, sanitize(#{hello => #{type => int}}, #{})), ?_assertThrow(?badarg, sanitize(#{hello => #{type => binary}}, #{})), ?_assertThrow(?badarg, sanitize(#{hello => #{type => int}}, #{hello => <<"world">>})), ?_assertEqual(#{}, sanitize(#{}, #{})), ?_assertEqual(#{hello => 1}, sanitize(#{hello => #{type => int}}, #{<<"hello">> => <<"1">>})), ?_assertEqual(#{hello => true}, sanitize(#{hello => #{type => bool}}, #{<<"hello">> => true})) ] ++ ?SANITIZE_TEST_INPUTS( #{hello => #{<<"hello">> => 1}}, #{hello => {map, binary, #{type=>int, gte=>0}}}, [ #{hello => #{<<"hello">> => 1}}, #{hello => [{<<"hello">>, 1}]} ]) ++ ?SANITIZE_TEST_INPUTS( #{hello => 1}, #{hello => #{type => int}}, [ #{hello => 1}, #{hello => "1"}, #{hello => <<"1">>} ]). sanitize_invalid_key_test_() -> [ ?_assertThrow(?badarg, sanitize(#{hello => #{type => int}}, #{<<"hello1234">> => <<"1">>})), ?_assertThrow(?badarg, sanitize(#{hello => #{type => int}}, #{"1234" => <<"1">>})) ]. sanitize_order_test_() -> ?SANITIZE_TEST_INPUTS( #{hello => 1, world => <<"world">>}, #{hello => #{type => int}, world => #{type => binary}}, [ #{hello => 1, world => world}, #{world => <<"world">>, hello => <<"1">>} ]) ++ ?SANITIZE_TEST_INPUTS( #{world => <<"world">>, hello => 1}, #{world => #{type => binary}, hello => #{type => int}}, [ #{hello => 1, world => world}, #{world => <<"world">>, hello => <<"1">>} ]). sanitize_shortcuts_test_() -> [ ?_assertEqual(#{hello => 1}, sanitize(#{hello => #{type => int}}, #{hello => <<"1">>})), ?_assertEqual(#{hello => [1,2]}, sanitize(#{hello => {list, int}}, #{hello => [<<"1">>, "2"]})) ]. sanitize_optional_test_() -> [ ?_assertEqual(#{hello => 1, world => undefined}, sanitize(#{hello => #{type => int}, world => #{type => int, optional => true}}, #{hello => <<"1">>})), ?_assertEqual(#{hello => 1, world => undefined}, sanitize(#{hello => #{type => int}, world => #{type => int, optional => true}}, #{hello => <<"1">>, world => <<"">>})), ?_assertEqual(#{hello => 1}, sanitize(#{hello => #{type => int, optional => false}}, #{hello => <<"1">>})) ]. sanitize_default_test_() -> [ ?_assertEqual(#{hello => 1, world => 10}, sanitize(#{hello => #{type => int}, world => #{type => int, default => 10}}, #{hello => <<"1">>})), ?_assertEqual(#{hello => 1, world => 3}, sanitize(#{hello => #{type => int}, world => #{type => int, default => 10}}, #{hello => <<"1">>, world => <<"3">>})) ]. validate_type_bool_test_() -> [ ?_assertEqual(true, validate_type(bool, true)), ?_assertEqual(false, validate_type(bool, false)), ?_assertEqual(true, validate_type(bool, <<"true">>)), ?_assertEqual(false, validate_type(bool, <<"false">>)), ?_assertEqual(false, validate_type(bool, 0)), ?_assertEqual(true, validate_type(bool, 1)), ?_assertEqual(true, validate_type(bool, 10)), ?_assertThrow(?badarg, validate_type(bool, <<"hello">>)) ]. validate_type_int_test_() -> [ ?_assertEqual(1, validate_type(int, 1)), ?_assertEqual(1, validate_type(int, "1")), ?_assertEqual(1, validate_type(int, <<"1">>)), ?_assertThrow(?badarg, validate_type(int, <<"hello">>)), ?_assertThrow(?badarg, validate_type(int, <<"1.234">>)) ]. validate_type_float_test_() -> [ ?_assertEqual(1, validate_type(float, 1)), ?_assertEqual(1, validate_type(float, "1")), ?_assertEqual(1, validate_type(float, <<"1">>)), ?_assertEqual(1.1, validate_type(float, <<"1.1">>)), ?_assertThrow(?badarg, validate_type(float, <<"hello">>)), ?_assertThrow(?badarg, validate_type(float, <<"1.">>)) ]. validate_type_atom_test_() -> [ ?_assertEqual(hello, validate_type(atom, hello)), ?_assertEqual(hello, validate_type(atom, <<"hello">>)), ?_assertThrow(?badarg, validate_type(atom, <<"non_existing_atom_something">>)) ]. validate_type_binary_test_() -> [ ?_assertEqual(<<"1">>, validate_type(binary, 1)), ?_assertEqual(<<"1">>, validate_type(binary, <<"1">>)), ?_assertEqual(<<"1">>, validate_type(binary, "1")) ]. validate_type_list_test_() -> [ ?_assertEqual([1], validate_type({list, int}, 1)), ?_assertEqual([1,2], validate_type({list, int}, [1,2])), ?_assertEqual([1,2], validate_type({list, int}, ["1",<<"2">>])), ?_assertThrow(?badarg, validate_type({list, int}, [1,2,"hello"])) ]. validate_type_map_test_() -> [ ?_assertEqual(#{1 => 2, 3 => 4}, validate_type( {map, int, int}, #{1 => 2, 3 =>4})), ?_assertEqual(#{1 => 2, 3 => 4}, validate_type( {map, int, int}, [{1, 2}, {3, 4}])), ?_assertEqual(#{1 => 2, 3 => 4}, validate_type( {map, int, int}, #{"1" => <<"2">>, <<"3">> => "4"})) ]. validate_badspec_test_() -> [ ?_assertThrow(badspec, validate_constraint(1, [{1, 2}])), ?_assertThrow(badspec, validate_type({map, int}, [{1, 2}])), ?_assertThrow(badspec, validate_type({list, int, int}, [1,2,"hello"])) ]. -endif.