diff --git a/rebar.config b/rebar.config index db63027..453895f 100644 --- a/rebar.config +++ b/rebar.config @@ -58,8 +58,7 @@ {dialyzer, [ {plt_apps, all_deps}, {plt_extra_apps, [ - nova, - routing_tree + nova ]}, {warnings, [no_unknown]} ]}. diff --git a/src/rebar3_nova.app.src b/src/rebar3_nova.app.src index 84597a0..609555b 100644 --- a/src/rebar3_nova.app.src +++ b/src/rebar3_nova.app.src @@ -6,7 +6,6 @@ kernel, stdlib, nova, - routing_tree, fs ]}, {env, []}, diff --git a/src/rebar3_nova_audit.erl b/src/rebar3_nova_audit.erl index 2972a13..6acf076 100644 --- a/src/rebar3_nova_audit.erl +++ b/src/rebar3_nova_audit.erl @@ -2,8 +2,10 @@ -export([init/1, do/1, format_error/1]). +%% Exported for rebar3_nova_dispatch_SUITE. +-export([collect_routes/1]). + -include("nova_router.hrl"). --include_lib("routing_tree/include/routing_tree.hrl"). -define(PROVIDER, audit). -define(DEPS, [{default, compile}]). @@ -106,74 +108,30 @@ is_mutation(<<"delete">>) -> true; is_mutation(<<"patch">>) -> true; is_mutation(_) -> false. -collect_routes(#host_tree{hosts = Hosts}) -> - lists:flatmap( - fun({_Host, #routing_tree{tree = Tree}}) -> - collect_nodes(Tree, <<>>) - end, - Hosts - ). +collect_routes(Dispatch) -> + lists:flatmap(fun classify_route/1, rebar3_nova_dispatch:routes(Dispatch)). -collect_nodes([], _Prefix) -> +classify_route({_Path, _Method, #nova_handler_value{module = nova_file_controller}}) -> []; -collect_nodes([#node{is_wildcard = true} | Tl], Prefix) -> - collect_nodes(Tl, Prefix); -collect_nodes([#node{segment = Segment} | Tl], Prefix) when is_integer(Segment) -> - collect_nodes(Tl, Prefix); -collect_nodes( - [#node{segment = Segment, is_binding = IsBinding, value = Value, children = Children} | Tl], - Prefix -) -> - SegBin = segment_to_binary(Segment, IsBinding), - NewPrefix = <>, - HandlerRoutes = lists:filtermap( - fun(NodeComp) -> classify_handler(NodeComp, NewPrefix) end, Value - ), - lists:flatten(HandlerRoutes) ++ collect_nodes(Children, NewPrefix) ++ collect_nodes(Tl, Prefix). - -segment_to_binary(Segment, true) when is_binary(Segment) -> - <<"{", Segment/binary, "}">>; -segment_to_binary(Segment, _) when is_binary(Segment) -> - Segment; -segment_to_binary(Segment, IsBinding) when is_list(Segment) -> - segment_to_binary(erlang:list_to_binary(Segment), IsBinding). - -classify_handler(#node_comp{value = #nova_handler_value{module = nova_file_controller}}, _Path) -> - false; -classify_handler(#node_comp{value = #nova_handler_value{module = nova_error_controller}}, _Path) -> - false; -classify_handler( - #node_comp{ - comparator = Method, - value = #nova_handler_value{ - module = undefined, - function = undefined, - callback = Callback, - secure = Secure - } - }, - Path +classify_route({_Path, _Method, #nova_handler_value{module = nova_error_controller}}) -> + []; +classify_route( + {Path, Method, #nova_handler_value{ + module = undefined, + function = undefined, + callback = Callback, + secure = Secure + }} ) -> {module, Module} = lists:keyfind(module, 1, erlang:fun_info(Callback)), - expand_methods(Method, Path, Module, Secure); -classify_handler( - #node_comp{ - comparator = Method, - value = #nova_handler_value{module = Module, secure = Secure} - }, - Path -) -> - expand_methods(Method, Path, Module, Secure); -classify_handler(#node_comp{value = #cowboy_handler_value{}}, _Path) -> - false. + expand_methods(Method, rebar3_nova_dispatch:openapi_path(Path), Module, Secure); +classify_route({Path, Method, #nova_handler_value{module = Module, secure = Secure}}) -> + expand_methods(Method, rebar3_nova_dispatch:openapi_path(Path), Module, Secure); +classify_route({_Path, _Method, #cowboy_handler_value{}}) -> + []. expand_methods('_', Path, Module, Secure) -> Methods = [<<"get">>, <<"post">>, <<"put">>, <<"delete">>, <<"patch">>], - {true, [{Path, M, Secure, Module, true} || M <- Methods]}; + [{Path, M, Secure, Module, true} || M <- Methods]; expand_methods(Method, Path, Module, Secure) -> - {true, [{Path, method_to_binary(Method), Secure, Module, false}]}. - -method_to_binary(Method) when is_atom(Method) -> - erlang:atom_to_binary(Method); -method_to_binary(Method) when is_binary(Method) -> - string:lowercase(Method). + [{Path, rebar3_nova_dispatch:method_to_binary(Method), Secure, Module, false}]. diff --git a/src/rebar3_nova_dispatch.erl b/src/rebar3_nova_dispatch.erl new file mode 100644 index 0000000..49af037 --- /dev/null +++ b/src/rebar3_nova_dispatch.erl @@ -0,0 +1,66 @@ +%%%------------------------------------------------------------------- +%%% @doc +%%% Reads a compiled Nova dispatch table. +%%% +%%% The routing table used to be a `routing_tree' record that every task +%%% walked for itself. It is now an opaque structure owned by +%%% `nova_routing_trie', so this module is the single place that talks to its +%%% introspection API and hands the tasks a flat list of routes. +%%% @end +%%%------------------------------------------------------------------- +-module(rebar3_nova_dispatch). + +-export([ + routes/1, + openapi_path/1, + method_to_binary/1 +]). + +-type route() :: {Path :: binary(), Method :: '_' | binary(), Payload :: term()}. +-export_type([route/0]). + +%%-------------------------------------------------------------------- +%% @doc +%% Every URL route in a compiled dispatch table. +%% +%% Status-code routes (Nova's error pages, which are keyed by integer rather +%% than by path) are left out, since none of the tasks that call this report +%% on them. +%% @end +%%-------------------------------------------------------------------- +-spec routes(Dispatch :: nova_routing_trie:trie()) -> [route()]. +routes(Dispatch) -> + [ + {Path, Method, Payload} + || {_Host, Path, Method, Payload} <- nova_routing_trie:routes(Dispatch), + is_binary(Path) + ]. + +%%-------------------------------------------------------------------- +%% @doc +%% Rewrite a Nova path into the OpenAPI style, so `/users/:id' becomes +%% `/users/{id}'. A trailing `[...]' catch-all has no OpenAPI equivalent and +%% is dropped. +%% @end +%%-------------------------------------------------------------------- +-spec openapi_path(Path :: binary()) -> binary(). +openapi_path(Path) -> + Segments = [openapi_segment(S) || S <- binary:split(Path, <<"/">>, [global]), S =/= <<>>], + case [S || S <- Segments, S =/= skip] of + [] -> <<"/">>; + Kept -> <<<<"/", S/binary>> || S <- Kept>> + end. + +openapi_segment(<<":", Name/binary>>) -> <<"{", Name/binary, "}">>; +openapi_segment(<<"[...]">>) -> skip; +openapi_segment(Segment) -> Segment. + +%%-------------------------------------------------------------------- +%% @doc +%% The lowercase name of an HTTP method, as the report formats want it. +%% @end +%%-------------------------------------------------------------------- +-spec method_to_binary(Method :: '_' | binary() | atom()) -> binary(). +method_to_binary('_') -> <<"any">>; +method_to_binary(Method) when is_binary(Method) -> string:lowercase(Method); +method_to_binary(Method) when is_atom(Method) -> string:lowercase(atom_to_binary(Method, utf8)). diff --git a/src/rebar3_nova_doctor.erl b/src/rebar3_nova_doctor.erl index a520b7f..d275132 100644 --- a/src/rebar3_nova_doctor.erl +++ b/src/rebar3_nova_doctor.erl @@ -2,8 +2,10 @@ -export([init/1, do/1, format_error/1]). +%% Exported for rebar3_nova_dispatch_SUITE. +-export([collect_route_handlers/1]). + -include("nova_router.hrl"). --include_lib("routing_tree/include/routing_tree.hrl"). -ifdef(TEST). -export([ @@ -236,65 +238,25 @@ check_routes(State) -> ] end. -collect_route_handlers(#host_tree{hosts = Hosts}) -> - lists:flatmap( - fun({_Host, #routing_tree{tree = Tree}}) -> walk_tree(Tree, <<>>) end, - Hosts - ). - -walk_tree([], _Prefix) -> - []; -walk_tree( - [#node{segment = Segment, is_binding = IsBinding, value = Value, children = Children} | Tl], - Prefix -) -> - Seg = seg_bin(Segment, IsBinding), - NewPrefix = <>, - [extract_handler(NC, NewPrefix) || NC <- Value] ++ - walk_tree(Children, NewPrefix) ++ - walk_tree(Tl, Prefix). - -seg_bin(S, true) when is_binary(S) -> <<"{", S/binary, "}">>; -seg_bin(S, true) when is_list(S) -> <<"{", (list_to_binary(S))/binary, "}">>; -seg_bin(S, _) when is_binary(S) -> S; -seg_bin(S, _) when is_list(S) -> list_to_binary(S); -seg_bin(S, _) when is_integer(S) -> integer_to_binary(S); -seg_bin(_, _) -> <<"_">>. +collect_route_handlers(Dispatch) -> + [extract_handler(Route) || Route <- rebar3_nova_dispatch:routes(Dispatch)]. extract_handler( - #node_comp{ - comparator = Method, - value = #nova_handler_value{ - module = undefined, - function = undefined, - callback = Cb - } - }, - Path -) when - is_function(Cb) --> + {Path, Method, #nova_handler_value{ + module = undefined, + function = undefined, + callback = Cb + }} +) when is_function(Cb) -> {module, M} = lists:keyfind(module, 1, erlang:fun_info(Cb)), {Path, Method, M, '$callback', captured}; -extract_handler( - #node_comp{ - comparator = Method, - value = #nova_handler_value{module = Mod, function = Func} - }, - Path -) when +extract_handler({Path, Method, #nova_handler_value{module = Mod, function = Func}}) when Mod =/= undefined -> {Path, Method, Mod, Func, 1}; -extract_handler( - #node_comp{ - comparator = Method, - value = #cowboy_handler_value{handler = Handler} - }, - Path -) -> +extract_handler({Path, Method, #cowboy_handler_value{handler = Handler}}) -> {Path, Method, Handler, init, 2}; -extract_handler(#node_comp{comparator = Method}, Path) -> +extract_handler({Path, Method, _Payload}) -> {Path, Method, unknown, unknown, 1}. check_handler({_Path, _Method, unknown, _, _}) -> diff --git a/src/rebar3_nova_openapi.erl b/src/rebar3_nova_openapi.erl index 5faecb2..36458dd 100644 --- a/src/rebar3_nova_openapi.erl +++ b/src/rebar3_nova_openapi.erl @@ -2,8 +2,10 @@ -export([init/1, do/1, format_error/1]). +%% Exported for rebar3_nova_dispatch_SUITE. +-export([collect_routes/1]). + -include("nova_router.hrl"). --include_lib("routing_tree/include/routing_tree.hrl"). -define(PROVIDER, openapi). -define(DEPS, [{default, compile}]). @@ -73,85 +75,43 @@ format_error(Reason) -> %% =================================================================== %% Route collection %% =================================================================== -collect_routes(#host_tree{hosts = Hosts}) -> - lists:flatmap( - fun({_Host, #routing_tree{tree = Tree}}) -> - collect_nodes(Tree, <<>>) - end, - Hosts - ). - -collect_nodes([], _Prefix) -> - []; -collect_nodes([#node{is_wildcard = true} | Tl], Prefix) -> - collect_nodes(Tl, Prefix); -collect_nodes([#node{segment = Segment} | Tl], Prefix) when is_integer(Segment) -> - collect_nodes(Tl, Prefix); -collect_nodes( - [#node{segment = Segment, is_binding = IsBinding, value = Value, children = Children} | Tl], - Prefix -) -> - SegBin = segment_to_binary(Segment, IsBinding), - NewPrefix = <>, - HandlerRoutes = lists:filtermap( - fun(NodeComp) -> classify_handler(NodeComp, NewPrefix) end, Value - ), - HandlerRoutes ++ collect_nodes(Children, NewPrefix) ++ collect_nodes(Tl, Prefix). - -segment_to_binary(Segment, true) when is_binary(Segment) -> - <<"{", Segment/binary, "}">>; -segment_to_binary(Segment, _) when is_binary(Segment) -> - Segment; -segment_to_binary(Segment, IsBinding) when is_list(Segment) -> - segment_to_binary(erlang:list_to_binary(Segment), IsBinding). +collect_routes(Dispatch) -> + lists:flatmap(fun classify_route/1, rebar3_nova_dispatch:routes(Dispatch)). %% =================================================================== %% Handler classification %% =================================================================== -classify_handler(#node_comp{value = #nova_handler_value{module = nova_file_controller}}, _Path) -> - false; -classify_handler(#node_comp{value = #nova_handler_value{module = nova_error_controller}}, _Path) -> - false; -classify_handler( - #node_comp{ - comparator = Method, - value = #nova_handler_value{ - module = undefined, - function = undefined, - callback = Callback, - extra_state = Extra - } - }, - Path +classify_route({_Path, _Method, #nova_handler_value{module = nova_file_controller}}) -> + []; +classify_route({_Path, _Method, #nova_handler_value{module = nova_error_controller}}) -> + []; +classify_route( + {Path, Method, #nova_handler_value{ + module = undefined, + function = undefined, + callback = Callback, + extra_state = Extra + }} ) -> {module, Module} = lists:keyfind(module, 1, erlang:fun_info(Callback)), {name, Function} = lists:keyfind(name, 1, erlang:fun_info(Callback)), - expand_methods(Method, Path, Module, Function, Extra); -classify_handler( - #node_comp{ - comparator = Method, - value = #nova_handler_value{ - module = Module, - function = Function, - extra_state = Extra - } - }, - Path + expand_methods(Method, rebar3_nova_dispatch:openapi_path(Path), Module, Function, Extra); +classify_route( + {Path, Method, #nova_handler_value{ + module = Module, + function = Function, + extra_state = Extra + }} ) -> - expand_methods(Method, Path, Module, Function, Extra); -classify_handler(#node_comp{value = #cowboy_handler_value{}}, _Path) -> - false. + expand_methods(Method, rebar3_nova_dispatch:openapi_path(Path), Module, Function, Extra); +classify_route({_Path, _Method, #cowboy_handler_value{}}) -> + []. expand_methods('_', Path, Module, Function, Extra) -> Methods = [<<"get">>, <<"post">>, <<"put">>, <<"delete">>, <<"patch">>], - {true, [{Path, M, Module, Function, Extra} || M <- Methods]}; + [{Path, M, Module, Function, Extra} || M <- Methods]; expand_methods(Method, Path, Module, Function, Extra) -> - {true, [{Path, method_to_binary(Method), Module, Function, Extra}]}. - -method_to_binary(Method) when is_atom(Method) -> - erlang:atom_to_binary(Method); -method_to_binary(Method) when is_binary(Method) -> - string:lowercase(Method). + [{Path, rebar3_nova_dispatch:method_to_binary(Method), Module, Function, Extra}]. %% =================================================================== %% Schema loading diff --git a/src/rebar3_nova_routes.erl b/src/rebar3_nova_routes.erl index b605880..4343d32 100644 --- a/src/rebar3_nova_routes.erl +++ b/src/rebar3_nova_routes.erl @@ -2,8 +2,10 @@ -export([init/1, do/1, format_error/1]). +%% Exported for rebar3_nova_dispatch_SUITE. +-export([print_routes/1]). + -include("nova_router.hrl"). --include_lib("routing_tree/include/routing_tree.hrl"). -define(PROVIDER, routes). -define(DEPS, [{default, compile}]). @@ -47,95 +49,39 @@ format_error(Reason) -> %% Private functions %% =================================================================== print_routes(Dispatch) -> - format_tree(Dispatch). + Routes = lists:sort(nova_routing_trie:routes(Dispatch)), + [print_route(Route) || Route <- Routes], + ok. + +print_route({Host, Path, Method, Payload}) -> + io:format( + "~-8ts ~-40ts ~ts~ts~n", + [ + rebar3_nova_dispatch:method_to_binary(Method), + format_path(Path), + format_handler(Payload), + format_host(Host) + ] + ). -format_tree([]) -> - ok; -format_tree(#host_tree{hosts = Hosts}) -> - format_tree(Hosts); -format_tree([{Host, #routing_tree{tree = Tree}} | Tl]) -> - io:format("Host: ~p~n", [Host]), - format_tree(Tree, 1) ++ format_tree(Tl). +format_path(Path) when is_integer(Path) -> + %% A status-code route rather than a URL. + io_lib:format("(status ~b)", [Path]); +format_path(Path) -> + Path. -format_tree([], _Depth) -> - []; -format_tree([#node{segment = Segment, value = [], children = Children} | Tl], Depth) -> - %% Just a plain node - Segment0 = - case false of - _ when - is_list(Segment) orelse - is_binary(Segment) - -> - Segment; - _ when is_integer(Segment) -> - erlang:integer_to_list(Segment); - _Catchall -> - "[...]" - end, - Prefix = [$\s || _X <- lists:seq(0, Depth * 4)], - case Tl of - [] -> - io:format("~ts~ts /~ts~n", [Prefix, <<226, 148, 148, 226, 148, 128, 32>>, Segment0]); - _ -> - io:format("~ts~ts /~ts~n", [Prefix, <<226, 148, 156, 226, 148, 128, 32>>, Segment0]) - end, - format_tree(Children, Depth + 1), - format_tree(Tl, Depth); -format_tree([#node{segment = Segment, value = Value, children = Children} | Tl], Depth) -> - Segment0 = - case false of - _ when - is_list(Segment) orelse - is_binary(Segment) - -> - Segment; - _ when is_integer(Segment) -> - erlang:integer_to_list(Segment); - _CatchAll -> - "[...]" - end, - Prefix = [$\s || _X <- lists:seq(0, Depth * 4)], +format_host('_') -> <<>>; +format_host(Host) -> io_lib:format(" [host ~ts]", [Host]). - lists:foreach( - fun(#node_comp{comparator = Method, value = Value0}) -> - {App, Mod, Func} = - case Value0 of - #nova_handler_value{ - app = App0, module = undefined, function = undefined, callback = Callback0 - } -> - {module, Module} = lists:keyfind(module, 1, erlang:fun_info(Callback0)), - {name, Function} = lists:keyfind(name, 1, erlang:fun_info(Callback0)), - {App0, Module, Function}; - #nova_handler_value{app = App0, module = Mod0, function = Func0} -> - {App0, Mod0, Func0}; - #cowboy_handler_value{app = App0, handler = Handler} -> - {App0, Handler, init} - end, - case Tl of - [] -> - io:format("~ts~ts ~ts /~ts (~ts, ~ts:~ts/1)~n", [ - Prefix, - <<226, 148, 148, 226, 148, 128, 32>>, - Method, - Segment0, - App, - Mod, - Func - ]); - _ -> - io:format("~ts~ts ~ts /~ts (~ts, ~ts:~ts/1)~n", [ - Prefix, - <<226, 148, 156, 226, 148, 128, 32>>, - Method, - Segment0, - App, - Mod, - Func - ]) - end - end, - Value - ), - format_tree(Children, Depth + 1), - format_tree(Tl, Depth). +format_handler(#nova_handler_value{module = undefined, function = undefined, callback = Callback}) when + is_function(Callback) +-> + {module, Module} = lists:keyfind(module, 1, erlang:fun_info(Callback)), + {name, Function} = lists:keyfind(name, 1, erlang:fun_info(Callback)), + io_lib:format("~ts:~ts/1", [Module, Function]); +format_handler(#nova_handler_value{module = Module, function = Function}) -> + io_lib:format("~ts:~ts/1", [Module, Function]); +format_handler(#cowboy_handler_value{handler = Handler}) -> + io_lib:format("~ts (cowboy handler)", [Handler]); +format_handler(Other) -> + io_lib:format("~p", [Other]). diff --git a/test/rebar3_nova_dispatch_SUITE.erl b/test/rebar3_nova_dispatch_SUITE.erl new file mode 100644 index 0000000..c0347e5 --- /dev/null +++ b/test/rebar3_nova_dispatch_SUITE.erl @@ -0,0 +1,190 @@ +%%% Covers reading a compiled Nova dispatch table. +%%% +%%% The route-reporting tasks used to walk routing_tree's records directly. +%%% They now go through rebar3_nova_dispatch, and this suite builds a real +%%% dispatch table with nova_router and checks each task can still read it. +-module(rebar3_nova_dispatch_SUITE). + +-compile([export_all, nowarn_export_all]). + +-include_lib("common_test/include/ct.hrl"). +-include_lib("stdlib/include/assert.hrl"). +-include_lib("nova/include/nova_router.hrl"). + +all() -> + [ + routes_are_flattened, + status_code_routes_are_left_out, + openapi_paths_use_braces, + catch_all_is_dropped_from_openapi_paths, + methods_are_lowercased, + audit_reads_the_table, + openapi_reads_the_table, + doctor_reads_the_table, + routes_task_prints_the_table + ]. + +init_per_suite(Config) -> + application:load(nova), + application:set_env(nova, dispatch_backend, persistent_term), + [{dispatch, dispatch()} | Config]. + +end_per_suite(_Config) -> + ok. + +%%==================================================================== +%% A dispatch table covering the shapes the tasks care about +%%==================================================================== + +dispatch() -> + Value = #nova_handler_value{ + app = test_app, + callback = fun test_controller:index/1, + secure = false, + plugins = [] + }, + Trie0 = nova_routing_trie:new(#{}), + {ok, Trie1} = nova_routing_trie:insert('_', "/users", <<"GET">>, Value, Trie0), + {ok, Trie2} = nova_routing_trie:insert('_', "/users", <<"POST">>, Value, Trie1), + {ok, Trie3} = nova_routing_trie:insert('_', "/users/:id", <<"GET">>, Value, Trie2), + {ok, Trie4} = nova_routing_trie:insert( + '_', + "/assets/[...]", + '_', + Value#nova_handler_value{module = nova_file_controller}, + Trie3 + ), + {ok, Trie5} = nova_routing_trie:insert( + '_', + "/ws", + '_', + #cowboy_handler_value{ + app = test_app, + handler = nova_ws_handler, + arguments = #{}, + plugins = [], + secure = false + }, + Trie4 + ), + {ok, Trie6} = nova_routing_trie:insert( + '_', + 404, + '_', + Value#nova_handler_value{module = nova_error_controller}, + Trie5 + ), + Trie6. + +%%==================================================================== +%% rebar3_nova_dispatch +%%==================================================================== + +routes_are_flattened(Config) -> + Routes = rebar3_nova_dispatch:routes(?config(dispatch, Config)), + Paths = lists:usort([Path || {Path, _Method, _Payload} <- Routes]), + ?assertEqual([<<"/assets/[...]">>, <<"/users">>, <<"/users/:id">>, <<"/ws">>], Paths), + %% /users carries two methods, so it appears twice. + ?assertEqual(2, length([P || {P, _M, _V} <- Routes, P =:= <<"/users">>])). + +status_code_routes_are_left_out(Config) -> + Routes = rebar3_nova_dispatch:routes(?config(dispatch, Config)), + ?assertEqual([], [R || R = {Path, _M, _V} <- Routes, not is_binary(Path)]). + +openapi_paths_use_braces(_Config) -> + ?assertEqual(<<"/users/{id}">>, rebar3_nova_dispatch:openapi_path(<<"/users/:id">>)), + ?assertEqual(<<"/users">>, rebar3_nova_dispatch:openapi_path(<<"/users">>)), + ?assertEqual(<<"/a/{b}/c/{d}">>, rebar3_nova_dispatch:openapi_path(<<"/a/:b/c/:d">>)), + ?assertEqual(<<"/">>, rebar3_nova_dispatch:openapi_path(<<"/">>)). + +catch_all_is_dropped_from_openapi_paths(_Config) -> + ?assertEqual(<<"/assets">>, rebar3_nova_dispatch:openapi_path(<<"/assets/[...]">>)), + ?assertEqual(<<"/">>, rebar3_nova_dispatch:openapi_path(<<"/[...]">>)). + +methods_are_lowercased(_Config) -> + ?assertEqual(<<"get">>, rebar3_nova_dispatch:method_to_binary(<<"GET">>)), + ?assertEqual(<<"any">>, rebar3_nova_dispatch:method_to_binary('_')), + ?assertEqual(<<"post">>, rebar3_nova_dispatch:method_to_binary(post)). + +%%==================================================================== +%% The tasks +%%==================================================================== + +%% The audit skips file and error controllers and websocket handlers, and +%% expands an any-method route across the verbs it reports on. +audit_reads_the_table(Config) -> + Routes = rebar3_nova_audit:collect_routes(?config(dispatch, Config)), + Paths = lists:usort([Path || {Path, _M, _S, _Mod, _Any} <- Routes]), + ?assertEqual([<<"/users">>, <<"/users/{id}">>], Paths), + ?assertEqual( + [<<"get">>, <<"post">>], + lists:usort([M || {<<"/users">>, M, _S, _Mod, _Any} <- Routes]) + ). + +openapi_reads_the_table(Config) -> + Routes = rebar3_nova_openapi:collect_routes(?config(dispatch, Config)), + Paths = lists:usort([Path || {Path, _M, _Mod, _F, _E} <- Routes]), + ?assertEqual([<<"/users">>, <<"/users/{id}">>], Paths), + ?assert(lists:all(fun({_P, _M, Mod, _F, _E}) -> Mod =:= test_controller end, Routes)). + +%% The doctor reports on every route, websockets included. +doctor_reads_the_table(Config) -> + Handlers = rebar3_nova_doctor:collect_route_handlers(?config(dispatch, Config)), + ?assertEqual(5, length(Handlers)), + ?assertMatch( + [{<<"/ws">>, '_', nova_ws_handler, init, 2}], + [H || H = {<<"/ws">>, _M, _Mod, _F, _A} <- Handlers] + ), + ?assert(lists:all(fun({_P, _M, Mod, _F, _A}) -> Mod =/= unknown end, Handlers)). + +routes_task_prints_the_table(Config) -> + Output = capture(fun() -> rebar3_nova_routes:print_routes(?config(dispatch, Config)) end), + ?assertNotEqual(nomatch, string:find(Output, "/users/:id")), + ?assertNotEqual(nomatch, string:find(Output, "test_controller:index/1")), + ?assertNotEqual(nomatch, string:find(Output, "(status 404)")), + ?assertNotEqual(nomatch, string:find(Output, "nova_ws_handler")). + +%%==================================================================== +%% Helpers +%%==================================================================== + +capture(Fun) -> + Self = self(), + Ref = make_ref(), + {Pid, MonRef} = + spawn_monitor(fun() -> + group_leader(spawn_capture(Self, Ref), self()), + Fun(), + exit(normal) + end), + receive + {'DOWN', MonRef, process, Pid, _Reason} -> ok + after 5000 -> + ct:fail(capture_timeout) + end, + collect_output(Ref, []). + +spawn_capture(Owner, Ref) -> + spawn(fun() -> capture_loop(Owner, Ref) end). + +capture_loop(Owner, Ref) -> + receive + {io_request, From, ReplyAs, {put_chars, unicode, Chars}} -> + Owner ! {Ref, unicode:characters_to_list(Chars)}, + From ! {io_reply, ReplyAs, ok}, + capture_loop(Owner, Ref); + {io_request, From, ReplyAs, {put_chars, unicode, M, F, A}} -> + Owner ! {Ref, unicode:characters_to_list(apply(M, F, A))}, + From ! {io_reply, ReplyAs, ok}, + capture_loop(Owner, Ref); + {io_request, From, ReplyAs, _Other} -> + From ! {io_reply, ReplyAs, ok}, + capture_loop(Owner, Ref) + end. + +collect_output(Ref, Acc) -> + receive + {Ref, Chars} -> collect_output(Ref, [Chars | Acc]) + after 0 -> + lists:flatten(lists:reverse(Acc)) + end.