src/nine_elli.erl

-module(nine_elli).

-export([generate/2]).

-type handler() :: {atom(), atom()}.
-type norm_route() :: {binary(), binary(), [handler()]}.
-type norm_routes() :: [norm_route()].

-spec generate(module(), norm_routes()) -> erl_parse:parse_tree().
generate(Module, Routes) ->
    {ok, Forms} = normalized_routes_to_forms(Module, Routes),
    Forms.

-spec normalized_routes_to_forms(module(), norm_routes()) -> {ok, erl_parse:parse_tree()}.
normalized_routes_to_forms(Module, NormRoutes) ->
    {ok,
     [codegen_module_form(Module)] ++ elli_exports() ++ routes_map_to_methods(NormRoutes)}.

-spec codegen_module_form(module()) -> erl_parse:erl_parse_tree().
codegen_module_form(Module) ->
    {attribute, 0, module, Module}.

-spec elli_exports() -> erl_parse:erl_parse_tree().
elli_exports() ->
    [{attribute, 0, export, [{handle, 2}, {handle_event, 3}]}].

-spec routes_map_to_methods(list()) -> list().
routes_map_to_methods(NormRoutes) ->
    codegen_handle() ++ codegen_handle_event() ++ codegen_routes(NormRoutes).

-spec codegen_handle_event() -> erl_parse:erl_parse_tree().
codegen_handle_event() ->
    [{function,
      0,
      handle_event,
      3,
      [{clause,
        0,
        [{var, 0, '_Event'}, {var, 0, '_Data'}, {var, 0, '_Args'}],
        [],
        [{atom, 0, ok}]}]}].

-spec codegen_handle() -> erl_parse:erl_parse_tree().
codegen_handle() ->
    [{function,
      0,
      handle,
      2,
      [{clause,
        0,
        [{var, 0, 'Req'}, {var, 0, '_Args'}],
        [],
        [{call,
          0,
          {atom, 0, handle},
          [{map,
            0,
            [{map_field_assoc, 0, {atom, 0, req}, {var, 0, 'Req'}},
             {map_field_assoc, 0, {atom, 0, method}, codegen_get_method()},
             {map_field_assoc, 0, {atom, 0, path}, codegen_get_path()}]}]}]}]}].

-spec codegen_get_method() -> erl_parse:erl_parse_tree().
codegen_get_method() ->
    {call, 1, {remote, 1, {atom, 1, nine_elli_util}, {atom, 1, get_method}}, [{var, 1, 'Req'}]}.

-spec codegen_get_path() -> erl_parse:erl_parse_tree().
codegen_get_path() ->
    {call, 0, {remote, 0, {atom, 0, elli_request}, {atom, 0, path}}, [{var, 0, 'Req'}]}.

-spec codegen_routes(list()) -> erl_parse:parse_tree().
codegen_routes(Routes) ->
    [{function, 0, handle, 1, lists:map(fun codegen_route/1, Routes)}].

-spec codegen_route({binary(), binary(), list()}) -> erl_parse:parse_tree().
codegen_route({Path0, Method, Middleware}) ->
    PathParsed = parse_path(Path0),
    Behavior = middleware_to_behavior(Middleware, filter_path_params(PathParsed)),
    Path = codegen_path(PathParsed),
    codegen_method_clause(Path, Method, Behavior).

-spec filter_path_params(list()) -> list().
filter_path_params(Path) ->
    lists:reverse(
        lists:foldl(fun(E, Acc) ->
                       case E of
                           all ->
                               Acc;
                           {all, Var} ->
                               [Var | Acc];
                           {_Prefix, Suffix} ->
                               [Suffix | Acc];
                           E2 when is_atom(E2) ->
                               [E2 | Acc];
                           _ ->
                               Acc
                       end
                    end,
                    [],
                    Path)).

-spec middleware_to_behavior(list(), list()) -> list().
middleware_to_behavior(Middleware, PathParams) ->
    Pp = codegen_path_params(PathParams),
    M = codegen_case_chain_init(enumerate_middleware(Middleware, 1)),
    [Pp, M].

