Browse Source

feat(message_validation_api): implement `reorder` API

Thales Macedo Garitezi 1 year atrás
parent
commit
74c03377f2

+ 72 - 3
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 = []) ->

+ 75 - 8
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} ->

+ 99 - 1
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">>,

+ 3 - 0
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"""