fix(ruleeng): ensure full backward compatibility
This commit is contained in:
parent
75cc663786
commit
69cfa740ea
|
@ -72,8 +72,8 @@ pre_process_action_args(
|
|||
Args#{
|
||||
preprocessed_tmpl => #{
|
||||
topic => emqx_template:parse(Topic),
|
||||
qos => parse_vars(QoS),
|
||||
retain => parse_vars(Retain),
|
||||
qos => parse_simple_var(QoS),
|
||||
retain => parse_simple_var(Retain),
|
||||
payload => parse_payload(Payload),
|
||||
mqtt_properties => parse_mqtt_properties(MQTTProperties),
|
||||
user_properties => parse_user_properties(UserProperties)
|
||||
|
@ -119,8 +119,8 @@ republish(
|
|||
}
|
||||
) ->
|
||||
% NOTE: rendering missing bindings as string "undefined"
|
||||
{TopicString, _Errors1} = emqx_template:render(TopicTemplate, Selected),
|
||||
{PayloadString, _Errors2} = emqx_template:render(PayloadTemplate, Selected),
|
||||
{TopicString, _Errors1} = render_template(TopicTemplate, Selected),
|
||||
{PayloadString, _Errors2} = render_template(PayloadTemplate, Selected),
|
||||
Topic = iolist_to_binary(TopicString),
|
||||
Payload = iolist_to_binary(PayloadString),
|
||||
QoS = render_simple_var(QoSTemplate, Selected, 0),
|
||||
|
@ -201,11 +201,17 @@ safe_publish(RuleId, Topic, QoS, Flags, Payload, PubProps) ->
|
|||
_ = emqx_broker:safe_publish(Msg),
|
||||
emqx_metrics:inc_msg(Msg).
|
||||
|
||||
parse_vars(Data) when is_binary(Data) ->
|
||||
parse_simple_var(Data) when is_binary(Data) ->
|
||||
emqx_template:parse(Data);
|
||||
parse_vars(Data) ->
|
||||
parse_simple_var(Data) ->
|
||||
{const, Data}.
|
||||
|
||||
parse_payload(Payload) ->
|
||||
case string:is_empty(Payload) of
|
||||
false -> emqx_template:parse(Payload);
|
||||
true -> emqx_template:parse("${.}")
|
||||
end.
|
||||
|
||||
parse_mqtt_properties(MQTTPropertiesTemplate) ->
|
||||
maps:map(
|
||||
fun(_Key, V) -> emqx_template:parse(V) end,
|
||||
|
@ -225,8 +231,12 @@ parse_user_properties(_) ->
|
|||
%% invalid, discard
|
||||
undefined.
|
||||
|
||||
render_template(Template, Bindings) ->
|
||||
Opts = #{var_lookup => fun emqx_template:lookup_loose_json/2},
|
||||
emqx_template:render(Template, Bindings, Opts).
|
||||
|
||||
render_simple_var([{var, _Name, Accessor}], Data, Default) ->
|
||||
case emqx_template:lookup_var(Accessor, Data) of
|
||||
case emqx_template:lookup_loose_json(Accessor, Data) of
|
||||
{ok, Var} -> Var;
|
||||
%% cannot find the variable from Data
|
||||
{error, _} -> Default
|
||||
|
@ -234,12 +244,6 @@ render_simple_var([{var, _Name, Accessor}], Data, Default) ->
|
|||
render_simple_var({const, Val}, _Data, _Default) ->
|
||||
Val.
|
||||
|
||||
parse_payload(Payload) ->
|
||||
case string:is_empty(Payload) of
|
||||
false -> emqx_template:parse(Payload);
|
||||
true -> emqx_template:parse("${.}")
|
||||
end.
|
||||
|
||||
render_pub_props(UserPropertiesTemplate, Selected, Env) ->
|
||||
UserProperties =
|
||||
case UserPropertiesTemplate of
|
||||
|
@ -257,26 +261,24 @@ render_mqtt_properties(MQTTPropertiesTemplate, Selected, Env) ->
|
|||
MQTTProperties =
|
||||
maps:fold(
|
||||
fun(K, Template, Acc) ->
|
||||
try
|
||||
V = unicode:characters_to_binary(
|
||||
emqx_template:render_strict(Template, Selected)
|
||||
),
|
||||
Acc#{K => V}
|
||||
catch
|
||||
Kind:Error ->
|
||||
{V, Errors} = render_template(Template, Selected),
|
||||
NAcc = Acc#{K => iolist_to_binary(V)},
|
||||
case Errors of
|
||||
[] ->
|
||||
ok;
|
||||
Errors ->
|
||||
?SLOG(
|
||||
debug,
|
||||
#{
|
||||
msg => "bad_mqtt_property_value_ignored",
|
||||
rule_id => RuleId,
|
||||
exception => Kind,
|
||||
reason => Error,
|
||||
reason => Errors,
|
||||
property => K,
|
||||
selected => Selected
|
||||
}
|
||||
),
|
||||
Acc
|
||||
end
|
||||
)
|
||||
end,
|
||||
NAcc
|
||||
end,
|
||||
#{},
|
||||
MQTTPropertiesTemplate
|
||||
|
|
|
@ -29,6 +29,7 @@
|
|||
-export([render_strict/3]).
|
||||
|
||||
-export([lookup_var/2]).
|
||||
-export([lookup_loose_json/2]).
|
||||
-export([to_string/1]).
|
||||
|
||||
-export_type([t/0]).
|
||||
|
@ -62,16 +63,23 @@
|
|||
-type binding() :: scalar() | list(scalar()) | bindings().
|
||||
-type bindings() :: #{atom() | binary() => binding()}.
|
||||
|
||||
-type reason() :: undefined | {location(), _InvalidType :: atom()}.
|
||||
-type location() :: non_neg_integer().
|
||||
|
||||
-type var_trans() ::
|
||||
fun((Value :: term()) -> unicode:chardata())
|
||||
| fun((varname(), Value :: term()) -> unicode:chardata()).
|
||||
|
||||
-type var_lookup() ::
|
||||
fun((accessor(), bindings()) -> {ok, binding()} | {error, reason()}).
|
||||
|
||||
-type parse_opts() :: #{
|
||||
strip_double_quote => boolean()
|
||||
}.
|
||||
|
||||
-type render_opts() :: #{
|
||||
var_trans => var_trans()
|
||||
var_trans => var_trans(),
|
||||
var_lookup => var_lookup()
|
||||
}.
|
||||
|
||||
-define(PH_VAR_THIS, '$this').
|
||||
|
@ -173,7 +181,7 @@ render_placeholder(Name) ->
|
|||
%% By default, all binding values are converted to strings using `to_string/1`
|
||||
%% function. Option `var_trans` can be used to override this behaviour.
|
||||
-spec render(t(), bindings()) ->
|
||||
{term(), [_Error :: {varname(), undefined}]}.
|
||||
{term(), [_Error :: {varname(), reason()}]}.
|
||||
render(Template, Bindings) ->
|
||||
render(Template, Bindings, #{}).
|
||||
|
||||
|
@ -195,7 +203,7 @@ render({'$tpl', Template}, Bindings, Opts) ->
|
|||
render_deep(Template, Bindings, Opts).
|
||||
|
||||
render_binding(Name, Accessor, Bindings, Opts) ->
|
||||
case lookup_var(Accessor, Bindings) of
|
||||
case lookup_value(Accessor, Bindings, Opts) of
|
||||
{ok, Value} ->
|
||||
{render_value(Name, Value, Opts), []};
|
||||
{error, Reason} ->
|
||||
|
@ -205,6 +213,11 @@ render_binding(Name, Accessor, Bindings, Opts) ->
|
|||
{render_value(Name, undefined, Opts), [{Name, Reason}]}
|
||||
end.
|
||||
|
||||
lookup_value(Accessor, Bindings, #{var_lookup := LookupFun}) ->
|
||||
LookupFun(Accessor, Bindings);
|
||||
lookup_value(Accessor, Bindings, #{}) ->
|
||||
lookup_var(Accessor, Bindings).
|
||||
|
||||
render_value(_Name, Value, #{var_trans := TransFun}) when is_function(TransFun, 1) ->
|
||||
TransFun(Value);
|
||||
render_value(Name, Value, #{var_trans := TransFun}) when is_function(TransFun, 2) ->
|
||||
|
@ -309,17 +322,60 @@ unparse_deep(Term) ->
|
|||
|
||||
%%
|
||||
|
||||
%% @doc Lookup a variable in the bindings accessible through the accessor.
|
||||
%% Lookup is "loose" in the sense that atom and binary keys in the bindings are
|
||||
%% treated equally. This is useful for both hand-crafted and JSON-like bindings.
|
||||
%% This is the default lookup function used by rendering functions.
|
||||
-spec lookup_var(accessor(), bindings()) ->
|
||||
{ok, binding()} | {error, undefined}.
|
||||
lookup_var(Var, Value) when Var == ?PH_VAR_THIS orelse Var == [] ->
|
||||
{ok, binding()} | {error, reason()}.
|
||||
lookup_var(Var, Bindings) ->
|
||||
lookup_var(0, Var, Bindings).
|
||||
|
||||
lookup_var(_, Var, Value) when Var == ?PH_VAR_THIS orelse Var == [] ->
|
||||
{ok, Value};
|
||||
lookup_var([Prop | Rest], Bindings) ->
|
||||
lookup_var(Loc, [Prop | Rest], Bindings) when is_map(Bindings) ->
|
||||
case lookup(Prop, Bindings) of
|
||||
{ok, Value} ->
|
||||
lookup_var(Rest, Value);
|
||||
lookup_var(Loc + 1, Rest, Value);
|
||||
{error, Reason} ->
|
||||
{error, Reason}
|
||||
end.
|
||||
end;
|
||||
lookup_var(Loc, _, Invalid) ->
|
||||
{error, {Loc, type_name(Invalid)}}.
|
||||
|
||||
%% @doc Lookup a variable in the bindings accessible through the accessor.
|
||||
%% Additionally to `lookup_var/2` behavior, this function also tries to parse any
|
||||
%% binary as JSON to a map if accessor needs to go deeper into it.
|
||||
-spec lookup_loose_json(accessor(), bindings() | binary()) ->
|
||||
{ok, binding()} | {error, reason()}.
|
||||
lookup_loose_json(Var, Bindings) ->
|
||||
lookup_loose_json(0, Var, Bindings).
|
||||
|
||||
lookup_loose_json(_, Var, Value) when Var == ?PH_VAR_THIS orelse Var == [] ->
|
||||
{ok, Value};
|
||||
lookup_loose_json(Loc, [Prop | Rest], Bindings) when is_map(Bindings) ->
|
||||
case lookup(Prop, Bindings) of
|
||||
{ok, Value} ->
|
||||
lookup_loose_json(Loc + 1, Rest, Value);
|
||||
{error, Reason} ->
|
||||
{error, Reason}
|
||||
end;
|
||||
lookup_loose_json(Loc, Rest, Json) when is_binary(Json) ->
|
||||
try emqx_utils_json:decode(Json) of
|
||||
Bindings ->
|
||||
% NOTE: This is intentional, we don't want to parse nested JSON.
|
||||
lookup_var(Loc, Rest, Bindings)
|
||||
catch
|
||||
error:_ ->
|
||||
{error, {Loc, binary}}
|
||||
end;
|
||||
lookup_loose_json(Loc, _, Invalid) ->
|
||||
{error, {Loc, type_name(Invalid)}}.
|
||||
|
||||
type_name(Term) when is_atom(Term) -> atom;
|
||||
type_name(Term) when is_number(Term) -> number;
|
||||
type_name(Term) when is_binary(Term) -> binary;
|
||||
type_name(Term) when is_list(Term) -> list.
|
||||
|
||||
-spec lookup(Prop :: binary(), bindings()) ->
|
||||
{ok, binding()} | {error, undefined}.
|
||||
|
|
|
@ -89,15 +89,15 @@ t_render_this(_) ->
|
|||
).
|
||||
|
||||
t_render_missing_bindings(_) ->
|
||||
Bindings = #{no => #{}},
|
||||
Bindings = #{no => #{}, c => #{<<"c1">> => 42}},
|
||||
Template = emqx_template:parse(
|
||||
<<"a:${a},b:${b},c:${c},d:${d.d1},e:${no.such_atom_i_swear}">>
|
||||
<<"a:${a},b:${b},c:${c.c1.c2},d:${d.d1},e:${no.such_atom_i_swear}">>
|
||||
),
|
||||
?assertEqual(
|
||||
{<<"a:undefined,b:undefined,c:undefined,d:undefined,e:undefined">>, [
|
||||
{"no.such_atom_i_swear", undefined},
|
||||
{"d.d1", undefined},
|
||||
{"c", undefined},
|
||||
{"c.c1.c2", {2, number}},
|
||||
{"b", undefined},
|
||||
{"a", undefined}
|
||||
]},
|
||||
|
@ -107,7 +107,7 @@ t_render_missing_bindings(_) ->
|
|||
[
|
||||
{"no.such_atom_i_swear", undefined},
|
||||
{"d.d1", undefined},
|
||||
{"c", undefined},
|
||||
{"c.c1.c2", {2, number}},
|
||||
{"b", undefined},
|
||||
{"a", undefined}
|
||||
],
|
||||
|
|
Loading…
Reference in New Issue