-module(erlquery).
-export([parse/1, codegen/1]).
-type erlq_module() :: iodata() | undefined.
-type erlq_query() :: {iodata(), iodata(), iodata()} | undefined.
-type erlq_method() :: {iodata(), iodata()}.
-type erlq_methods() :: [erlq_method()].
-type erlq_behaviour() :: iodata().
-type erlq_behaviours() :: [erlq_behaviour()].
-type erlq_value() :: erlq_module() | erlq_query() | erlq_methods().
-record(config,
{module :: erlq_module(),
query :: erlq_query(),
methods :: erlq_methods(),
behaviours :: erlq_behaviours()}).
-type config() :: #config{}.
-spec parse(iodata()) -> {ok, config()} | {error, any()}.
parse(S) ->
S2 = strip_comments(S),
Clauses = split_clauses(S2),
reduce_clauses(Clauses, init_config()).
-spec init_config() -> config().
init_config() ->
#config{methods = [], behaviours = []}.
-spec strip_comments(iodata()) -> iodata().
strip_comments(S) ->
Quotes = split_quotes(S),
lists:map(fun(Q) ->
case Q of
{noquote, S2} ->
re:replace(S2, "(?<!')%(?!')(.*?)\n", "", [global, {return, list}]);
{quote, S2} ->
S2
end
end,
Quotes).
-spec split_quotes(iodata()) -> [tuple()].
split_quotes(S) ->
lists:reverse(split_quotes(S, [])).
-spec split_quotes(iodata(), [tuple()]) -> [tuple()].
split_quotes(S, Acc) ->
case re:run(S, "'(.*?)'") of
nomatch ->
[{noquote, S} | Acc];
{match, [{Start, End}, _]} ->
PreSlice = string:slice(S, 0, Start),
QuoteSlice = string:slice(S, Start, Start + End),
PostSlice = string:slice(S, Start + End),
split_quotes(PostSlice, [{quote, QuoteSlice}, {noquote, PreSlice} | Acc])
end.
-spec split_clauses(iodata()) -> [{integer(), iodata()}].
split_clauses(S) ->
Clauses0 = string:split(S, ".\n", all),
CLs = add_line_numbers(Clauses0),
Clauses1 = lists:filter(fun not_whitespace/1, CLs),
Clauses2 = lists:map(fun({L, X}) -> {L, string:trim(X, both)} end, Clauses1),
lists:reverse(Clauses2).
-spec add_line_numbers([iodata()]) -> [{integer(), iodata()}].
add_line_numbers(Lines) ->
add_line_numbers(Lines, 1, []).
-spec add_line_numbers([iodata()], integer(), [{integer(), iodata()}]) ->
[{integer(), iodata()}].
add_line_numbers([], _, Acc) ->
Acc;
add_line_numbers([H | T], Counter, Acc) ->
N = length(string:split(H, <<"\n">>, all)),
add_line_numbers(T, Counter + 1 + N, [{Counter, H} | Acc]).
-spec not_whitespace({integer(), iodata()}) -> boolean().
not_whitespace({_, S}) ->
Res = re:replace(S, "[\s\n\t]", "", [global]),
B = iolist_to_binary(Res),
B =/= <<"">>.
-spec reduce_clauses([{integer(), iodata()}], config()) ->
{ok, config()} | {error, any()}.
reduce_clauses([], Config) ->
{ok, Config};
reduce_clauses([H | T], Config) ->
case reduce_clause(H, Config) of
{error, _Err} = Err ->
Err;
NewConfig ->
reduce_clauses(T, NewConfig)
end.
-spec reduce_clause({integer(), iodata()}, config()) -> config() | {error, any()}.
reduce_clause({Line, S}, Config) ->
case clause_type(S) of
module ->
M = match_module(S),
case validate_module(M) of
true ->
Config#config{module = M};
false ->
{error, {invalid_module_clause, {Line, S}}}
end;
behaviour ->
B = match_behaviour(S),
case validate_behaviour(B) of
true ->
Config#config{behaviours = [B | Config#config.behaviours]};
false ->
{error, {invalid_behaviour_clause, {Line, S}}}
end;
query ->
Q = match_query(S),
case validate_query(Q) of
true ->
case Q of
{_, _, <<"2">>} ->
Config#config{query = Q};
{_, _, <<"3">>} ->
Config#config{query = Q};
_ ->
{error, {invalid_query_arity, {Line, S}}}
end;
false ->
{error, {invalid_query_clause, {Line, S}}}
end;
method ->
Method = match_method(S),
case validate_method(Method) of
true ->
NewMethods = [Method | Config#config.methods],
Config#config{methods = NewMethods};
false ->
{error, {invalid_method_clause, {Line, S}}}
end;
_ ->
{error, {invalid_clause, {Line, S}}}
end.
-spec clause_type(iodata()) -> module | query | method | behaviour | nomatch.
clause_type(S) ->
case is_module_clause(S) of
true ->
module;
false ->
case is_query_clause(S) of
true ->
query;
false ->
case is_behaviour_clause(S) of
true ->
behaviour;
false ->
case is_method_clause(S) of
true ->
method;
false ->
nomatch
end
end
end
end.
-spec is_module_clause(iodata()) -> boolean().
is_module_clause(S) ->
case string:prefix(S, "-module(") of
nomatch ->
false;
_ ->
true
end.
-spec is_query_clause(iodata()) -> boolean().
is_query_clause(S) ->
case string:prefix(S, "-query(") of
nomatch ->
false;
_ ->
true
end.
-spec is_method_clause(iodata()) -> boolean().
is_method_clause(S) ->
case re:run(S, "([a-zA-Z0-9]+)\s?+->") of
{match, _} ->
true;
_ ->
false
end.
-spec is_behaviour_clause(iodata()) -> boolean().
is_behaviour_clause(S) ->
case string:prefix(S, "-behaviour(") of
nomatch ->
false;
_ ->
true
end.
%% Expecting the .\n suffix to be stripped by split_clauses
-spec match_method(iodata()) -> erlq_method() | nomatch.
match_method(S) ->
case re:run(S, "([a-zA-Z0-9_]+)[\s\n]?+(->)", [{capture, [1, 2]}]) of
{match, [{Start0, End0}, {Start1, End1}]} ->
Name = string:slice(S, Start0, End0),
Query =
string:trim(
string:slice(S, Start1 + End1), both),
case {Name, Query} of
{[], _} ->
nomatch;
{_, []} ->
nomatch;
_ ->
{bcast(Name), bcast(Query)}
end;
_ ->
nomatch
end.
-spec match_module(iodata()) -> erlq_module() | nomatch.
match_module(S) ->
case re:run(S, "-module\\(([a-zA-Z0-9_]+)\\)", [{capture, [1]}]) of
{match, [{Start, End}]} ->
case string:slice(S, Start, End) of
[] ->
nomatch;
Slice ->
bcast(Slice)
end;
_ ->
nomatch
end.
-spec match_behaviour(iodata()) -> erlq_module() | nomatch.
match_behaviour(S) ->
case re:run(S, "-behaviour\\(([a-zA-Z0-9_]+)\\)", [{capture, [1]}]) of
{match, [{Start, End}]} ->
case string:slice(S, Start, End) of
[] ->
nomatch;
Slice ->
bcast(Slice)
end;
_ ->
nomatch
end.
%% Expecting query to look like -query(pgo:query/2).
-spec match_query(iodata()) -> erlq_query() | nomatch.
match_query(S) ->
case re:run(S,
"-query\\(([a-zA-Z0-9_]+):([a-zA-Z0-9_]+)/([0-9]+)\\)",
[{capture, [1, 2, 3]}])
of
{match, [{Start0, End0}, {Start1, End1}, {Start2, End2}]} ->
Mod = string:slice(S, Start0, End0),
Fun = string:slice(S, Start1, End1),
Arity = string:slice(S, Start2, End2),
case {Mod, Fun, Arity} of
{[], _, _} ->
nomatch;
{_, [], _} ->
nomatch;
{_, _, []} ->
nomatch;
_ ->
{bcast(Mod), bcast(Fun), bcast(Arity)}
end;
_ ->
nomatch
end.
-spec bcast(binary() | iolist()) -> binary().
bcast(<<S/binary>>) ->
S;
bcast(S = [_ | _]) ->
list_to_binary(S).
-spec codegen(config()) ->
{ok, erl_parse:abstract_form()} | {error, {atom(), erlq_value()}}.
codegen(Config) ->
case validate_config(Config) of
ok ->
M = codegen_module_form(Config#config.module),
B = codegen_behaviour_forms(Config#config.behaviours),
Q = {_, Arity} = codegen_query_form(Config#config.query),
E = codegen_export_form(Config#config.methods, Arity),
Ms = codegen_method_forms(Q, Config#config.methods),
{ok, [M] ++ B ++ [E] ++ Ms};
E ->
E
end.
-spec codegen_module_form(binary()) -> erl_parse:erl_parse_tree().
codegen_module_form(Module) ->
{attribute, 0, module, binary_to_atom(Module)}.
-spec codegen_behaviour_forms(erlq_behaviours()) -> [erl_parse:erl_parse_tree()].
codegen_behaviour_forms(Behaviours) ->
lists:map(fun codegen_behaviour_form/1, Behaviours).
-spec codegen_behaviour_form(binary()) -> erl_parse:erl_parse_tree().
codegen_behaviour_form(Behaviour) ->
{attribute, 0, behaviour, binary_to_atom(Behaviour)}.
-spec codegen_query_form(erlq_query()) ->
{erl_parse:erl_parse_tree(), iodata()} | {undefined, iodata()}.
codegen_query_form({Module, Method, Arity}) ->
{{remote,
0,
{atom, 0, binary_to_atom(bcast(Module))},
{atom, 0, binary_to_atom(bcast(Method))}},
Arity};
codegen_query_form(undefined) ->
{undefined, <<"0">>}.
-spec codegen_method_forms({erl_parse:erl_parse_tree(), iodata()} | undefined,
[{iodata(), iodata()}]) ->
[erl_parse:erl_parse_tree()].
codegen_method_forms(Query, Methods) ->
reduce_method_forms(Query, Methods, []).
-spec reduce_method_forms(erl_parse:erl_parse_tree() | undefined,
[{iodata(), iodata()}],
[erl_parse:erl_parse_tree()]) ->
[erl_parse:erl_parse_tree()].
reduce_method_forms(Query, [H | T], Acc) ->
Res = codegen_method_form(Query, H),
reduce_method_forms(Query, T, Acc ++ Res);
reduce_method_forms(_Query, [], Acc) ->
Acc.
-spec codegen_method_form({erl_parse:erl_parse_tree(), iodata()} | {undefined, iodata()},
{iodata(), iodata()}) ->
erl_parse:erl_parse_tree().
codegen_method_form({undefined, _}, {Method, QueryText}) ->
AM = binary_to_atom(bcast(Method)),
QueryS = lcast(QueryText),
[{function, 0, AM, 0, [{clause, 0, [], [], [{call, 0, {atom, 0, AM}, [{nil, 0}]}]}]},
{function,
0,
AM,
1,
[{clause,
0,
[{var, 0, '_Args'}],
[],
[{bin, 0, [{bin_element, 0, {string, 0, QueryS}, default, default}]}]}]}];
codegen_method_form({Query, QueryArity}, {Method, QueryText}) ->
case QueryArity of
<<"2">> ->
codegen_method_form_2(Query, Method, QueryText);
"2" ->
codegen_method_form_2(Query, Method, QueryText);
<<"3">> ->
codegen_method_form_3(Query, Method, QueryText);
"3" ->
codegen_method_form_3(Query, Method, QueryText)
end.
-spec codegen_method_form_2(erl_parse:erl_parse_tree(), iodata(), iodata()) ->
erl_parse:erl_parse_tree().
codegen_method_form_2(Query, Method, QueryText) ->
AM = binary_to_atom(bcast(Method)),
QueryS = lcast(QueryText),
[{function,
0,
AM,
0,
[{clause, 0, [], [], [{call, 0, {atom, 0, AM}, [{nil, 0}]}]}]}, %% Optional arg
{function,
0,
AM,
1,
[{clause,
0,
[{var, 0, 'Args'}],
[],
[{call,
0,
Query,
[{bin, 0, [{bin_element, 0, {string, 0, QueryS}, default, default}]},
{var, 0, 'Args'}]}]}]}].
-spec codegen_method_form_3(erl_parse:erl_parse_tree(), iodata(), iodata()) ->
erl_parse:erl_parse_tree().
codegen_method_form_3(Query, Method, QueryText) ->
AM = binary_to_atom(bcast(Method)),
QueryS = lcast(QueryText),
[{function,
0,
AM,
1,
[{clause,
0,
[{var, 0, 'Conn'}],
[],
[{call, 0, {atom, 0, AM}, [{var, 0, 'Conn'}, {nil, 0}]}]}]},
{function,
0,
AM,
2,
[{clause,
0,
[{var, 0, 'Conn'}, {var, 0, 'Args'}],
[],
[{call,
0,
Query,
[{var, 0, 'Conn'},
{bin, 0, [{bin_element, 0, {string, 0, QueryS}, default, default}]},
{var, 0, 'Args'}]}]}]}].
-spec lcast(iodata()) -> string().
lcast(<<S/binary>>) ->
binary_to_list(S);
lcast(S) ->
S.
-spec codegen_export_form([{binary(), binary()}], iodata()) -> erl_parse:erl_parse_tree().
codegen_export_form(Methods, Arity) ->
{attribute, 0, export, reduce_exports_form(Methods, Arity, [])}.
-spec reduce_exports_form([{binary(), binary()}],
iodata(),
[erl_parse:erl_parse_tree()]) ->
[erl_parse:erl_parse_tree()].
reduce_exports_form([H | T], Arity, Acc) ->
{Res1, Res0} = codegen_export_method_form(H, Arity),
reduce_exports_form(T, Arity, Acc ++ [Res1, Res0]);
reduce_exports_form([], _Arity, Acc) ->
Acc.
-spec codegen_export_method_form({iodata(), iodata()}, iodata()) ->
{erl_parse:erl_parse_tree(), erl_parse:erl_parse_tree()}.
codegen_export_method_form({Name, _QueryText}, Arity) ->
case Arity of
<<"0">> ->
{{binary_to_atom(bcast(Name)), 1}, {binary_to_atom(bcast(Name)), 0}};
"0" ->
{{binary_to_atom(bcast(Name)), 1}, {binary_to_atom(bcast(Name)), 0}};
<<"2">> ->
{{binary_to_atom(bcast(Name)), 1}, {binary_to_atom(bcast(Name)), 0}};
"2" ->
{{binary_to_atom(bcast(Name)), 1}, {binary_to_atom(bcast(Name)), 0}};
<<"3">> ->
{{binary_to_atom(bcast(Name)), 2}, {binary_to_atom(bcast(Name)), 1}};
"3" ->
{{binary_to_atom(bcast(Name)), 2}, {binary_to_atom(bcast(Name)), 1}}
end.
-spec validate_config(config()) -> ok | {error, {atom(), any()}}.
validate_config(Config) ->
case validate_module(Config#config.module) of
true ->
case validate_behaviours(Config#config.behaviours) of
true ->
case validate_query(Config#config.query) of
true ->
case validate_query_arity(Config#config.query) of
true ->
case validate_methods(Config#config.methods) of
true ->
ok;
false ->
{error, {invalid_methods, Config#config.methods}}
end;
false ->
{error, {invalid_query_arity, Config#config.query}}
end;
false ->
{error, {invalid_query, Config#config.query}}
end;
false ->
{error, {invalid_behaviours, Config#config.behaviours}}
end;
false ->
{error, {invalid_module, Config#config.module}}
end.
-spec validate_module(erlq_module() | any()) -> boolean().
validate_module(<<_/binary>>) ->
true;
validate_module(_) ->
false.
-spec validate_behaviour(erlq_behaviour() | any()) -> boolean().
validate_behaviour(<<_/binary>>) ->
true;
validate_behaviour(_) ->
false.
-spec validate_query(erlq_query()) -> boolean().
validate_query({<<_/binary>>, <<_/binary>>, <<_/binary>>}) ->
true;
validate_query(undefined) ->
true;
validate_query(_) ->
false.
-spec validate_query_arity(erlq_query()) -> boolean().
validate_query_arity(undefined) ->
true;
validate_query_arity({_, _, <<"2">>}) ->
true;
validate_query_arity({_, _, <<"3">>}) ->
true;
validate_query_arity({_, _, "2"}) ->
true;
validate_query_arity({_, _, "3"}) ->
true;
validate_query_arity(_) ->
false.
-spec validate_behaviours(erlq_behaviours() | any()) -> boolean().
validate_behaviours(_ = []) ->
true;
validate_behaviours(_ = [H | T]) ->
case validate_behaviour(H) of
true ->
validate_behaviours(T);
false ->
false
end.
-spec validate_methods(erlq_methods() | any()) -> boolean().
validate_methods(_ = []) ->
true;
validate_methods(_ = [H | T]) ->
case validate_method(H) of
true ->
validate_methods(T);
false ->
false
end.
-spec validate_method(erlq_method() | any()) -> boolean().
validate_method(_ = {<<_/binary>>, <<_/binary>>}) ->
true;
validate_method(_) ->
false.
-ifdef(TEST).
-include_lib("eunit/include/eunit.hrl").
match_module_test() ->
?assertEqual(<<"foobar">>, match_module("-module(foobar).")),
?assertEqual(<<"foo_bar">>, match_module("-module(foo_bar).")).
match_query_test() ->
?assertEqual({<<"pgo">>, <<"query">>, <<"2">>}, match_query("-query(pgo:query/2)")),
?assertEqual({<<"pgo">>, <<"super_query">>, <<"2">>},
match_query("-query(pgo:super_query/2)")),
?assertEqual({<<"p_go">>, <<"super_query">>, <<"2">>},
match_query("-query(p_go:super_query/2)")).
match_method_test() ->
?assertEqual({<<"foobar">>, <<"SELECT * FROM foobar">>},
match_method("foobar ->\n SELECT * FROM foobar")),
?assertEqual({<<"foobar">>, <<"SELECT * FROM foobar">>},
match_method("foobar->\n SELECT * FROM foobar")),
?assertEqual({<<"foobar">>,
<<"SELECT * FROM foobar\nINNER JOIN accounts ON foobar.id = accounts.id">>},
match_method("foobar ->\n SELECT * FROM foobar\nINNER JOIN accounts ON foobar.id = accounts.id")),
?assertEqual({<<"foo_bar">>, <<"SELECT * FROM foobar">>},
match_method("foo_bar ->\n SELECT * FROM foobar")).
match_behaviour_test() ->
?assertEqual(<<"foobar">>, match_behaviour("-behaviour(foobar).")),
?assertEqual(<<"foo_bar">>, match_behaviour("-behaviour(foo_bar).")).
is_module_clause_test() ->
?assert(is_module_clause("-module(foobar).")),
?assertNot(is_module_clause("foobar")).
is_query_clause_test() ->
?assert(is_query_clause("-query(foobar:goo/3).")),
?assertNot(is_query_clause("foobar")).
is_method_clause_test() ->
?assert(is_method_clause("foobar -> jhgjgj")),
?assert(is_method_clause("foobar-> jhgjgj")),
?assertNot(is_method_clause("foobar")),
?assert(is_method_clause("foo->bar")).
is_behaviour_clause_test() ->
?assert(is_behaviour_clause("-behaviour(foobar).")),
?assertNot(is_behaviour_clause("-foobar(behaviour).")),
?assertNot(is_behaviour_clause("foobar")).
clause_type_test() ->
?assertEqual(module, clause_type("-module(foobar)")),
?assertEqual(query, clause_type("-query(foobar:goo/2)")),
?assertEqual(method, clause_type("foo -> SELECT * FROM foobar")),
?assertEqual(behaviour, clause_type("-behaviour(foobar)")),
?assertEqual(nomatch, clause_type("foobar")).
reduce_clause_test() ->
?assertEqual(#config{module = <<"foo">>}, reduce_clause({0, "-module(foo)"}, #config{})),
?assertEqual(#config{query = {<<"foo">>, <<"bar">>, <<"2">>}},
reduce_clause({0, "-query(foo:bar/2)"}, #config{})),
?assertEqual(#config{query = {<<"foo">>, <<"bar">>, <<"3">>}},
reduce_clause({0, "-query(foo:bar/3)"}, #config{})),
?assertEqual(#config{methods = [{<<"foobar">>, <<"SELECT * FROM foobars">>}]},
reduce_clause({0, "foobar -> SELECT * FROM foobars"}, #config{methods = []})),
?assertEqual(#config{behaviours = [<<"foobar">>]},
reduce_clause({0, "-behaviour(foobar)"}, #config{behaviours = []})),
?assertMatch({error, {invalid_clause, {_, _}}}, reduce_clause({0, "stuff"}, #config{})),
?assertMatch({error, {invalid_module_clause, {_, _}}},
reduce_clause({0, "-module()"}, #config{})),
?assertMatch({error, {invalid_module_clause, {_, _}}},
reduce_clause({0, "-module( )"}, #config{})),
?assertMatch({error, {invalid_query_arity, {_, _}}},
reduce_clause({0, "-query(foo:bar/4)"}, #config{})),
?assertMatch({error, {invalid_query_clause, {_, _}}},
reduce_clause({0, "-query(foo: /3)"}, #config{})),
?assertMatch({error, {invalid_query_clause, {_, _}}},
reduce_clause({0, "-query()"}, #config{})),
?assertMatch({error, {invalid_method_clause, {_, _}}},
reduce_clause({0, "foobar ->"}, #config{})),
?assertMatch({error, {invalid_clause, {_, _}}},
reduce_clause({0, " -> foobar"}, #config{})).
add_line_numbers_test() ->
?assertEqual([], add_line_numbers([])),
?assertEqual([{1, "foobar"}], add_line_numbers(["foobar"])),
?assertEqual([{5, "foobar"}, {1, "foobar\n\nhello"}],
add_line_numbers(["foobar\n\nhello", "foobar"])).
not_whitespace_test() ->
?assert(not_whitespace({0, "foobar"})),
?assertNot(not_whitespace({0, <<"">>})),
?assertNot(not_whitespace({0, <<"\t">>})),
?assertNot(not_whitespace({0, <<" ">>})),
?assert(not_whitespace({0, <<"foobar sdfsdfsdf">>})).
split_clauses_test() ->
S = "-module(foobar).\n\n-query(pgo:query/2).\n\nbarfoo->\n SELECT * FROM foobars.\n",
?assertMatch([{_, "-module(foobar)"},
{_, "-query(pgo:query/2)"},
{_, "barfoo->\n SELECT * FROM foobars"}],
split_clauses(S)).
split_clauses_weird_test() ->
S = "-module(foobar).\n-query(pgo:query/2).\n\nbarfoo->\n SELECT * FROM foobars.\n\t",
?assertMatch([{_, "-module(foobar)"},
{_, "-query(pgo:query/2)"},
{_, "barfoo->\n SELECT * FROM foobars"}],
split_clauses(S)).
split_quotes_test() ->
S = "'foobar' hello there 'hello'",
?assertEqual([{noquote, []},
{quote, "'foobar'"},
{noquote, " hello there "},
{quote, "'hello'"},
{noquote, []}],
split_quotes(S)).
strip_comments_test() ->
S =
"%% This is a comment\n%yet another comment\n%%This is another comment\n-module(foobar).\n\n-query(pgo:query/2).\n\nbarfoo->\n SELECT * FROM foobars.\n",
?assert(string:equal("-module(foobar).\n\n-query(pgo:query/2).\n\nbarfoo->\n SELECT * FROM foobars.\n",
strip_comments(S))).
parse_test() ->
?assertEqual({ok,
#config{module = <<"fooq">>,
query = {<<"foo">>, <<"bar">>, <<"2">>},
methods = [{<<"foobar">>, <<"SELECT * FROM foobars">>}],
behaviours = []}},
parse(list_to_binary(["-module(fooq).\n\n",
"-query(foo:bar/2).\n\n",
"foobar ->\n",
" SELECT * FROM foobars.\n\n"]))).
parse_with_comments_test() ->
?assertEqual({ok,
#config{module = <<"fooq">>,
query = {<<"foo">>, <<"bar">>, <<"2">>},
methods = [{<<"foobar">>, <<"SELECT * FROM foobars">>}],
behaviours = []}},
parse(list_to_binary(["%% This is a comment\n"
"%% This is a comment\n",
"-module(fooq).%This is a comment\n\n",
"-query(foo:bar/2).\n\n",
"%This is a comment\n",
"%\n",
"foobar ->\n",
"%% This is a comment here in the function clause\n",
" SELECT * FROM foobars.\n\n"]))).
parse_with_comments_with_wildcard_test() ->
?assertEqual({ok,
#config{module = <<"fooq">>,
query = {<<"foo">>, <<"bar">>, <<"2">>},
methods =
[{<<"foobar">>, <<"SELECT * FROM foobars WHERE name LIKE '%foo%'">>}],
behaviours = []}},
parse(list_to_binary(["-module(fooq).\n\n",
"-query(foo:bar/2).\n\n",
"foobar ->\n",
" SELECT * FROM foobars WHERE name LIKE '%foo%'.\n\n"]))),
?assertEqual({ok,
#config{module = <<"fooq">>,
query = {<<"foo">>, <<"bar">>, <<"2">>},
methods =
[{<<"foobar">>,
<<"SELECT * FROM foobars WHERE name LIKE ' % foo % '">>}],
behaviours = []}},
parse(list_to_binary(["-module(fooq).\n\n",
"-query(foo:bar/2).\n\n",
"foobar ->\n",
" SELECT * FROM foobars WHERE name LIKE ' % foo % '.\n\n"]))),
?assertEqual({ok,
#config{module = <<"fooq">>,
query = {<<"foo">>, <<"bar">>, <<"2">>},
methods =
[{<<"foobar">>,
<<"SELECT * FROM foobars WHERE name LIKE '%\nfoo\n%'">>}],
behaviours = []}},
parse(list_to_binary(["-module(fooq).\n\n",
"-query(foo:bar/2).\n\n",
"foobar ->\n",
" SELECT * FROM foobars WHERE name LIKE '%\nfoo\n%'.\n\n"]))),
?assertEqual({ok,
#config{module = <<"fooq">>,
query = {<<"foo">>, <<"bar">>, <<"2">>},
methods =
[{<<"foobar">>,
<<"SELECT * FROM foobars WHERE name LIKE 'abc%abcfooabc%bca'">>}],
behaviours = []}},
parse(list_to_binary(["-module(fooq).\n\n",
"-query(foo:bar/2).\n\n",
"foobar ->\n",
" SELECT * FROM foobars WHERE name LIKE 'abc%abcfooabc%bca'.\n\n"]))),
?assertEqual({ok,
#config{module = <<"fooq">>,
query = {<<"foo">>, <<"bar">>, <<"2">>},
methods =
[{<<"foobar">>,
<<"SELECT * FROM foobars WHERE name LIKE ' abc %abcfooabc %bca'">>}],
behaviours = []}},
parse(list_to_binary(["-module(fooq).\n\n",
"-query(foo:bar/2).\n\n",
"foobar ->\n",
" SELECT * FROM foobars WHERE name LIKE ' abc %abcfooabc %bca'.\n\n"]))).
parse_no_query_test() ->
?assertEqual({ok,
#config{module = <<"fooq">>,
methods = [{<<"foobar">>, <<"SELECT * FROM foobars">>}],
behaviours = []}},
parse(list_to_binary(["-module(fooq).\n\n",
"foobar ->\n",
" SELECT * FROM foobars.\n\n"]))).
parse_query_3_test() ->
?assertEqual({ok,
#config{module = <<"fooq">>,
query = {<<"foo">>, <<"bar">>, <<"3">>},
methods = [{<<"foobar">>, <<"SELECT * FROM foobars">>}],
behaviours = []}},
parse(list_to_binary(["-module(fooq).\n\n",
"-query(foo:bar/3).\n\n",
"foobar ->\n",
" SELECT * FROM foobars.\n\n"]))).
codegen_export_method_form_test() ->
?assertEqual({{foobar, 1}, {foobar, 0}},
codegen_export_method_form({<<"foobar">>, <<"barfoo">>}, <<"2">>)).
codegen_export_form_test() ->
?assertEqual({attribute, 0, export, [{foobar, 1}, {foobar, 0}, {barfoo, 1}, {barfoo, 0}]},
codegen_export_form([{"foobar", "blah"}, {"barfoo", "blahblah"}], <<"2">>)).
codegen_query_form_test() ->
?assertEqual({{remote, 0, {atom, 0, barfoo}, {atom, 0, query}}, <<"2">>},
codegen_query_form({<<"barfoo">>, <<"query">>, <<"2">>})),
?assertEqual({{remote, 0, {atom, 0, barfoo}, {atom, 0, query}}, "2"},
codegen_query_form({"barfoo", "query", "2"})),
?assertEqual({{remote, 0, {atom, 0, barfoo}, {atom, 0, query}}, <<"3">>},
codegen_query_form({<<"barfoo">>, <<"query">>, <<"3">>})),
?assertEqual({{remote, 0, {atom, 0, barfoo}, {atom, 0, query}}, "3"},
codegen_query_form({"barfoo", "query", "3"})),
?assertEqual({undefined, <<"0">>}, codegen_query_form(undefined)).
codegen_module_form_test() ->
?assertEqual({attribute, 0, module, hello}, codegen_module_form(<<"hello">>)).
codegen_behaviour_form_test() ->
?assertEqual({attribute, 0, behaviour, hello}, codegen_behaviour_form(<<"hello">>)).
codegen_behaviour_forms_test() ->
?assertEqual([{attribute, 0, behaviour, hello}, {attribute, 0, behaviour, world}],
codegen_behaviour_forms([<<"hello">>, <<"world">>])).
codegen_method_form_test() ->
?assertEqual([{function,
0,
foobar,
0,
[{clause, 0, [], [], [{call, 0, {atom, 0, foobar}, [{nil, 0}]}]}]},
{function,
0,
foobar,
1,
[{clause,
0,
[{var, 0, 'Args'}],
[],
[{call,
0,
{remote, 0, {atom, 0, barfoo}, {atom, 0, query}},
[{bin,
0,
[{bin_element,
0,
{string, 0, "SELECT * FROM foobars"},
default,
default}]},
{var, 0, 'Args'}]}]}]}],
codegen_method_form({{remote, 0, {atom, 0, barfoo}, {atom, 0, query}}, <<"2">>},
{<<"foobar">>, <<"SELECT * FROM foobars">>})),
?assertEqual([{function,
0,
foobar,
0,
[{clause, 0, [], [], [{call, 0, {atom, 0, foobar}, [{nil, 0}]}]}]},
{function,
0,
foobar,
1,
[{clause,
0,
[{var, 0, '_Args'}],
[],
[{bin,
0,
[{bin_element,
0,
{string, 0, "SELECT * FROM foobars"},
default,
default}]}]}]}],
codegen_method_form({undefined, <<"0">>},
{<<"foobar">>, <<"SELECT * FROM foobars">>})),
?assertEqual([{function,
0,
foobar,
1,
[{clause,
0,
[{var, 0, 'Conn'}],
[],
[{call, 0, {atom, 0, foobar}, [{var, 0, 'Conn'}, {nil, 0}]}]}]},
{function,
0,
foobar,
2,
[{clause,
0,
[{var, 0, 'Conn'}, {var, 0, 'Args'}],
[],
[{call,
0,
{remote, 0, {atom, 0, barfoo}, {atom, 0, query}},
[{var, 0, 'Conn'},
{bin,
0,
[{bin_element,
0,
{string, 0, "SELECT * FROM foobars"},
default,
default}]},
{var, 0, 'Args'}]}]}]}],
codegen_method_form({{remote, 0, {atom, 0, barfoo}, {atom, 0, query}}, <<"3">>},
{<<"foobar">>, <<"SELECT * FROM foobars">>})).
codegen_method_forms_test() ->
?assertEqual([{function,
0,
foobar,
0,
[{clause, 0, [], [], [{call, 0, {atom, 0, foobar}, [{nil, 0}]}]}]},
{function,
0,
foobar,
1,
[{clause,
0,
[{var, 0, 'Args'}],
[],
[{call,
0,
{remote, 0, {atom, 0, barfoo}, {atom, 0, query}},
[{bin,
0,
[{bin_element,
0,
{string, 0, "SELECT * FROM foobars"},
default,
default}]},
{var, 0, 'Args'}]}]}]}],
codegen_method_forms({{remote, 0, {atom, 0, barfoo}, {atom, 0, query}}, <<"2">>},
[{<<"foobar">>, <<"SELECT * FROM foobars">>}])),
?assertEqual([{function,
0,
foobar,
0,
[{clause, 0, [], [], [{call, 0, {atom, 0, foobar}, [{nil, 0}]}]}]},
{function,
0,
foobar,
1,
[{clause,
0,
[{var, 0, '_Args'}],
[],
[{bin,
0,
[{bin_element,
0,
{string, 0, "SELECT * FROM foobars"},
default,
default}]}]}]}],
codegen_method_forms({undefined, <<"0">>},
[{<<"foobar">>, <<"SELECT * FROM foobars">>}])).
codegen_forms_test() ->
?assertEqual({ok,
[{attribute, 0, module, things},
{attribute, 0, behaviour, hello},
{attribute, 0, behaviour, world},
{attribute, 0, export, [{foobar, 1}, {foobar, 0}]},
{function,
0,
foobar,
0,
[{clause, 0, [], [], [{call, 0, {atom, 0, foobar}, [{nil, 0}]}]}]},
{function,
0,
foobar,
1,
[{clause,
0,
[{var, 0, 'Args'}],
[],
[{call,
0,
{remote, 0, {atom, 0, bar}, {atom, 0, foo}},
[{bin,
0,
[{bin_element,
0,
{string, 0, "SELECT * FROM foobars"},
default,
default}]},
{var, 0, 'Args'}]}]}]}]},
codegen(#config{module = <<"things">>,
query = {<<"bar">>, <<"foo">>, <<"2">>},
methods = [{<<"foobar">>, <<"SELECT * FROM foobars">>}],
behaviours = [<<"hello">>, <<"world">>]})).
codegen_forms_no_query_test() ->
?assertEqual({ok,
[{attribute, 0, module, things},
{attribute, 0, export, [{foobar, 1}, {foobar, 0}]},
{function,
0,
foobar,
0,
[{clause, 0, [], [], [{call, 0, {atom, 0, foobar}, [{nil, 0}]}]}]},
{function,
0,
foobar,
1,
[{clause,
0,
[{var, 0, '_Args'}],
[],
[{bin,
0,
[{bin_element,
0,
{string, 0, "SELECT * FROM foobars"},
default,
default}]}]}]}]},
codegen(#config{module = <<"things">>,
methods = [{<<"foobar">>, <<"SELECT * FROM foobars">>}],
behaviours = []})).
compile_forms_test() ->
{ok, Forms} =
codegen(#config{module = <<"ftest">>,
query = {<<"erlquery_mock">>, <<"query">>, <<"2">>},
methods = [{<<"foobar">>, <<"SELECT * FROM foobars">>}],
behaviours = [<<"foo">>]}),
Res = compile:forms(Forms),
?assertMatch({ok, ftest, _}, Res),
{_, _, Bin} = Res,
?assertEqual({module, ftest}, code:load_binary(ftest, "ftest.beam", Bin)),
?assertEqual({<<"SELECT * FROM foobars">>, [abc]}, ftest:foobar([abc])),
?assertEqual({<<"SELECT * FROM foobars">>, []}, ftest:foobar()).
compile_forms_query_3_test() ->
{ok, Forms} =
codegen(#config{module = <<"ftest3">>,
query = {<<"erlquery_mock">>, <<"query">>, <<"3">>},
methods = [{<<"foobar">>, <<"SELECT * FROM foobars">>}],
behaviours = []}),
Res = compile:forms(Forms),
?assertMatch({ok, ftest3, _}, Res),
{_, _, Bin} = Res,
?assertEqual({module, ftest3}, code:load_binary(ftest3, "ftest3.beam", Bin)),
Conn = mock_connection,
?assertEqual({Conn, <<"SELECT * FROM foobars">>, []}, ftest3:foobar(Conn)),
?assertEqual({Conn, <<"SELECT * FROM foobars">>, [abc]}, ftest3:foobar(Conn, [abc])).
compile_forms_no_query_test() ->
{ok, Forms} =
codegen(#config{module = <<"fqtest">>,
methods = [{<<"foobar">>, <<"SELECT * FROM foobars">>}],
behaviours = []}),
Res = compile:forms(Forms),
?assertMatch({ok, fqtest, _}, Res),
{_, _, Bin} = Res,
?assertEqual({module, fqtest}, code:load_binary(fqtest, "fqtest.beam", Bin)),
?assertEqual(<<"SELECT * FROM foobars">>, fqtest:foobar([])),
?assertEqual(<<"SELECT * FROM foobars">>, fqtest:foobar()).
validate_module_test() ->
?assert(validate_module(<<"foo">>)),
?assertNot(validate_module("foo")),
?assertNot(validate_module(1)),
?assertNot(validate_module(1.0)),
?assertNot(validate_module(true)),
?assertNot(validate_module([])),
?assertNot(validate_module(#{})).
validate_behaviour_test() ->
?assert(validate_behaviour(<<"foo">>)),
?assertNot(validate_behaviour("foo")),
?assertNot(validate_behaviour(1)),
?assertNot(validate_behaviour(1.0)),
?assertNot(validate_behaviour(true)),
?assertNot(validate_behaviour([])),
?assertNot(validate_behaviour(#{})).
validate_behaviours_test() ->
?assert(validate_behaviours([])),
?assert(validate_behaviours([<<"foo">>])),
?assert(validate_behaviours([<<"foo">>, <<"bar">>, <<"foobar">>])),
?assertNot(validate_behaviours(["foo", "bar", 1, 2])).
validate_query_test() ->
?assert(validate_query({<<"foo">>, <<"bar">>, <<"boo">>})),
?assertNot(validate_query({"foo", "bar", "boo"})),
?assertNot(validate_query({1, 2, 3})),
?assertNot(validate_query({1.0, 2.3, 4.5})),
?assert(validate_query(undefined)).
validate_query_arity_test() ->
?assert(validate_query_arity({<<"foo">>, <<"bar">>, <<"2">>})),
?assert(validate_query_arity({<<"foo">>, <<"bar">>, <<"3">>})),
?assert(validate_query_arity({<<"foo">>, <<"bar">>, "2"})),
?assert(validate_query_arity({<<"foo">>, <<"bar">>, "3"})),
?assert(validate_query_arity(undefined)),
?assertNot(validate_query_arity({<<"foo">>, <<"bar">>, <<"4">>})),
?assertNot(validate_query_arity({<<"foo">>, <<"bar">>, <<"1">>})),
?assertNot(validate_query_arity({<<"foo">>, <<"bar">>, <<"42">>})),
?assertNot(validate_query_arity({<<"foo">>, <<"bar">>, <<"22">>})).
validate_method_test() ->
?assert(validate_method({<<"foo">>, <<"bar">>})),
?assertNot(validate_method({"foo", "bar"})),
?assertNot(validate_method({1, 2})),
?assertNot(validate_method(nomatch)).
validate_methods_test() ->
?assert(validate_methods([])),
?assert(validate_methods([{<<"foo">>, <<"bar">>}])),
?assert(validate_methods([{<<"foo">>, <<"bar">>},
{<<"foo">>, <<"bar">>},
{<<"foo">>, <<"bar">>}])),
?assertNot(validate_methods([{<<"foo">>, <<"bar">>}, {"foo", "bar"}, {1, 2}])).
validate_config_test() ->
?assertEqual(ok,
validate_config(#config{module = <<"foobar">>,
query = {<<"foob">>, <<"bar">>, <<"2">>},
methods = [{<<"hello">>, <<"yep">>}],
behaviours = []})),
?assertEqual(ok,
validate_config(#config{module = <<"foobar">>,
query = {<<"foo">>, <<"bar">>, <<"3">>},
methods = [{<<"stuff">>, <<"hello">>}],
behaviours = []})),
?assertEqual(ok,
validate_config(#config{module = <<"foobar">>,
query = {<<"foo">>, <<"bar">>, <<"3">>},
methods = [{<<"stuff">>, <<"hello">>}],
behaviours = [<<"foo">>, <<"bar">>]})),
?assertEqual(ok,
validate_config(#config{module = <<"foobar">>,
methods = [{<<"hello">>, <<"yep">>}],
behaviours = []})),
?assertMatch({error, {invalid_module, _}},
validate_config(#config{module = 42,
query = {<<"foo">>, <<"bar">>, <<"foobar">>},
methods = [{<<"stuff">>, <<"hello world">>}],
behaviours = []})),
?assertMatch({error, {invalid_query, _}},
validate_config(#config{module = <<"foobar">>,
query = {1, 2, 3},
methods = [{<<"stuff">>, <<"hello">>}],
behaviours = []})),
?assertMatch({error, {invalid_methods, _}},
validate_config(#config{module = <<"foobar">>,
query = {<<"foo">>, <<"bar">>, <<"2">>},
methods = [{1, 2}],
behaviours = []})),
?assertMatch({error, {invalid_behaviours, _}},
validate_config(#config{module = <<"foobar">>,
query = {<<"foo">>, <<"bar">>, <<"3">>},
methods = [{<<"stuff">>, <<"hello">>}],
behaviours = [1, 2]})).
-endif.