| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372 |
- %%--------------------------------------------------------------------
- %% Copyright (c) 2020-2024 EMQ Technologies Co., Ltd. All Rights Reserved.
- %%
- %% Licensed under the Apache License, Version 2.0 (the "License");
- %% you may not use this file except in compliance with the License.
- %% You may obtain a copy of the License at
- %%
- %% http://www.apache.org/licenses/LICENSE-2.0
- %%
- %% Unless required by applicable law or agreed to in writing, software
- %% distributed under the License is distributed on an "AS IS" BASIS,
- %% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- %% See the License for the specific language governing permissions and
- %% limitations under the License.
- %%--------------------------------------------------------------------
- -module(emqx_template_SUITE).
- -compile(export_all).
- -compile(nowarn_export_all).
- -include_lib("emqx/include/emqx_placeholder.hrl").
- -include_lib("eunit/include/eunit.hrl").
- all() -> emqx_common_test_helpers:all(?MODULE).
- t_render(_) ->
- Context = #{
- a => <<"1">>,
- b => 1,
- c => 1.0,
- d => #{<<"d1">> => <<"hi">>},
- l => [0, 1, 1000],
- u => "utf-8 is ǝɹǝɥ"
- },
- Template = emqx_template:parse(
- <<"a:${a},b:${b},c:${c},d:${d},d1:${d.d1},l:${l},u:${u}">>
- ),
- ?assertEqual(
- {<<"a:1,b:1,c:1.0,d:{\"d1\":\"hi\"},d1:hi,l:[0,1,1000],u:utf-8 is ǝɹǝɥ"/utf8>>, []},
- render_string(Template, Context)
- ).
- t_render_var_trans(_) ->
- Context = #{a => <<"1">>, b => 1, c => #{prop => 1.0}},
- Template = emqx_template:parse(<<"a:${a},b:${b},c:${c.prop}">>),
- {String, Errors} = emqx_template:render(
- Template,
- Context,
- #{var_trans => fun(Name, _) -> "<" ++ Name ++ ">" end}
- ),
- ?assertEqual(
- {<<"a:<a>,b:<b>,c:<c.prop>">>, []},
- {bin(String), Errors}
- ).
- t_render_path(_) ->
- Context = #{d => #{d1 => <<"hi">>}},
- Template = emqx_template:parse(<<"d.d1:${d.d1}">>),
- ?assertEqual(
- ok,
- emqx_template:validate(["d.d1"], Template)
- ),
- ?assertEqual(
- {<<"d.d1:hi">>, []},
- render_string(Template, Context)
- ).
- t_render_custom_ph(_) ->
- Context = #{a => <<"a">>, b => <<"b">>},
- Template = emqx_template:parse(<<"a:${a},b:${b}">>),
- ?assertEqual(
- {error, [{"b", disallowed}]},
- emqx_template:validate(["a"], Template)
- ),
- ?assertEqual(
- <<"a:a,b:b">>,
- render_strict_string(Template, Context)
- ).
- t_render_this(_) ->
- Context = #{a => <<"a">>, b => [1, 2, 3]},
- Template = emqx_template:parse(<<"this:${} / also:${.}">>),
- ?assertEqual(ok, emqx_template:validate(["."], Template)),
- ?assertEqual(
- % NOTE: order of the keys in the JSON object depends on the JSON encoder
- <<"this:{\"b\":[1,2,3],\"a\":\"a\"} / also:{\"b\":[1,2,3],\"a\":\"a\"}">>,
- render_strict_string(Template, Context)
- ).
- t_render_missing_bindings(_) ->
- Context = #{no => #{}, c => #{<<"c1">> => 42}},
- Template = emqx_template:parse(
- <<"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.c1.c2", {2, number}},
- {"b", undefined},
- {"a", undefined}
- ]},
- render_string(Template, Context)
- ),
- ?assertError(
- [
- {"no.such_atom_i_swear", undefined},
- {"d.d1", undefined},
- {"c.c1.c2", {2, number}},
- {"b", undefined},
- {"a", undefined}
- ],
- render_strict_string(Template, Context)
- ).
- t_render_custom_bindings(_) ->
- _ = erlang:put(a, <<"foo">>),
- _ = erlang:put(b, #{<<"bar">> => #{atom => 42}}),
- Template = emqx_template:parse(
- <<"a:${a},b:${b.bar.atom},c:${c},oops:${b.bar.atom.oops}">>
- ),
- ?assertEqual(
- {<<"a:foo,b:42,c:undefined,oops:undefined">>, [
- {"b.bar.atom.oops", {2, number}},
- {"c", undefined}
- ]},
- render_string(Template, {?MODULE, []})
- ).
- t_unparse(_) ->
- TString = <<"a:${a},b:${b},c:$${c},d:{${d.d1}},e:${$}{e},lit:${$}{$}">>,
- Template = emqx_template:parse(TString),
- ?assertEqual(
- TString,
- unicode:characters_to_binary(emqx_template:unparse(Template))
- ).
- t_const(_) ->
- ?assertEqual(
- true,
- emqx_template:is_const(emqx_template:parse(<<"">>))
- ),
- ?assertEqual(
- false,
- emqx_template:is_const(
- emqx_template:parse(<<"a:${a},b:${b},c:${$}{c}">>)
- )
- ),
- ?assertEqual(
- true,
- emqx_template:is_const(
- emqx_template:parse(<<"a:${$}{a},b:${$}{b}">>)
- )
- ).
- t_render_partial_ph(_) ->
- Context = #{a => <<"1">>, b => 1, c => 1.0, d => #{d1 => <<"hi">>}},
- Template = emqx_template:parse(<<"a:$a,b:b},c:{c},d:${d">>),
- ?assertEqual(
- <<"a:$a,b:b},c:{c},d:${d">>,
- render_strict_string(Template, Context)
- ).
- t_parse_escaped(_) ->
- Context = #{a => <<"1">>, b => 1, c => "VAR"},
- Template = emqx_template:parse(<<"a:${a},b:${$}{b},c:${$}{${c}},lit:${$}{$}">>),
- ?assertEqual(
- <<"a:1,b:${b},c:${VAR},lit:${$}">>,
- render_strict_string(Template, Context)
- ).
- t_parse_escaped_dquote(_) ->
- Context = #{a => <<"1">>, b => 1},
- Template = emqx_template:parse(<<"a:\"${a}\",b:\"${$}{b}\"">>, #{
- strip_double_quote => true
- }),
- ?assertEqual(
- <<"a:1,b:\"${b}\"">>,
- render_strict_string(Template, Context)
- ).
- t_parse_sql_prepstmt(_) ->
- Context = #{a => <<"1">>, b => 1, c => 1.0, d => #{d1 => <<"hi">>}},
- {PrepareStatement, RowTemplate} =
- emqx_template_sql:parse_prepstmt(<<"a:${a},b:${b},c:${c},d:${d}">>, #{
- parameters => '?'
- }),
- ?assertEqual(<<"a:?,b:?,c:?,d:?">>, bin(PrepareStatement)),
- ?assertEqual(
- {[<<"1">>, 1, 1.0, <<"{\"d1\":\"hi\"}">>], _Errors = []},
- emqx_template_sql:render_prepstmt(RowTemplate, Context)
- ).
- t_parse_sql_prepstmt_n(_) ->
- Context = #{a => undefined, b => true, c => atom, d => #{d1 => 42.1337}},
- {PrepareStatement, RowTemplate} =
- emqx_template_sql:parse_prepstmt(<<"a:${a},b:${b},c:${c},d:${d}">>, #{
- parameters => '$n'
- }),
- ?assertEqual(<<"a:$1,b:$2,c:$3,d:$4">>, bin(PrepareStatement)),
- ?assertEqual(
- [null, true, <<"atom">>, <<"{\"d1\":42.1337}">>],
- emqx_template_sql:render_prepstmt_strict(RowTemplate, Context)
- ).
- t_parse_sql_prepstmt_colon(_) ->
- {PrepareStatement, _RowTemplate} =
- emqx_template_sql:parse_prepstmt(<<"a=${a},b=${b},c=${c},d=${d}">>, #{
- parameters => ':n'
- }),
- ?assertEqual(<<"a=:1,b=:2,c=:3,d=:4">>, bin(PrepareStatement)).
- t_parse_sql_prepstmt_partial_ph(_) ->
- Context = #{a => <<"1">>, b => 1, c => 1.0, d => #{d1 => <<"hi">>}},
- {PrepareStatement, RowTemplate} =
- emqx_template_sql:parse_prepstmt(<<"a:$a,b:b},c:{c},d:${d">>, #{parameters => '?'}),
- ?assertEqual(<<"a:$a,b:b},c:{c},d:${d">>, bin(PrepareStatement)),
- ?assertEqual([], emqx_template_sql:render_prepstmt_strict(RowTemplate, Context)).
- t_render_sql(_) ->
- Context = #{
- a => <<"1">>,
- b => 1,
- c => 1.0,
- d => #{d1 => <<"hi">>},
- n => undefined,
- u => "utf8's cool 🐸"
- },
- Template = emqx_template:parse(<<"a:${a},b:${b},c:${c},d:${d},n:${n},u:${u}">>),
- ?assertMatch(
- {_String, _Errors = []},
- emqx_template_sql:render(Template, Context, #{})
- ),
- ?assertEqual(
- <<"a:'1',b:1,c:1.0,d:'{\"d1\":\"hi\"}',n:NULL,u:'utf8\\'s cool 🐸'"/utf8>>,
- bin(emqx_template_sql:render_strict(Template, Context, #{}))
- ),
- ?assertEqual(
- <<"a:'1',b:1,c:1.0,d:'{\"d1\":\"hi\"}',n:'undefined',u:'utf8\\'s cool 🐸'"/utf8>>,
- bin(emqx_template_sql:render_strict(Template, Context, #{undefined => "undefined"}))
- ).
- t_render_mysql(_) ->
- %% with apostrophes
- %% https://github.com/emqx/emqx/issues/4135
- Context = #{
- a => <<"1''2">>,
- b => 1,
- c => 1.0,
- d => #{d1 => <<"someone's phone">>},
- e => <<$\\, 0, "💩"/utf8>>,
- f => <<"non-utf8", 16#DCC900:24>>,
- g => "utf8's cool 🐸",
- h => imgood
- },
- Template = emqx_template_sql:parse(
- <<"a:${a},b:${b},c:${c},d:${d},e:${e},f:${f},g:${g},h:${h}">>
- ),
- ?assertEqual(
- <<
- "a:'1\\'\\'2',b:1,c:1.0,d:'{\"d1\":\"someone\\'s phone\"}',"
- "e:'\\\\\\0💩',f:0x6E6F6E2D75746638DCC900,g:'utf8\\'s cool 🐸',"/utf8,
- "h:'imgood'"
- >>,
- bin(emqx_template_sql:render_strict(Template, Context, #{escaping => mysql}))
- ).
- t_render_cql(_) ->
- %% with apostrophes for cassandra
- %% https://github.com/emqx/emqx/issues/4148
- Context = #{
- a => <<"1''2">>,
- b => 1,
- c => 1.0,
- d => #{d1 => <<"someone's phone">>}
- },
- Template = emqx_template:parse(<<"a:${a},b:${b},c:${c},d:${d}">>),
- ?assertEqual(
- <<"a:'1''''2',b:1,c:1.0,d:'{\"d1\":\"someone''s phone\"}'">>,
- bin(emqx_template_sql:render_strict(Template, Context, #{escaping => cql}))
- ).
- t_render_sql_custom_ph(_) ->
- {PrepareStatement, RowTemplate} =
- emqx_template_sql:parse_prepstmt(<<"a:${a},b:${b.c}">>, #{parameters => '$n'}),
- ?assertEqual(
- {error, [{"b.c", disallowed}]},
- emqx_template:validate(["a"], RowTemplate)
- ),
- ?assertEqual(<<"a:$1,b:$2">>, bin(PrepareStatement)).
- t_render_sql_strip_double_quote(_) ->
- Context = #{a => <<"a">>, b => <<"b">>},
- %% no strip_double_quote option: "${key}" -> "value"
- {PrepareStatement1, RowTemplate1} = emqx_template_sql:parse_prepstmt(
- <<"a:\"${a}\",b:\"${b}\"">>,
- #{parameters => '$n'}
- ),
- ?assertEqual(<<"a:\"$1\",b:\"$2\"">>, bin(PrepareStatement1)),
- ?assertEqual(
- [<<"a">>, <<"b">>],
- emqx_template_sql:render_prepstmt_strict(RowTemplate1, Context)
- ),
- %% strip_double_quote = true: "${key}" -> value
- {PrepareStatement2, RowTemplate2} = emqx_template_sql:parse_prepstmt(
- <<"a:\"${a}\",b:\"${b}\"">>,
- #{parameters => '$n', strip_double_quote => true}
- ),
- ?assertEqual(<<"a:$1,b:$2">>, bin(PrepareStatement2)),
- ?assertEqual(
- [<<"a">>, <<"b">>],
- emqx_template_sql:render_prepstmt_strict(RowTemplate2, Context)
- ).
- t_render_tmpl_deep(_) ->
- Context = #{a => <<"1">>, b => 1, c => 1.0, d => #{d1 => <<"hi">>}},
- Template = emqx_template:parse_deep(
- #{<<"${a}">> => [<<"$${b}">>, "c", 2, 3.0, '${d}', {[<<"${c}">>, <<"${$}{d}">>], 0}]}
- ),
- ?assertEqual(
- {error, [{V, disallowed} || V <- ["b", "c"]]},
- emqx_template:validate(["a"], Template)
- ),
- ?assertEqual(
- #{<<"1">> => [<<"$1">>, "c", 2, 3.0, '${d}', {[<<"1.0">>, <<"${d}">>], 0}]},
- emqx_template:render_strict(Template, Context)
- ).
- t_unparse_tmpl_deep(_) ->
- Term = #{<<"${a}">> => [<<"$${b}">>, "c", 2, 3.0, '${d}', {[<<"${c}">>], <<"${$}{d}">>, 0}]},
- Template = emqx_template:parse_deep(Term),
- ?assertEqual(Term, emqx_template:unparse(Template)).
- t_allow_var_by_namespace(_) ->
- Context = #{d => #{d1 => <<"hi">>}},
- Template = emqx_template:parse(<<"d.d1:${d.d1}">>),
- ?assertEqual(
- ok,
- emqx_template:validate([{var_namespace, "d"}], Template)
- ),
- ?assertEqual(
- {<<"d.d1:hi">>, []},
- render_string(Template, Context)
- ).
- %%
- render_string(Template, Context) ->
- {String, Errors} = emqx_template:render(Template, Context),
- {bin(String), Errors}.
- render_strict_string(Template, Context) ->
- bin(emqx_template:render_strict(Template, Context)).
- bin(String) ->
- unicode:characters_to_binary(String).
- %% Access module API
- lookup([], _) ->
- {error, undefined};
- lookup([Prop | Rest], _) ->
- case erlang:get(binary_to_atom(Prop)) of
- undefined -> {error, undefined};
- Value -> emqx_template:lookup_var(Rest, Value)
- end.
|