Current section
Files
Jump to
Current section
Files
src/erlquery.erl
-module(erlquery).
-export([parse/1, codegen/1]).
-record(config,
{module :: binary(),
query :: {binary(), binary(), binary()},
methods :: [{binary(), binary()}]}).
-type config() :: #config{}.
-spec parse(iodata()) -> {ok, config()} | {error, any()}.
parse(S) ->
Clauses = split_clauses(S),
reduce_clauses(Clauses, init_config()).
-spec init_config() -> config().
init_config() ->
#config{module = <<>>,
query = {<<>>, <<>>, <<>>},
methods = []}.
-spec split_clauses(iodata()) -> [iodata()].
split_clauses(S) ->
Clauses0 = string:split(S, ".\n", all),
Clauses1 = lists:filter(fun not_whitespace/1, Clauses0),
lists:map(fun(X) -> string:trim(X, both) end, Clauses1).
-spec not_whitespace(iodata()) -> boolean().
not_whitespace(S) ->
Res = re:replace(S, "[\s\n]", "", [global]),
B = iolist_to_binary(Res),
B =/= <<"">>.
-spec reduce_clauses([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(iodata(), config()) -> config() | {error, any()}.
reduce_clause(S, Config) ->
case clause_type(S) of
module ->
M = match_module(S),
Config#config{module = M};
query ->
Q = match_query(S),
Config#config{query = Q};
method ->
Method = match_method(S),
NewMethods = [Method | Config#config.methods],
Config#config{methods = NewMethods};
_ ->
{error, {invalid_clause, S}}
end.
-spec clause_type(iodata()) -> module | query | method | nomatch.
clause_type(S) ->
case is_module_clause(S) of
true ->
module;
false ->
case is_query_clause(S) of
true ->
query;
false ->
case is_method_clause(S) of
true ->
method;
false ->
nomatch
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.
%% Expecting the .\n suffix to be stripped by split_clauses
-spec match_method(iodata()) -> {iodata(), iodata()} | 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),
{bcast(Name), bcast(Query)};
_ ->
nomatch
end.
-spec match_module(iodata()) -> iodata() | nomatch.
match_module(S) ->
case re:run(S, "-module\\(([a-zA-Z0-9_]+)\\)", [{capture, [1]}]) of
{match, [{Start, End}]} ->
bcast(string:slice(S, Start, End));
_ ->
nomatch
end.
%% Expecting query to look like -query(pgo:query/2).
-spec match_query(iodata()) -> {iodata(), iodata(), iodata()} | 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),
{bcast(Mod), bcast(Fun), bcast(Arity)};
_ ->
nomatch
end.
-spec bcast(iolist() | binary()) -> binary().
bcast(<<S/binary>>) ->
S;
bcast(S = [_ | _]) ->
list_to_binary(S).
-spec codegen(config()) -> binary().
codegen(Config) ->
M = codegen_module(Config#config.module),
Q = codegen_query(Config#config.query),
E = codegen_export(Config#config.methods),
Ms = codegen_methods(Q, Config#config.methods),
list_to_binary([M, E, Ms]).
-spec codegen_module(binary()) -> binary().
codegen_module(Module) ->
list_to_binary([<<"-module(">>, Module, <<").\n\n">>]).
-spec codegen_query({binary(), binary(), binary()}) -> binary().
codegen_query({Module, Method, _Arity}) ->
list_to_binary([Module, <<":">>, Method]).
-spec codegen_methods(binary(), [{binary(), binary()}]) -> binary().
codegen_methods(Query, Methods) ->
list_to_binary(lists:map(fun(M) -> codegen_method(Query, M) end, Methods)).
-spec codegen_method(binary(), {binary(), binary()}) -> binary().
codegen_method(Query, {Method, QueryText}) ->
list_to_binary([Method,
<<"(Args) ->\n">>,
<<" ">>,
Query,
<<"(<<\"">>,
QueryText,
<<"\">>, Args).\n\n">>]).
-spec codegen_export([{binary(), binary()}]) -> binary().
codegen_export(Methods) ->
list_to_binary([<<"-export([">>, reduce_exports(Methods, []), <<"]).\n\n">>]).
-spec reduce_exports([{binary(), binary()}], [binary()]) -> [binary()].
reduce_exports([H], Acc) ->
lists:reverse([codegen_export_method(H) | Acc]);
reduce_exports([H | T], Acc) ->
Res = codegen_export_method(H),
Res2 = list_to_binary([Res, <<",">>]),
reduce_exports(T, [Res2 | Acc]).
-spec codegen_export_method({binary(), binary()}) -> binary().
codegen_export_method({Name, _QueryText}) ->
list_to_binary([Name, <<"/1\n">>]).
-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)")).
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")).
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")).
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(nomatch, clause_type("foobar")).
reduce_clause_test() ->
?assertEqual(#config{module = <<"foo">>}, reduce_clause("-module(foo)", #config{})),
?assertEqual(#config{query = {<<"foo">>, <<"bar">>, <<"2">>}},
reduce_clause("-query(foo:bar/2)", #config{})),
?assertEqual(#config{methods = [{<<"foobar">>, <<"SELECT * FROM foobars">>}]},
reduce_clause("foobar -> SELECT * FROM foobars", #config{methods = []})).
not_whitespace_test() ->
?assert(not_whitespace("foobar")),
?assertNot(not_whitespace(<<"">>)),
?assert(not_whitespace(<<"foobar sdfsdfsdf">>)).
split_clauses_test() ->
S = "-module(foobar).\n\n-query(pgo:query/2).\n\nbarfoo->\n SELECT * FROM foobars.\n",
?assertEqual(["-module(foobar)",
"-query(pgo:query/2)",
"barfoo->\n SELECT * FROM foobars"],
split_clauses(S)).
parse_test() ->
?assertEqual({ok,
#config{module = <<"fooq">>,
query = {<<"foo">>, <<"bar">>, <<"2">>},
methods = [{<<"foobar">>, <<"SELECT * FROM foobars">>}]}},
parse(list_to_binary(["-module(fooq).\n\n",
"-query(foo:bar/2).\n\n",
"foobar ->\n",
" SELECT * FROM foobars"]))).
codegen_export_method_test() ->
?assertEqual(<<"foobar/1\n">>, codegen_export_method({"foobar", "barfoo"})).
codegen_export_test() ->
?assertEqual(<<"-export([foobar/1\n,barfoo/1\n]).\n\n">>,
codegen_export([{"foobar", "blah"}, {"barfoo", "blahblah"}])).
codegen_query_test() ->
?assertEqual(<<"barfoo:query">>, codegen_query({<<"barfoo">>, <<"query">>, <<"2">>})).
codegen_module_test() ->
?assertEqual(<<"-module(foobar).\n\n">>, codegen_module(<<"foobar">>)).
codegen_method_test() ->
?assertEqual(<<"foobar(Args) ->\n barfoo:query(<<\"SELECT * FROM foobars\">>, Args).\n\n">>,
codegen_method(<<"barfoo:query">>, {<<"foobar">>, <<"SELECT * FROM foobars">>})).
codegen_methods_test() ->
?assertEqual(<<"foobar(Args) ->\n barfoo:query(<<\"SELECT * FROM foobars\">>, Args).\n\n">>,
codegen_methods(<<"barfoo:query">>,
[{<<"foobar">>, <<"SELECT * FROM foobars">>}])).
codegen_test() ->
?assertEqual(<<"-module(things).\n\n-export([foobar/1\n]).\n\nfoobar(Args) ->\n bar:foo(<<\"SELECT * FROM foobars\">>, Args).\n\n">>,
codegen(#config{module = <<"things">>,
query = {<<"bar">>, <<"foo">>, <<"2">>},
methods = [{<<"foobar">>, <<"SELECT * FROM foobars">>}]})).
compile_test() ->
Bin = codegen(#config{module = <<"testme">>,
query = {<<"erlquery_mock">>, <<"query">>, <<"2">>},
methods = [{<<"foobar">>, <<"SELECT * FROM foobars">>}]}),
file:write_file("testme.erl", Bin),
{ok, testme} = compile:file("testme.erl"),
?assertEqual(ok, testme:foobar([])),
file:delete("testme.erl").
-endif.