Packages
nova
0.9.1
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
%%%
%%% @end
%%%-------------------------------------------------------------------
-module(nova_router).
-behaviour(cowboy_middleware).
%% Cowboy middleware-callbacks
-export([
execute/2
]).
%% API
-export([
compile/1,
lookup_url/1,
lookup_url/2,
lookup_url/3,
render_status_page/2,
render_status_page/3,
render_status_page/5,
%% Expose the router-callback
routes/1
]).
-include_lib("kernel/include/logger.hrl").
-include_lib("routing_tree/include/routing_tree.hrl").
-include("nova_router.hrl").
-type bindings() :: #{binary() := binary()}.
-export_type([bindings/0]).
%% This module is also exposing callbacks for routers
-callback routes(Env :: atom()) -> {ok, Routes :: any()}.
-spec compile(Apps :: [atom()]) -> host_tree().
compile(Apps) ->
UseStrict = application:get_env(nova, use_strict_routing, false),
Dispatch = compile(Apps, routing_tree:new(#{use_strict => UseStrict, convert_to_binary => true}), #{}),
persistent_put(nova_dispatch, Dispatch),
Dispatch.
-spec execute(Req, Env :: cowboy_middleware:env()) -> {ok, Req, Env0} | {stop, Req}
when Req::cowboy_req:req(),
Env0::cowboy_middleware:env().
execute(Req = #{host := Host, path := Path, method := Method}, Env) ->
Dispatch = persistent_get(nova_dispatch, Env),
case routing_tree:lookup(Host, Path, Method, Dispatch) of
{error, not_found} -> render_status_page('_', 404, #{error => "Not found in path"}, Req, Env);
{ok, Bindings, #nova_handler_value{app = App, module = Module, function = Function,
secure = Secure, plugins = Plugins, extra_state = ExtraState}} ->
{ok,
Req#{plugins => Plugins,
bindings => Bindings},
Env#{app => App,
module => Module,
function => Function,
secure => Secure,
controller_data => #{},
extra_state => ExtraState
}
};
{ok, Bindings, #cowboy_handler_value{app = App, handler = Handler, arguments = Args,
plugins = Plugins, secure = Secure}} ->
{ok,
Req#{plugins => Plugins,
bindings => Bindings},
Env#{app => App,
cowboy_handler => Handler,
arguments => Args,
secure => Secure
}
};
{ok, Bindings, #cowboy_handler_value{app = App, handler = Handler, arguments = Args,
plugins = Plugins, secure = Secure}, PathInfo} ->
{ok,
Req#{path_info => PathInfo,
plugins => Plugins,
bindings => Bindings},
Env#{app => App,
cowboy_handler => Handler,
arguments => Args,
secure => Secure
}
};
Error ->
logger:error(#{"reason" => "Unexpected return from routing_tree:lookup/4", "return_object" => Error}),
render_status_page(Host, 404, #{error => Error}, Req, Env)
end.
lookup_url(Path) ->
lookup_url('_', Path).
lookup_url(Host, Path) ->
lookup_url(Host, Path, '_').
lookup_url(Host, Path, Method) ->
Dispatch = persistent_get(nova_dispatch),
lookup_url(Host, Path, Method, Dispatch).
lookup_url(Host, Path, Method, Dispatch) ->
routing_tree:lookup(Host, Path, Method, Dispatch).
%%%%%%%%%%%%%%%%%%%%%%%%
%% INTERNAL FUNCTIONS %%
%%%%%%%%%%%%%%%%%%%%%%%%
-spec compile(Apps :: [atom()], Dispatch :: host_tree(), Options :: map()) -> host_tree().
compile([], Dispatch, _Options) -> Dispatch;
compile([App|Tl], Dispatch, Options) ->
%% Fetch the router-module for this application
Router = erlang:list_to_atom(erlang:atom_to_list(App) ++ "_router"),
Env = nova:get_environment(),
%% Ensure that the module is loaded
Routes = erlang:apply(Router, routes, [Env]),
Options1 = Options#{app => App},
{ok, Dispatch1, Options2} = compile_paths(Routes, Dispatch, Options1),
compile(Tl, Dispatch1, Options2).
compile_paths([], Dispatch, Options) -> {ok, Dispatch, Options};
compile_paths([RouteInfo|Tl], Dispatch, Options) ->
App = maps:get(app, Options),
%% Fetch the global plugins
GlobalPlugins = application:get_env(nova, plugins, []),
Plugins = maps:get(plugins, RouteInfo, GlobalPlugins),
Value = #nova_handler_value{secure = maps:get(secure, Options, maps:get(security, RouteInfo, false)),
app = App, plugins = normalize_plugins(Plugins),
extra_state = maps:get(extra_state, RouteInfo, #{})},
Prefix = maps:get(prefix, Options, "") ++ maps:get(prefix, RouteInfo, ""),
Host = maps:get(host, RouteInfo, '_'),
SubApps = maps:get(apps, RouteInfo, []),
%% We need to add this app info to nova-env
NovaEnv = nova:get_env(apps, []),
NovaEnv0 = [{App, #{prefix => Prefix}} | NovaEnv],
nova:set_env(apps, NovaEnv0),
{ok, Dispatch1} = parse_url(Host, maps:get(routes, RouteInfo, []), Prefix, Value, Dispatch),
Dispatch2 = compile(SubApps, Dispatch1, Options#{value => Value, prefix => Prefix}),
compile_paths(Tl, Dispatch2, Options).
parse_url(_Host, [], _Prefix, _Value, Tree) -> {ok, Tree};
parse_url(Host, [{StatusCode, StaticFile}|Tl], Prefix, Value, Tree) when is_integer(StatusCode),
is_list(StaticFile) ->
%% We need to signal that we have a static file here somewhere
Value0 = Value#nova_handler_value{
module = nova_static,
function = execute},
%% TODO! Fix status-code so it's being threated specially
Tree0 = insert(Host, StatusCode, '_', Value0, Tree),
parse_url(Host, Tl, Prefix, Value0, Tree0);
parse_url(Host, [{StatusCode, {Module, Function}, Options}|Tl], Prefix, Value, Tree) when is_integer(StatusCode) ->
Value0 = Value#nova_handler_value{
module = Module,
function = Function},
Res = lists:foldl(fun(Method, Tree0) ->
insert(Host, StatusCode, Method, Value0, Tree0)
end, Tree, maps:get(methods, Options, ['_'])),
parse_url(Host, Tl, Prefix, Value, Res);
parse_url(Host,
[{RemotePath, LocalPath}|Tl],
Prefix, Value = #nova_handler_value{app = App, secure = Secure},
Tree) when is_list(RemotePath), is_list(LocalPath) ->
%% Static assets - check that the path exists
PrivPath = filename:join(code:lib_dir(App, priv), LocalPath),
Payload =
case {filelib:is_dir(LocalPath), filelib:is_dir(PrivPath)} of
{false, false} ->
%% No directory - check if it's a file
case {filelib:is_file(LocalPath), filelib:is_file(PrivPath)} of
{false, false} ->
%% No dir nor file
logger:warning(#{"reason" => "Could not find local path for the given resource",
"local_path" => LocalPath, "remote_path" => RemotePath}),
not_found;
{true, false} ->
{file, LocalPath};
{_, true} ->
{priv_file, App, LocalPath}
end;
{true, false} ->
{dir, LocalPath};
{_, true} ->
{priv_dir, App, LocalPath}
end,
Value0 = #cowboy_handler_value{
app = App,
handler = cowboy_static,
arguments = Payload,
plugins = Value#nova_handler_value.plugins,
secure = Secure
},
Tree0 = insert(Host, string:concat(Prefix, RemotePath), '_', Value0, Tree),
parse_url(Host, Tl, Prefix, Value, Tree0);
parse_url(Host,
[{RemotePath, LocalPath}|Tl],
Prefix,
Value = #nova_handler_value{app = App, secure = Secure}, Tree)
when is_list(RemotePath), is_list(LocalPath) ->
%% Static assets - check that the path exists
PrivPath = filename:join(code:lib_dir(App, priv), LocalPath),
Payload =
case {filelib:is_dir(LocalPath), filelib:is_dir(PrivPath)} of
{false, false} ->
%% No directory - check if it's a file
case {filelib:is_file(LocalPath), filelib:is_file(PrivPath)} of
{false, false} ->
%% No dir nor file
logger:warning(#{"reason" => "Could not find local path for the given resource",
"local_path" => LocalPath, "remote_path" => RemotePath}),
not_found;
{true, false} ->
{file, LocalPath};
{_, true} ->
{priv_file, App, LocalPath}
end;
{true, false} ->
{dir, LocalPath};
{_, true} ->
{priv_dir, App, LocalPath}
end,
Value0 = #cowboy_handler_value{
app = App,
handler = cowboy_static,
arguments = Payload,
plugins = Value#nova_handler_value.plugins,
secure = Secure
},
logger:debug(#{"action" => "Adding route", "route" => string:concat(Prefix, RemotePath), "app" => App}),
Tree0 = insert(Host, string:concat(Prefix, RemotePath), '_', Value0, Tree),
parse_url(Host, Tl, Prefix, Value, Tree0);
parse_url(Host,
[{Path, {Mod, Func}, Options}|Tl],
Prefix,
Value = #nova_handler_value{app = App, secure = _Secure},
Tree) ->
RealPath = case Path of
_ when is_list(Path) -> string:concat(Prefix, Path);
_ when is_integer(Path) -> Path;
_ -> throw({unknown_path, Path})
end,
Methods = maps:get(methods, Options, ['_']),
CompiledPaths =
lists:foldl(
fun(Method, Tree0) ->
BinMethod = method_to_binary(Method),
Value0 =
case maps:get(protocol, Options, http) of
http ->
Value#nova_handler_value{
module = Mod,
function = Func
}
end,
logger:debug(#{"action" => "Adding route", "route" => RealPath, "app" => App}),
insert(Host, RealPath, BinMethod, Value0, Tree0)
end, Tree, Methods),
parse_url(Host, Tl, Prefix, Value, CompiledPaths);
parse_url(Host,
[{Path, Mod, #{protocol := ws}} | Tl],
Prefix, #nova_handler_value{app = App, secure = Secure} = Value,
Tree) ->
Value0 = #cowboy_handler_value{
app = App,
handler = nova_ws_handler,
arguments = #{module => Mod},
plugins = Value#nova_handler_value.plugins,
secure = Secure},
logger:debug(#{"action" => "Adding route", "protocol" => "ws", "route" => Path, "app" => App}),
RealPath = string:concat(Prefix, Path),
CompiledPaths = insert(Host, RealPath, '_', Value0, Tree),
parse_url(Host, Tl, Prefix, Value, CompiledPaths);
parse_url(Host, [{Path, {Mod, Func}}|Tl], Prefix, Value, Tree) ->
parse_url(Host, [{Path, {Mod, Func}, #{}}|Tl], Prefix, Value, Tree).
-spec render_status_page(StatusCode :: integer(), Req :: cowboy_req:req()) ->
{ok, Req0 :: cowboy_req:req(), Env :: map()}.
render_status_page(StatusCode, Req) ->
render_status_page(StatusCode, #{}, Req).
-spec render_status_page(StatusCode :: integer(), Data :: map(), Req :: cowboy_req:req()) ->
{ok, Req0 :: cowboy_req:req(), Env :: map()}.
render_status_page(StatusCode, Data, Req) ->
Dispatch = persistent_get(nova_dispatch),
render_status_page('_', StatusCode, Data, Req, #{dispatch => Dispatch}).
-spec render_status_page(Host :: binary() | atom(),
StatusCode :: integer(),
Data :: map(),
Req :: cowboy_req:req(),
Env :: map()) -> {ok, Req0 :: cowboy_req:req(), Env :: map()}.
render_status_page(Host, StatusCode, Data, Req, Env) ->
Dispatch = persistent_get(nova_dispatch, Env),
Env0 =
case routing_tree:lookup(Host, StatusCode, '_', Dispatch) of
{error, _} ->
%% Render nova page if exists - We need to determine where to find this path?
Env#{app => nova,
module => nova_error_controller,
function => status_code,
secure => false,
controller_data => #{status => StatusCode, data => Data}};
{ok, Bindings, #nova_handler_value{app = App,
module = Module,
function = Function,
secure = Secure,
extra_state = ExtraState}} ->
Env#{app => App,
module => Module,
function => Function,
secure => Secure,
controller_data => #{status => StatusCode, data => Data},
bindings => Bindings,
extra_state => ExtraState}
end,
{ok, Req#{resp_status_code => StatusCode}, Env0}.
insert(Host, Path, Combinator, Value, Tree) ->
try routing_tree:insert(Host, Path, Combinator, Value, Tree) of
Tree0 -> Tree0
catch
throw:Exception ->
logger:error(#{"reason" => "Error when inserting route", "route" => Path, "combinator" => Combinator}),
throw(Exception);
Type:Exception ->
logger:error(#{"reason" => "Unexpected exit", "type" => Type, "exception" => Exception}),
throw(Exception)
end.
normalize_plugins(Plugins) ->
lists:reverse(normalize_plugins(Plugins, [])).
normalize_plugins([], Ack) -> Ack;
normalize_plugins([{Type, PluginName, Options}|Tl], Ack) ->
ExistingPlugins = proplists:get_value(Type, Ack, []),
normalize_plugins(Tl, [{Type, [{PluginName, Options}|ExistingPlugins]}|proplists:delete(Type, Ack)]).
method_to_binary(get) -> <<"GET">>;
method_to_binary(post) -> <<"POST">>;
method_to_binary(put) -> <<"PUT">>;
method_to_binary(delete) -> <<"DELETE">>;
method_to_binary(options) -> <<"OPTIONS">>;
method_to_binary(head) -> <<"HEAD">>;
method_to_binary(connect) -> <<"CONNECT">>;
method_to_binary(trace) -> <<"TRACE">>;
method_to_binary(patch) -> <<"PATCH">>;
method_to_binary(_) -> '_'.
persistent_put(Key, Value) ->
case application:get_env(nova, use_persistent_term, true) of
true ->
persistent_term:put(Key, Value);
_ ->
ok
end.
persistent_get(Key) ->
case application:get_env(nova, use_persistent_term, true) of
true ->
persistent_term:get(Key);
_ ->
logger:warning(#{"reason" => "Called persistent_get/1 without option use_persistent_term set to true"}),
throw({unsupported_operation, persistent_get})
end.
persistent_get(Key, Env) ->
case application:get_env(nova, use_persistent_term, true) of
true ->
persistent_term:get(Key);
_ ->
maps:get(Key, Env)
end.
routes(_) ->
[#{
routes => [
{404, { nova_error_controller, not_found }, #{}},
{500, { nova_error_controller, server_error }, #{}}
]
}].
-ifdef(TEST).
-compile(export_all). %% Export all functions for testing purpose
-include_lib("eunit/include/eunit.hrl").
-endif.