From e4b62f3a5fdaea9929f2eff0f5dd63cb819f2328 Mon Sep 17 00:00:00 2001 From: EMQ-YangM Date: Fri, 15 Apr 2022 16:12:39 +0800 Subject: [PATCH] feat: add rule-engine functions --- apps/emqx_rule_engine/src/emqx_rule_date.erl | 210 ++++++++++++++++++ apps/emqx_rule_engine/src/emqx_rule_funcs.erl | 22 ++ .../test/emqx_rule_funcs_SUITE.erl | 21 +- 3 files changed, 252 insertions(+), 1 deletion(-) create mode 100644 apps/emqx_rule_engine/src/emqx_rule_date.erl diff --git a/apps/emqx_rule_engine/src/emqx_rule_date.erl b/apps/emqx_rule_engine/src/emqx_rule_date.erl new file mode 100644 index 000000000..a06571b64 --- /dev/null +++ b/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). diff --git a/apps/emqx_rule_engine/src/emqx_rule_funcs.erl b/apps/emqx_rule_engine/src/emqx_rule_funcs.erl index c06ba1130..e7f5da52f 100644 --- a/apps/emqx_rule_engine/src/emqx_rule_funcs.erl +++ b/apps/emqx_rule_engine/src/emqx_rule_funcs.erl @@ -190,6 +190,9 @@ , rfc3339_to_unix_ts/2 , now_timestamp/0 , now_timestamp/1 + , format_date/3 + , format_date/4 + , date_to_unix_ts/4 ]). %% Proc Dict Func @@ -880,6 +883,25 @@ time_unit(<<"millisecond">>) -> millisecond; time_unit(<<"microsecond">>) -> microsecond; 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. %% Here the emqx_rule_funcs module acts as a proxy, forwarding %% the function handling to the worker module. diff --git a/apps/emqx_rule_engine/test/emqx_rule_funcs_SUITE.erl b/apps/emqx_rule_engine/test/emqx_rule_funcs_SUITE.erl index 846191098..a795e2cda 100644 --- a/apps/emqx_rule_engine/test/emqx_rule_funcs_SUITE.erl +++ b/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)) 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 %%------------------------------------------------------------------------------ @@ -822,4 +842,3 @@ all() -> suite() -> [{ct_hooks, [cth_surefire]}, {timetrap, {seconds, 30}}]. -