From fb0da9848c9134ef4ae3aa74d4eb98835ba5627c Mon Sep 17 00:00:00 2001 From: Andrew Mayorov Date: Tue, 11 Jun 2024 11:42:43 +0200 Subject: [PATCH] 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. --- apps/emqx_utils/src/emqx_template.erl | 35 +++++++++++++++++--- apps/emqx_utils/test/emqx_template_SUITE.erl | 18 ++++++++++ 2 files changed, 48 insertions(+), 5 deletions(-) diff --git a/apps/emqx_utils/src/emqx_template.erl b/apps/emqx_utils/src/emqx_template.erl index 1383f90e1..02e18017e 100644 --- a/apps/emqx_utils/src/emqx_template.erl +++ b/apps/emqx_utils/src/emqx_template.erl @@ -20,6 +20,7 @@ -export([parse/2]). -export([parse_deep/1]). -export([parse_deep/2]). +-export([placeholders/1]). -export([validate/2]). -export([is_const/1]). -export([unparse/1]). @@ -143,14 +144,19 @@ parse_accessor(Var) -> Name 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. %% If the given template contains any variable not in the allowed set, an error %% is returned. -spec validate([varname() | {var_namespace, varname()}], t()) -> ok | {error, [_Error :: {varname(), disallowed}]}. validate(Allowed, Template) -> - {_, Errors} = render(Template, #{}), - {Used, _} = lists:unzip(Errors), + Used = placeholders(Template), case find_disallowed(lists:usort(Used), Allowed) of [] -> ok; @@ -192,10 +198,13 @@ is_allowed(Var, [{var_namespace, VarPrefix} | Allowed]) -> false -> is_allowed(Var, Allowed) 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; -is_allowed(Var, [_ | Allowed]) -> - is_allowed(Var, Allowed). +is_same_varname(V1, V2) -> + V1 =:= V2. %% @doc Check if a template is constant with respect to rendering, i.e. does not %% contain any placeholders. @@ -322,6 +331,22 @@ parse_deep_term(Term, Opts) when is_binary(Term) -> parse_deep_term(Term, _Opts) -> 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) -> maps:fold( fun(KT, VT, {Acc, Errors}) -> diff --git a/apps/emqx_utils/test/emqx_template_SUITE.erl b/apps/emqx_utils/test/emqx_template_SUITE.erl index 0a3273170..a049ebfbc 100644 --- a/apps/emqx_utils/test/emqx_template_SUITE.erl +++ b/apps/emqx_utils/test/emqx_template_SUITE.erl @@ -128,6 +128,14 @@ t_render_custom_bindings(_) -> 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(_) -> TString = <<"a:${a},b:${b},c:$${c},d:{${d.d1}},e:${$}{e},lit:${$}{$}">>, Template = emqx_template:parse(TString), @@ -337,6 +345,16 @@ t_unparse_tmpl_deep(_) -> Template = emqx_template:parse_deep(Term), ?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(_) -> Context = #{d => #{d1 => <<"hi">>}}, Template = emqx_template:parse(<<"d.d1:${d.d1}">>),