Kaynağa Gözat

feat: add rule-engine functions

EMQ-YangM 3 yıl önce
ebeveyn
işleme
e4b62f3a5f

+ 210 - 0
apps/emqx_rule_engine/src/emqx_rule_date.erl

@@ -0,0 +1,210 @@
+%%--------------------------------------------------------------------
+%% Copyright (c) 2020-2022 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_date).
+
+-export([date/3, date/4, parse_date/4]).
+
+-export([ is_int_char/1
+        , is_symbol_char/1
+        , is_m_char/1
+        ]).
+
+-record(result, {
+       year     = "1970" :: string() %%year()
+     , month    = "1" :: string() %%month()
+     , day      = "1" :: string() %%day()
+     , hour     = "0" :: string() %%hour()
+     , minute   = "0" :: string() %%minute() %% epoch in millisecond precision
+     , second   = "0" :: string() %%second() %% epoch in millisecond precision
+     , zone     = "+00:00" :: string() %%integer() %% zone maybe some value
+}).
+
+%% -type time_unit() :: 'microsecond'
+%%                    | 'millisecond'
+%%                    | 'nanosecond'
+%%                    | 'second'.
+%% -type offset() :: [byte()] | (Time :: integer()).
+date(TimeUnit, Offset, FormatString) ->
+    date(TimeUnit, Offset, FormatString, erlang:system_time(TimeUnit)).
+
+date(TimeUnit, Offset, FormatString, TimeEpoch) ->
+    [Head|Other] = string:split(FormatString, "%", all),
+    R = create_tag([{st, Head}], Other),
+    Res = lists:map(fun(Expr) ->
+              eval_tag(rmap(make_time(TimeUnit, Offset, TimeEpoch)), Expr) end, R),
+    lists:concat(Res).
+
+parse_date(TimeUnit, Offset, FormatString, InputString) ->
+    [Head|Other] = string:split(FormatString, "%", all),
+    R = create_tag([{st, Head}], Other),
+    IsZ = fun(V) -> case V of
+                     {tag, $Z} -> true;
+                     _ -> false
+                    end end,
+    R1 = lists:filter(IsZ, R),
+    IfFun = fun(Con, A, B) ->
+                case Con of
+                   [] -> A;
+                   _ -> B
+                end end,
+    Res = parse_input(FormatString, InputString),
+    Str = Res#result.year ++ "-"
+        ++ Res#result.month ++ "-"
+        ++ Res#result.day ++ "T"
+        ++ Res#result.hour ++ ":"
+        ++ Res#result.minute ++ ":"
+        ++ Res#result.second ++
+        IfFun(R1, Offset, Res#result.zone),
+    calendar:rfc3339_to_system_time(Str, [{unit, TimeUnit}]).
+
+mlist(R)->
+    [ {$H, R#result.hour}    %% %H	Shows hour in 24-hour format [15]
+    , {$M, R#result.minute}  %% %M	Displays minutes [00-59]
+    , {$S, R#result.second}  %% %S	Displays seconds [00-59]
+    , {$y, R#result.year}    %% %y	Displays year YYYY [2021]
+    , {$m, R#result.month}   %% %m	Displays the number of the month [01-12]
+    , {$d, R#result.day}     %% %d	Displays the number of the month [01-12]
+    , {$Z, R#result.zone}    %% %Z	Displays Time zone
+    ].
+
+rmap(Result) ->
+    maps:from_list(mlist(Result)).
+
+support_char() -> "HMSymdZ".
+
+create_tag(Head, []) ->
+    Head;
+create_tag(Head, [Val1|RVal]) ->
+    case Val1 of
+        [] -> create_tag(Head ++ [{st, [$%]}], RVal);
+        [H| Other] ->
+            case lists:member(H, support_char()) of
+                true -> create_tag(Head ++ [{tag, H}, {st, Other}], RVal);
+                false -> create_tag(Head ++ [{st, [$%|Val1]}], RVal)
+            end
+    end.
+
+eval_tag(_,{st, Str}) ->
+    Str;
+eval_tag(Map,{tag, Char}) ->
+    maps:get(Char, Map, "undefined").
+
+%% make_time(TimeUnit, Offset) ->
+%%     make_time(TimeUnit, Offset, erlang:system_time(TimeUnit)).
+make_time(TimeUnit, Offset, TimeEpoch) ->
+    Res = calendar:system_time_to_rfc3339(TimeEpoch,
+                                         [{unit, TimeUnit}, {offset, Offset}]),
+    [Y1, Y2, Y3, Y4, $-, Mon1, Mon2, $-, D1, D2, _T,
+     H1, H2, $:, Min1, Min2, $:, S1, S2 | TimeStr] = Res,
+    IsFractionChar = fun(C) -> C >= $0 andalso C =< $9 orelse C =:= $. end,
+    {FractionStr, UtcOffset} = lists:splitwith(IsFractionChar, TimeStr),
+    #result{
+         year = [Y1, Y2, Y3, Y4]
+       , month = [Mon1, Mon2]
+       , day = [D1, D2]
+       , hour = [H1, H2]
+       , minute = [Min1, Min2]
+       , second = [S1, S2] ++ FractionStr
+       , zone = UtcOffset
+      }.
+
+
+is_int_char(C) ->
+    C >= $0 andalso C =< $9 .
+is_symbol_char(C) ->
+    C =:= $- orelse C =:= $+ .
+is_m_char(C) ->
+    C =:= $:.
+
+parse_char_with_fun(_, []) -> error(null_input);
+parse_char_with_fun(ValidFun, [C|Other]) ->
+    Res = case erlang:is_function(ValidFun) of
+              true -> ValidFun(C);
+              false -> erlang:apply(emqx_rule_date, ValidFun, [C])
+          end,
+    case Res of
+        true -> {C, Other};
+        false -> error({unexpected,[C|Other]})
+    end.
+parse_string([], Input) -> {[], Input};
+parse_string([C|Other], Input) ->
+    {C1, Input1} = parse_char_with_fun(fun(V) -> V =:= C end, Input),
+    {Res, Input2} = parse_string(Other, Input1),
+    {[C1|Res], Input2}.
+
+parse_times(0, _, Input) -> {[], Input};
+parse_times(Times, Fun, Input) ->
+    {C1, Input1} = parse_char_with_fun(Fun, Input),
+    {Res, Input2} = parse_times((Times - 1), Fun, Input1),
+    {[C1|Res], Input2}.
+
+parse_int_times(Times, Input) ->
+    parse_times(Times, is_int_char, Input).
+
+parse_fraction(Input) ->
+    IsFractionChar = fun(C) -> C >= $0 andalso C =< $9 orelse C =:= $. end,
+    lists:splitwith(IsFractionChar, Input).
+
+parse_second(Input) ->
+    {M, Input1} = parse_int_times(2, Input),
+    {M1, Input2} = parse_fraction(Input1),
+    {M++M1, Input2}.
+
+parse_zone(Input) ->
+    {S, Input1} = parse_char_with_fun(is_symbol_char, Input),
+    {M, Input2} = parse_int_times(2, Input1),
+    {C, Input3} = parse_char_with_fun(is_m_char, Input2),
+    {V, Input4} = parse_int_times(2, Input3),
+    {[S|M++[C|V]], Input4}.
+
+mlist1()->
+    maps:from_list(
+      [ {$H, fun(Input) -> parse_int_times(2, Input) end}    %% %H	Shows hour in 24-hour format [15]
+      , {$M, fun(Input) -> parse_int_times(2, Input) end}  %% %M	Displays minutes [00-59]
+      , {$S, fun(Input) -> parse_second(Input) end}  %% %S	Displays seconds [00-59]
+      , {$y, fun(Input) -> parse_int_times(4, Input) end}    %% %y	Displays year YYYY [2021]
+      , {$m, fun(Input) -> parse_int_times(2, Input) end}   %% %m	Displays the number of the month [01-12]
+      , {$d, fun(Input) -> parse_int_times(2, Input) end}     %% %d	Displays the number of the month [01-12]
+      , {$Z, fun(Input) -> parse_zone(Input) end}    %% %Z	Displays Time zone
+      ]).
+
+update_result($H, Res, Str) -> Res#result{hour=Str};
+update_result($M, Res, Str) -> Res#result{minute=Str};
+update_result($S, Res, Str) -> Res#result{second=Str};
+update_result($y, Res, Str) -> Res#result{year=Str};
+update_result($m, Res, Str) -> Res#result{month=Str};
+update_result($d, Res, Str) -> Res#result{day=Str};
+update_result($Z, Res, Str) -> Res#result{zone=Str}.
+
+parse_tag(Res, {st, St}, InputString) ->
+    {_A, B} = parse_string(St, InputString),
+    {Res, B};
+parse_tag(Res, {tag, St}, InputString) ->
+    Fun = maps:get(St, mlist1()),
+    {A, B} = Fun(InputString),
+    NRes = update_result(St, Res, A),
+    {NRes, B}.
+
+parse_tags(Res, [], _) -> Res;
+parse_tags(Res, [Tag|Others], InputString) ->
+    {NRes, B} = parse_tag(Res, Tag, InputString),
+    parse_tags(NRes, Others, B).
+
+parse_input(FormatString, InputString) ->
+    [Head|Other] = string:split(FormatString, "%", all),
+    R = create_tag([{st, Head}], Other),
+    parse_tags(#result{}, R, InputString).

+ 22 - 0
apps/emqx_rule_engine/src/emqx_rule_funcs.erl

@@ -190,6 +190,9 @@
         , rfc3339_to_unix_ts/2
         , rfc3339_to_unix_ts/2
         , now_timestamp/0
         , now_timestamp/0
         , now_timestamp/1
         , now_timestamp/1
+        , format_date/3
+        , format_date/4
+        , date_to_unix_ts/4
         ]).
         ]).
 
 
 %% Proc Dict Func
 %% Proc Dict Func
@@ -880,6 +883,25 @@ time_unit(<<"millisecond">>) -> millisecond;
 time_unit(<<"microsecond">>) -> microsecond;
 time_unit(<<"microsecond">>) -> microsecond;
 time_unit(<<"nanosecond">>) -> nanosecond.
 time_unit(<<"nanosecond">>) -> nanosecond.
 
 
+format_date(TimeUnit, Offset, FormatString) ->
+    emqx_plugin_libs_rule:bin(
+      emqx_rule_date:date(time_unit(TimeUnit),
+                          emqx_plugin_libs_rule:str(Offset),
+                          emqx_plugin_libs_rule:str(FormatString))).
+
+format_date(TimeUnit, Offset, FormatString, TimeEpoch) ->
+    emqx_plugin_libs_rule:bin(
+      emqx_rule_date:date(time_unit(TimeUnit),
+                          emqx_plugin_libs_rule:str(Offset),
+                          emqx_plugin_libs_rule:str(FormatString),
+                          TimeEpoch)).
+
+date_to_unix_ts(TimeUnit, Offset, FormatString, InputString) ->
+    emqx_rule_date:parse_date(time_unit(TimeUnit),
+                        emqx_plugin_libs_rule:str(Offset),
+                        emqx_plugin_libs_rule:str(FormatString),
+                        emqx_plugin_libs_rule:str(InputString)).
+
 %% @doc This is for sql funcs that should be handled in the specific modules.
 %% @doc This is for sql funcs that should be handled in the specific modules.
 %% Here the emqx_rule_funcs module acts as a proxy, forwarding
 %% Here the emqx_rule_funcs module acts as a proxy, forwarding
 %% the function handling to the worker module.
 %% the function handling to the worker module.

+ 20 - 1
apps/emqx_rule_engine/test/emqx_rule_funcs_SUITE.erl

@@ -664,6 +664,26 @@ t_rfc3339_to_unix_ts(_) ->
         ?assertEqual(Epoch, emqx_rule_funcs:rfc3339_to_unix_ts(DateTime, BUnit))
         ?assertEqual(Epoch, emqx_rule_funcs:rfc3339_to_unix_ts(DateTime, BUnit))
      end || Unit <- [second,millisecond,microsecond,nanosecond]].
      end || Unit <- [second,millisecond,microsecond,nanosecond]].
 
 
+t_format_date_funcs(_) ->
+    ?PROPTEST(prop_format_date_fun).
+
+prop_format_date_fun() ->
+    Args1 = [<<"second">>, <<"+07:00">>, <<"%m--%d--%y---%H:%M:%S%Z">>],
+    ?FORALL(S, erlang:system_time(second),
+            S == apply_func(date_to_unix_ts,
+                            Args1 ++ [apply_func(format_date,
+                                                Args1 ++ [S])])),
+    Args2 = [<<"millisecond">>, <<"+04:00">>, <<"--%m--%d--%y---%H:%M:%S%Z">>],
+    ?FORALL(S, erlang:system_time(millisecond),
+            S == apply_func(date_to_unix_ts,
+                            Args2 ++ [apply_func(format_date,
+                                                 Args2 ++ [S])])),
+    Args = [<<"second">>, <<"+08:00">>, <<"%y-%m-%d-%H:%M:%S%Z">>],
+    ?FORALL(S, erlang:system_time(second),
+            S == apply_func(date_to_unix_ts,
+                           Args ++ [apply_func(format_date,
+                                               Args ++ [S])])).
+
 %%------------------------------------------------------------------------------
 %%------------------------------------------------------------------------------
 %% Utility functions
 %% Utility functions
 %%------------------------------------------------------------------------------
 %%------------------------------------------------------------------------------
@@ -822,4 +842,3 @@ all() ->
 
 
 suite() ->
 suite() ->
     [{ct_hooks, [cth_surefire]}, {timetrap, {seconds, 30}}].
     [{ct_hooks, [cth_surefire]}, {timetrap, {seconds, 30}}].
-