-spec codegen_path_params(list()) -> erl_parse:parse_tree().
codegen_path_params(PathParams) ->
    {match,
     0,
     {var, 0, 'Req1'},
     {map,
      0,
      {var, 0, 'Req0'},
      [{map_field_assoc,
        0,
        {atom, 0, params},
        {map, 0, codegen_path_param_elements(PathParams)}}]}}.

-spec codegen_path_param_elements(list()) -> list().
codegen_path_param_elements(PathParams) ->
    lists:map(fun(P) ->
                 Lvar = binary_to_atom(string:lowercase(atom_to_binary(P))),
                 {map_field_assoc, 0, {atom, 0, Lvar}, {var, 0, P}}
              end,
              PathParams).

-spec enumerate_middleware([{atom(), atom()}], integer()) ->
                              [{atom(), atom(), integer()}].
enumerate_middleware(Middleware, Counter) ->
    enumerate_middleware(Middleware, [], Counter).

-spec enumerate_middleware([{atom(), atom()}],
                           [{atom(), atom(), integer()}],
                           integer()) ->
                              [{atom(), atom(), integer()}].
enumerate_middleware([], Acc, _Counter) ->
    Acc;
enumerate_middleware([{Module, Function} | Middleware], Acc, Counter) ->
    enumerate_middleware(Middleware, [{Module, Function, Counter} | Acc], Counter + 1).

-spec codegen_case_chain_init([{atom(), atom(), integer()}]) -> erl_parse:parse_tree().
codegen_case_chain_init([{Module, Function, Counter} | Rest]) ->
    codegen_case_chain(Rest,
                       codegen_case_wrapper(Module,
                                            Function,
                                            Counter,
                                            {var, 0, req_atom(Counter + 1)})).

-spec codegen_case_chain([{atom(), atom(), integer()}], erl_parse:parse_tree()) ->
                            erl_parse:parse_tree().
codegen_case_chain([], Acc) ->
    Acc;
codegen_case_chain([{Module, Function, Counter} | Mid], Acc) ->
    codegen_case_chain(Mid, codegen_case_wrapper(Module, Function, Counter, Acc)).

-spec codegen_case_wrapper(atom(), atom(), integer(), erl_parse:parse_tree()) ->
                              erl_parse:parse_tree().
codegen_case_wrapper(Module, Function, Counter, Behavior) ->
    {'case',
     0,
     codegen_handle_req(Module, Function, req_atom(Counter)),
     [{clause,
       0,
       [{map, 0, [{map_field_exact, 0, {atom, 0, resp}, {var, 0, resp_atom(Counter)}}]}],
       [],
       [{var, 0, resp_atom(Counter)}]},
      {clause, 0, [{var, 0, req_atom(Counter + 1)}], [], [Behavior]}]}.

-spec req_atom(integer()) -> atom().
req_atom(Counter) ->
    binary_to_atom(list_to_binary(["Req", integer_to_list(Counter)])).

-spec resp_atom(integer()) -> atom().
resp_atom(Counter) ->
    binary_to_atom(list_to_binary(["Resp", integer_to_list(Counter)])).

-spec codegen_handle_req(atom(), atom(), atom()) -> erl_parse:parse_tree().
codegen_handle_req(Module, Function, Req) ->
    {call, 0, {remote, 0, {atom, 0, Module}, {atom, 0, Function}}, [{var, 0, Req}]}.

-spec parse_path(binary()) ->
                    [atom() | binary() | {binary(), atom()} | {all, atom()} | all].
parse_path(Path0) ->
    [_ | Rest] = string:split(Path0, "/", all),
    lists:map(fun translate_path_param/1, Rest).

-spec translate_path_param(binary()) ->
                              binary() | atom() | {binary(), atom()} | {all, atom()} | all.
translate_path_param(<<"*">>) ->
    all;
translate_path_param(<<"*", Name/binary>>) ->
    {all, binary_to_var(Name)};
translate_path_param(S) ->
    case string:split(S, ":") of
        [_] ->
            S;
        [<<>>, Suffix] ->
            binary_to_var(Suffix);
        [Prefix, Suffix] ->
            {Prefix, binary_to_var(Suffix)}
    end.

-spec binary_to_var(binary()) -> atom().
binary_to_var(B) ->
    binary_to_atom(string:titlecase(B)).

