feat(tpl): factor out loose json concept into a separate module
Which is called `emqx_jsonish`. Also introduce an _access module_ abstraction to extract information from such data during rendering.
This commit is contained in:
parent
69cfa740ea
commit
02c1bd70b6
|
@ -232,11 +232,10 @@ parse_user_properties(_) ->
|
|||
undefined.
|
||||
|
||||
render_template(Template, Bindings) ->
|
||||
Opts = #{var_lookup => fun emqx_template:lookup_loose_json/2},
|
||||
emqx_template:render(Template, Bindings, Opts).
|
||||
emqx_template:render(Template, {emqx_jsonish, Bindings}).
|
||||
|
||||
render_simple_var([{var, _Name, Accessor}], Data, Default) ->
|
||||
case emqx_template:lookup_loose_json(Accessor, Data) of
|
||||
case emqx_jsonish:lookup(Accessor, Data) of
|
||||
{ok, Var} -> Var;
|
||||
%% cannot find the variable from Data
|
||||
{error, _} -> Default
|
||||
|
|
|
@ -0,0 +1,71 @@
|
|||
%%--------------------------------------------------------------------
|
||||
%% Copyright (c) 2020-2022 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_jsonish).
|
||||
|
||||
-export([lookup/2]).
|
||||
|
||||
-export_type([t/0]).
|
||||
|
||||
%% @doc Either a map or a JSON serial.
|
||||
%% Think of it as a kind of lazily parsed and/or constructed JSON.
|
||||
-type t() :: propmap() | serial().
|
||||
|
||||
%% @doc JSON in serialized form.
|
||||
-type serial() :: binary().
|
||||
|
||||
-type propmap() :: #{prop() => value()}.
|
||||
-type prop() :: atom() | binary().
|
||||
-type value() :: scalar() | [scalar() | propmap()] | t().
|
||||
-type scalar() :: atom() | unicode:chardata() | number().
|
||||
|
||||
%%
|
||||
|
||||
%% @doc Lookup a value in the JSON-ish map accessible through the given accessor.
|
||||
%% If accessor implies drilling down into a binary, it will be treated as JSON serial.
|
||||
%% Failure to parse the binary as JSON will result in an _invalid type_ error.
|
||||
%% Nested JSON is NOT parsed recursively.
|
||||
-spec lookup(emqx_template:accessor(), t()) ->
|
||||
{ok, value()}
|
||||
| {error, undefined | {_Location :: non_neg_integer(), _InvalidType :: atom()}}.
|
||||
lookup(Var, Jsonish) ->
|
||||
lookup(0, _Decoded = false, Var, Jsonish).
|
||||
|
||||
lookup(_, _, [], Value) ->
|
||||
{ok, Value};
|
||||
lookup(Loc, Decoded, [Prop | Rest], Jsonish) when is_map(Jsonish) ->
|
||||
case emqx_template:lookup(Prop, Jsonish) of
|
||||
{ok, Value} ->
|
||||
lookup(Loc + 1, Decoded, Rest, Value);
|
||||
{error, Reason} ->
|
||||
{error, Reason}
|
||||
end;
|
||||
lookup(Loc, _Decoded = false, Rest, Json) when is_binary(Json) ->
|
||||
try emqx_utils_json:decode(Json) of
|
||||
Value ->
|
||||
% NOTE: This is intentional, we don't want to parse nested JSON.
|
||||
lookup(Loc, true, Rest, Value)
|
||||
catch
|
||||
error:_ ->
|
||||
{error, {Loc, binary}}
|
||||
end;
|
||||
lookup(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.
|
|
@ -29,7 +29,8 @@
|
|||
-export([render_strict/3]).
|
||||
|
||||
-export([lookup_var/2]).
|
||||
-export([lookup_loose_json/2]).
|
||||
-export([lookup/2]).
|
||||
|
||||
-export([to_string/1]).
|
||||
|
||||
-export_type([t/0]).
|
||||
|
@ -38,6 +39,10 @@
|
|||
-export_type([placeholder/0]).
|
||||
-export_type([varname/0]).
|
||||
-export_type([bindings/0]).
|
||||
-export_type([accessor/0]).
|
||||
|
||||
-export_type([context/0]).
|
||||
-export_type([render_opts/0]).
|
||||
|
||||
-type t() :: str() | {'$tpl', deeptpl()}.
|
||||
|
||||
|
@ -70,19 +75,22 @@
|
|||
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_lookup => var_lookup()
|
||||
var_trans => var_trans()
|
||||
}.
|
||||
|
||||
-define(PH_VAR_THIS, '$this').
|
||||
-type context() ::
|
||||
%% Map with (potentially nested) bindings.
|
||||
bindings()
|
||||
%% Arbitrary term accessible via an access module with `lookup/2` function.
|
||||
| {_AccessModule :: module(), _Bindings}.
|
||||
|
||||
%% Access module API
|
||||
-callback lookup(accessor(), _Bindings) -> {ok, _Value} | {error, reason()}.
|
||||
|
||||
-define(RE_PLACEHOLDER, "\\$\\{[.]?([a-zA-Z0-9._]*)\\}").
|
||||
-define(RE_ESCAPE, "\\$\\{(\\$)\\}").
|
||||
|
@ -130,7 +138,7 @@ prepend(Head, To) ->
|
|||
parse_accessor(Var) ->
|
||||
case string:split(Var, <<".">>, all) of
|
||||
[<<>>] ->
|
||||
?PH_VAR_THIS;
|
||||
[];
|
||||
Name ->
|
||||
Name
|
||||
end.
|
||||
|
@ -180,18 +188,18 @@ render_placeholder(Name) ->
|
|||
%% If one or more placeholders are not found in bindings, an error is returned.
|
||||
%% 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()) ->
|
||||
-spec render(t(), context()) ->
|
||||
{term(), [_Error :: {varname(), reason()}]}.
|
||||
render(Template, Bindings) ->
|
||||
render(Template, Bindings, #{}).
|
||||
render(Template, Context) ->
|
||||
render(Template, Context, #{}).
|
||||
|
||||
-spec render(t(), bindings(), render_opts()) ->
|
||||
-spec render(t(), context(), render_opts()) ->
|
||||
{term(), [_Error :: {varname(), undefined}]}.
|
||||
render(Template, Bindings, Opts) when is_list(Template) ->
|
||||
render(Template, Context, Opts) when is_list(Template) ->
|
||||
lists:mapfoldl(
|
||||
fun
|
||||
({var, Name, Accessor}, EAcc) ->
|
||||
{String, Errors} = render_binding(Name, Accessor, Bindings, Opts),
|
||||
{String, Errors} = render_binding(Name, Accessor, Context, Opts),
|
||||
{String, Errors ++ EAcc};
|
||||
(String, EAcc) ->
|
||||
{String, EAcc}
|
||||
|
@ -199,11 +207,11 @@ render(Template, Bindings, Opts) when is_list(Template) ->
|
|||
[],
|
||||
Template
|
||||
);
|
||||
render({'$tpl', Template}, Bindings, Opts) ->
|
||||
render_deep(Template, Bindings, Opts).
|
||||
render({'$tpl', Template}, Context, Opts) ->
|
||||
render_deep(Template, Context, Opts).
|
||||
|
||||
render_binding(Name, Accessor, Bindings, Opts) ->
|
||||
case lookup_value(Accessor, Bindings, Opts) of
|
||||
render_binding(Name, Accessor, Context, Opts) ->
|
||||
case lookup_value(Accessor, Context) of
|
||||
{ok, Value} ->
|
||||
{render_value(Name, Value, Opts), []};
|
||||
{error, Reason} ->
|
||||
|
@ -213,9 +221,9 @@ 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_value(Accessor, {AccessMod, Bindings}) ->
|
||||
AccessMod:lookup(Accessor, Bindings);
|
||||
lookup_value(Accessor, Bindings) ->
|
||||
lookup_var(Accessor, Bindings).
|
||||
|
||||
render_value(_Name, Value, #{var_trans := TransFun}) when is_function(TransFun, 1) ->
|
||||
|
@ -228,19 +236,19 @@ render_value(_Name, Value, #{}) ->
|
|||
%% @doc Render a template with given bindings.
|
||||
%% Behaves like `render/2`, but raises an error exception if one or more placeholders
|
||||
%% are not found in the bindings.
|
||||
-spec render_strict(t(), bindings()) ->
|
||||
-spec render_strict(t(), context()) ->
|
||||
term().
|
||||
render_strict(Template, Bindings) ->
|
||||
render_strict(Template, Bindings, #{}).
|
||||
render_strict(Template, Context) ->
|
||||
render_strict(Template, Context, #{}).
|
||||
|
||||
-spec render_strict(t(), bindings(), render_opts()) ->
|
||||
-spec render_strict(t(), context(), render_opts()) ->
|
||||
term().
|
||||
render_strict(Template, Bindings, Opts) ->
|
||||
case render(Template, Bindings, Opts) of
|
||||
render_strict(Template, Context, Opts) ->
|
||||
case render(Template, Context, Opts) of
|
||||
{Render, []} ->
|
||||
Render;
|
||||
{_, Errors = [_ | _]} ->
|
||||
error(Errors, [unparse(Template), Bindings])
|
||||
error(Errors, [unparse(Template), Context])
|
||||
end.
|
||||
|
||||
%% @doc Parse an arbitrary Erlang term into a "deep" template.
|
||||
|
@ -275,30 +283,30 @@ parse_deep_term(Term, Opts) when is_binary(Term) ->
|
|||
parse_deep_term(Term, _Opts) ->
|
||||
Term.
|
||||
|
||||
render_deep(Template, Bindings, Opts) when is_map(Template) ->
|
||||
render_deep(Template, Context, Opts) when is_map(Template) ->
|
||||
maps:fold(
|
||||
fun(KT, VT, {Acc, Errors}) ->
|
||||
{K, KErrors} = render_deep(KT, Bindings, Opts),
|
||||
{V, VErrors} = render_deep(VT, Bindings, Opts),
|
||||
{K, KErrors} = render_deep(KT, Context, Opts),
|
||||
{V, VErrors} = render_deep(VT, Context, Opts),
|
||||
{Acc#{K => V}, KErrors ++ VErrors ++ Errors}
|
||||
end,
|
||||
{#{}, []},
|
||||
Template
|
||||
);
|
||||
render_deep({list, Template}, Bindings, Opts) when is_list(Template) ->
|
||||
render_deep({list, Template}, Context, Opts) when is_list(Template) ->
|
||||
lists:mapfoldr(
|
||||
fun(T, Errors) ->
|
||||
{E, VErrors} = render_deep(T, Bindings, Opts),
|
||||
{E, VErrors} = render_deep(T, Context, Opts),
|
||||
{E, VErrors ++ Errors}
|
||||
end,
|
||||
[],
|
||||
Template
|
||||
);
|
||||
render_deep({tuple, Template}, Bindings, Opts) when is_list(Template) ->
|
||||
{Term, Errors} = render_deep({list, Template}, Bindings, Opts),
|
||||
render_deep({tuple, Template}, Context, Opts) when is_list(Template) ->
|
||||
{Term, Errors} = render_deep({list, Template}, Context, Opts),
|
||||
{list_to_tuple(Term), Errors};
|
||||
render_deep(Template, Bindings, Opts) when is_list(Template) ->
|
||||
{String, Errors} = render(Template, Bindings, Opts),
|
||||
render_deep(Template, Context, Opts) when is_list(Template) ->
|
||||
{String, Errors} = render(Template, Context, Opts),
|
||||
{unicode:characters_to_binary(String), Errors};
|
||||
render_deep(Term, _Bindings, _Opts) ->
|
||||
{Term, []}.
|
||||
|
@ -331,7 +339,7 @@ unparse_deep(Term) ->
|
|||
lookup_var(Var, Bindings) ->
|
||||
lookup_var(0, Var, Bindings).
|
||||
|
||||
lookup_var(_, Var, Value) when Var == ?PH_VAR_THIS orelse Var == [] ->
|
||||
lookup_var(_, [], Value) ->
|
||||
{ok, Value};
|
||||
lookup_var(Loc, [Prop | Rest], Bindings) when is_map(Bindings) ->
|
||||
case lookup(Prop, Bindings) of
|
||||
|
@ -343,35 +351,6 @@ lookup_var(Loc, [Prop | Rest], Bindings) when is_map(Bindings) ->
|
|||
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;
|
||||
|
|
|
@ -27,9 +27,9 @@
|
|||
|
||||
-export_type([row_template/0]).
|
||||
|
||||
-type template() :: emqx_template:t().
|
||||
-type template() :: emqx_template:str().
|
||||
-type row_template() :: [emqx_template:placeholder()].
|
||||
-type bindings() :: emqx_template:bindings().
|
||||
-type context() :: emqx_template:context().
|
||||
|
||||
-type values() :: [emqx_utils_sql:value()].
|
||||
|
||||
|
@ -62,19 +62,19 @@ parse(String, Opts) ->
|
|||
%% @doc Render an SQL statement template given a set of bindings.
|
||||
%% Interpolation generally follows the SQL syntax, strings are escaped according to the
|
||||
%% `escaping` option.
|
||||
-spec render(template(), bindings(), render_opts()) ->
|
||||
-spec render(template(), context(), render_opts()) ->
|
||||
{unicode:chardata(), [_Error]}.
|
||||
render(Template, Bindings, Opts) ->
|
||||
emqx_template:render(Template, Bindings, #{
|
||||
render(Template, Context, Opts) ->
|
||||
emqx_template:render(Template, Context, #{
|
||||
var_trans => fun(Value) -> emqx_utils_sql:to_sql_string(Value, Opts) end
|
||||
}).
|
||||
|
||||
%% @doc Render an SQL statement template given a set of bindings.
|
||||
%% Errors are raised if any placeholders are not bound.
|
||||
-spec render_strict(template(), bindings(), render_opts()) ->
|
||||
-spec render_strict(template(), context(), render_opts()) ->
|
||||
unicode:chardata().
|
||||
render_strict(Template, Bindings, Opts) ->
|
||||
emqx_template:render_strict(Template, Bindings, #{
|
||||
render_strict(Template, Context, Opts) ->
|
||||
emqx_template:render_strict(Template, Context, #{
|
||||
var_trans => fun(Value) -> emqx_utils_sql:to_sql_string(Value, Opts) end
|
||||
}).
|
||||
|
||||
|
@ -124,14 +124,14 @@ mk_replace(':n', N) ->
|
|||
%% An _SQL value_ is a vaguely defined concept here, it is something that's considered
|
||||
%% compatible with the protocol of the database being used. See the definition of
|
||||
%% `emqx_utils_sql:value()` for more details.
|
||||
-spec render_prepstmt(template(), bindings()) ->
|
||||
-spec render_prepstmt(template(), context()) ->
|
||||
{values(), [_Error]}.
|
||||
render_prepstmt(Template, Bindings) ->
|
||||
render_prepstmt(Template, Context) ->
|
||||
Opts = #{var_trans => fun emqx_utils_sql:to_sql_value/1},
|
||||
emqx_template:render(Template, Bindings, Opts).
|
||||
emqx_template:render(Template, Context, Opts).
|
||||
|
||||
-spec render_prepstmt_strict(template(), bindings()) ->
|
||||
-spec render_prepstmt_strict(template(), context()) ->
|
||||
values().
|
||||
render_prepstmt_strict(Template, Bindings) ->
|
||||
render_prepstmt_strict(Template, Context) ->
|
||||
Opts = #{var_trans => fun emqx_utils_sql:to_sql_value/1},
|
||||
emqx_template:render_strict(Template, Bindings, Opts).
|
||||
emqx_template:render_strict(Template, Context, Opts).
|
||||
|
|
|
@ -0,0 +1,97 @@
|
|||
%%--------------------------------------------------------------------
|
||||
%% Copyright (c) 2020-2022 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_jsonish_tests).
|
||||
|
||||
-include_lib("eunit/include/eunit.hrl").
|
||||
|
||||
prop_prio_test_() ->
|
||||
[
|
||||
?_assertEqual(
|
||||
{ok, 42},
|
||||
emqx_jsonish:lookup([<<"foo">>], #{<<"foo">> => 42, foo => 1337})
|
||||
),
|
||||
?_assertEqual(
|
||||
{ok, 1337},
|
||||
emqx_jsonish:lookup([<<"foo">>], #{foo => 1337})
|
||||
)
|
||||
].
|
||||
|
||||
undefined_test() ->
|
||||
?assertEqual(
|
||||
{error, undefined},
|
||||
emqx_jsonish:lookup([<<"foo">>], #{})
|
||||
).
|
||||
|
||||
undefined_deep_test() ->
|
||||
?assertEqual(
|
||||
{error, undefined},
|
||||
emqx_jsonish:lookup([<<"foo">>, <<"bar">>], #{})
|
||||
).
|
||||
|
||||
undefined_deep_json_test() ->
|
||||
?assertEqual(
|
||||
{error, undefined},
|
||||
emqx_jsonish:lookup(
|
||||
[<<"foo">>, <<"bar">>, <<"baz">>],
|
||||
<<"{\"foo\":{\"bar\":{\"no\":{}}}}">>
|
||||
)
|
||||
).
|
||||
|
||||
invalid_type_test() ->
|
||||
?assertEqual(
|
||||
{error, {0, number}},
|
||||
emqx_jsonish:lookup([<<"foo">>], <<"42">>)
|
||||
).
|
||||
|
||||
invalid_type_deep_test() ->
|
||||
?assertEqual(
|
||||
{error, {2, atom}},
|
||||
emqx_jsonish:lookup([<<"foo">>, <<"bar">>, <<"tuple">>], #{foo => #{bar => baz}})
|
||||
).
|
||||
|
||||
decode_json_test() ->
|
||||
?assertEqual(
|
||||
{ok, 42},
|
||||
emqx_jsonish:lookup([<<"foo">>, <<"bar">>], <<"{\"foo\":{\"bar\":42}}">>)
|
||||
).
|
||||
|
||||
decode_json_deep_test() ->
|
||||
?assertEqual(
|
||||
{ok, 42},
|
||||
emqx_jsonish:lookup([<<"foo">>, <<"bar">>], #{<<"foo">> => <<"{\"bar\": 42}">>})
|
||||
).
|
||||
|
||||
decode_json_invalid_type_test() ->
|
||||
?assertEqual(
|
||||
{error, {1, list}},
|
||||
emqx_jsonish:lookup([<<"foo">>, <<"bar">>], #{<<"foo">> => <<"[1,2,3]">>})
|
||||
).
|
||||
|
||||
decode_no_json_test() ->
|
||||
?assertEqual(
|
||||
{error, {1, binary}},
|
||||
emqx_jsonish:lookup([<<"foo">>, <<"bar">>], #{<<"foo">> => <<0, 1, 2, 3>>})
|
||||
).
|
||||
|
||||
decode_json_no_nested_test() ->
|
||||
?assertEqual(
|
||||
{error, {2, binary}},
|
||||
emqx_jsonish:lookup(
|
||||
[<<"foo">>, <<"bar">>, <<"baz">>],
|
||||
#{<<"foo">> => <<"{\"bar\":\"{\\\"baz\\\":42}\"}">>}
|
||||
)
|
||||
).
|
|
@ -25,7 +25,7 @@
|
|||
all() -> emqx_common_test_helpers:all(?MODULE).
|
||||
|
||||
t_render(_) ->
|
||||
Bindings = #{
|
||||
Context = #{
|
||||
a => <<"1">>,
|
||||
b => 1,
|
||||
c => 1.0,
|
||||
|
@ -38,15 +38,15 @@ t_render(_) ->
|
|||
),
|
||||
?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, Bindings)
|
||||
render_string(Template, Context)
|
||||
).
|
||||
|
||||
t_render_var_trans(_) ->
|
||||
Bindings = #{a => <<"1">>, b => 1, c => #{prop => 1.0}},
|
||||
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,
|
||||
Bindings,
|
||||
Context,
|
||||
#{var_trans => fun(Name, _) -> "<" ++ Name ++ ">" end}
|
||||
),
|
||||
?assertEqual(
|
||||
|
@ -55,7 +55,7 @@ t_render_var_trans(_) ->
|
|||
).
|
||||
|
||||
t_render_path(_) ->
|
||||
Bindings = #{d => #{d1 => <<"hi">>}},
|
||||
Context = #{d => #{d1 => <<"hi">>}},
|
||||
Template = emqx_template:parse(<<"d.d1:${d.d1}">>),
|
||||
?assertEqual(
|
||||
ok,
|
||||
|
@ -63,11 +63,11 @@ t_render_path(_) ->
|
|||
),
|
||||
?assertEqual(
|
||||
{<<"d.d1:hi">>, []},
|
||||
render_string(Template, Bindings)
|
||||
render_string(Template, Context)
|
||||
).
|
||||
|
||||
t_render_custom_ph(_) ->
|
||||
Bindings = #{a => <<"a">>, b => <<"b">>},
|
||||
Context = #{a => <<"a">>, b => <<"b">>},
|
||||
Template = emqx_template:parse(<<"a:${a},b:${b}">>),
|
||||
?assertEqual(
|
||||
{error, [{"b", disallowed}]},
|
||||
|
@ -75,21 +75,21 @@ t_render_custom_ph(_) ->
|
|||
),
|
||||
?assertEqual(
|
||||
<<"a:a,b:b">>,
|
||||
render_strict_string(Template, Bindings)
|
||||
render_strict_string(Template, Context)
|
||||
).
|
||||
|
||||
t_render_this(_) ->
|
||||
Bindings = #{a => <<"a">>, b => [1, 2, 3]},
|
||||
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, Bindings)
|
||||
render_strict_string(Template, Context)
|
||||
).
|
||||
|
||||
t_render_missing_bindings(_) ->
|
||||
Bindings = #{no => #{}, c => #{<<"c1">> => 42}},
|
||||
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}">>
|
||||
),
|
||||
|
@ -101,7 +101,7 @@ t_render_missing_bindings(_) ->
|
|||
{"b", undefined},
|
||||
{"a", undefined}
|
||||
]},
|
||||
render_string(Template, Bindings)
|
||||
render_string(Template, Context)
|
||||
),
|
||||
?assertError(
|
||||
[
|
||||
|
@ -111,7 +111,21 @@ t_render_missing_bindings(_) ->
|
|||
{"b", undefined},
|
||||
{"a", undefined}
|
||||
],
|
||||
render_strict_string(Template, Bindings)
|
||||
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(_) ->
|
||||
|
@ -141,33 +155,33 @@ t_const(_) ->
|
|||
).
|
||||
|
||||
t_render_partial_ph(_) ->
|
||||
Bindings = #{a => <<"1">>, b => 1, c => 1.0, d => #{d1 => <<"hi">>}},
|
||||
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, Bindings)
|
||||
render_strict_string(Template, Context)
|
||||
).
|
||||
|
||||
t_parse_escaped(_) ->
|
||||
Bindings = #{a => <<"1">>, b => 1, c => "VAR"},
|
||||
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, Bindings)
|
||||
render_strict_string(Template, Context)
|
||||
).
|
||||
|
||||
t_parse_escaped_dquote(_) ->
|
||||
Bindings = #{a => <<"1">>, b => 1},
|
||||
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, Bindings)
|
||||
render_strict_string(Template, Context)
|
||||
).
|
||||
|
||||
t_parse_sql_prepstmt(_) ->
|
||||
Bindings = #{a => <<"1">>, b => 1, c => 1.0, d => #{d1 => <<"hi">>}},
|
||||
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 => '?'
|
||||
|
@ -175,11 +189,11 @@ t_parse_sql_prepstmt(_) ->
|
|||
?assertEqual(<<"a:?,b:?,c:?,d:?">>, bin(PrepareStatement)),
|
||||
?assertEqual(
|
||||
{[<<"1">>, 1, 1.0, <<"{\"d1\":\"hi\"}">>], _Errors = []},
|
||||
emqx_template_sql:render_prepstmt(RowTemplate, Bindings)
|
||||
emqx_template_sql:render_prepstmt(RowTemplate, Context)
|
||||
).
|
||||
|
||||
t_parse_sql_prepstmt_n(_) ->
|
||||
Bindings = #{a => undefined, b => true, c => atom, d => #{d1 => 42.1337}},
|
||||
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'
|
||||
|
@ -187,7 +201,7 @@ t_parse_sql_prepstmt_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, Bindings)
|
||||
emqx_template_sql:render_prepstmt_strict(RowTemplate, Context)
|
||||
).
|
||||
|
||||
t_parse_sql_prepstmt_colon(_) ->
|
||||
|
@ -198,14 +212,14 @@ t_parse_sql_prepstmt_colon(_) ->
|
|||
?assertEqual(<<"a=:1,b=:2,c=:3,d=:4">>, bin(PrepareStatement)).
|
||||
|
||||
t_parse_sql_prepstmt_partial_ph(_) ->
|
||||
Bindings = #{a => <<"1">>, b => 1, c => 1.0, d => #{d1 => <<"hi">>}},
|
||||
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, Bindings)).
|
||||
?assertEqual([], emqx_template_sql:render_prepstmt_strict(RowTemplate, Context)).
|
||||
|
||||
t_render_sql(_) ->
|
||||
Bindings = #{
|
||||
Context = #{
|
||||
a => <<"1">>,
|
||||
b => 1,
|
||||
c => 1.0,
|
||||
|
@ -216,17 +230,17 @@ t_render_sql(_) ->
|
|||
Template = emqx_template:parse(<<"a:${a},b:${b},c:${c},d:${d},n:${n},u:${u}">>),
|
||||
?assertMatch(
|
||||
{_String, _Errors = []},
|
||||
emqx_template_sql:render(Template, Bindings, #{})
|
||||
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, Bindings, #{}))
|
||||
bin(emqx_template_sql:render_strict(Template, Context, #{}))
|
||||
).
|
||||
|
||||
t_render_mysql(_) ->
|
||||
%% with apostrophes
|
||||
%% https://github.com/emqx/emqx/issues/4135
|
||||
Bindings = #{
|
||||
Context = #{
|
||||
a => <<"1''2">>,
|
||||
b => 1,
|
||||
c => 1.0,
|
||||
|
@ -245,13 +259,13 @@ t_render_mysql(_) ->
|
|||
"e:'\\\\\\0💩',f:0x6E6F6E2D75746638DCC900,g:'utf8\\'s cool 🐸',"/utf8,
|
||||
"h:'imgood'"
|
||||
>>,
|
||||
bin(emqx_template_sql:render_strict(Template, Bindings, #{escaping => mysql}))
|
||||
bin(emqx_template_sql:render_strict(Template, Context, #{escaping => mysql}))
|
||||
).
|
||||
|
||||
t_render_cql(_) ->
|
||||
%% with apostrophes for cassandra
|
||||
%% https://github.com/emqx/emqx/issues/4148
|
||||
Bindings = #{
|
||||
Context = #{
|
||||
a => <<"1''2">>,
|
||||
b => 1,
|
||||
c => 1.0,
|
||||
|
@ -260,7 +274,7 @@ t_render_cql(_) ->
|
|||
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, Bindings, #{escaping => cql}))
|
||||
bin(emqx_template_sql:render_strict(Template, Context, #{escaping => cql}))
|
||||
).
|
||||
|
||||
t_render_sql_custom_ph(_) ->
|
||||
|
@ -273,7 +287,7 @@ t_render_sql_custom_ph(_) ->
|
|||
?assertEqual(<<"a:$1,b:$2">>, bin(PrepareStatement)).
|
||||
|
||||
t_render_sql_strip_double_quote(_) ->
|
||||
Bindings = #{a => <<"a">>, b => <<"b">>},
|
||||
Context = #{a => <<"a">>, b => <<"b">>},
|
||||
|
||||
%% no strip_double_quote option: "${key}" -> "value"
|
||||
{PrepareStatement1, RowTemplate1} = emqx_template_sql:parse_prepstmt(
|
||||
|
@ -283,7 +297,7 @@ t_render_sql_strip_double_quote(_) ->
|
|||
?assertEqual(<<"a:\"$1\",b:\"$2\"">>, bin(PrepareStatement1)),
|
||||
?assertEqual(
|
||||
[<<"a">>, <<"b">>],
|
||||
emqx_template_sql:render_prepstmt_strict(RowTemplate1, Bindings)
|
||||
emqx_template_sql:render_prepstmt_strict(RowTemplate1, Context)
|
||||
),
|
||||
|
||||
%% strip_double_quote = true: "${key}" -> value
|
||||
|
@ -294,11 +308,11 @@ t_render_sql_strip_double_quote(_) ->
|
|||
?assertEqual(<<"a:$1,b:$2">>, bin(PrepareStatement2)),
|
||||
?assertEqual(
|
||||
[<<"a">>, <<"b">>],
|
||||
emqx_template_sql:render_prepstmt_strict(RowTemplate2, Bindings)
|
||||
emqx_template_sql:render_prepstmt_strict(RowTemplate2, Context)
|
||||
).
|
||||
|
||||
t_render_tmpl_deep(_) ->
|
||||
Bindings = #{a => <<"1">>, b => 1, c => 1.0, d => #{d1 => <<"hi">>}},
|
||||
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}]}
|
||||
|
@ -311,7 +325,7 @@ t_render_tmpl_deep(_) ->
|
|||
|
||||
?assertEqual(
|
||||
#{<<"1">> => [<<"$1">>, "c", 2, 3.0, '${d}', {[<<"1.0">>, <<"${d}">>], 0}]},
|
||||
emqx_template:render_strict(Template, Bindings)
|
||||
emqx_template:render_strict(Template, Context)
|
||||
).
|
||||
|
||||
t_unparse_tmpl_deep(_) ->
|
||||
|
@ -321,12 +335,22 @@ t_unparse_tmpl_deep(_) ->
|
|||
|
||||
%%
|
||||
|
||||
render_string(Template, Bindings) ->
|
||||
{String, Errors} = emqx_template:render(Template, Bindings),
|
||||
render_string(Template, Context) ->
|
||||
{String, Errors} = emqx_template:render(Template, Context),
|
||||
{bin(String), Errors}.
|
||||
|
||||
render_strict_string(Template, Bindings) ->
|
||||
bin(emqx_template:render_strict(Template, Bindings)).
|
||||
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.
|
||||
|
|
Loading…
Reference in New Issue