chore: move template modules to `emqx_utils`

Even though most of the time these modules will be used by
connectors, there are exceptions (namely, `emqx_rule_engine`).
Besides, they are general enough to land there, more so given
that `emqx_placeholder` is already there.
This commit is contained in:
Andrew Mayorov 2023-07-14 18:40:11 +02:00
parent 343b679741
commit 8e4585d64f
No known key found for this signature in database
GPG Key ID: 2837C62ACFBFED5D
15 changed files with 130 additions and 287 deletions

View File

@ -108,22 +108,22 @@ check_password_from_selected_map(Algorithm, Selected, Password) ->
end.
parse_deep(Template) ->
Result = emqx_connector_template:parse_deep(Template),
Result = emqx_template:parse_deep(Template),
handle_disallowed_placeholders(Result, {deep, Template}).
parse_str(Template) ->
Result = emqx_connector_template:parse(Template),
Result = emqx_template:parse(Template),
handle_disallowed_placeholders(Result, {string, Template}).
parse_sql(Template, ReplaceWith) ->
{Statement, Result} = emqx_connector_template_sql:parse_prepstmt(
{Statement, Result} = emqx_template_sql:parse_prepstmt(
Template,
#{parameters => ReplaceWith, strip_double_quote => true}
),
{Statement, handle_disallowed_placeholders(Result, {string, Template})}.
handle_disallowed_placeholders(Template, Source) ->
case emqx_connector_template:validate(?ALLOWED_VARS, Template) of
case emqx_template:validate(?ALLOWED_VARS, Template) of
ok ->
Template;
{error, Disallowed} ->
@ -139,14 +139,14 @@ handle_disallowed_placeholders(Template, Source) ->
Result = prerender_disallowed_placeholders(Template),
case Source of
{string, _} ->
emqx_connector_template:parse(Result);
emqx_template:parse(Result);
{deep, _} ->
emqx_connector_template:parse_deep(Result)
emqx_template:parse_deep(Result)
end
end.
prerender_disallowed_placeholders(Template) ->
{Result, _} = emqx_connector_template:render(Template, #{}, #{
{Result, _} = emqx_template:render(Template, #{}, #{
var_trans => fun(Name, _) ->
% NOTE
% Rendering disallowed placeholders in escaped form, which will then
@ -162,7 +162,7 @@ prerender_disallowed_placeholders(Template) ->
render_deep(Template, Credential) ->
% NOTE
% Ignoring errors here, undefined bindings will be replaced with empty string.
{Term, _Errors} = emqx_connector_template:render(
{Term, _Errors} = emqx_template:render(
Template,
mapping_credential(Credential),
#{var_trans => fun to_string/2}
@ -172,7 +172,7 @@ render_deep(Template, Credential) ->
render_str(Template, Credential) ->
% NOTE
% Ignoring errors here, undefined bindings will be replaced with empty string.
{String, _Errors} = emqx_connector_template:render(
{String, _Errors} = emqx_template:render(
Template,
mapping_credential(Credential),
#{var_trans => fun to_string/2}
@ -182,7 +182,7 @@ render_str(Template, Credential) ->
render_urlencoded_str(Template, Credential) ->
% NOTE
% Ignoring errors here, undefined bindings will be replaced with empty string.
{String, _Errors} = emqx_connector_template:render(
{String, _Errors} = emqx_template:render(
Template,
mapping_credential(Credential),
#{var_trans => fun to_urlencoded_string/2}
@ -192,7 +192,7 @@ render_urlencoded_str(Template, Credential) ->
render_sql_params(ParamList, Credential) ->
% NOTE
% Ignoring errors here, undefined bindings will be replaced with empty string.
{Row, _Errors} = emqx_connector_template:render(
{Row, _Errors} = emqx_template:render(
ParamList,
mapping_credential(Credential),
#{var_trans => fun to_sql_valaue/2}
@ -322,10 +322,10 @@ to_urlencoded_string(Name, Value) ->
emqx_http_lib:uri_encode(to_string(Name, Value)).
to_string(Name, Value) ->
emqx_connector_template:to_string(render_var(Name, Value)).
emqx_template:to_string(render_var(Name, Value)).
to_sql_valaue(Name, Value) ->
emqx_connector_sql:to_sql_value(render_var(Name, Value)).
emqx_utils_sql:to_sql_value(render_var(Name, Value)).
render_var(_, undefined) ->
% NOTE

View File

@ -184,7 +184,7 @@ compile_topic({eq, Topic}) ->
{eq, emqx_topic:words(bin(Topic))};
compile_topic(Topic) ->
Template = emqx_authz_utils:parse_str(Topic, [?VAR_USERNAME, ?VAR_CLIENTID]),
case emqx_connector_template:is_const(Template) of
case emqx_template:is_const(Template) of
true -> emqx_topic:words(bin(Topic));
false -> {pattern, Template}
end.
@ -302,7 +302,7 @@ match_who(_, _) ->
match_topics(_ClientInfo, _Topic, []) ->
false;
match_topics(ClientInfo, Topic, [{pattern, PatternFilter} | Filters]) ->
TopicFilter = bin(emqx_connector_template:render_strict(PatternFilter, ClientInfo)),
TopicFilter = bin(emqx_template:render_strict(PatternFilter, ClientInfo)),
match_topic(emqx_topic:words(Topic), emqx_topic:words(TopicFilter)) orelse
match_topics(ClientInfo, Topic, Filters);
match_topics(ClientInfo, Topic, [TopicFilter | Filters]) ->

View File

@ -110,15 +110,15 @@ update_config(Path, ConfigRequest) ->
}).
parse_deep(Template, PlaceHolders) ->
Result = emqx_connector_template:parse_deep(Template),
Result = emqx_template:parse_deep(Template),
handle_disallowed_placeholders(Result, {deep, Template}, PlaceHolders).
parse_str(Template, PlaceHolders) ->
Result = emqx_connector_template:parse(Template),
Result = emqx_template:parse(Template),
handle_disallowed_placeholders(Result, {string, Template}, PlaceHolders).
parse_sql(Template, ReplaceWith, PlaceHolders) ->
{Statement, Result} = emqx_connector_template_sql:parse_prepstmt(
{Statement, Result} = emqx_template_sql:parse_prepstmt(
Template,
#{parameters => ReplaceWith, strip_double_quote => true}
),
@ -126,7 +126,7 @@ parse_sql(Template, ReplaceWith, PlaceHolders) ->
{Statement, FResult}.
handle_disallowed_placeholders(Template, Source, Allowed) ->
case emqx_connector_template:validate(Allowed, Template) of
case emqx_template:validate(Allowed, Template) of
ok ->
Template;
{error, Disallowed} ->
@ -142,14 +142,14 @@ handle_disallowed_placeholders(Template, Source, Allowed) ->
Result = prerender_disallowed_placeholders(Template, Allowed),
case Source of
{string, _} ->
emqx_connector_template:parse(Result);
emqx_template:parse(Result);
{deep, _} ->
emqx_connector_template:parse_deep(Result)
emqx_template:parse_deep(Result)
end
end.
prerender_disallowed_placeholders(Template, Allowed) ->
{Result, _} = emqx_connector_template:render(Template, #{}, #{
{Result, _} = emqx_template:render(Template, #{}, #{
var_trans => fun(Name, _) ->
% NOTE
% Rendering disallowed placeholders in escaped form, which will then
@ -165,7 +165,7 @@ prerender_disallowed_placeholders(Template, Allowed) ->
render_deep(Template, Values) ->
% NOTE
% Ignoring errors here, undefined bindings will be replaced with empty string.
{Term, _Errors} = emqx_connector_template:render(
{Term, _Errors} = emqx_template:render(
Template,
client_vars(Values),
#{var_trans => fun to_string/2}
@ -175,7 +175,7 @@ render_deep(Template, Values) ->
render_str(Template, Values) ->
% NOTE
% Ignoring errors here, undefined bindings will be replaced with empty string.
{String, _Errors} = emqx_connector_template:render(
{String, _Errors} = emqx_template:render(
Template,
client_vars(Values),
#{var_trans => fun to_string/2}
@ -185,7 +185,7 @@ render_str(Template, Values) ->
render_urlencoded_str(Template, Values) ->
% NOTE
% Ignoring errors here, undefined bindings will be replaced with empty string.
{String, _Errors} = emqx_connector_template:render(
{String, _Errors} = emqx_template:render(
Template,
client_vars(Values),
#{var_trans => fun to_urlencoded_string/2}
@ -195,7 +195,7 @@ render_urlencoded_str(Template, Values) ->
render_sql_params(ParamList, Values) ->
% NOTE
% Ignoring errors here, undefined bindings will be replaced with empty string.
{Row, _Errors} = emqx_connector_template:render(
{Row, _Errors} = emqx_template:render(
ParamList,
client_vars(Values),
#{var_trans => fun to_sql_value/2}
@ -270,10 +270,10 @@ to_urlencoded_string(Name, Value) ->
emqx_http_lib:uri_encode(to_string(Name, Value)).
to_string(Name, Value) ->
emqx_connector_template:to_string(render_var(Name, Value)).
emqx_template:to_string(render_var(Name, Value)).
to_sql_value(Name, Value) ->
emqx_connector_sql:to_sql_value(render_var(Name, Value)).
emqx_utils_sql:to_sql_value(render_var(Name, Value)).
render_var(_, undefined) ->
% NOTE

View File

@ -69,8 +69,8 @@ set_special_configs(_App) ->
t_compile(_) ->
% NOTE
% Some of the following testcase are relying on the internal representation of
% `emqx_connector_template:t()`. If the internal representation is changed, these
% testcases may fail.
% `emqx_template:t()`. If the internal representation is changed, these testcases
% may fail.
?assertEqual({deny, all, all, [['#']]}, emqx_authz_rule:compile({deny, all})),
?assertEqual(

View File

@ -535,7 +535,7 @@ maybe_parse_template(Key, Conf) ->
end.
parse_template(String) ->
emqx_connector_template:parse(String).
emqx_template:parse(String).
process_request(
#{
@ -573,7 +573,7 @@ render_headers(HeaderTks, Msg) ->
render_template(Template, Msg) ->
% NOTE: ignoring errors here, missing variables will be rendered as `"undefined"`.
{String, _Errors} = emqx_connector_template:render(Template, Msg),
{String, _Errors} = emqx_template:render(Template, Msg),
String.
render_template_string(Template, Msg) ->

View File

@ -84,7 +84,7 @@ is_wrapped(_Other) ->
false.
untmpl(Tpl) ->
iolist_to_binary(emqx_connector_template:render_strict(Tpl, #{})).
iolist_to_binary(emqx_template:render_strict(Tpl, #{})).
is_unwrapped_headers(Headers) ->
lists:all(fun is_unwrapped_header/1, Headers).

View File

@ -1,159 +0,0 @@
%%--------------------------------------------------------------------
%% Copyright (c) 2022-2023 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_connector_sql).
-export([get_statement_type/1]).
-export([parse_insert/1]).
-export([to_sql_value/1]).
-export([to_sql_string/2]).
-export([escape_sql/1]).
-export([escape_cql/1]).
-export([escape_mysql/1]).
-export_type([value/0]).
-type statement_type() :: select | insert | delete.
-type value() :: null | binary() | number() | boolean() | [value()].
-dialyzer({no_improper_lists, [escape_mysql/4, escape_prepend/4]}).
-spec get_statement_type(iodata()) -> statement_type() | {error, unknown}.
get_statement_type(Query) ->
KnownTypes = #{
<<"select">> => select,
<<"insert">> => insert,
<<"delete">> => delete
},
case re:run(Query, <<"^\\s*([a-zA-Z]+)">>, [{capture, all_but_first, binary}]) of
{match, [Token]} ->
maps:get(string:lowercase(Token), KnownTypes, {error, unknown});
_ ->
{error, unknown}
end.
%% @doc Parse an INSERT SQL statement into its INSERT part and the VALUES part.
%% SQL = <<"INSERT INTO \"abc\" (c1, c2, c3) VALUES (${a}, ${b}, ${c.prop})">>
%% {ok, {<<"INSERT INTO \"abc\" (c1, c2, c3)">>, <<"(${a}, ${b}, ${c.prop})">>}}
-spec parse_insert(iodata()) ->
{ok, {_Statement :: binary(), _Rows :: binary()}} | {error, not_insert_sql}.
parse_insert(SQL) ->
case re:split(SQL, "((?i)values)", [{return, binary}]) of
[Part1, _, Part3] ->
case string:trim(Part1, leading) of
<<"insert", _/binary>> = InsertSQL ->
{ok, {InsertSQL, Part3}};
<<"INSERT", _/binary>> = InsertSQL ->
{ok, {InsertSQL, Part3}};
_ ->
{error, not_insert_sql}
end;
_ ->
{error, not_insert_sql}
end.
%% @doc Convert an Erlang term to a value that can be used primarily in
%% prepared SQL statements.
-spec to_sql_value(term()) -> value().
to_sql_value(undefined) -> null;
to_sql_value(List) when is_list(List) -> List;
to_sql_value(Bin) when is_binary(Bin) -> Bin;
to_sql_value(Num) when is_number(Num) -> Num;
to_sql_value(Bool) when is_boolean(Bool) -> Bool;
to_sql_value(Atom) when is_atom(Atom) -> atom_to_binary(Atom, utf8);
to_sql_value(Map) when is_map(Map) -> emqx_utils_json:encode(Map).
%% @doc Convert an Erlang term to a string that can be interpolated in literal
%% SQL statements. The value is escaped if necessary.
-spec to_sql_string(term(), Options) -> iodata() when
Options :: #{
escaping => cql | mysql | sql
}.
to_sql_string(String, #{escaping := mysql}) when is_binary(String) ->
try
escape_mysql(String)
catch
throw:invalid_utf8 ->
[<<"0x">>, binary:encode_hex(String)]
end;
to_sql_string(Term, #{escaping := mysql}) ->
maybe_escape(Term, fun escape_mysql/1);
to_sql_string(Term, #{escaping := cql}) ->
maybe_escape(Term, fun escape_cql/1);
to_sql_string(Term, #{}) ->
maybe_escape(Term, fun escape_sql/1).
-spec maybe_escape(_Value, fun((binary()) -> iodata())) -> iodata().
maybe_escape(undefined, _EscapeFun) ->
<<"NULL">>;
maybe_escape(Str, EscapeFun) when is_binary(Str) ->
EscapeFun(Str);
maybe_escape(Str, EscapeFun) when is_list(Str) ->
case unicode:characters_to_binary(Str) of
Bin when is_binary(Bin) ->
EscapeFun(Bin);
Otherwise ->
error(Otherwise)
end;
maybe_escape(Val, EscapeFun) when is_atom(Val) orelse is_map(Val) ->
EscapeFun(emqx_connector_template:to_string(Val));
maybe_escape(Val, _EscapeFun) ->
emqx_connector_template:to_string(Val).
-spec escape_sql(binary()) -> iodata().
escape_sql(S) ->
% NOTE
% This is a bit misleading: currently, escaping logic in `escape_sql/1` likely
% won't work with pgsql since it does not support C-style escapes by default.
% https://www.postgresql.org/docs/14/sql-syntax-lexical.html#SQL-SYNTAX-CONSTANTS
ES = binary:replace(S, [<<"\\">>, <<"'">>], <<"\\">>, [global, {insert_replaced, 1}]),
[$', ES, $'].
-spec escape_cql(binary()) -> iodata().
escape_cql(S) ->
ES = binary:replace(S, <<"'">>, <<"'">>, [global, {insert_replaced, 1}]),
[$', ES, $'].
-spec escape_mysql(binary()) -> iodata().
escape_mysql(S0) ->
% https://dev.mysql.com/doc/refman/8.0/en/string-literals.html
[$', escape_mysql(S0, 0, 0, S0), $'].
%% NOTE
%% This thing looks more complicated than needed because it's optimized for as few
%% intermediate memory (re)allocations as possible.
escape_mysql(<<$', Rest/binary>>, I, Run, Src) ->
escape_prepend(I, Run, Src, [<<"\\'">> | escape_mysql(Rest, I + Run + 1, 0, Src)]);
escape_mysql(<<$\\, Rest/binary>>, I, Run, Src) ->
escape_prepend(I, Run, Src, [<<"\\\\">> | escape_mysql(Rest, I + Run + 1, 0, Src)]);
escape_mysql(<<0, Rest/binary>>, I, Run, Src) ->
escape_prepend(I, Run, Src, [<<"\\0">> | escape_mysql(Rest, I + Run + 1, 0, Src)]);
escape_mysql(<<_/utf8, Rest/binary>> = S, I, Run, Src) ->
CWidth = byte_size(S) - byte_size(Rest),
escape_mysql(Rest, I, Run + CWidth, Src);
escape_mysql(<<>>, 0, _, Src) ->
Src;
escape_mysql(<<>>, I, Run, Src) ->
binary:part(Src, I, Run);
escape_mysql(_, _I, _Run, _Src) ->
throw(invalid_utf8).
escape_prepend(_RunI, 0, _Src, Tail) ->
Tail;
escape_prepend(I, Run, Src, Tail) ->
[binary:part(Src, I, Run) | Tail].

View File

@ -46,7 +46,7 @@
default_port => ?MYSQL_DEFAULT_PORT
}).
-type template() :: {unicode:chardata(), emqx_connector_template:str()}.
-type template() :: {unicode:chardata(), emqx_template:str()}.
-type state() ::
#{
pool_name := binary(),
@ -387,16 +387,16 @@ parse_prepare_sql(Config) ->
#{query_templates => Templates}.
parse_prepare_sql(Key, Query, Acc) ->
Template = emqx_connector_template_sql:parse_prepstmt(Query, #{parameters => '?'}),
Template = emqx_template_sql:parse_prepstmt(Query, #{parameters => '?'}),
AccNext = Acc#{{Key, prepstmt} => Template},
parse_batch_sql(Key, Query, AccNext).
parse_batch_sql(Key, Query, Acc) ->
case emqx_connector_sql:get_statement_type(Query) of
case emqx_utils_sql:get_statement_type(Query) of
insert ->
case emqx_connector_sql:parse_insert(Query) of
case emqx_utils_sql:parse_insert(Query) of
{ok, {Insert, Params}} ->
RowTemplate = emqx_connector_template_sql:parse(Params),
RowTemplate = emqx_template_sql:parse(Params),
Acc#{{Key, batch} => {Insert, RowTemplate}};
{error, Reason} ->
?SLOG(error, #{
@ -427,7 +427,7 @@ proc_sql_params(TypeOrKey, SQLOrData, Params, #{query_templates := Templates}) -
{SQLOrData, Params};
{_InsertPart, RowTemplate} ->
% NOTE: ignoring errors here, missing variables are set to `null`.
{Row, _Errors} = emqx_connector_template_sql:render_prepstmt(RowTemplate, SQLOrData),
{Row, _Errors} = emqx_template_sql:render_prepstmt(RowTemplate, SQLOrData),
{TypeOrKey, Row}
end.
@ -438,7 +438,7 @@ on_batch_insert(InstId, BatchReqs, {InsertPart, RowTemplate}, State) ->
render_row(RowTemplate, Data) ->
% NOTE: ignoring errors here, missing variables are set to "NULL".
{Row, _Errors} = emqx_connector_template_sql:render(RowTemplate, Data, #{escaping => mysql}),
{Row, _Errors} = emqx_template_sql:render(RowTemplate, Data, #{escaping => mysql}),
Row.
on_sql_query(

View File

@ -52,7 +52,7 @@
default_port => ?PGSQL_DEFAULT_PORT
}).
-type template() :: {unicode:chardata(), emqx_connector_template_sql:row_template()}.
-type template() :: {unicode:chardata(), emqx_template_sql:row_template()}.
-type state() ::
#{
pool_name := binary(),
@ -428,12 +428,12 @@ parse_prepare_sql(Config) ->
#{query_templates => Templates}.
parse_prepare_sql(Key, Query, Acc) ->
Template = emqx_connector_template_sql:parse_prepstmt(Query, #{parameters => '$n'}),
Template = emqx_template_sql:parse_prepstmt(Query, #{parameters => '$n'}),
Acc#{Key => Template}.
render_prepare_sql_row(RowTemplate, Data) ->
% NOTE: ignoring errors here, missing variables will be replaced with `null`.
{Row, _Errors} = emqx_connector_template_sql:render_prepstmt(RowTemplate, Data),
{Row, _Errors} = emqx_template_sql:render_prepstmt(RowTemplate, Data),
Row.
init_prepare(State = #{query_templates := Templates}) when map_size(Templates) == 0 ->

View File

@ -114,8 +114,8 @@ handle_info(_Msg, State) ->
push_to_push_gateway(Uri, Headers, JobName) when is_list(Headers) ->
[Name, Ip] = string:tokens(atom_to_list(node()), "@"),
% NOTE: allowing errors here to keep rough backward compatibility
{JobName1, Errors} = emqx_connector_template:render(
emqx_connector_template:parse(JobName),
{JobName1, Errors} = emqx_template:render(
emqx_template:parse(JobName),
#{<<"name">> => Name, <<"host">> => Ip}
),
_ =

View File

@ -71,7 +71,7 @@ pre_process_action_args(
) ->
Args#{
preprocessed_tmpl => #{
topic => emqx_connector_template:parse(Topic),
topic => emqx_template:parse(Topic),
qos => parse_vars(QoS),
retain => parse_vars(Retain),
payload => parse_payload(Payload),
@ -119,8 +119,8 @@ republish(
}
) ->
% NOTE: rendering missing bindings as string "undefined"
{TopicString, _Errors1} = emqx_connector_template:render(TopicTemplate, Selected),
{PayloadString, _Errors2} = emqx_connector_template:render(PayloadTemplate, Selected),
{TopicString, _Errors1} = emqx_template:render(TopicTemplate, Selected),
{PayloadString, _Errors2} = emqx_template:render(PayloadTemplate, Selected),
Topic = iolist_to_binary(TopicString),
Payload = iolist_to_binary(PayloadString),
QoS = render_simple_var(QoSTemplate, Selected, 0),
@ -202,13 +202,13 @@ safe_publish(RuleId, Topic, QoS, Flags, Payload, PubProps) ->
emqx_metrics:inc_msg(Msg).
parse_vars(Data) when is_binary(Data) ->
emqx_connector_template:parse(Data);
emqx_template:parse(Data);
parse_vars(Data) ->
{const, Data}.
parse_mqtt_properties(MQTTPropertiesTemplate) ->
maps:map(
fun(_Key, V) -> emqx_connector_template:parse(V) end,
fun(_Key, V) -> emqx_template:parse(V) end,
MQTTPropertiesTemplate
).
@ -220,13 +220,13 @@ parse_user_properties(<<"${pub_props.'User-Property'}">>) ->
?ORIGINAL_USER_PROPERTIES;
parse_user_properties(<<"${", _/binary>> = V) ->
%% use a variable
emqx_connector_template:parse(V);
emqx_template:parse(V);
parse_user_properties(_) ->
%% invalid, discard
undefined.
render_simple_var([{var, _Name, Accessor}], Data, Default) ->
case emqx_connector_template:lookup_var(Accessor, Data) of
case emqx_template:lookup_var(Accessor, Data) of
{ok, Var} -> Var;
%% cannot find the variable from Data
{error, _} -> Default
@ -236,8 +236,8 @@ render_simple_var({const, Val}, _Data, _Default) ->
parse_payload(Payload) ->
case string:is_empty(Payload) of
false -> emqx_connector_template:parse(Payload);
true -> emqx_connector_template:parse("${.}")
false -> emqx_template:parse(Payload);
true -> emqx_template:parse("${.}")
end.
render_pub_props(UserPropertiesTemplate, Selected, Env) ->
@ -259,7 +259,7 @@ render_mqtt_properties(MQTTPropertiesTemplate, Selected, Env) ->
fun(K, Template, Acc) ->
try
V = unicode:characters_to_binary(
emqx_connector_template:render_strict(Template, Selected)
emqx_template:render_strict(Template, Selected)
),
Acc#{K => V}
catch

View File

@ -14,9 +14,7 @@
%% limitations under the License.
%%--------------------------------------------------------------------
-module(emqx_connector_template).
-include_lib("emqx/include/emqx_placeholder.hrl").
-module(emqx_template).
-export([parse/1]).
-export([parse/2]).
@ -76,6 +74,8 @@
var_trans => var_trans()
}.
-define(PH_VAR_THIS, '$this').
-define(RE_PLACEHOLDER, "\\$\\{[.]?([a-zA-Z0-9._]*)\\}").
-define(RE_ESCAPE, "\\$\\{(\\$)\\}").

View File

@ -14,7 +14,7 @@
%% limitations under the License.
%%--------------------------------------------------------------------
-module(emqx_connector_template_sql).
-module(emqx_template_sql).
-export([parse/1]).
-export([parse/2]).
@ -27,15 +27,15 @@
-export_type([row_template/0]).
-type template() :: emqx_connector_template:t().
-type row_template() :: [emqx_connector_template:placeholder()].
-type bindings() :: emqx_connector_template:bindings().
-type template() :: emqx_template:t().
-type row_template() :: [emqx_template:placeholder()].
-type bindings() :: emqx_template:bindings().
-type values() :: [emqx_connector_sql:value()].
-type values() :: [emqx_utils_sql:value()].
-type parse_opts() :: #{
parameters => '$n' | ':n' | '?',
% Inherited from `emqx_connector_template:parse_opts()`
% Inherited from `emqx_template:parse_opts()`
strip_double_quote => boolean()
}.
@ -57,7 +57,7 @@ parse(String) ->
-spec parse(unicode:chardata(), parse_opts()) ->
template().
parse(String, Opts) ->
emqx_connector_template:parse(String, Opts).
emqx_template: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
@ -65,8 +65,8 @@ parse(String, Opts) ->
-spec render(template(), bindings(), render_opts()) ->
{unicode:chardata(), [_Error]}.
render(Template, Bindings, Opts) ->
emqx_connector_template:render(Template, Bindings, #{
var_trans => fun(Value) -> emqx_connector_sql:to_sql_string(Value, Opts) end
emqx_template:render(Template, Bindings, #{
var_trans => fun(Value) -> emqx_utils_sql:to_sql_string(Value, Opts) end
}).
%% @doc Render an SQL statement template given a set of bindings.
@ -74,8 +74,8 @@ render(Template, Bindings, Opts) ->
-spec render_strict(template(), bindings(), render_opts()) ->
unicode:chardata().
render_strict(Template, Bindings, Opts) ->
emqx_connector_template:render_strict(Template, Bindings, #{
var_trans => fun(Value) -> emqx_connector_sql:to_sql_string(Value, Opts) end
emqx_template:render_strict(Template, Bindings, #{
var_trans => fun(Value) -> emqx_utils_sql:to_sql_string(Value, Opts) end
}).
%% @doc Parse an SQL statement string into a prepared statement and a row template.
@ -83,7 +83,7 @@ render_strict(Template, Bindings, Opts) ->
%% during the execution of the prepared statement.
%% Example:
%% ```
%% {Statement, RowTemplate} = emqx_connector_template_sql:parse_prepstmt(
%% {Statement, RowTemplate} = emqx_template_sql:parse_prepstmt(
%% "INSERT INTO table (id, name, age) VALUES (${id}, ${name}, 42)",
%% #{parameters => '$n'}
%% ),
@ -93,7 +93,7 @@ render_strict(Template, Bindings, Opts) ->
-spec parse_prepstmt(unicode:chardata(), parse_opts()) ->
{unicode:chardata(), row_template()}.
parse_prepstmt(String, Opts) ->
Template = emqx_connector_template:parse(String, maps:with(?TEMPLATE_PARSE_OPTS, Opts)),
Template = emqx_template:parse(String, maps:with(?TEMPLATE_PARSE_OPTS, Opts)),
Statement = mk_prepared_statement(Template, Opts),
Placeholders = [Placeholder || Placeholder <- Template, element(1, Placeholder) == var],
{Statement, Placeholders}.
@ -123,15 +123,15 @@ mk_replace(':n', N) ->
%% @doc Render a row template into a list of SQL values.
%% 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_connector_sql:value()` for more details.
%% `emqx_utils_sql:value()` for more details.
-spec render_prepstmt(template(), bindings()) ->
{values(), [_Error]}.
render_prepstmt(Template, Bindings) ->
Opts = #{var_trans => fun emqx_connector_sql:to_sql_value/1},
emqx_connector_template:render(Template, Bindings, Opts).
Opts = #{var_trans => fun emqx_utils_sql:to_sql_value/1},
emqx_template:render(Template, Bindings, Opts).
-spec render_prepstmt_strict(template(), bindings()) ->
values().
render_prepstmt_strict(Template, Bindings) ->
Opts = #{var_trans => fun emqx_connector_sql:to_sql_value/1},
emqx_connector_template:render_strict(Template, Bindings, Opts).
Opts = #{var_trans => fun emqx_utils_sql:to_sql_value/1},
emqx_template:render_strict(Template, Bindings, Opts).

View File

@ -80,7 +80,7 @@ to_sql_value(Map) when is_map(Map) -> emqx_utils_json:encode(Map).
%% @doc Convert an Erlang term to a string that can be interpolated in literal
%% SQL statements. The value is escaped if necessary.
-spec to_sql_string(term(), Options) -> iodata() when
-spec to_sql_string(term(), Options) -> unicode:chardata() when
Options :: #{
escaping => cql | mysql | sql
}.
@ -98,7 +98,9 @@ to_sql_string(Term, #{escaping := cql}) ->
to_sql_string(Term, #{}) ->
maybe_escape(Term, fun escape_sql/1).
-spec maybe_escape(_Value, fun((binary()) -> iodata())) -> iodata().
-spec maybe_escape(_Value, fun((binary()) -> iodata())) -> unicode:chardata().
maybe_escape(undefined, _EscapeFun) ->
<<"NULL">>;
maybe_escape(Str, EscapeFun) when is_binary(Str) ->
EscapeFun(Str);
maybe_escape(Str, EscapeFun) when is_list(Str) ->
@ -109,9 +111,9 @@ maybe_escape(Str, EscapeFun) when is_list(Str) ->
error(Otherwise)
end;
maybe_escape(Val, EscapeFun) when is_atom(Val) orelse is_map(Val) ->
EscapeFun(emqx_utils_conv:bin(Val));
EscapeFun(emqx_template:to_string(Val));
maybe_escape(Val, _EscapeFun) ->
emqx_utils_conv:bin(Val).
emqx_template:to_string(Val).
-spec escape_sql(binary()) -> iodata().
escape_sql(S) ->

View File

@ -14,7 +14,7 @@
%% limitations under the License.
%%--------------------------------------------------------------------
-module(emqx_connector_template_SUITE).
-module(emqx_template_SUITE).
-compile(export_all).
-compile(nowarn_export_all).
@ -33,7 +33,7 @@ t_render(_) ->
l => [0, 1, 1000],
u => "utf-8 is ǝɹǝɥ"
},
Template = emqx_connector_template:parse(
Template = emqx_template:parse(
<<"a:${a},b:${b},c:${c},d:${d},d1:${d.d1},l:${l},u:${u}">>
),
?assertEqual(
@ -43,8 +43,8 @@ t_render(_) ->
t_render_var_trans(_) ->
Bindings = #{a => <<"1">>, b => 1, c => #{prop => 1.0}},
Template = emqx_connector_template:parse(<<"a:${a},b:${b},c:${c.prop}">>),
{String, Errors} = emqx_connector_template:render(
Template = emqx_template:parse(<<"a:${a},b:${b},c:${c.prop}">>),
{String, Errors} = emqx_template:render(
Template,
Bindings,
#{var_trans => fun(Name, _) -> "<" ++ Name ++ ">" end}
@ -56,10 +56,10 @@ t_render_var_trans(_) ->
t_render_path(_) ->
Bindings = #{d => #{d1 => <<"hi">>}},
Template = emqx_connector_template:parse(<<"d.d1:${d.d1}">>),
Template = emqx_template:parse(<<"d.d1:${d.d1}">>),
?assertEqual(
ok,
emqx_connector_template:validate(["d.d1"], Template)
emqx_template:validate(["d.d1"], Template)
),
?assertEqual(
{<<"d.d1:hi">>, []},
@ -68,10 +68,10 @@ t_render_path(_) ->
t_render_custom_ph(_) ->
Bindings = #{a => <<"a">>, b => <<"b">>},
Template = emqx_connector_template:parse(<<"a:${a},b:${b}">>),
Template = emqx_template:parse(<<"a:${a},b:${b}">>),
?assertEqual(
{error, [{"b", disallowed}]},
emqx_connector_template:validate(["a"], Template)
emqx_template:validate(["a"], Template)
),
?assertEqual(
<<"a:a,b:b">>,
@ -80,8 +80,8 @@ t_render_custom_ph(_) ->
t_render_this(_) ->
Bindings = #{a => <<"a">>, b => [1, 2, 3]},
Template = emqx_connector_template:parse(<<"this:${} / also:${.}">>),
?assertEqual(ok, emqx_connector_template:validate(["."], Template)),
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\"}">>,
@ -90,7 +90,7 @@ t_render_this(_) ->
t_render_missing_bindings(_) ->
Bindings = #{no => #{}},
Template = emqx_connector_template:parse(
Template = emqx_template:parse(
<<"a:${a},b:${b},c:${c},d:${d.d1},e:${no.such_atom_i_swear}">>
),
?assertEqual(
@ -116,33 +116,33 @@ t_render_missing_bindings(_) ->
t_unparse(_) ->
TString = <<"a:${a},b:${b},c:$${c},d:{${d.d1}},e:${$}{e},lit:${$}{$}">>,
Template = emqx_connector_template:parse(TString),
Template = emqx_template:parse(TString),
?assertEqual(
TString,
unicode:characters_to_binary(emqx_connector_template:unparse(Template))
unicode:characters_to_binary(emqx_template:unparse(Template))
).
t_const(_) ->
?assertEqual(
true,
emqx_connector_template:is_const(emqx_connector_template:parse(<<"">>))
emqx_template:is_const(emqx_template:parse(<<"">>))
),
?assertEqual(
false,
emqx_connector_template:is_const(
emqx_connector_template:parse(<<"a:${a},b:${b},c:${$}{c}">>)
emqx_template:is_const(
emqx_template:parse(<<"a:${a},b:${b},c:${$}{c}">>)
)
),
?assertEqual(
true,
emqx_connector_template:is_const(
emqx_connector_template:parse(<<"a:${$}{a},b:${$}{b}">>)
emqx_template:is_const(
emqx_template:parse(<<"a:${$}{a},b:${$}{b}">>)
)
).
t_render_partial_ph(_) ->
Bindings = #{a => <<"1">>, b => 1, c => 1.0, d => #{d1 => <<"hi">>}},
Template = emqx_connector_template:parse(<<"a:$a,b:b},c:{c},d:${d">>),
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)
@ -150,7 +150,7 @@ t_render_partial_ph(_) ->
t_parse_escaped(_) ->
Bindings = #{a => <<"1">>, b => 1, c => "VAR"},
Template = emqx_connector_template:parse(<<"a:${a},b:${$}{b},c:${$}{${c}},lit:${$}{$}">>),
Template = emqx_template:parse(<<"a:${a},b:${$}{b},c:${$}{${c}},lit:${$}{$}">>),
?assertEqual(
<<"a:1,b:${b},c:${VAR},lit:${$}">>,
render_strict_string(Template, Bindings)
@ -158,7 +158,7 @@ t_parse_escaped(_) ->
t_parse_escaped_dquote(_) ->
Bindings = #{a => <<"1">>, b => 1},
Template = emqx_connector_template:parse(<<"a:\"${a}\",b:\"${$}{b}\"">>, #{
Template = emqx_template:parse(<<"a:\"${a}\",b:\"${$}{b}\"">>, #{
strip_double_quote => true
}),
?assertEqual(
@ -169,30 +169,30 @@ t_parse_escaped_dquote(_) ->
t_parse_sql_prepstmt(_) ->
Bindings = #{a => <<"1">>, b => 1, c => 1.0, d => #{d1 => <<"hi">>}},
{PrepareStatement, RowTemplate} =
emqx_connector_template_sql:parse_prepstmt(<<"a:${a},b:${b},c:${c},d:${d}">>, #{
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_connector_template_sql:render_prepstmt(RowTemplate, Bindings)
emqx_template_sql:render_prepstmt(RowTemplate, Bindings)
).
t_parse_sql_prepstmt_n(_) ->
Bindings = #{a => undefined, b => true, c => atom, d => #{d1 => 42.1337}},
{PrepareStatement, RowTemplate} =
emqx_connector_template_sql:parse_prepstmt(<<"a:${a},b:${b},c:${c},d:${d}">>, #{
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_connector_template_sql:render_prepstmt_strict(RowTemplate, Bindings)
emqx_template_sql:render_prepstmt_strict(RowTemplate, Bindings)
).
t_parse_sql_prepstmt_colon(_) ->
{PrepareStatement, _RowTemplate} =
emqx_connector_template_sql:parse_prepstmt(<<"a=${a},b=${b},c=${c},d=${d}">>, #{
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)).
@ -200,9 +200,9 @@ t_parse_sql_prepstmt_colon(_) ->
t_parse_sql_prepstmt_partial_ph(_) ->
Bindings = #{a => <<"1">>, b => 1, c => 1.0, d => #{d1 => <<"hi">>}},
{PrepareStatement, RowTemplate} =
emqx_connector_template_sql:parse_prepstmt(<<"a:$a,b:b},c:{c},d:${d">>, #{parameters => '?'}),
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_connector_template_sql:render_prepstmt_strict(RowTemplate, Bindings)).
?assertEqual([], emqx_template_sql:render_prepstmt_strict(RowTemplate, Bindings)).
t_render_sql(_) ->
Bindings = #{
@ -213,14 +213,14 @@ t_render_sql(_) ->
n => undefined,
u => "utf8's cool 🐸"
},
Template = emqx_connector_template:parse(<<"a:${a},b:${b},c:${c},d:${d},n:${n},u:${u}">>),
Template = emqx_template:parse(<<"a:${a},b:${b},c:${c},d:${d},n:${n},u:${u}">>),
?assertMatch(
{_String, _Errors = []},
emqx_connector_template_sql:render(Template, Bindings, #{})
emqx_template_sql:render(Template, Bindings, #{})
),
?assertEqual(
<<"a:'1',b:1,c:1.0,d:'{\"d1\":\"hi\"}',n:NULL,u:'utf8\\'s cool 🐸'"/utf8>>,
bin(emqx_connector_template_sql:render_strict(Template, Bindings, #{}))
bin(emqx_template_sql:render_strict(Template, Bindings, #{}))
).
t_render_mysql(_) ->
@ -236,7 +236,7 @@ t_render_mysql(_) ->
g => "utf8's cool 🐸",
h => imgood
},
Template = emqx_connector_template_sql:parse(
Template = emqx_template_sql:parse(
<<"a:${a},b:${b},c:${c},d:${d},e:${e},f:${f},g:${g},h:${h}">>
),
?assertEqual(
@ -245,7 +245,7 @@ t_render_mysql(_) ->
"e:'\\\\\\0💩',f:0x6E6F6E2D75746638DCC900,g:'utf8\\'s cool 🐸',"/utf8,
"h:'imgood'"
>>,
bin(emqx_connector_template_sql:render_strict(Template, Bindings, #{escaping => mysql}))
bin(emqx_template_sql:render_strict(Template, Bindings, #{escaping => mysql}))
).
t_render_cql(_) ->
@ -257,18 +257,18 @@ t_render_cql(_) ->
c => 1.0,
d => #{d1 => <<"someone's phone">>}
},
Template = emqx_connector_template:parse(<<"a:${a},b:${b},c:${c},d:${d}">>),
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_connector_template_sql:render_strict(Template, Bindings, #{escaping => cql}))
bin(emqx_template_sql:render_strict(Template, Bindings, #{escaping => cql}))
).
t_render_sql_custom_ph(_) ->
{PrepareStatement, RowTemplate} =
emqx_connector_template_sql:parse_prepstmt(<<"a:${a},b:${b.c}">>, #{parameters => '$n'}),
emqx_template_sql:parse_prepstmt(<<"a:${a},b:${b.c}">>, #{parameters => '$n'}),
?assertEqual(
{error, [{"b.c", disallowed}]},
emqx_connector_template:validate(["a"], RowTemplate)
emqx_template:validate(["a"], RowTemplate)
),
?assertEqual(<<"a:$1,b:$2">>, bin(PrepareStatement)).
@ -276,57 +276,57 @@ t_render_sql_strip_double_quote(_) ->
Bindings = #{a => <<"a">>, b => <<"b">>},
%% no strip_double_quote option: "${key}" -> "value"
{PrepareStatement1, RowTemplate1} = emqx_connector_template_sql:parse_prepstmt(
{PrepareStatement1, RowTemplate1} = emqx_template_sql:parse_prepstmt(
<<"a:\"${a}\",b:\"${b}\"">>,
#{parameters => '$n'}
),
?assertEqual(<<"a:\"$1\",b:\"$2\"">>, bin(PrepareStatement1)),
?assertEqual(
[<<"a">>, <<"b">>],
emqx_connector_template_sql:render_prepstmt_strict(RowTemplate1, Bindings)
emqx_template_sql:render_prepstmt_strict(RowTemplate1, Bindings)
),
%% strip_double_quote = true: "${key}" -> value
{PrepareStatement2, RowTemplate2} = emqx_connector_template_sql:parse_prepstmt(
{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_connector_template_sql:render_prepstmt_strict(RowTemplate2, Bindings)
emqx_template_sql:render_prepstmt_strict(RowTemplate2, Bindings)
).
t_render_tmpl_deep(_) ->
Bindings = #{a => <<"1">>, b => 1, c => 1.0, d => #{d1 => <<"hi">>}},
Template = emqx_connector_template:parse_deep(
Template = emqx_template:parse_deep(
#{<<"${a}">> => [<<"$${b}">>, "c", 2, 3.0, '${d}', {[<<"${c}">>, <<"${$}{d}">>], 0}]}
),
?assertEqual(
{error, [{V, disallowed} || V <- ["b", "c"]]},
emqx_connector_template:validate(["a"], Template)
emqx_template:validate(["a"], Template)
),
?assertEqual(
#{<<"1">> => [<<"$1">>, "c", 2, 3.0, '${d}', {[<<"1.0">>, <<"${d}">>], 0}]},
emqx_connector_template:render_strict(Template, Bindings)
emqx_template:render_strict(Template, Bindings)
).
t_unparse_tmpl_deep(_) ->
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)).
Template = emqx_template:parse_deep(Term),
?assertEqual(Term, emqx_template:unparse(Template)).
%%
render_string(Template, Bindings) ->
{String, Errors} = emqx_connector_template:render(Template, Bindings),
{String, Errors} = emqx_template:render(Template, Bindings),
{bin(String), Errors}.
render_strict_string(Template, Bindings) ->
bin(emqx_connector_template:render_strict(Template, Bindings)).
bin(emqx_template:render_strict(Template, Bindings)).
bin(String) ->
unicode:characters_to_binary(String).