-spec codegen_path(list()) -> erl_parse:parse_tree().
codegen_path([]) ->
    {var, 0, '_'};
codegen_path([<<>>]) ->
    {nil, 0};
codegen_path(Path) ->
    Path2 = lists:reverse(Path),
    case Path2 of
        [{all, Var} | Rest] ->
            codegen_path(Rest, {var, 0, Var});
        [all | Rest] ->
            codegen_path(Rest, {var, 0, '_'});
        _ ->
            codegen_path(Path2, {nil, 0})
    end.

-spec codegen_path(list(), erl_parse:parse_tree()) -> erl_parse:parse_tree().
codegen_path([], Acc) ->
    Acc;
codegen_path([E | Rest], Acc) ->
    codegen_path(Rest, {cons, 0, codegen_path_element(E), Acc}).

-spec codegen_path_element(binary() | atom() | {binary(), atom()}) ->
                              erl_parse:parse_tree().
codegen_path_element(E) ->
    case E of
        all ->
            {var, 0, '_'};
        {B, Var} ->
            codegen_binary_var(B, Var);
        E when is_atom(E) ->
            {var, 0, E};
        E when is_binary(E) ->
            codegen_binary(E)
    end.

-spec codegen_binary(binary()) -> erl_parse:parse_tree().
codegen_binary(B) ->
    {bin, 0, [{bin_element, 0, {string, 0, binary_to_list(B)}, default, default}]}.

-spec codegen_binary_var(binary(), atom()) -> erl_parse:parse_tree().
codegen_binary_var(B, Var) ->
    {bin,
     0,
     [{bin_element, 0, {string, 0, binary_to_list(B)}, default, default},
      {bin_element, 0, {var, 0, Var}, default, [binary]}]}.

%% TODO: change name method to something else
-spec codegen_method_clause(erl_parse:tree(), binary(), erl_parse:parse_tree()) ->
                               erl_parse:parse_tree().
codegen_method_clause(Path, Method, Behavior) ->
    {clause,
     0,
     [{match, 0, {var, 0, 'Req0'}, {map, 0, codegen_method_clause_helper(Path, Method)}}],
     [],
     Behavior}.

-spec codegen_method_clause_helper(erl_parse:tree(), binary()) -> erl_parse:parse_tree().
codegen_method_clause_helper(Path, Method) ->
    [{map_field_exact, 0, {atom, 0, method}, codegen_method_value(translate_method(Method))},
     {map_field_exact, 0, {atom, 0, path}, Path}].

-spec codegen_method_value(atom()) ->
                              {var, integer(), '_Method'} | {atom, integer(), atom()}.
codegen_method_value('_Method') ->
    {var, 0, '_Method'};
codegen_method_value(Method) ->
    {atom, 0, Method}.

-spec translate_method(binary()) -> atom().
translate_method(<<"GET">>) ->
    'GET';
translate_method(<<"POST">>) ->
    'POST';
translate_method(<<"PUT">>) ->
    'PUT';
translate_method(<<"PATCH">>) ->
    'PATCH';
translate_method(<<"DELETE">>) ->
    'DELETE';
translate_method(<<"_">>) ->
    '_Method'.

-ifdef(TEST).

-include_lib("eunit/include/eunit.hrl").

codegen_module_form_test() ->
    ?assertEqual({attribute, 0, module, foobar}, codegen_module_form(foobar)).

elli_exports_test() ->
    ?assertEqual([{attribute, 0, export, [{handle, 2}, {handle_event, 3}]}], elli_exports()).

codegen_handle_event_test() ->
    ?assertEqual([{function,
                   0,
                   handle_event,
                   3,
                   [{clause,
                     0,
                     [{var, 0, '_Event'}, {var, 0, '_Data'}, {var, 0, '_Args'}],
                     [],
                     [{atom, 0, ok}]}]}],
                 codegen_handle_event()).

codegen_get_method_test() ->
    ?assertEqual({call,
                  1,
                  {remote, 1, {atom, 1, nine_elli_util}, {atom, 1, get_method}},
                  [{var, 1, 'Req'}]},
                 codegen_get_method()).

