feat(tpl): make escaping mechanism more foolproof
Treat "${$}" as literal "$". This allows to template express strings, for example, of the form "${some_var_value}" where `some_var_value` is interpolated from bindings.
This commit is contained in:
parent
f689d6c233
commit
343b679741
|
@ -133,7 +133,7 @@ handle_disallowed_placeholders(Template, Source) ->
|
|||
allowed => #{placeholders => ?ALLOWED_VARS},
|
||||
notice =>
|
||||
"Disallowed placeholders will be rendered as is."
|
||||
" However, consider using `$${...}` escaping for literal `${...}` where"
|
||||
" However, consider using `${$}` escaping for literal `$` where"
|
||||
" needed to avoid unexpected results."
|
||||
}),
|
||||
Result = prerender_disallowed_placeholders(Template),
|
||||
|
@ -153,7 +153,7 @@ prerender_disallowed_placeholders(Template) ->
|
|||
% parse as a literal string.
|
||||
case lists:member(Name, ?ALLOWED_VARS) of
|
||||
true -> "${" ++ Name ++ "}";
|
||||
false -> "$${" ++ Name ++ "}"
|
||||
false -> "${$}{" ++ Name ++ "}"
|
||||
end
|
||||
end
|
||||
}),
|
||||
|
|
|
@ -136,7 +136,7 @@ handle_disallowed_placeholders(Template, Source, Allowed) ->
|
|||
allowed => #{placeholders => Allowed},
|
||||
notice =>
|
||||
"Disallowed placeholders will be rendered as is."
|
||||
" However, consider using `$${...}` escaping for literal `${...}` where"
|
||||
" However, consider using `${$}` escaping for literal `$` where"
|
||||
" needed to avoid unexpected results."
|
||||
}),
|
||||
Result = prerender_disallowed_placeholders(Template, Allowed),
|
||||
|
@ -156,7 +156,7 @@ prerender_disallowed_placeholders(Template, Allowed) ->
|
|||
% parse as a literal string.
|
||||
case lists:member(Name, Allowed) of
|
||||
true -> "${" ++ Name ++ "}";
|
||||
false -> "$${" ++ Name ++ "}"
|
||||
false -> "${$}{" ++ Name ++ "}"
|
||||
end
|
||||
end
|
||||
}),
|
||||
|
|
|
@ -42,7 +42,7 @@
|
|||
|
||||
-type t() :: str() | {'$tpl', deeptpl()}.
|
||||
|
||||
-type str() :: [unicode:chardata() | placeholder()].
|
||||
-type str() :: [iodata() | byte() | placeholder()].
|
||||
-type deep() :: {'$tpl', deeptpl()}.
|
||||
|
||||
-type deeptpl() ::
|
||||
|
@ -76,7 +76,8 @@
|
|||
var_trans => var_trans()
|
||||
}.
|
||||
|
||||
-define(RE_PLACEHOLDER, "\\$(\\$?)\\{[.]?([a-zA-Z0-9._]*)\\}").
|
||||
-define(RE_PLACEHOLDER, "\\$\\{[.]?([a-zA-Z0-9._]*)\\}").
|
||||
-define(RE_ESCAPE, "\\$\\{(\\$)\\}").
|
||||
|
||||
%% @doc Parse a unicode string into a template.
|
||||
%% String might contain zero or more of placeholders in the form of `${var}`,
|
||||
|
@ -95,22 +96,21 @@ parse(String, Opts) ->
|
|||
RE =
|
||||
case Opts of
|
||||
#{strip_double_quote := true} ->
|
||||
<<"((?|" ?RE_PLACEHOLDER "|\"" ?RE_PLACEHOLDER "\"))">>;
|
||||
<<"((?|" ?RE_PLACEHOLDER "|\"" ?RE_PLACEHOLDER "\")|" ?RE_ESCAPE ")">>;
|
||||
#{} ->
|
||||
<<"(" ?RE_PLACEHOLDER ")">>
|
||||
<<"(" ?RE_PLACEHOLDER "|" ?RE_ESCAPE ")">>
|
||||
end,
|
||||
Splits = re:split(String, RE, [{return, binary}, group, trim, unicode]),
|
||||
Components = lists:flatmap(fun parse_split/1, Splits),
|
||||
Components.
|
||||
|
||||
parse_split([Part, _PH, <<>>, Var]) ->
|
||||
parse_split([Part, _PH, Var, <<>>]) ->
|
||||
% Regular placeholder
|
||||
prepend(Part, [{var, unicode:characters_to_list(Var), parse_accessor(Var)}]);
|
||||
parse_split([Part, _PH = <<B1, $$, Rest/binary>>, <<"$">>, _]) ->
|
||||
% Escaped literal, take all but the second byte, which is always `$`.
|
||||
% Important to make a whole token starting with `$` so the `unparse/11`
|
||||
% function can distinguish escaped literals.
|
||||
prepend(Part, [<<B1, Rest/binary>>]);
|
||||
parse_split([Part, _Escape, <<>>, <<"$">>]) ->
|
||||
% Escaped literal `$`.
|
||||
% Use single char as token so the `unparse/1` function can distinguish escaped `$`.
|
||||
prepend(Part, [$$]);
|
||||
parse_split([Tail]) ->
|
||||
[Tail].
|
||||
|
||||
|
@ -159,8 +159,8 @@ unparse(Template) ->
|
|||
|
||||
unparse_part({var, Name, _Accessor}) ->
|
||||
render_placeholder(Name);
|
||||
unparse_part(Part = <<"${", _/binary>>) ->
|
||||
<<"$", Part/binary>>;
|
||||
unparse_part($$) ->
|
||||
<<"${$}">>;
|
||||
unparse_part(Part) ->
|
||||
Part.
|
||||
|
||||
|
|
|
@ -115,7 +115,7 @@ t_render_missing_bindings(_) ->
|
|||
).
|
||||
|
||||
t_unparse(_) ->
|
||||
TString = <<"a:${a},b:${b},c:$${c},d:{${d.d1}}">>,
|
||||
TString = <<"a:${a},b:${b},c:$${c},d:{${d.d1}},e:${$}{e},lit:${$}{$}">>,
|
||||
Template = emqx_connector_template:parse(TString),
|
||||
?assertEqual(
|
||||
TString,
|
||||
|
@ -129,12 +129,14 @@ t_const(_) ->
|
|||
),
|
||||
?assertEqual(
|
||||
false,
|
||||
emqx_connector_template:is_const(emqx_connector_template:parse(<<"a:${a},b:${b},c:$${c}">>))
|
||||
emqx_connector_template:is_const(
|
||||
emqx_connector_template:parse(<<"a:${a},b:${b},c:${$}{c}">>)
|
||||
)
|
||||
),
|
||||
?assertEqual(
|
||||
true,
|
||||
emqx_connector_template:is_const(
|
||||
emqx_connector_template:parse(<<"a:$${a},b:$${b},c:$${c}">>)
|
||||
emqx_connector_template:parse(<<"a:${$}{a},b:${$}{b}">>)
|
||||
)
|
||||
).
|
||||
|
||||
|
@ -147,16 +149,16 @@ t_render_partial_ph(_) ->
|
|||
).
|
||||
|
||||
t_parse_escaped(_) ->
|
||||
Bindings = #{a => <<"1">>, b => 1},
|
||||
Template = emqx_connector_template:parse(<<"a:${a},b:$${b}">>),
|
||||
Bindings = #{a => <<"1">>, b => 1, c => "VAR"},
|
||||
Template = emqx_connector_template:parse(<<"a:${a},b:${$}{b},c:${$}{${c}},lit:${$}{$}">>),
|
||||
?assertEqual(
|
||||
<<"a:1,b:${b}">>,
|
||||
<<"a:1,b:${b},c:${VAR},lit:${$}">>,
|
||||
render_strict_string(Template, Bindings)
|
||||
).
|
||||
|
||||
t_parse_escaped_dquote(_) ->
|
||||
Bindings = #{a => <<"1">>, b => 1},
|
||||
Template = emqx_connector_template:parse(<<"a:\"${a}\",b:\"$${b}\"">>, #{
|
||||
Template = emqx_connector_template:parse(<<"a:\"${a}\",b:\"${$}{b}\"">>, #{
|
||||
strip_double_quote => true
|
||||
}),
|
||||
?assertEqual(
|
||||
|
@ -299,7 +301,7 @@ t_render_tmpl_deep(_) ->
|
|||
Bindings = #{a => <<"1">>, b => 1, c => 1.0, d => #{d1 => <<"hi">>}},
|
||||
|
||||
Template = emqx_connector_template:parse_deep(
|
||||
#{<<"${a}">> => [<<"${b}">>, "c", 2, 3.0, '${d}', {[<<"${c}">>, <<"$${d}">>], 0}]}
|
||||
#{<<"${a}">> => [<<"$${b}">>, "c", 2, 3.0, '${d}', {[<<"${c}">>, <<"${$}{d}">>], 0}]}
|
||||
),
|
||||
|
||||
?assertEqual(
|
||||
|
@ -308,12 +310,12 @@ t_render_tmpl_deep(_) ->
|
|||
),
|
||||
|
||||
?assertEqual(
|
||||
#{<<"1">> => [<<"1">>, "c", 2, 3.0, '${d}', {[<<"1.0">>, <<"${d}">>], 0}]},
|
||||
#{<<"1">> => [<<"$1">>, "c", 2, 3.0, '${d}', {[<<"1.0">>, <<"${d}">>], 0}]},
|
||||
emqx_connector_template:render_strict(Template, Bindings)
|
||||
).
|
||||
|
||||
t_unparse_tmpl_deep(_) ->
|
||||
Term = #{<<"${a}">> => [<<"$${b}">>, "c", 2, 3.0, '${d}', {[<<"${c}">>], 0}]},
|
||||
Term = #{<<"${a}">> => [<<"$${b}">>, "c", 2, 3.0, '${d}', {[<<"${c}">>], <<"${$}{d}">>, 0}]},
|
||||
Template = emqx_connector_template:parse_deep(Term),
|
||||
?assertEqual(Term, emqx_connector_template:unparse(Template)).
|
||||
|
||||
|
|
Loading…
Reference in New Issue