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