|
|
@@ -22,110 +22,124 @@
|
|
|
, validate_spec/1
|
|
|
]).
|
|
|
|
|
|
--type(params_spec() :: #{atom() => term()}).
|
|
|
--type(params() :: #{binary() => term()}).
|
|
|
-
|
|
|
--define(DATA_TYPES, [string, password, number, float, boolean, object, array, file, cfgselect]).
|
|
|
+-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 the spec.
|
|
|
+%% 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()) -> ok).
|
|
|
+-spec(validate_params(params(), params_spec()) -> params()).
|
|
|
+validate_params(Params, any) -> Params;
|
|
|
validate_params(Params, ParamsSepc) ->
|
|
|
- F = fun(Name, Spec) ->
|
|
|
- do_validate_param(Name, Spec, Params)
|
|
|
- end,
|
|
|
- map_foreach(F, 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(ParamsSepc) -> map_foreach(fun do_validate_spec/2, ParamsSepc).
|
|
|
+validate_spec(any) -> ok;
|
|
|
+validate_spec(ParamsSepc) ->
|
|
|
+ map_foreach(fun do_validate_spec/2, ParamsSepc).
|
|
|
|
|
|
%%------------------------------------------------------------------------------
|
|
|
%% Internal Functions
|
|
|
%%------------------------------------------------------------------------------
|
|
|
|
|
|
-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)).
|
|
|
-
|
|
|
-do_validate_param(Name, Spec = #{required := true}, Params) ->
|
|
|
- find_field(Name, Params,
|
|
|
- fun (not_found) -> error({required_field_missing, Name});
|
|
|
- (Val) -> do_validate_param(Val, Spec)
|
|
|
- end);
|
|
|
-do_validate_param(Name, Spec, Params) ->
|
|
|
- find_field(Name, Params,
|
|
|
- fun (not_found) -> ok; %% optional field 'Name'
|
|
|
- (Val) -> do_validate_param(Val, Spec)
|
|
|
- end).
|
|
|
-
|
|
|
-do_validate_param(Val, Spec = #{type := Type}) ->
|
|
|
- case maps:find(enum, Spec) of
|
|
|
- {ok, Enum} -> validate_enum(Val, Enum);
|
|
|
- error -> ok
|
|
|
- end,
|
|
|
+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) ->
|
|
|
- ok = validate_file(Val);
|
|
|
-validate_type(Val, string, Spec) ->
|
|
|
- ok = validate_string(Val, reg_exp(maps:get(format, Spec, any)));
|
|
|
-validate_type(Val, password, Spec) ->
|
|
|
- ok = validate_string(Val, reg_exp(maps:get(format, Spec, any)));
|
|
|
+ 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) ->
|
|
|
- ok = validate_number(Val, maps:get(range, Spec, any));
|
|
|
+ validate_number(Val, maps:get(range, Spec, any));
|
|
|
validate_type(Val, boolean, _Spec) ->
|
|
|
- ok = validate_boolean(Val);
|
|
|
+ validate_boolean(Val);
|
|
|
validate_type(Val, array, Spec) ->
|
|
|
- [do_validate_param(V, maps:get(items, Spec)) || V <- Val],
|
|
|
- ok;
|
|
|
-validate_type(_Val, cfgselect, _Spec) ->
|
|
|
- ok;
|
|
|
-validate_type(Val, object, Spec) ->
|
|
|
- ok = validate_object(Val, maps:get(schema, Spec, any)).
|
|
|
+ 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 -> ok;
|
|
|
- false -> error({invalid_data_type, {enum, {Val, Enum}}})
|
|
|
+ true -> Val;
|
|
|
+ false -> throw({invalid_data_type, {enum, {Val, Enum}}})
|
|
|
end.
|
|
|
|
|
|
validate_string(Val, RegExp) ->
|
|
|
try re:run(Val, RegExp) of
|
|
|
- nomatch -> error({invalid_data_type, {string, Val}});
|
|
|
- _Match -> ok
|
|
|
+ nomatch -> throw({invalid_data_type, {string, Val}});
|
|
|
+ _Match -> Val
|
|
|
catch
|
|
|
- _:_ -> error({invalid_data_type, {string, Val}})
|
|
|
+ _:_ -> throw({invalid_data_type, {string, Val}})
|
|
|
end.
|
|
|
|
|
|
validate_number(Val, any) when is_integer(Val); is_float(Val) ->
|
|
|
- ok;
|
|
|
+ Val;
|
|
|
validate_number(Val, _Range = [Min, Max])
|
|
|
when (is_integer(Val) orelse is_float(Val)),
|
|
|
(Val >= Min andalso Val =< Max) ->
|
|
|
- ok;
|
|
|
+ Val;
|
|
|
validate_number(Val, Range) ->
|
|
|
- error({invalid_data_type, {number, {Val, Range}}}).
|
|
|
+ throw({invalid_data_type, {number, {Val, Range}}}).
|
|
|
|
|
|
-validate_object(Val, Schema) ->
|
|
|
- validate_params(Val, Schema).
|
|
|
+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_boolean(true) -> ok;
|
|
|
-validate_boolean(false) -> ok;
|
|
|
-validate_boolean(Val) -> error({invalid_data_type, {boolean, Val}}).
|
|
|
-
|
|
|
-validate_file(Val) when is_map(Val) -> ok;
|
|
|
-validate_file(Val) when is_list(Val) -> ok;
|
|
|
-validate_file(Val) when is_binary(Val) -> ok;
|
|
|
-validate_file(Val) -> error({invalid_data_type, {file, 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|\\#|\\+))*/?$";
|
|
|
@@ -133,39 +147,39 @@ reg_exp(resource_type) -> "[a-zA-Z0-9_:-]";
|
|
|
reg_exp(any) -> ".*";
|
|
|
reg_exp(RegExp) -> RegExp.
|
|
|
|
|
|
-do_validate_spec(Name, Spec = #{type := object}) ->
|
|
|
+do_validate_spec(Name, #{type := object} = Spec) ->
|
|
|
find_field(schema, Spec,
|
|
|
- fun (not_found) -> error({required_field_missing, {schema, {in, Name}}});
|
|
|
+ fun (not_found) -> throw({required_field_missing, {schema, {in, Name}}});
|
|
|
(Schema) -> validate_spec(Schema)
|
|
|
end);
|
|
|
-do_validate_spec(Name, Spec = #{type := array}) ->
|
|
|
+do_validate_spec(Name, #{type := array} = Spec) ->
|
|
|
find_field(items, Spec,
|
|
|
- fun (not_found) -> error({required_field_missing, {items, {in, Name}}});
|
|
|
+ fun (not_found) -> throw({required_field_missing, {items, {in, Name}}});
|
|
|
(Items) -> do_validate_spec(Name, Items)
|
|
|
end);
|
|
|
-do_validate_spec(Name, Spec = #{type := Type}) ->
|
|
|
- ok = supported_data_type(Type, ?DATA_TYPES),
|
|
|
- ok = validate_default_value(Name, Spec),
|
|
|
- ok.
|
|
|
+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 -> error({unsupported_data_types, Type});
|
|
|
+ false -> throw({unsupported_data_types, Type});
|
|
|
true -> ok
|
|
|
end.
|
|
|
|
|
|
-validate_default_value(Name, Spec) ->
|
|
|
- case maps:get(required, Spec, false) of
|
|
|
- true -> ok;
|
|
|
- false ->
|
|
|
- find_field(default, Spec,
|
|
|
- fun (not_found) -> error({required_field_missing, {default, Name}});
|
|
|
- (_Default) -> ok
|
|
|
- end)
|
|
|
- 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([Field, bin(Field)], Spec, Func).
|
|
|
+ do_find_field([bin(Field), Field], Spec, Func).
|
|
|
|
|
|
do_find_field([], _Spec, Func) ->
|
|
|
Func(not_found);
|