codegen_get_path_test() ->
    ?assertEqual({call,
                  0,
                  {remote, 0, {atom, 0, elli_request}, {atom, 0, path}},
                  [{var, 0, 'Req'}]},
                 codegen_get_path()).

codegen_handle_test() ->
    ?assertEqual([{function,
                   0,
                   handle,
                   2,
                   [{clause,
                     0,
                     [{var, 0, 'Req'}, {var, 0, '_Args'}],
                     [],
                     [{call,
                       0,
                       {atom, 0, handle},
                       [{map,
                         0,
                         [{map_field_assoc, 0, {atom, 0, req}, {var, 0, 'Req'}},
                          {map_field_assoc, 0, {atom, 0, method}, codegen_get_method()},
                          {map_field_assoc, 0, {atom, 0, path}, codegen_get_path()}]}]}]}]}],
                 codegen_handle()).

codegen_case_init_test() ->
    Expected = codegen_case_wrapper(foo, bar, 0, {var, 0, 'Req1'}),
    ?assertEqual(Expected, codegen_case_chain_init([{foo, bar, 0}])).

codegen_case_chain_test() ->
    ?assertEqual({nil, 0}, codegen_case_chain([], {nil, 0})),
    Expected = codegen_case_wrapper(foo, bar, 0, {nil, 0}),
    ?assertEqual(Expected, codegen_case_chain([{foo, bar, 0}], {nil, 0})).

codegen_case_wrapper_test() ->
    Expected =
        {'case',
         0,
         codegen_handle_req(foo, bar, 'Req0'),
         [{clause,
           0,
           [{map, 0, [{map_field_exact, 0, {atom, 0, resp}, {var, 0, 'Resp0'}}]}],
           [],
           [{var, 0, 'Resp0'}]},
          {clause, 0, [{var, 0, 'Req1'}], [], [{nil, 0}]}]},
    ?assertEqual(Expected, codegen_case_wrapper(foo, bar, 0, {nil, 0})).

codegen_path_param_elements_test() ->
    Expected = [{map_field_assoc, 0, {atom, 0, foo}, {var, 0, 'Foo'}}],
    ?assertEqual(Expected, codegen_path_param_elements(['Foo'])).

codegen_path_params_test() ->
    Expected =
        {match,
         0,
         {var, 0, 'Req1'},
         {map,
          0,
          {var, 0, 'Req0'},
          [{map_field_assoc,
            0,
            {atom, 0, params},
            {map, 0, codegen_path_param_elements(['Foo'])}}]}},
    ?assertEqual(Expected, codegen_path_params(['Foo'])).

filter_path_params_test() ->
    ?assertEqual([foo, bar],
                 filter_path_params([<<"foo">>, foo, <<"bar">>, bar, 123, "foobar"])),
    ?assertEqual([foo, bar, baz],
                 filter_path_params([<<"foo">>,
                                     foo,
                                     <<"bar">>,
                                     {<<"bar">>, bar},
                                     {all, baz},
                                     all])).

enumerate_middleware_test() ->
    InputData = [{foo, bar}, {bar, foo}],
    OutputData = enumerate_middleware(InputData, 0),
    ?assertEqual([{bar, foo, 1}, {foo, bar, 0}], OutputData).

codegen_handle_req_test() ->
    Expected = {call, 0, {remote, 0, {atom, 0, foo}, {atom, 0, bar}}, [{var, 0, 'Req0'}]},
    ?assertEqual(Expected, codegen_handle_req(foo, bar, 'Req0')).

codegen_binary_test() ->
    Expected = {bin, 0, [{bin_element, 0, {string, 0, "foobar"}, default, default}]},
    ?assertEqual(Expected, codegen_binary(<<"foobar">>)).

codegen_binary_var_test() ->
    Expected =
        {bin,
         0,
         [{bin_element, 0, {string, 0, "foobar"}, default, default},
          {bin_element, 0, {var, 0, 'Var'}, default, [binary]}]},

    ?assertEqual(Expected, codegen_binary_var(<<"foobar">>, 'Var')).

