-module(nine).
-export([compile/1,
compile/2,
compile/3,
compile/4,
compile_routes/1]).
compile(#{routes := RouteConfig,
router := Router,
generator := Generator}) ->
compile(RouteConfig, Router, Generator).
compile(#{routes := RouteConfig,
router := Router,
generator := Generator}, DirPath) ->
compile(RouteConfig, Router, Generator, DirPath).
compile(RouteConfig, Router, Generator) ->
Forms = do_compile(RouteConfig, Router, Generator),
compile_binary(Forms).
compile(RouteConfig, Router, Generator, DirPath) ->
Forms = do_compile(RouteConfig, Router, Generator),
compile_file(Forms, DirPath).
do_compile(RouteConfig, Router, Generator) ->
NormalizedRoutes = compile_routes(RouteConfig),
Generator:generate(Router, NormalizedRoutes).
compile_file(Forms, DirPath) ->
{ok, Module, Bin} = compile:forms(Forms, [debug_info]),
FinalPath = filename:join(DirPath, module_to_beam_filename(Module)),
file:write_file(FinalPath, Bin).
compile_binary(Forms) ->
{ok, Module, Binary} = compile:forms(Forms),
code:load_binary(Module, module_to_beam_filename(Module), Binary).
module_to_beam_filename(Module) ->
binary_to_list(list_to_binary([
atom_to_binary(Module),
".beam"
])).
compile_routes(Config) when is_list(Config) ->
lists:concat(lists:map(fun compile_routes/1, Config));
compile_routes(Config) when is_map(Config) ->
compile_routes(Config, #{<<"pre">> => [],
<<"post">> => [],
<<"routes">> => [],
<<"method">> => <<"_">>,
<<"path">> => <<"/">>}).
compile_routes(Config, Context) when is_list(Config) ->
lists:concat(lists:map(fun (C) -> compile_routes(C, Context) end, Config));
compile_routes(Config, Context) ->
do_compile(validate_config(normalize_keys(Config)), Context).
normalize_keys(Config) ->
L = maps:to_list(Config),
L2 = lists:map(fun normalize_key/1, L),
maps:from_list(L2).
normalize_key({K, V}) when is_atom(K) ->
{atom_to_binary(K), V};
normalize_key({K, _V} = Pair) when is_binary(K) ->
Pair;
normalize_key(_) ->
throw("Expected key to be binary or atom").
validate_config(#{<<"handle">> := _Handler} = Config) ->
Config;
validate_config(_Config) ->
throw("Missing handle key in config").
do_compile(#{<<"pre">> := PreConfig} = Config, #{<<"pre">> := PreContext} = Context) ->
NewPre = normalize_middleware(PreConfig),
do_compile(maps:remove(<<"pre">>, Config), Context#{<<"pre">> => PreContext ++ NewPre});
do_compile(#{<<"post">> := PostConfig} = Config, #{<<"post">> := PostContext} = Context) ->
NewPost = normalize_middleware(PostConfig),
do_compile(maps:remove(<<"post">>, Config), Context#{<<"post">> => PostContext ++ NewPost});
do_compile(#{<<"path">> := PathConfig} = Config, #{<<"path">> := PathContext} = Context) ->
PathConfig2 =
case starts_with(PathConfig, <<"/">>) of
true ->
PathConfig;
false ->
list_to_binary([<<"/">>, PathConfig])
end,
do_compile(maps:remove(<<"path">>, Config), Context#{<<"path">> => url_join(PathContext, PathConfig2)});
do_compile(#{<<"method">> := MethodConfig} = Config, #{<<"method">> := _MethodContext} = Context) ->
do_compile(maps:remove(<<"method">>, Config), Context#{<<"method">> => MethodConfig});
do_compile(#{<<"handle">> := HandleConfig}=Config, #{<<"routes">> := Routes,
<<"path">> := PathContext,
<<"method">> := MethodContext,
<<"pre">> := PreContext,
<<"post">> := PostContext} = Context) ->
Result =
case HandleConfig of
{_Module, _Function} = Route ->
[{PathContext, MethodContext, PreContext ++ [Route] ++ PostContext}];
InnerConfig when is_list(InnerConfig) orelse is_map(InnerConfig) ->
compile_routes(InnerConfig, Context)
end,
do_compile(maps:remove(<<"handle">>, Config), Context#{<<"routes">> => Routes ++ Result});
do_compile(_Config, #{<<"routes">> := RoutesContext}) ->
RoutesContext.
-spec normalize_middleware(tuple() | list()) -> list().
normalize_middleware(Mid) when is_tuple(Mid) ->
[Mid];
normalize_middleware(Mid) when is_list(Mid) ->
Mid.
-spec url_join(binary(), binary()) -> binary().
url_join(A, B) ->
list_to_binary([
string:trim(A, trailing, "/"),
<<"/">>,
string:trim(B, both, "/")
]).
-spec starts_with(binary(), binary()) -> boolean().
starts_with(S, Prefix) ->
string:prefix(S, Prefix) =/= S.
-ifdef(TEST).
-include_lib("eunit/include/eunit.hrl").
url_join_test() ->
?assertEqual(<<"/admin/login">>, url_join(<<"/admin">>, <<"/login">>)).
starts_with_test() ->
?assert(starts_with(<<"/admin">>, <<"/">>)).
compile_routes_test() ->
Config = [#{<<"path">> => <<"foo">>,
<<"method">> => <<"GET">>,
<<"handle">> => {foo, bar}}],
Result = compile_routes(Config),
Expected = [{<<"/foo">>, <<"GET">>, [{foo, bar}]}],
?assertEqual(Expected, Result).
compile_routes2_test() ->
Config = [#{<<"path">> => <<"foo">>,
<<"method">> => <<"GET">>,
<<"handle">> => {foo, bar},
<<"pre">> => [{auth_mid, verify}],
<<"post">> => [{log_mid, info}]}],
Result = compile_routes(Config),
Expected = [{<<"/foo">>, <<"GET">>, [{auth_mid, verify}, {foo, bar}, {log_mid, info}]}],
?assertEqual(Expected, Result).
compile_routes4_test() ->
Config = [#{<<"handle">> => {foo, bar}}],
Result = compile_routes(Config),
Expected = [{<<"/">>, <<"_">>, [{foo, bar}]}],
?assertEqual(Expected, Result).
compile_routes5_test() ->
Config = [#{<<"pre">> => [{auth, verify}],
<<"handle">> => {foo, bar}}],
Expected = [{<<"/">>, <<"_">>, [{auth, verify}, {foo, bar}]}],
Result = compile_routes(Config),
?assertEqual(Expected, Result).
compile_routes6_test() ->
Config = [#{<<"path">> => <<"/admin">>,
<<"handle">> => [#{<<"path">> => <<"/login">>,
<<"method">> => <<"POST">>,
<<"handle">> => {admin, login}},
#{<<"path">> => <<"/user">>,
<<"handle">> => [#{<<"method">> => <<"GET">>, <<"handle">> => {admin, get_user}},
#{<<"method">> => <<"POST">>, <<"handle">> => {admin, new_user}},
#{<<"method">> => <<"DELETE">>, <<"handle">> => {admin, delete_user}},
#{<<"method">> => <<"PUT">>, <<"handle">> => {admin, edit_user}}]}]}],
Expected = [{<<"/admin/login">>, <<"POST">>, [{admin, login}]},
{<<"/admin/user">>, <<"GET">>, [{admin, get_user}]},
{<<"/admin/user">>, <<"POST">>, [{admin, new_user}]},
{<<"/admin/user">>, <<"DELETE">>, [{admin, delete_user}]},
{<<"/admin/user">>, <<"PUT">>, [{admin, edit_user}]}],
Result = compile_routes(Config),
?assertEqual(Expected, Result).
%% /admin/login
%% /admin/user
%% - GET
%% - POST
%% - DELETE
%% - PATCH/PUT
compile_routes3_test() ->
Config = [
#{
<<"path">> => <<"/admin">>,
<<"handle">> => [
#{
<<"path">> => <<"/login">>,
<<"handle">> => {admin, login},
<<"method">> => <<"POST">>
},
#{
<<"path">> => <<"/user">>,
<<"handle">> => [
#{<<"handle">> => {admin, get_user},
<<"method">> => <<"GET">>},
#{<<"handle">> => {admin, new_user},
<<"method">> => <<"POST">>},
#{<<"handle">> => {admin, update_user},
<<"method">> => <<"PATCH">>},
#{<<"handle">> => {admin, delete_user},
<<"method">> => <<"DELETE">>}
]
}
]
},
#{<<"path">> => <<"*">>,
<<"handle">> => {util, not_found}}
],
Expected = [{<<"/admin/login">>, <<"POST">>, [{admin, login}]},
{<<"/admin/user">>, <<"GET">>, [{admin, get_user}]},
{<<"/admin/user">>, <<"POST">>, [{admin, new_user}]},
{<<"/admin/user">>, <<"PATCH">>, [{admin, update_user}]},
{<<"/admin/user">>, <<"DELETE">>, [{admin, delete_user}]},
{<<"/*">>, <<"_">>, [{util,not_found}]}],
Result = compile_routes(Config),
?assertEqual(Expected, Result).
compile_routes7_test() ->
Config = [#{<<"path">> => <<"/todo">>,
<<"handle">> => [
#{<<"method">> => <<"POST">>,
<<"handle">> => {todo_handler, post_todo}},
#{<<"method">> => <<"DELETE">>,
<<"handle">> => {todo_handler, delete_todo}}
]}],
Expected = [
{<<"/todo">>, <<"POST">>, [{todo_handler, post_todo}]},
{<<"/todo">>, <<"DELETE">>, [{todo_handler, delete_todo}]}
],
Result = compile_routes(Config),
?assertEqual(Expected, Result).
compile_routes_atoms_test() ->
Config = [#{path => <<"foo">>,
method => <<"GET">>,
handle => {foo, bar}}],
Result = compile_routes(Config),
Expected = [{<<"/foo">>, <<"GET">>, [{foo, bar}]}],
?assertEqual(Expected, Result).
compile_routes_atoms_nested_test() ->
Config = [#{path => <<"/admin">>,
handle => [#{path => <<"/login">>,
method => <<"POST">>,
handle => {admin, login}},
#{path => <<"/user">>,
handle => [#{method => <<"GET">>,
handle => {user, get}},
#{method => <<"PUT">>,
handle => {user, update}},
#{method => <<"DELETE">>,
handle => {user, delete}}]}]}],
Expected = [{<<"/admin/login">>, <<"POST">>, [{admin, login}]},
{<<"/admin/user">>, <<"GET">>, [{user, get}]},
{<<"/admin/user">>, <<"PUT">>, [{user, update}]},
{<<"/admin/user">>, <<"DELETE">>, [{user, delete}]}],
Result = compile_routes(Config),
?assertEqual(Expected, Result).
compile_routes_tuple_middleware_test() ->
Config = #{handle => {index_handler, index},
pre => {hello_mid, pre},
post => {hello_mid, post}},
Expected = [{<<"/">>, <<"_">>, [{hello_mid, pre}, {index_handler, index}, {hello_mid, post}]}],
Result = compile_routes(Config),
?assertEqual(Expected, Result).
compile_routes_missing_handle_test() ->
Config = #{path => <<"/admin">>},
try compile_routes(Config) of
_ ->
throw("This shouldn't happen")
catch
_:_ -> ok
end.
-endif.