Merge pull request #12938 from zmstone/0416-variform-add-iif
feat: add conditions to variform expressions
This commit is contained in:
commit
e40d298752
|
@ -132,8 +132,6 @@
|
|||
|
||||
%% String Funcs
|
||||
-export([
|
||||
coalesce/1,
|
||||
coalesce/2,
|
||||
lower/1,
|
||||
ltrim/1,
|
||||
reverse/1,
|
||||
|
@ -759,10 +757,6 @@ is_array(_) -> false.
|
|||
%% String Funcs
|
||||
%%------------------------------------------------------------------------------
|
||||
|
||||
coalesce(List) -> emqx_variform_bif:coalesce(List).
|
||||
|
||||
coalesce(A, B) -> emqx_variform_bif:coalesce(A, B).
|
||||
|
||||
lower(S) -> emqx_variform_bif:lower(S).
|
||||
|
||||
ltrim(S) -> emqx_variform_bif:ltrim(S).
|
||||
|
|
|
@ -42,14 +42,7 @@
|
|||
M =:= maps)
|
||||
).
|
||||
|
||||
-define(COALESCE_BADARG,
|
||||
throw(#{
|
||||
reason => coalesce_badarg,
|
||||
explain =>
|
||||
"must be an array, or a call to a function which returns an array, "
|
||||
"for example: coalesce([a,b,c]) or coalesce(tokens(var,','))"
|
||||
})
|
||||
).
|
||||
-define(IS_EMPTY(X), (X =:= <<>> orelse X =:= "" orelse X =:= undefined)).
|
||||
|
||||
%% @doc Render a variform expression with bindings.
|
||||
%% A variform expression is a template string which supports variable substitution
|
||||
|
@ -99,6 +92,7 @@ eval_as_string(Expr, Bindings, _Opts) ->
|
|||
return_str(Str) when is_binary(Str) -> Str;
|
||||
return_str(Num) when is_integer(Num) -> integer_to_binary(Num);
|
||||
return_str(Num) when is_float(Num) -> float_to_binary(Num, [{decimals, 10}, compact]);
|
||||
return_str(Atom) when is_atom(Atom) -> atom_to_binary(Atom, utf8);
|
||||
return_str(Other) ->
|
||||
throw(#{
|
||||
reason => bad_return,
|
||||
|
@ -133,6 +127,10 @@ decompile(#{expr := Expression}) ->
|
|||
decompile(Expression) ->
|
||||
Expression.
|
||||
|
||||
eval(Atom, _Bindings, _Opts) when is_atom(Atom) ->
|
||||
%% There is no atom literals in variform,
|
||||
%% but some bif functions such as regex_match may return an atom.
|
||||
atom_to_binary(Atom, utf8);
|
||||
eval({str, Str}, _Bindings, _Opts) ->
|
||||
unicode:characters_to_binary(Str);
|
||||
eval({integer, Num}, _Bindings, _Opts) ->
|
||||
|
@ -145,6 +143,8 @@ eval({call, FuncNameStr, Args}, Bindings, Opts) ->
|
|||
{Mod, Fun} = resolve_func_name(FuncNameStr),
|
||||
ok = assert_func_exported(Mod, Fun, length(Args)),
|
||||
case {Mod, Fun} of
|
||||
{?BIF_MOD, iif} ->
|
||||
eval_iif(Args, Bindings, Opts);
|
||||
{?BIF_MOD, coalesce} ->
|
||||
eval_coalesce(Args, Bindings, Opts);
|
||||
_ ->
|
||||
|
@ -158,19 +158,41 @@ eval_loop([H | T], Bindings, Opts) -> [eval(H, Bindings, Opts) | eval_loop(T, Bi
|
|||
|
||||
%% coalesce treats var_unbound exception as empty string ''
|
||||
eval_coalesce([{array, Args}], Bindings, Opts) ->
|
||||
NewArgs = [lists:map(fun(Arg) -> try_eval(Arg, Bindings, Opts) end, Args)],
|
||||
call(?BIF_MOD, coalesce, NewArgs);
|
||||
%% The input arg is an array
|
||||
eval_coalesce_loop(Args, Bindings, Opts);
|
||||
eval_coalesce([Arg], Bindings, Opts) ->
|
||||
%% Arg is an expression (which is expected to return an array)
|
||||
case try_eval(Arg, Bindings, Opts) of
|
||||
List when is_list(List) ->
|
||||
call(?BIF_MOD, coalesce, List);
|
||||
case lists:dropwhile(fun(I) -> ?IS_EMPTY(I) end, List) of
|
||||
[] ->
|
||||
<<>>;
|
||||
[H | _] ->
|
||||
H
|
||||
end;
|
||||
<<>> ->
|
||||
<<>>;
|
||||
_ ->
|
||||
?COALESCE_BADARG
|
||||
throw(#{
|
||||
reason => coalesce_badarg,
|
||||
explain => "the arg expression did not yield an array"
|
||||
})
|
||||
end;
|
||||
eval_coalesce(_Args, _Bindings, _Opts) ->
|
||||
?COALESCE_BADARG.
|
||||
eval_coalesce(Args, Bindings, Opts) ->
|
||||
%% It also accepts arbitrary number of args
|
||||
%% equivalent to [{array, Args}]
|
||||
eval_coalesce_loop(Args, Bindings, Opts).
|
||||
|
||||
eval_coalesce_loop([], _Bindings, _Opts) ->
|
||||
<<>>;
|
||||
eval_coalesce_loop([Arg | Args], Bindings, Opts) ->
|
||||
Result = try_eval(Arg, Bindings, Opts),
|
||||
case ?IS_EMPTY(Result) of
|
||||
true ->
|
||||
eval_coalesce_loop(Args, Bindings, Opts);
|
||||
false ->
|
||||
Result
|
||||
end.
|
||||
|
||||
try_eval(Arg, Bindings, Opts) ->
|
||||
try
|
||||
|
@ -180,6 +202,21 @@ try_eval(Arg, Bindings, Opts) ->
|
|||
<<>>
|
||||
end.
|
||||
|
||||
eval_iif([Cond, If, Else], Bindings, Opts) ->
|
||||
CondVal = try_eval(Cond, Bindings, Opts),
|
||||
case is_iif_condition_met(CondVal) of
|
||||
true ->
|
||||
eval(If, Bindings, Opts);
|
||||
false ->
|
||||
eval(Else, Bindings, Opts)
|
||||
end.
|
||||
|
||||
%% If iif condition expression yielded boolean, use the boolean value.
|
||||
%% otherwise it's met as long as it's not an empty string.
|
||||
is_iif_condition_met(true) -> true;
|
||||
is_iif_condition_met(false) -> false;
|
||||
is_iif_condition_met(V) -> not ?IS_EMPTY(V).
|
||||
|
||||
%% Some functions accept arbitrary number of arguments but implemented as /1.
|
||||
call(Mod, Fun, Args) ->
|
||||
erlang:apply(Mod, Fun, Args).
|
||||
|
@ -237,6 +274,10 @@ resolve_var_value(VarName, Bindings, _Opts) ->
|
|||
})
|
||||
end.
|
||||
|
||||
assert_func_exported(?BIF_MOD, coalesce, _Arity) ->
|
||||
ok;
|
||||
assert_func_exported(?BIF_MOD, iif, _Arity) ->
|
||||
ok;
|
||||
assert_func_exported(Mod, Fun, Arity) ->
|
||||
ok = try_load(Mod),
|
||||
case erlang:function_exported(Mod, Fun, Arity) of
|
||||
|
|
|
@ -58,9 +58,6 @@
|
|||
%% Array functions
|
||||
-export([nth/2]).
|
||||
|
||||
%% Control functions
|
||||
-export([coalesce/1, coalesce/2]).
|
||||
|
||||
%% Random functions
|
||||
-export([rand_str/1, rand_int/1]).
|
||||
|
||||
|
@ -76,26 +73,16 @@
|
|||
%% Hash functions
|
||||
-export([hash/2, hash_to_range/3, map_to_range/3]).
|
||||
|
||||
-define(IS_EMPTY(X), (X =:= <<>> orelse X =:= "" orelse X =:= undefined)).
|
||||
%% String compare functions
|
||||
-export([str_comp/2, str_eq/2, str_lt/2, str_lte/2, str_gt/2, str_gte/2]).
|
||||
|
||||
%% Number compare functions
|
||||
-export([num_comp/2, num_eq/2, num_lt/2, num_lte/2, num_gt/2, num_gte/2]).
|
||||
|
||||
%%------------------------------------------------------------------------------
|
||||
%% String Funcs
|
||||
%%------------------------------------------------------------------------------
|
||||
|
||||
%% @doc Return the first non-empty string
|
||||
coalesce(A, B) when ?IS_EMPTY(A) andalso ?IS_EMPTY(B) ->
|
||||
<<>>;
|
||||
coalesce(A, B) when ?IS_EMPTY(A) ->
|
||||
B;
|
||||
coalesce(A, _B) ->
|
||||
A.
|
||||
|
||||
%% @doc Return the first non-empty string
|
||||
coalesce([]) ->
|
||||
<<>>;
|
||||
coalesce([H | T]) ->
|
||||
coalesce(H, coalesce(T)).
|
||||
|
||||
lower(S) when is_binary(S) ->
|
||||
string:lowercase(S).
|
||||
|
||||
|
@ -523,3 +510,57 @@ map_to_range(Int, Min, Max) when
|
|||
Min + (Int rem Range);
|
||||
map_to_range(_, _, _) ->
|
||||
throw(#{reason => badarg, function => ?FUNCTION_NAME}).
|
||||
|
||||
compare(A, A) -> eq;
|
||||
compare(A, B) when A < B -> lt;
|
||||
compare(_A, _B) -> gt.
|
||||
|
||||
%% @doc Compare two strings, returns
|
||||
%% - 'eq' if they are the same.
|
||||
%% - 'lt' if arg-1 is ordered before arg-2
|
||||
%% - `gt` if arg-1 is ordered after arg-2
|
||||
str_comp(A0, B0) ->
|
||||
A = any_to_str(A0),
|
||||
B = any_to_str(B0),
|
||||
compare(A, B).
|
||||
|
||||
%% @doc Return 'true' if two strings are the same, otherwise 'false'.
|
||||
str_eq(A, B) -> eq =:= str_comp(A, B).
|
||||
|
||||
%% @doc Return 'true' if arg-1 is ordered before arg-2, otherwise 'false'.
|
||||
str_lt(A, B) -> lt =:= str_comp(A, B).
|
||||
|
||||
%% @doc Return 'true' if arg-1 is ordered after arg-2, otherwise 'false'.
|
||||
str_gt(A, B) -> gt =:= str_comp(A, B).
|
||||
|
||||
%% @doc Return 'true' if arg-1 is not ordered after arg-2, otherwise 'false'.
|
||||
str_lte(A, B) ->
|
||||
R = str_comp(A, B),
|
||||
R =:= lt orelse R =:= eq.
|
||||
|
||||
%% @doc Return 'true' if arg-1 is not ordered bfore arg-2, otherwise 'false'.
|
||||
str_gte(A, B) ->
|
||||
R = str_comp(A, B),
|
||||
R =:= gt orelse R =:= eq.
|
||||
|
||||
num_comp(A, B) when is_number(A) andalso is_number(B) ->
|
||||
compare(A, B).
|
||||
|
||||
%% @doc Return 'true' if two numbers are the same, otherwise 'false'.
|
||||
num_eq(A, B) -> eq =:= num_comp(A, B).
|
||||
|
||||
%% @doc Return 'true' if arg-1 is ordered before arg-2, otherwise 'false'.
|
||||
num_lt(A, B) -> lt =:= num_comp(A, B).
|
||||
|
||||
%% @doc Return 'true' if arg-1 is ordered after arg-2, otherwise 'false'.
|
||||
num_gt(A, B) -> gt =:= num_comp(A, B).
|
||||
|
||||
%% @doc Return 'true' if arg-1 is not ordered after arg-2, otherwise 'false'.
|
||||
num_lte(A, B) ->
|
||||
R = num_comp(A, B),
|
||||
R =:= lt orelse R =:= eq.
|
||||
|
||||
%% @doc Return 'true' if arg-1 is not ordered bfore arg-2, otherwise 'false'.
|
||||
num_gte(A, B) ->
|
||||
R = num_comp(A, B),
|
||||
R =:= gt orelse R =:= eq.
|
||||
|
|
|
@ -151,6 +151,9 @@ coalesce_test_() ->
|
|||
{"arg from other func", fun() ->
|
||||
?assertEqual({ok, <<"b">>}, render("coalesce(tokens(a,','))", #{a => <<",,b,c">>}))
|
||||
end},
|
||||
{"arg from other func, but no result", fun() ->
|
||||
?assertEqual({ok, <<"">>}, render("coalesce(tokens(a,','))", #{a => <<",,,">>}))
|
||||
end},
|
||||
{"var unbound", fun() -> ?assertEqual({ok, <<>>}, render("coalesce(a)", #{})) end},
|
||||
{"var unbound in call", fun() ->
|
||||
?assertEqual({ok, <<>>}, render("coalesce(concat(a))", #{}))
|
||||
|
@ -158,18 +161,70 @@ coalesce_test_() ->
|
|||
{"var unbound in calls", fun() ->
|
||||
?assertEqual({ok, <<"c">>}, render("coalesce([any_to_str(a),any_to_str(b),'c'])", #{}))
|
||||
end},
|
||||
{"badarg", fun() ->
|
||||
?assertMatch(
|
||||
{error, #{reason := coalesce_badarg}}, render("coalesce(a,b)", #{a => 1, b => 2})
|
||||
{"coalesce n-args", fun() ->
|
||||
?assertEqual(
|
||||
{ok, <<"2">>}, render("coalesce(a,b)", #{a => <<"">>, b => 2})
|
||||
)
|
||||
end},
|
||||
{"badarg from return", fun() ->
|
||||
{"coalesce 1-arg", fun() ->
|
||||
?assertMatch(
|
||||
{error, #{reason := coalesce_badarg}}, render("coalesce(any_to_str(a))", #{a => 1})
|
||||
)
|
||||
end}
|
||||
].
|
||||
|
||||
compare_string_test_() ->
|
||||
[
|
||||
%% Testing str_eq/2
|
||||
?_assertEqual({ok, <<"true">>}, render("str_eq('a', 'a')", #{})),
|
||||
?_assertEqual({ok, <<"false">>}, render("str_eq('a', 'b')", #{})),
|
||||
?_assertEqual({ok, <<"true">>}, render("str_eq('', '')", #{})),
|
||||
?_assertEqual({ok, <<"false">>}, render("str_eq('a', '')", #{})),
|
||||
|
||||
%% Testing str_lt/2
|
||||
?_assertEqual({ok, <<"true">>}, render("str_lt('a', 'b')", #{})),
|
||||
?_assertEqual({ok, <<"false">>}, render("str_lt('b', 'a')", #{})),
|
||||
?_assertEqual({ok, <<"false">>}, render("str_lt('a', 'a')", #{})),
|
||||
?_assertEqual({ok, <<"false">>}, render("str_lt('', '')", #{})),
|
||||
|
||||
?_assertEqual({ok, <<"true">>}, render("str_gt('b', 'a')", #{})),
|
||||
?_assertEqual({ok, <<"false">>}, render("str_gt('a', 'b')", #{})),
|
||||
?_assertEqual({ok, <<"false">>}, render("str_gt('a', 'a')", #{})),
|
||||
?_assertEqual({ok, <<"false">>}, render("str_gt('', '')", #{})),
|
||||
|
||||
?_assertEqual({ok, <<"true">>}, render("str_lte('a', 'b')", #{})),
|
||||
?_assertEqual({ok, <<"true">>}, render("str_lte('a', 'a')", #{})),
|
||||
?_assertEqual({ok, <<"false">>}, render("str_lte('b', 'a')", #{})),
|
||||
?_assertEqual({ok, <<"true">>}, render("str_lte('', '')", #{})),
|
||||
|
||||
?_assertEqual({ok, <<"true">>}, render("str_gte('b', 'a')", #{})),
|
||||
?_assertEqual({ok, <<"true">>}, render("str_gte('a', 'a')", #{})),
|
||||
?_assertEqual({ok, <<"false">>}, render("str_gte('a', 'b')", #{})),
|
||||
?_assertEqual({ok, <<"true">>}, render("str_gte('', '')", #{})),
|
||||
|
||||
?_assertEqual({ok, <<"true">>}, render("str_gt(9, 10)", #{}))
|
||||
].
|
||||
|
||||
compare_numbers_test_() ->
|
||||
[
|
||||
?_assertEqual({ok, <<"true">>}, render("num_eq(1, 1)", #{})),
|
||||
?_assertEqual({ok, <<"false">>}, render("num_eq(2, 1)", #{})),
|
||||
|
||||
?_assertEqual({ok, <<"true">>}, render("num_lt(1, 2)", #{})),
|
||||
?_assertEqual({ok, <<"false">>}, render("num_lt(2, 2)", #{})),
|
||||
|
||||
?_assertEqual({ok, <<"true">>}, render("num_gt(2, 1)", #{})),
|
||||
?_assertEqual({ok, <<"false">>}, render("num_gt(1, 1)", #{})),
|
||||
|
||||
?_assertEqual({ok, <<"true">>}, render("num_lte(1, 1)", #{})),
|
||||
?_assertEqual({ok, <<"true">>}, render("num_lte(1, 2)", #{})),
|
||||
?_assertEqual({ok, <<"false">>}, render("num_lte(2, 1)", #{})),
|
||||
|
||||
?_assertEqual({ok, <<"true">>}, render("num_gte(2, -1)", #{})),
|
||||
?_assertEqual({ok, <<"true">>}, render("num_gte(2, 2)", #{})),
|
||||
?_assertEqual({ok, <<"false">>}, render("num_gte(-1, 2)", #{}))
|
||||
].
|
||||
|
||||
syntax_error_test_() ->
|
||||
[
|
||||
{"empty expression", fun() -> ?assertMatch(?SYNTAX_ERROR, render("", #{})) end},
|
||||
|
@ -218,3 +273,16 @@ to_range_badarg_test_() ->
|
|||
?ASSERT_BADARG(map_to_range, "('a','1',2)"),
|
||||
?ASSERT_BADARG(map_to_range, "('a',2,1)")
|
||||
].
|
||||
|
||||
iif_test_() ->
|
||||
%% if clientid has to words separated by a -, take the suffix, and append with `/#`
|
||||
Expr1 = "iif(nth(2,tokens(clientid,'-')),concat([nth(2,tokens(clientid,'-')),'/#']),'')",
|
||||
[
|
||||
?_assertEqual({ok, <<"yes-A">>}, render("iif(a,'yes-A','no-A')", #{a => <<"x">>})),
|
||||
?_assertEqual({ok, <<"no-A">>}, render("iif(a,'yes-A','no-A')", #{})),
|
||||
?_assertEqual({ok, <<"2">>}, render("iif(str_eq(a,1),2,3)", #{a => 1})),
|
||||
?_assertEqual({ok, <<"3">>}, render("iif(str_eq(a,1),2,3)", #{a => <<"not-1">>})),
|
||||
?_assertEqual({ok, <<"3">>}, render("iif(str_eq(a,1),2,3)", #{})),
|
||||
?_assertEqual({ok, <<"">>}, render(Expr1, #{clientid => <<"a">>})),
|
||||
?_assertEqual({ok, <<"suffix/#">>}, render(Expr1, #{clientid => <<"a-suffix">>}))
|
||||
].
|
||||
|
|
Loading…
Reference in New Issue