feat(message_validation_api): implement `reorder` API

This commit is contained in:
Thales Macedo Garitezi 2024-03-18 14:30:52 -03:00
parent bcb7fe96d5
commit 74c03377f2
4 changed files with 249 additions and 12 deletions

View File

@ -17,6 +17,7 @@
list/0, list/0,
move/2, move/2,
reorder/1,
lookup/1, lookup/1,
insert/1, insert/1,
update/1, update/1,
@ -87,6 +88,15 @@ move(Name, Position) ->
#{override_to => cluster} #{override_to => cluster}
). ).
-spec reorder([validation_name()]) ->
{ok, _} | {error, _}.
reorder(Order) ->
emqx:update_config(
?VALIDATIONS_CONF_PATH,
{reorder, Order},
#{override_to => cluster}
).
-spec lookup(validation_name()) -> {ok, validation()} | {error, not_found}. -spec lookup(validation_name()) -> {ok, validation()} | {error, not_found}.
lookup(Name) -> lookup(Name) ->
Validations = emqx:get_config(?VALIDATIONS_CONF_PATH, []), Validations = emqx:get_config(?VALIDATIONS_CONF_PATH, []),
@ -165,7 +175,9 @@ pre_config_update(?VALIDATIONS_CONF_PATH, {update, Validation}, OldValidations)
pre_config_update(?VALIDATIONS_CONF_PATH, {delete, Validation}, OldValidations) -> pre_config_update(?VALIDATIONS_CONF_PATH, {delete, Validation}, OldValidations) ->
delete(OldValidations, Validation); delete(OldValidations, Validation);
pre_config_update(?VALIDATIONS_CONF_PATH, {move, Name, Position}, OldValidations) -> pre_config_update(?VALIDATIONS_CONF_PATH, {move, Name, Position}, OldValidations) ->
move(OldValidations, Name, Position). move(OldValidations, Name, Position);
pre_config_update(?VALIDATIONS_CONF_PATH, {reorder, Order}, OldValidations) ->
reorder(OldValidations, Order).
post_config_update(?VALIDATIONS_CONF_PATH, {append, #{<<"name">> := Name}}, New, _Old, _AppEnvs) -> post_config_update(?VALIDATIONS_CONF_PATH, {append, #{<<"name">> := Name}}, New, _Old, _AppEnvs) ->
{Pos, Validation} = fetch_with_index(New, Name), {Pos, Validation} = fetch_with_index(New, Name),
@ -181,6 +193,9 @@ post_config_update(?VALIDATIONS_CONF_PATH, {delete, Name}, _New, Old, _AppEnvs)
ok = emqx_message_validation_registry:delete(Validation), ok = emqx_message_validation_registry:delete(Validation),
ok; ok;
post_config_update(?VALIDATIONS_CONF_PATH, {move, _Name, _Position}, New, _Old, _AppEnvs) -> post_config_update(?VALIDATIONS_CONF_PATH, {move, _Name, _Position}, New, _Old, _AppEnvs) ->
ok = emqx_message_validation_registry:reindex_positions(New),
ok;
post_config_update(?VALIDATIONS_CONF_PATH, {reorder, _Order}, New, _Old, _AppEnvs) ->
ok = emqx_message_validation_registry:reindex_positions(New), ok = emqx_message_validation_registry:reindex_positions(New),
ok. ok.
@ -348,6 +363,52 @@ move(OldValidations, Name, {before, OtherName}) ->
{OtherValidation, Front2, Rear2} = take(OtherName, Front1 ++ Rear1), {OtherValidation, Front2, Rear2} = take(OtherName, Front1 ++ Rear1),
{ok, Front2 ++ [Validation, OtherValidation] ++ Rear2}. {ok, Front2 ++ [Validation, OtherValidation] ++ Rear2}.
reorder(Validations, Order) ->
Context = #{
not_found => sets:new([{version, 2}]),
duplicated => sets:new([{version, 2}]),
res => [],
seen => sets:new([{version, 2}])
},
reorder(Validations, Order, Context).
reorder(NotReordered, _Order = [], #{not_found := NotFound0, duplicated := Duplicated0, res := Res}) ->
NotFound = sets:to_list(NotFound0),
Duplicated = sets:to_list(Duplicated0),
case {NotReordered, NotFound, Duplicated} of
{[], [], []} ->
{ok, lists:reverse(Res)};
{_, _, _} ->
Error = #{
not_found => NotFound,
duplicated => Duplicated,
not_reordered => [N || #{<<"name">> := N} <- NotReordered]
},
{error, Error}
end;
reorder(RemainingValidations, [Name | Rest], Context0 = #{seen := Seen0}) ->
case sets:is_element(Name, Seen0) of
true ->
Context = maps:update_with(
duplicated, fun(S) -> sets:add_element(Name, S) end, Context0
),
reorder(RemainingValidations, Rest, Context);
false ->
case safe_take(Name, RemainingValidations) of
error ->
Context = maps:update_with(
not_found, fun(S) -> sets:add_element(Name, S) end, Context0
),
reorder(RemainingValidations, Rest, Context);
{ok, {Validation, Front, Rear}} ->
Context1 = maps:update_with(
seen, fun(S) -> sets:add_element(Name, S) end, Context0
),
Context = maps:update_with(res, fun(Vs) -> [Validation | Vs] end, Context1),
reorder(Front ++ Rear, Rest, Context)
end
end.
fetch_with_index([{Pos, #{name := Name} = Validation} | _Rest], Name) -> fetch_with_index([{Pos, #{name := Name} = Validation} | _Rest], Name) ->
{Pos, Validation}; {Pos, Validation};
fetch_with_index([{_, _} | Rest], Name) -> fetch_with_index([{_, _} | Rest], Name) ->
@ -356,11 +417,19 @@ fetch_with_index(Validations, Name) ->
fetch_with_index(lists:enumerate(Validations), Name). fetch_with_index(lists:enumerate(Validations), Name).
take(Name, Validations) -> take(Name, Validations) ->
case safe_take(Name, Validations) of
error ->
throw({validation_not_found, Name});
{ok, {Found, Front, Rear}} ->
{Found, Front, Rear}
end.
safe_take(Name, Validations) ->
case lists:splitwith(fun(#{<<"name">> := N}) -> N =/= Name end, Validations) of case lists:splitwith(fun(#{<<"name">> := N}) -> N =/= Name end, Validations) of
{_Front, []} -> {_Front, []} ->
throw({validation_not_found, Name}); error;
{Front, [Found | Rear]} -> {Front, [Found | Rear]} ->
{Found, Front, Rear} {ok, {Found, Front, Rear}}
end. end.
do_lookup(_Name, _Validations = []) -> do_lookup(_Name, _Validations = []) ->

View File

@ -22,6 +22,7 @@
%% `minirest' handlers %% `minirest' handlers
-export([ -export([
'/message_validations'/2, '/message_validations'/2,
'/message_validations/reorder'/2,
'/message_validations/validation/:name'/2, '/message_validations/validation/:name'/2,
'/message_validations/validation/:name/move'/2 '/message_validations/validation/:name/move'/2
]). ]).
@ -44,6 +45,7 @@ api_spec() ->
paths() -> paths() ->
[ [
"/message_validations", "/message_validations",
"/message_validations/reorder",
"/message_validations/validation/:name", "/message_validations/validation/:name",
"/message_validations/validation/:name/move" "/message_validations/validation/:name/move"
]. ].
@ -59,7 +61,7 @@ schema("/message_validations") ->
#{ #{
200 => 200 =>
emqx_dashboard_swagger:schema_with_examples( emqx_dashboard_swagger:schema_with_examples(
hoconsc:array( array(
emqx_message_validation_schema:api_schema(list) emqx_message_validation_schema:api_schema(list)
), ),
#{ #{
@ -107,6 +109,35 @@ schema("/message_validations") ->
} }
} }
}; };
schema("/message_validations/reorder") ->
#{
'operationId' => '/message_validations/reorder',
post => #{
tags => ?TAGS,
summary => <<"Reorder all validations">>,
description => ?DESC("reorder_validations"),
'requestBody' =>
emqx_dashboard_swagger:schema_with_examples(
ref(reorder),
example_input_reorder()
),
responses =>
#{
204 => <<"No Content">>,
400 => error_schema(
'BAD_REQUEST',
<<"Bad request">>,
[
{not_found, mk(array(binary()), #{desc => "Validations not found"})},
{not_reordered,
mk(array(binary()), #{desc => "Validations not referenced in input"})},
{duplicated,
mk(array(binary()), #{desc => "Duplicated validations in input"})}
]
)
}
}
};
schema("/message_validations/validation/:name") -> schema("/message_validations/validation/:name") ->
#{ #{
'operationId' => '/message_validations/validation/:name', 'operationId' => '/message_validations/validation/:name',
@ -119,7 +150,7 @@ schema("/message_validations/validation/:name") ->
#{ #{
200 => 200 =>
emqx_dashboard_swagger:schema_with_examples( emqx_dashboard_swagger:schema_with_examples(
hoconsc:array( array(
emqx_message_validation_schema:api_schema(lookup) emqx_message_validation_schema:api_schema(lookup)
), ),
#{ #{
@ -189,6 +220,10 @@ fields(before) ->
[ [
{position, mk(before, #{default => before, required => true, in => body})}, {position, mk(before, #{default => before, required => true, in => body})},
{validation, mk(binary(), #{required => true, in => body})} {validation, mk(binary(), #{required => true, in => body})}
];
fields(reorder) ->
[
{order, mk(array(binary()), #{required => true, in => body})}
]. ].
%%------------------------------------------------------------------------------------------------- %%-------------------------------------------------------------------------------------------------
@ -255,12 +290,16 @@ fields(before) ->
not_found(Name) not_found(Name)
). ).
'/message_validations/reorder'(post, #{body := #{<<"order">> := Order}}) ->
do_reorder(Order).
%%------------------------------------------------------------------------------------------------- %%-------------------------------------------------------------------------------------------------
%% Internal fns %% Internal fns
%%------------------------------------------------------------------------------------------------- %%-------------------------------------------------------------------------------------------------
ref(Struct) -> hoconsc:ref(?MODULE, Struct). ref(Struct) -> hoconsc:ref(?MODULE, Struct).
mk(Type, Opts) -> hoconsc:mk(Type, Opts). mk(Type, Opts) -> hoconsc:mk(Type, Opts).
array(Type) -> hoconsc:array(Type).
example_input_create() -> example_input_create() ->
%% TODO %% TODO
@ -270,6 +309,10 @@ example_input_update() ->
%% TODO %% TODO
#{}. #{}.
example_input_reorder() ->
%% TODO
#{}.
example_return_list() -> example_return_list() ->
%% TODO %% TODO
[]. [].
@ -290,12 +333,15 @@ example_position() ->
%% TODO %% TODO
#{}. #{}.
error_schema(Code, Message) when is_atom(Code) -> error_schema(Code, Message) ->
error_schema([Code], Message); error_schema(Code, Message, _ExtraFields = []).
error_schema(Codes, Message) when is_list(Message) ->
error_schema(Codes, list_to_binary(Message)); error_schema(Code, Message, ExtraFields) when is_atom(Code) ->
error_schema(Codes, Message) when is_list(Codes) andalso is_binary(Message) -> error_schema([Code], Message, ExtraFields);
emqx_dashboard_swagger:error_codes(Codes, Message). error_schema(Codes, Message, ExtraFields) when is_list(Message) ->
error_schema(Codes, list_to_binary(Message), ExtraFields);
error_schema(Codes, Message, ExtraFields) when is_list(Codes) andalso is_binary(Message) ->
ExtraFields ++ emqx_dashboard_swagger:error_codes(Codes, Message).
position_union_member_selector(all_union_members) -> position_union_member_selector(all_union_members) ->
position_refs(); position_refs();
@ -359,6 +405,27 @@ do_move(ValidationName, Position) ->
?BAD_REQUEST(Error) ?BAD_REQUEST(Error)
end. end.
do_reorder(Order) ->
case emqx_message_validation:reorder(Order) of
{ok, _} ->
?NO_CONTENT;
{error,
{pre_config_update, _HandlerMod, #{
not_found := NotFound,
duplicated := Duplicated,
not_reordered := NotReordered
}}} ->
Msg0 = ?ERROR_MSG('BAD_REQUEST', <<"Bad request">>),
Msg = Msg0#{
not_found => NotFound,
duplicated => Duplicated,
not_reordered => NotReordered
},
{400, Msg};
{error, Error} ->
?BAD_REQUEST(Error)
end.
with_validation(Name, FoundFn, NotFoundFn) -> with_validation(Name, FoundFn, NotFoundFn) ->
case emqx_message_validation:lookup(Name) of case emqx_message_validation:lookup(Name) of
{ok, Validation} -> {ok, Validation} ->

View File

@ -182,7 +182,8 @@ move(Name, Pos) ->
reorder(Order) -> reorder(Order) ->
Path = emqx_mgmt_api_test_util:api_path([api_root(), "reorder"]), Path = emqx_mgmt_api_test_util:api_path([api_root(), "reorder"]),
Res = request(post, Path, Order), Params = #{<<"order">> => Order},
Res = request(post, Path, Params),
ct:pal("reorder result:\n ~p", [Res]), ct:pal("reorder result:\n ~p", [Res]),
simplify_result(Res). simplify_result(Res).
@ -514,6 +515,103 @@ t_move(_Config) ->
ok. ok.
%% test the "reorder" API
t_reorder(_Config) ->
%% no validations to reorder
?assertMatch({204, _}, reorder([])),
%% unknown validation
?assertMatch(
{400, #{<<"not_found">> := [<<"nonexistent">>]}},
reorder([<<"nonexistent">>])
),
Topic = <<"t">>,
Name1 = <<"foo">>,
Validation1 = validation(Name1, [sql_check()], #{<<"topics">> => Topic}),
{201, _} = insert(Validation1),
%% unknown validation
?assertMatch(
{400, #{
%% Note: minirest currently encodes empty lists as a "[]" string...
<<"duplicated">> := "[]",
<<"not_found">> := [<<"nonexistent">>],
<<"not_reordered">> := [Name1]
}},
reorder([<<"nonexistent">>])
),
%% repeated validations
?assertMatch(
{400, #{
<<"not_found">> := "[]",
<<"duplicated">> := [Name1],
<<"not_reordered">> := "[]"
}},
reorder([Name1, Name1])
),
%% mixed known, unknown and repeated validations
?assertMatch(
{400, #{
<<"not_found">> := [<<"nonexistent">>],
<<"duplicated">> := [Name1],
%% Note: minirest currently encodes empty lists as a "[]" string...
<<"not_reordered">> := "[]"
}},
reorder([Name1, <<"nonexistent">>, <<"nonexistent">>, Name1])
),
?assertMatch({204, _}, reorder([Name1])),
?assertMatch({200, [#{<<"name">> := Name1}]}, list()),
?assertIndexOrder([Name1], Topic),
Name2 = <<"bar">>,
Validation2 = validation(Name2, [sql_check()], #{<<"topics">> => Topic}),
{201, _} = insert(Validation2),
Name3 = <<"baz">>,
Validation3 = validation(Name3, [sql_check()], #{<<"topics">> => Topic}),
{201, _} = insert(Validation3),
?assertMatch(
{200, [#{<<"name">> := Name1}, #{<<"name">> := Name2}, #{<<"name">> := Name3}]},
list()
),
?assertIndexOrder([Name1, Name2, Name3], Topic),
%% Doesn't mention all validations
?assertMatch(
{400, #{
%% Note: minirest currently encodes empty lists as a "[]" string...
<<"not_found">> := "[]",
<<"not_reordered">> := [_, _]
}},
reorder([Name1])
),
?assertMatch(
{200, [#{<<"name">> := Name1}, #{<<"name">> := Name2}, #{<<"name">> := Name3}]},
list()
),
?assertIndexOrder([Name1, Name2, Name3], Topic),
?assertMatch({204, _}, reorder([Name3, Name2, Name1])),
?assertMatch(
{200, [#{<<"name">> := Name3}, #{<<"name">> := Name2}, #{<<"name">> := Name1}]},
list()
),
?assertIndexOrder([Name3, Name2, Name1], Topic),
?assertMatch({204, _}, reorder([Name1, Name3, Name2])),
?assertMatch(
{200, [#{<<"name">> := Name1}, #{<<"name">> := Name3}, #{<<"name">> := Name2}]},
list()
),
?assertIndexOrder([Name1, Name3, Name2], Topic),
ok.
%% Check the `all_pass' strategy %% Check the `all_pass' strategy
t_all_pass(_Config) -> t_all_pass(_Config) ->
Name1 = <<"foo">>, Name1 = <<"foo">>,

View File

@ -18,6 +18,9 @@ emqx_message_validation_http_api {
move_validation.desc: move_validation.desc:
"""Change the order of a validation in the list of validations""" """Change the order of a validation in the list of validations"""
reorder_validations.desc:
"""Reorder of all validations"""
param_path_name.desc: param_path_name.desc:
"""Validation name""" """Validation name"""