src/erlquery.erl

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