From 74c03377f25817d0180f15cacb074d82acc0149e Mon Sep 17 00:00:00 2001 From: Thales Macedo Garitezi Date: Mon, 18 Mar 2024 14:30:52 -0300 Subject: [PATCH] feat(message_validation_api): implement `reorder` API --- .../src/emqx_message_validation.erl | 75 ++++++++++++- .../src/emqx_message_validation_http_api.erl | 83 +++++++++++++-- ...emqx_message_validation_http_api_SUITE.erl | 100 +++++++++++++++++- .../emqx_message_validation_http_api.hocon | 3 + 4 files changed, 249 insertions(+), 12 deletions(-) diff --git a/apps/emqx_message_validation/src/emqx_message_validation.erl b/apps/emqx_message_validation/src/emqx_message_validation.erl index 364192e58..72861f3c5 100644 --- a/apps/emqx_message_validation/src/emqx_message_validation.erl +++ b/apps/emqx_message_validation/src/emqx_message_validation.erl @@ -17,6 +17,7 @@ list/0, move/2, + reorder/1, lookup/1, insert/1, update/1, @@ -87,6 +88,15 @@ move(Name, Position) -> #{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}. lookup(Name) -> 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) -> delete(OldValidations, Validation); 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) -> {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; 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. @@ -348,6 +363,52 @@ move(OldValidations, Name, {before, OtherName}) -> {OtherValidation, Front2, Rear2} = take(OtherName, Front1 ++ Rear1), {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) -> {Pos, Validation}; fetch_with_index([{_, _} | Rest], Name) -> @@ -356,11 +417,19 @@ fetch_with_index(Validations, Name) -> fetch_with_index(lists:enumerate(Validations), Name). 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 {_Front, []} -> - throw({validation_not_found, Name}); + error; {Front, [Found | Rear]} -> - {Found, Front, Rear} + {ok, {Found, Front, Rear}} end. do_lookup(_Name, _Validations = []) -> diff --git a/apps/emqx_message_validation/src/emqx_message_validation_http_api.erl b/apps/emqx_message_validation/src/emqx_message_validation_http_api.erl index 69d63f2ed..34ed584fa 100644 --- a/apps/emqx_message_validation/src/emqx_message_validation_http_api.erl +++ b/apps/emqx_message_validation/src/emqx_message_validation_http_api.erl @@ -22,6 +22,7 @@ %% `minirest' handlers -export([ '/message_validations'/2, + '/message_validations/reorder'/2, '/message_validations/validation/:name'/2, '/message_validations/validation/:name/move'/2 ]). @@ -44,6 +45,7 @@ api_spec() -> paths() -> [ "/message_validations", + "/message_validations/reorder", "/message_validations/validation/:name", "/message_validations/validation/:name/move" ]. @@ -59,7 +61,7 @@ schema("/message_validations") -> #{ 200 => emqx_dashboard_swagger:schema_with_examples( - hoconsc:array( + array( 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") -> #{ 'operationId' => '/message_validations/validation/:name', @@ -119,7 +150,7 @@ schema("/message_validations/validation/:name") -> #{ 200 => emqx_dashboard_swagger:schema_with_examples( - hoconsc:array( + array( emqx_message_validation_schema:api_schema(lookup) ), #{ @@ -189,6 +220,10 @@ fields(before) -> [ {position, mk(before, #{default => before, 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) ). +'/message_validations/reorder'(post, #{body := #{<<"order">> := Order}}) -> + do_reorder(Order). + %%------------------------------------------------------------------------------------------------- %% Internal fns %%------------------------------------------------------------------------------------------------- ref(Struct) -> hoconsc:ref(?MODULE, Struct). mk(Type, Opts) -> hoconsc:mk(Type, Opts). +array(Type) -> hoconsc:array(Type). example_input_create() -> %% TODO @@ -270,6 +309,10 @@ example_input_update() -> %% TODO #{}. +example_input_reorder() -> + %% TODO + #{}. + example_return_list() -> %% TODO []. @@ -290,12 +333,15 @@ example_position() -> %% TODO #{}. -error_schema(Code, Message) when is_atom(Code) -> - error_schema([Code], Message); -error_schema(Codes, Message) when is_list(Message) -> - error_schema(Codes, list_to_binary(Message)); -error_schema(Codes, Message) when is_list(Codes) andalso is_binary(Message) -> - emqx_dashboard_swagger:error_codes(Codes, Message). +error_schema(Code, Message) -> + error_schema(Code, Message, _ExtraFields = []). + +error_schema(Code, Message, ExtraFields) when is_atom(Code) -> + error_schema([Code], Message, ExtraFields); +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_refs(); @@ -359,6 +405,27 @@ do_move(ValidationName, Position) -> ?BAD_REQUEST(Error) 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) -> case emqx_message_validation:lookup(Name) of {ok, Validation} -> diff --git a/apps/emqx_message_validation/test/emqx_message_validation_http_api_SUITE.erl b/apps/emqx_message_validation/test/emqx_message_validation_http_api_SUITE.erl index 22b58cd4f..67910d1aa 100644 --- a/apps/emqx_message_validation/test/emqx_message_validation_http_api_SUITE.erl +++ b/apps/emqx_message_validation/test/emqx_message_validation_http_api_SUITE.erl @@ -182,7 +182,8 @@ move(Name, Pos) -> reorder(Order) -> 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]), simplify_result(Res). @@ -514,6 +515,103 @@ t_move(_Config) -> 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 t_all_pass(_Config) -> Name1 = <<"foo">>, diff --git a/rel/i18n/emqx_message_validation_http_api.hocon b/rel/i18n/emqx_message_validation_http_api.hocon index d169e2f76..bb26ae146 100644 --- a/rel/i18n/emqx_message_validation_http_api.hocon +++ b/rel/i18n/emqx_message_validation_http_api.hocon @@ -18,6 +18,9 @@ emqx_message_validation_http_api { move_validation.desc: """Change the order of a validation in the list of validations""" + reorder_validations.desc: + """Reorder of all validations""" + param_path_name.desc: """Validation name"""