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