-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; 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.