Packages
nova
0.2.2
0.15.1
0.14.3
0.14.1
0.13.7
0.13.1
0.13.0
0.12.1
0.12.0
0.11.0
0.10.4
0.10.3
0.10.2
0.10.1
0.10.0
0.9.24
0.9.23
0.9.22
0.9.21
0.9.20
0.9.19
0.9.18
0.9.17
0.9.16
0.9.15
0.9.11
0.9.5
0.9.4
0.9.3
0.9.2
0.9.1
0.9.0
0.8.6
0.8.5
0.8.4
0.8.3
0.8.2
0.8.1
0.8.0
0.7.2
0.7.1
0.7.0
0.6.2
0.6.1
0.6.0
0.5.2
0.5.1
0.4.4
0.4.3
0.4.2
0.4.1
0.2.2
0.1.0
0.0.1
0.0.0
Nova is a web application framework
Current section
Files
Jump to
Current section
Files
src/nova_router.erl
%%% @author Niclas Axelsson <niclas@burbas.se>
%%% @doc
%%% Nova router is responsible for handling all incoming request and
%%% routes them to the correct controller.
%%% @end
-module(nova_router).
-behaviour(gen_server).
%% API
-export([
start_link/0,
get_status_page/2,
get_main_app/0,
process_routefile/2,
add_route/4,
add_route/5,
add_route/6,
apply_routes/0,
remove_route/2,
get_routes/0,
get_route_info/1
]).
%% gen_server callbacks
-export([
init/1,
handle_call/3,
handle_cast/2,
handle_info/2,
terminate/2,
code_change/3
]).
-include_lib("nova/include/nova.hrl").
-ifdef(TEST).
-include_lib("eunit/include/eunit.hrl").
-endif.
-define(SERVER, ?MODULE).
-record(state, {
main_app :: atom(),
listener = nova_listener :: atom(),
dispatch_table = [] :: list()
}).
%%%===================================================================
%%% API
%%%===================================================================
%%--------------------------------------------------------------------
%% @doc
%% Starts the server
%%
%% @spec start_link() -> {ok, Pid} | ignore | {error, Error}
%% @end
%%--------------------------------------------------------------------
start_link() ->
gen_server:start_link({local, ?SERVER}, ?MODULE, [], []).
get_status_page(Error, Req) ->
cowboy_req:reply(Error, #{}, Req).
get_main_app() ->
gen_server:call(?SERVER, get_main_app).
process_routefile(App, Routefile) ->
gen_server:cast(?SERVER, {process_routes, App, Routefile}).
add_route(App, CallbackInfo, Route, Security) ->
add_route(App, CallbackInfo, '_', Route, Security).
add_route(App, CallbackInfo, Host, Route, Security) ->
add_route(App, CallbackInfo, Host, Route, Security, #{}).
add_route(App, CallbackInfo, Host, Route, Security, Options) ->
gen_server:cast(?SERVER, {add_route, App, CallbackInfo, Host, Route, Security, Options}).
add_static(App, Path, Host, Route) ->
gen_server:cast(?SERVER, {add_static, App, Path, Host, Route}).
apply_routes() ->
gen_server:cast(?SERVER, apply_routes).
remove_route(Host, Route) ->
gen_server:cast(?SERVER, {remove_route, Host, Route}).
get_routes() ->
gen_server:call(?SERVER, get_routes).
get_route_info(Route) ->
gen_server:call(?SERVER, {get_route_info, Route}).
%%%===================================================================
%%% gen_server callbacks
%%%===================================================================
%%--------------------------------------------------------------------
%% @private
%% @doc
%% Initializes the server
%%
%% @spec init(Args) -> {ok, State} |
%% {ok, State, Timeout} |
%% ignore |
%% {stop, Reason}
%% @end
%%--------------------------------------------------------------------
init([]) ->
Apps =
case application:get_env(nova_applications) of
undefined -> [];
{ok, AppsList} -> AppsList
end,
lists:foreach(fun load_app_route/1, Apps),
{ok, MainApp} = application:get_application(),
process_flag(trap_exit, true),
{ok, #state{main_app = MainApp}}.
%%--------------------------------------------------------------------
%% @private
%% @doc
%% Handling call messages
%%
%% @spec handle_call(Request, From, State) ->
%% {reply, Reply, State} |
%% {reply, Reply, State, Timeout} |
%% {noreply, State} |
%% {noreply, State, Timeout} |
%% {stop, Reason, Reply, State} |
%% {stop, Reason, State}
%% @end
%%--------------------------------------------------------------------
handle_call(get_main_app, _From, State = #state{main_app = Application}) ->
{reply, {ok, Application}, State};
handle_call(get_routes, _From, State = #state{dispatch_table = DT}) ->
{reply, {ok, DT}, State};
handle_call({get_route_info, Route}, _From, State = #state{dispatch_table = DT}) ->
case lists:keyfind(Route, 1, DT) of
false ->
%% Not found
{reply, {error, not_found}, State};
Route ->
{reply, {ok, Route}, State}
end;
handle_call(_Request, _From, State) ->
Reply = ok,
{reply, Reply, State}.
%%--------------------------------------------------------------------
%% @private
%% @doc
%% Handling cast messages
%%
%% @spec handle_cast(Msg, State) -> {noreply, State} |
%% {noreply, State, Timeout} |
%% {stop, Reason, State}
%% @end
%%--------------------------------------------------------------------
handle_cast({process_routes, App, Routefile}, State) ->
{ok, HostRoutes} = file:consult(Routefile),
process_routes(App, HostRoutes),
apply_routes(),
{noreply, State};
handle_cast({remove_route, Host, Route}, State = #state{dispatch_table = DT}) ->
case proplists:get_value(Host, DT) of
undefined ->
?WARNING("Could not remove route: ~p for host: ~p. Host not found!", [Route, Host]),
{noreply, State};
{Host, Routes} ->
NewRoutes = proplists:delete(Route, Routes),
case NewRoutes of
Routes ->
?WARNING("Could not remove route: ~p for host: ~p. Route not found!", [Route, Host]),
{noreply, State};
_ ->
?INFO("Removed route: ~p from host: ~p", [Route, Host]),
DT2 = proplists:delete(Host, DT),
NewDT = [{Host, NewRoutes}|DT2],
{noreply, State#state{dispatch_table = NewDT}}
end
end;
handle_cast(apply_routes, State = #state{dispatch_table = DT,
listener = Listener}) ->
?INFO("Applying routes. ~p", [DT]),
Dispatch = cowboy_router:compile(DT),
cowboy:set_env(Listener, dispatch, Dispatch),
{noreply, State};
handle_cast({add_static, App, Path, Host, Route}, State = #state{dispatch_table = DT}) ->
RouteInfo = {Route, cowboy_static, {priv_dir, App, Path}},
NewDT =
case proplists:get_value(Host, DT) of
undefined ->
[{Host, [RouteInfo]}|DT];
Routes ->
[{Host, [RouteInfo|Routes]}|proplists:delete(Host, DT)]
end,
{noreply, State#state{dispatch_table = NewDT}};
handle_cast({add_route, App, CallbackInfo, Host, Route, Secure, Options}, State = #state{dispatch_table = DT}) ->
InitialState = #{app => App,
secure => Secure
},
RouteInfo =
case maps:get(protocol, Options, http) of
http ->
{Module, Func} = CallbackInfo,
HttpState = InitialState#{mod => Module,
func => Func,
methods => get_methods(Options),
protocol => http},
{Route, nova_http_controller, HttpState};
ws ->
SubProtocols = maps:get(subprotocols, Options, []),
WSState = InitialState#{mod => CallbackInfo,
subprotocols => SubProtocols,
protocol => websocket},
{Route, nova_ws_controller, WSState}
end,
NewDT =
case proplists:get_value(Host, DT) of
undefined ->
[{Host, [RouteInfo]}|DT];
Routes ->
[{Host, [RouteInfo|Routes]}|proplists:delete(Host, DT)]
end,
{noreply, State#state{dispatch_table = NewDT}};
handle_cast(_Msg, State) ->
{noreply, State}.
%%--------------------------------------------------------------------
%% @private
%% @doc
%% Handling all non call/cast messages
%%
%% @spec handle_info(Info, State) -> {noreply, State} |
%% {noreply, State, Timeout} |
%% {stop, Reason, State}
%% @end
%%--------------------------------------------------------------------
handle_info(_Info, State) ->
{noreply, State}.
%%--------------------------------------------------------------------
%% @private
%% @doc
%% This function is called by a gen_server when it is about to
%% terminate. It should be the opposite of Module:init/1 and do any
%% necessary cleaning up. When it returns, the gen_server terminates
%% with Reason. The return value is ignored.
%%
%% @spec terminate(Reason, State) -> void()
%% @end
%%--------------------------------------------------------------------
terminate(_Reason, _State) ->
ok.
%%--------------------------------------------------------------------
%% @private
%% @doc
%% Convert process state when code is changed
%%
%% @spec code_change(OldVsn, State, Extra) -> {ok, NewState}
%% @end
%%--------------------------------------------------------------------
code_change(_OldVsn, State, _Extra) ->
{ok, State}.
%%%===================================================================
%%% Internal functions
%%%===================================================================
load_app_route(#{name := AppName, routes_file := RouteFile}) ->
case code:lib_dir(AppName) of
{error, bad_name} ->
?WARNING("Could not find the application ~p. Check your config and rerun the application", [AppName]),
ok;
Filepath ->
RouteFilePath = filename:join([Filepath, RouteFile]),
process_routefile(AppName, RouteFilePath)
end;
load_app_route(RouteInfo = #{name := AppName}) ->
Routename = "priv/" ++ erlang:atom_to_list(AppName) ++ ".routes.erl",
load_app_route(RouteInfo#{routes_file => Routename}).
process_routes(_, []) ->
ok;
process_routes(App, [RoutesLine|T]) ->
Prefix = maps:get(prefix, RoutesLine, ""),
Host = maps:get(host, RoutesLine, '_'),
Routes = maps:get(routes, RoutesLine, []),
Statics = maps:get(statics, RoutesLine, []),
Secure = maps:get(security, RoutesLine, false),
add_routes(App, Host, Prefix, Secure, Routes),
add_statics(App, Host, Prefix, Statics),
process_routes(App, T).
add_routes(_, _, _, _, []) ->
ok;
add_routes(App, Host, Prefix, Secure, [{Route, CallbackInfo} | T]) ->
add_route(App, CallbackInfo, Host, Prefix ++ Route, Secure),
add_routes(App, Host, Prefix, Secure, T);
add_routes(App, Host, Prefix, Secure, [{Route, CallbackInfo, Options} | T]) when is_map(Options) ->
add_route(App, CallbackInfo, Host, Prefix ++ Route, Secure, Options),
add_routes(App, Host, Prefix, Secure, T);
add_routes(App, Host, Prefix, Secure, [{Route, Module, Function} | T]) ->
add_route(App, {Module, Function}, Host, Prefix ++ Route, Secure),
add_routes(App, Host, Prefix, Secure, T);
add_routes(App, Host, Prefix, Secure, [{Route, Module, Function, Options} | T]) ->
add_route(App, {Module, Function}, Host, Prefix ++ Route, Secure, Options),
add_routes(App, Host, Prefix, Secure, T).
add_statics(_, _, _, []) ->
ok;
add_statics(App, Host, Prefix, [{Route, Path} | T]) ->
add_static(App, Path, Host, Prefix ++ Route),
add_statics(App, Host, Prefix, T).
get_methods(#{methods := M}) when is_list(M) ->
Res = lists:map(fun(get) -> <<"GET">>;
(post) -> <<"POST">>;
(put) -> <<"PUT">>;
(delete) -> <<"DELETE">>;
(_) -> throw(unknown_method)
end, M),
case length(Res) of
4 -> '_';
_ -> Res
end;
get_methods(#{methods := M}) ->
get_methods(#{methods => [M]});
get_methods(_) ->
'_'.
%%%%%%%%%%%%%%%%%%%%%%%%%%%
% Eunit functions %
%%%%%%%%%%%%%%%%%%%%%%%%%%%
-ifdef(TEST).
process_empty_route_test_() ->
?_assert(ok =:= process_routes(test, [])).
methods_test_() ->
[
?_assertMatch([<<"GET">>], get_methods(#{methods => [get]})),
?_assertMatch([<<"POST">>], get_methods(#{methods => [post]})),
?_assertMatch([<<"PUT">>], get_methods(#{methods => [put]})),
?_assertMatch([<<"DELETE">>], get_methods(#{methods => [delete]})),
?_assertMatch([<<"GET">>, <<"PUT">>], get_methods(#{methods => [get, put]}))
].
methods_wildcard_test_() ->
[?_assertException(throw, unknown_method, get_methods(#{methods => [something_else]})),
?_assertMatch('_', get_methods(undefined)),
?_assertMatch('_', get_methods(#{methods => [get,put,delete,post]}))
].
-endif.