emqx/apps/emqx_rule_engine/src/emqx_rule_validator.erl

196 lines
6.6 KiB
Erlang

%%--------------------------------------------------------------------
%% Copyright (c) 2020-2021 EMQ Technologies Co., Ltd. All Rights Reserved.
%%
%% Licensed under the Apache License, Version 2.0 (the "License");
%% you may not use this file except in compliance with the License.
%% You may obtain a copy of the License at
%%
%% http://www.apache.org/licenses/LICENSE-2.0
%%
%% Unless required by applicable law or agreed to in writing, software
%% distributed under the License is distributed on an "AS IS" BASIS,
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
%% See the License for the specific language governing permissions and
%% limitations under the License.
%%--------------------------------------------------------------------
-module(emqx_rule_validator).
-include("rule_engine.hrl").
-export([ validate_params/2
, validate_spec/1
]).
-type name() :: atom().
-type spec() :: #{
type := data_type(),
required => boolean(),
default => term(),
enum => list(term()),
schema => spec()
}.
-type data_type() :: string | password | number | boolean
| object | array | file | cfgselect.
-type params_spec() :: #{name() => spec()} | any.
-type params() :: #{binary() => term()}.
-define(DATA_TYPES,
[ string
, password %% TODO: [5.0] remove this, use string instead
, number
, boolean
, object
, array
, file
, cfgselect %% TODO: [5.0] refactor this
]).
%%------------------------------------------------------------------------------
%% APIs
%%------------------------------------------------------------------------------
%% Validate the params according to the spec.
%% Some keys will be added into the result if they have default values in spec.
%% Note that this function throws exception in case of validation failure.
-spec(validate_params(params(), params_spec()) -> params()).
validate_params(Params, any) -> Params;
validate_params(Params, ParamsSepc) ->
maps:fold(fun(Name, Spec, Params0) ->
IsRequired = maps:get(required, Spec, false),
BinName = bin(Name),
find_field(Name, Params,
fun (not_found) when IsRequired =:= true ->
throw({required_field_missing, BinName});
(not_found) when IsRequired =:= false ->
case maps:find(default, Spec) of
{ok, Default} -> Params0#{BinName => Default};
error -> Params0
end;
(Val) ->
Params0#{BinName => validate_value(Val, Spec)}
end)
end, Params, ParamsSepc).
-spec(validate_spec(params_spec()) -> ok).
validate_spec(any) -> ok;
validate_spec(ParamsSepc) ->
map_foreach(fun do_validate_spec/2, ParamsSepc).
%%------------------------------------------------------------------------------
%% Internal Functions
%%------------------------------------------------------------------------------
validate_value(Val, #{enum := Enum}) ->
validate_enum(Val, Enum);
validate_value(Val, #{type := object} = Spec) ->
validate_params(Val, maps:get(schema, Spec, any));
validate_value(Val, #{type := Type} = Spec) ->
validate_type(Val, Type, Spec).
validate_type(Val, file, _Spec) ->
validate_file(Val);
validate_type(Val, String, Spec) when String =:= string;
String =:= password ->
validate_string(Val, reg_exp(maps:get(format, Spec, any)));
validate_type(Val, number, Spec) ->
validate_number(Val, maps:get(range, Spec, any));
validate_type(Val, boolean, _Spec) ->
validate_boolean(Val);
validate_type(Val, array, Spec) ->
ItemsSpec = maps:get(items, Spec),
[validate_value(V, ItemsSpec) || V <- Val];
validate_type(Val, cfgselect, _Spec) ->
%% TODO: [5.0] refactor this.
Val.
validate_enum(Val, Enum) ->
case lists:member(Val, Enum) of
true -> Val;
false -> throw({invalid_data_type, {enum, {Val, Enum}}})
end.
validate_string(Val, RegExp) ->
try re:run(Val, RegExp) of
nomatch -> throw({invalid_data_type, {string, Val}});
_Match -> Val
catch
_:_ -> throw({invalid_data_type, {string, Val}})
end.
validate_number(Val, any) when is_integer(Val); is_float(Val) ->
Val;
validate_number(Val, _Range = [Min, Max])
when (is_integer(Val) orelse is_float(Val)),
(Val >= Min andalso Val =< Max) ->
Val;
validate_number(Val, Range) ->
throw({invalid_data_type, {number, {Val, Range}}}).
validate_boolean(true) -> true;
validate_boolean(<<"true">>) -> true;
validate_boolean(false) -> false;
validate_boolean(<<"false">>) -> false;
validate_boolean(Val) -> throw({invalid_data_type, {boolean, Val}}).
validate_file(Val) when is_map(Val) -> Val;
validate_file(Val) when is_list(Val) -> Val;
validate_file(Val) when is_binary(Val) -> Val;
validate_file(Val) -> throw({invalid_data_type, {file, Val}}).
reg_exp(url) -> "^https?://\\w+(\.\\w+)*(:[0-9]+)?";
reg_exp(topic) -> "^/?(\\w|\\#|\\+)+(/?(\\w|\\#|\\+))*/?$";
reg_exp(resource_type) -> "[a-zA-Z0-9_:-]";
reg_exp(any) -> ".*";
reg_exp(RegExp) -> RegExp.
do_validate_spec(Name, #{type := object} = Spec) ->
find_field(schema, Spec,
fun (not_found) -> throw({required_field_missing, {schema, {in, Name}}});
(Schema) -> validate_spec(Schema)
end);
do_validate_spec(Name, #{type := array} = Spec) ->
find_field(items, Spec,
fun (not_found) -> throw({required_field_missing, {items, {in, Name}}});
(Items) -> do_validate_spec(Name, Items)
end);
do_validate_spec(_Name, #{type := Type}) ->
_ = supported_data_type(Type, ?DATA_TYPES);
do_validate_spec(Name, _Spec) ->
throw({required_field_missing, {type, {in, Name}}}).
supported_data_type(Type, Supported) ->
case lists:member(Type, Supported) of
false -> throw({unsupported_data_types, Type});
true -> ok
end.
map_foreach(Fun, Map) ->
Iterator = maps:iterator(Map),
map_foreach_loop(Fun, maps:next(Iterator)).
map_foreach_loop(_Fun, none) -> ok;
map_foreach_loop(Fun, {Key, Value, Iterator}) ->
_ = Fun(Key, Value),
map_foreach_loop(Fun, maps:next(Iterator)).
find_field(Field, Spec, Func) ->
do_find_field([bin(Field), Field], Spec, Func).
do_find_field([], _Spec, Func) ->
Func(not_found);
do_find_field([F | Fields], Spec, Func) ->
case maps:find(F, Spec) of
{ok, Value} -> Func(Value);
error ->
do_find_field(Fields, Spec, Func)
end.
bin(Atom) when is_atom(Atom) -> atom_to_binary(Atom, utf8);
bin(Str) when is_list(Str) -> iolist_to_binary(Str);
bin(Bin) when is_binary(Bin) -> Bin.