codegen_path_element_test() ->
    ?assertEqual({var, 0, '_'}, codegen_path_element(all)),
    ?assertEqual({var, 0, foo}, codegen_path_element(foo)),
    ?assertEqual(codegen_binary(<<"foobar">>), codegen_path_element(<<"foobar">>)),
    ?assertEqual(codegen_binary_var(<<"foobar">>, 'Var'),
                 codegen_path_element({<<"foobar">>, 'Var'})).

codegen_path_test() ->
    ?assertEqual({var, 0, '_'}, codegen_path([])),
    ?assertEqual({nil, 0}, codegen_path([<<>>])),
    ?assertEqual({nil, 0}, codegen_path([], {nil, 0})),
    ?assertEqual({cons, 0, {var, 0, foo}, {nil, 0}}, codegen_path([foo], {nil, 0})),
    ?assertEqual({cons, 0, {var, 0, foo}, {nil, 0}}, codegen_path([foo])),
    ?assertEqual({cons, 0, {var, 0, foo}, {var, 0, bar}}, codegen_path([foo, {all, bar}])),
    ?assertEqual({cons, 0, {var, 0, foo}, {var, 0, '_'}}, codegen_path([foo, all])).

codegen_method_clause_helper_test() ->
    ?assertEqual([{map_field_exact, 0, {atom, 0, method}, {atom, 0, 'GET'}},
                  {map_field_exact, 0, {atom, 0, path}, {nil, 0}}],
                 codegen_method_clause_helper({nil, 0}, <<"GET">>)).

codegen_method_clause_test() ->
    ?assertEqual({clause,
                  0,
                  [{match,
                    0,
                    {var, 0, 'Req0'},
                    {map,
                     0,
                     [{map_field_exact, 0, {atom, 0, method}, {atom, 0, 'GET'}},
                      {map_field_exact, 0, {atom, 0, path}, {nil, 0}}]}}],
                  [],
                  {nil, 0}},
                 codegen_method_clause({nil, 0}, <<"GET">>, {nil, 0})).

translate_method_test() ->
    ?assertEqual('GET', translate_method(<<"GET">>)),
    ?assertEqual('POST', translate_method(<<"POST">>)),
    ?assertEqual('PUT', translate_method(<<"PUT">>)),
    ?assertEqual('PATCH', translate_method(<<"PATCH">>)),
    ?assertEqual('DELETE', translate_method(<<"DELETE">>)),
    ?assertEqual('_Method', translate_method(<<"_">>)).

codegen_method_value_test() ->
    ?assertEqual({var, 0, '_Method'}, codegen_method_value('_Method')),
    ?assertEqual({atom, 0, thing}, codegen_method_value(thing)),
    ?assertEqual({atom, 0, stuff}, codegen_method_value(stuff)).

parse_path_test() ->
    ?assertEqual([<<"foo">>, <<"bar">>], parse_path(<<"/foo/bar">>)),

    ?assertEqual([<<"foo">>, 'Bar'], parse_path(<<"/foo/:bar">>)),
    ?assertEqual([<<>>], parse_path(<<"/">>)),

    ?assertEqual([all], parse_path(<<"/*">>)),
    ?assertEqual([<<"api">>, {<<"v">>, 'Version'}, <<"pages">>, 'Id'],
                 parse_path(<<"/api/v:version/pages/:id">>)),
    ?assertEqual([<<"pages">>, {all, 'Page'}], parse_path(<<"/pages/*page">>)),
    ?assertEqual([<<"pages">>, {<<"he">>, 'Page'}, {all, 'Rest'}],
                 parse_path(<<"/pages/he:page/*rest">>)).

translate_path_param_test() ->
    ?assertEqual(<<"foo">>, translate_path_param(<<"foo">>)),
    ?assertEqual('Foo', translate_path_param(<<":foo">>)),
    ?assertEqual({<<"v">>, 'Version'}, translate_path_param(<<"v:version">>)),
    ?assertEqual({all, 'Splat'}, translate_path_param(<<"*splat">>)),
    ?assertEqual(all, translate_path_param(<<"*">>)).

resp_atom_test() ->
    ?assertEqual('Resp1', resp_atom(1)),
    ?assertEqual('Resp2', resp_atom(2)).

req_atom_test() ->
    ?assertEqual('Req1', req_atom(1)),
    ?assertEqual('Req2', req_atom(2)).

-endif.