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
|
%% String Funcs
|
||||||
-export([
|
-export([
|
||||||
coalesce/1,
|
|
||||||
coalesce/2,
|
|
||||||
lower/1,
|
lower/1,
|
||||||
ltrim/1,
|
ltrim/1,
|
||||||
reverse/1,
|
reverse/1,
|
||||||
|
@ -759,10 +757,6 @@ is_array(_) -> false.
|
||||||
%% String Funcs
|
%% 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).
|
lower(S) -> emqx_variform_bif:lower(S).
|
||||||
|
|
||||||
ltrim(S) -> emqx_variform_bif:ltrim(S).
|
ltrim(S) -> emqx_variform_bif:ltrim(S).
|
||||||
|
|
|
@ -42,14 +42,7 @@
|
||||||
M =:= maps)
|
M =:= maps)
|
||||||
).
|
).
|
||||||
|
|
||||||
-define(COALESCE_BADARG,
|
-define(IS_EMPTY(X), (X =:= <<>> orelse X =:= "" orelse X =:= undefined)).
|
||||||
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,','))"
|
|
||||||
})
|
|
||||||
).
|
|
||||||
|
|
||||||
%% @doc Render a variform expression with bindings.
|
%% @doc Render a variform expression with bindings.
|
||||||
%% A variform expression is a template string which supports variable substitution
|
%% 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(Str) when is_binary(Str) -> Str;
|
||||||
return_str(Num) when is_integer(Num) -> integer_to_binary(Num);
|
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(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) ->
|
return_str(Other) ->
|
||||||
throw(#{
|
throw(#{
|
||||||
reason => bad_return,
|
reason => bad_return,
|
||||||
|
@ -133,6 +127,10 @@ decompile(#{expr := Expression}) ->
|
||||||
decompile(Expression) ->
|
decompile(Expression) ->
|
||||||
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) ->
|
eval({str, Str}, _Bindings, _Opts) ->
|
||||||
unicode:characters_to_binary(Str);
|
unicode:characters_to_binary(Str);
|
||||||
eval({integer, Num}, _Bindings, _Opts) ->
|
eval({integer, Num}, _Bindings, _Opts) ->
|
||||||
|
@ -145,6 +143,8 @@ eval({call, FuncNameStr, Args}, Bindings, Opts) ->
|
||||||
{Mod, Fun} = resolve_func_name(FuncNameStr),
|
{Mod, Fun} = resolve_func_name(FuncNameStr),
|
||||||
ok = assert_func_exported(Mod, Fun, length(Args)),
|
ok = assert_func_exported(Mod, Fun, length(Args)),
|
||||||
case {Mod, Fun} of
|
case {Mod, Fun} of
|
||||||
|
{?BIF_MOD, iif} ->
|
||||||
|
eval_iif(Args, Bindings, Opts);
|
||||||
{?BIF_MOD, coalesce} ->
|
{?BIF_MOD, coalesce} ->
|
||||||
eval_coalesce(Args, Bindings, Opts);
|
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 ''
|
%% coalesce treats var_unbound exception as empty string ''
|
||||||
eval_coalesce([{array, Args}], Bindings, Opts) ->
|
eval_coalesce([{array, Args}], Bindings, Opts) ->
|
||||||
NewArgs = [lists:map(fun(Arg) -> try_eval(Arg, Bindings, Opts) end, Args)],
|
%% The input arg is an array
|
||||||
call(?BIF_MOD, coalesce, NewArgs);
|
eval_coalesce_loop(Args, Bindings, Opts);
|
||||||
eval_coalesce([Arg], 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
|
case try_eval(Arg, Bindings, Opts) of
|
||||||
List when is_list(List) ->
|
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;
|
end;
|
||||||
eval_coalesce(_Args, _Bindings, _Opts) ->
|
eval_coalesce(Args, Bindings, Opts) ->
|
||||||
?COALESCE_BADARG.
|
%% 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_eval(Arg, Bindings, Opts) ->
|
||||||
try
|
try
|
||||||
|
@ -180,6 +202,21 @@ try_eval(Arg, Bindings, Opts) ->
|
||||||
<<>>
|
<<>>
|
||||||
end.
|
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.
|
%% Some functions accept arbitrary number of arguments but implemented as /1.
|
||||||
call(Mod, Fun, Args) ->
|
call(Mod, Fun, Args) ->
|
||||||
erlang:apply(Mod, Fun, Args).
|
erlang:apply(Mod, Fun, Args).
|
||||||
|
@ -237,6 +274,10 @@ resolve_var_value(VarName, Bindings, _Opts) ->
|
||||||
})
|
})
|
||||||
end.
|
end.
|
||||||
|
|
||||||
|
assert_func_exported(?BIF_MOD, coalesce, _Arity) ->
|
||||||
|
ok;
|
||||||
|
assert_func_exported(?BIF_MOD, iif, _Arity) ->
|
||||||
|
ok;
|
||||||
assert_func_exported(Mod, Fun, Arity) ->
|
assert_func_exported(Mod, Fun, Arity) ->
|
||||||
ok = try_load(Mod),
|
ok = try_load(Mod),
|
||||||
case erlang:function_exported(Mod, Fun, Arity) of
|
case erlang:function_exported(Mod, Fun, Arity) of
|
||||||
|
|
|
@ -58,9 +58,6 @@
|
||||||
%% Array functions
|
%% Array functions
|
||||||
-export([nth/2]).
|
-export([nth/2]).
|
||||||
|
|
||||||
%% Control functions
|
|
||||||
-export([coalesce/1, coalesce/2]).
|
|
||||||
|
|
||||||
%% Random functions
|
%% Random functions
|
||||||
-export([rand_str/1, rand_int/1]).
|
-export([rand_str/1, rand_int/1]).
|
||||||
|
|
||||||
|
@ -76,26 +73,16 @@
|
||||||
%% Hash functions
|
%% Hash functions
|
||||||
-export([hash/2, hash_to_range/3, map_to_range/3]).
|
-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
|
%% 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) ->
|
lower(S) when is_binary(S) ->
|
||||||
string:lowercase(S).
|
string:lowercase(S).
|
||||||
|
|
||||||
|
@ -523,3 +510,57 @@ map_to_range(Int, Min, Max) when
|
||||||
Min + (Int rem Range);
|
Min + (Int rem Range);
|
||||||
map_to_range(_, _, _) ->
|
map_to_range(_, _, _) ->
|
||||||
throw(#{reason => badarg, function => ?FUNCTION_NAME}).
|
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() ->
|
{"arg from other func", fun() ->
|
||||||
?assertEqual({ok, <<"b">>}, render("coalesce(tokens(a,','))", #{a => <<",,b,c">>}))
|
?assertEqual({ok, <<"b">>}, render("coalesce(tokens(a,','))", #{a => <<",,b,c">>}))
|
||||||
end},
|
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", fun() -> ?assertEqual({ok, <<>>}, render("coalesce(a)", #{})) end},
|
||||||
{"var unbound in call", fun() ->
|
{"var unbound in call", fun() ->
|
||||||
?assertEqual({ok, <<>>}, render("coalesce(concat(a))", #{}))
|
?assertEqual({ok, <<>>}, render("coalesce(concat(a))", #{}))
|
||||||
|
@ -158,18 +161,70 @@ coalesce_test_() ->
|
||||||
{"var unbound in calls", fun() ->
|
{"var unbound in calls", fun() ->
|
||||||
?assertEqual({ok, <<"c">>}, render("coalesce([any_to_str(a),any_to_str(b),'c'])", #{}))
|
?assertEqual({ok, <<"c">>}, render("coalesce([any_to_str(a),any_to_str(b),'c'])", #{}))
|
||||||
end},
|
end},
|
||||||
{"badarg", fun() ->
|
{"coalesce n-args", fun() ->
|
||||||
?assertMatch(
|
?assertEqual(
|
||||||
{error, #{reason := coalesce_badarg}}, render("coalesce(a,b)", #{a => 1, b => 2})
|
{ok, <<"2">>}, render("coalesce(a,b)", #{a => <<"">>, b => 2})
|
||||||
)
|
)
|
||||||
end},
|
end},
|
||||||
{"badarg from return", fun() ->
|
{"coalesce 1-arg", fun() ->
|
||||||
?assertMatch(
|
?assertMatch(
|
||||||
{error, #{reason := coalesce_badarg}}, render("coalesce(any_to_str(a))", #{a => 1})
|
{error, #{reason := coalesce_badarg}}, render("coalesce(any_to_str(a))", #{a => 1})
|
||||||
)
|
)
|
||||||
end}
|
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_() ->
|
syntax_error_test_() ->
|
||||||
[
|
[
|
||||||
{"empty expression", fun() -> ?assertMatch(?SYNTAX_ERROR, render("", #{})) end},
|
{"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','1',2)"),
|
||||||
?ASSERT_BADARG(map_to_range, "('a',2,1)")
|
?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