feat(tpl): add separate `placeholders/1` function

The purpose is to have a clearer view of placeholders used in a
template, without going the usual `render(Template, #{})` route that is
actually subtly misleading: it won't mention that `${}` / `${.}`
placeholder has been used.

Also unify handling of `${}` / `${.}` in a couple of places.
This commit is contained in:
Andrew Mayorov 2024-06-11 11:42:43 +02:00
parent b657dc537c
commit fb0da9848c
No known key found for this signature in database
GPG Key ID: 2837C62ACFBFED5D
2 changed files with 48 additions and 5 deletions

View File

@ -20,6 +20,7 @@
-export([parse/2]). -export([parse/2]).
-export([parse_deep/1]). -export([parse_deep/1]).
-export([parse_deep/2]). -export([parse_deep/2]).
-export([placeholders/1]).
-export([validate/2]). -export([validate/2]).
-export([is_const/1]). -export([is_const/1]).
-export([unparse/1]). -export([unparse/1]).
@ -143,14 +144,19 @@ parse_accessor(Var) ->
Name Name
end. end.
-spec placeholders(t()) -> [varname()].
placeholders(Template) when is_list(Template) ->
[Name || {var, Name, _} <- Template];
placeholders({'$tpl', Template}) ->
placeholders_deep(Template).
%% @doc Validate a template against a set of allowed variables. %% @doc Validate a template against a set of allowed variables.
%% If the given template contains any variable not in the allowed set, an error %% If the given template contains any variable not in the allowed set, an error
%% is returned. %% is returned.
-spec validate([varname() | {var_namespace, varname()}], t()) -> -spec validate([varname() | {var_namespace, varname()}], t()) ->
ok | {error, [_Error :: {varname(), disallowed}]}. ok | {error, [_Error :: {varname(), disallowed}]}.
validate(Allowed, Template) -> validate(Allowed, Template) ->
{_, Errors} = render(Template, #{}), Used = placeholders(Template),
{Used, _} = lists:unzip(Errors),
case find_disallowed(lists:usort(Used), Allowed) of case find_disallowed(lists:usort(Used), Allowed) of
[] -> [] ->
ok; ok;
@ -192,10 +198,13 @@ is_allowed(Var, [{var_namespace, VarPrefix} | Allowed]) ->
false -> false ->
is_allowed(Var, Allowed) is_allowed(Var, Allowed)
end; end;
is_allowed(Var, [Var | _Allowed]) -> is_allowed(Var, [VarAllowed | Rest]) ->
is_same_varname(Var, VarAllowed) orelse is_allowed(Var, Rest).
is_same_varname("", ".") ->
true; true;
is_allowed(Var, [_ | Allowed]) -> is_same_varname(V1, V2) ->
is_allowed(Var, Allowed). V1 =:= V2.
%% @doc Check if a template is constant with respect to rendering, i.e. does not %% @doc Check if a template is constant with respect to rendering, i.e. does not
%% contain any placeholders. %% contain any placeholders.
@ -322,6 +331,22 @@ parse_deep_term(Term, Opts) when is_binary(Term) ->
parse_deep_term(Term, _Opts) -> parse_deep_term(Term, _Opts) ->
Term. Term.
-spec placeholders_deep(deeptpl()) -> [varname()].
placeholders_deep(Template) when is_map(Template) ->
maps:fold(
fun(KT, VT, Acc) -> placeholders_deep(KT) ++ placeholders_deep(VT) ++ Acc end,
[],
Template
);
placeholders_deep({list, Template}) when is_list(Template) ->
lists:flatmap(fun placeholders_deep/1, Template);
placeholders_deep({tuple, Template}) when is_list(Template) ->
lists:flatmap(fun placeholders_deep/1, Template);
placeholders_deep(Template) when is_list(Template) ->
placeholders(Template);
placeholders_deep(_Term) ->
[].
render_deep(Template, Context, Opts) when is_map(Template) -> render_deep(Template, Context, Opts) when is_map(Template) ->
maps:fold( maps:fold(
fun(KT, VT, {Acc, Errors}) -> fun(KT, VT, {Acc, Errors}) ->

View File

@ -128,6 +128,14 @@ t_render_custom_bindings(_) ->
render_string(Template, {?MODULE, []}) render_string(Template, {?MODULE, []})
). ).
t_placeholders(_) ->
TString = <<"a:${a},b:${b},c:$${c},d:{${d.d1}},e:${$}{e},lit:${$}{$}">>,
Template = emqx_template:parse(TString),
?assertEqual(
["a", "b", "c", "d.d1"],
emqx_template:placeholders(Template)
).
t_unparse(_) -> t_unparse(_) ->
TString = <<"a:${a},b:${b},c:$${c},d:{${d.d1}},e:${$}{e},lit:${$}{$}">>, TString = <<"a:${a},b:${b},c:$${c},d:{${d.d1}},e:${$}{e},lit:${$}{$}">>,
Template = emqx_template:parse(TString), Template = emqx_template:parse(TString),
@ -337,6 +345,16 @@ t_unparse_tmpl_deep(_) ->
Template = emqx_template:parse_deep(Term), Template = emqx_template:parse_deep(Term),
?assertEqual(Term, emqx_template:unparse(Template)). ?assertEqual(Term, emqx_template:unparse(Template)).
t_allow_this(_) ->
?assertEqual(
{error, [{"", disallowed}]},
emqx_template:validate(["d"], emqx_template:parse(<<"this:${}">>))
),
?assertEqual(
{error, [{"", disallowed}]},
emqx_template:validate(["d"], emqx_template:parse(<<"this:${.}">>))
).
t_allow_var_by_namespace(_) -> t_allow_var_by_namespace(_) ->
Context = #{d => #{d1 => <<"hi">>}}, Context = #{d => #{d1 => <<"hi">>}},
Template = emqx_template:parse(<<"d.d1:${d.d1}">>), Template = emqx_template:parse(<<"d.d1:${d.d1}">>),