Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions .github/labeler.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
labels:
code-change:
- changed-files:
- any-glob-to-any-file:
- "src/**"
- "include/**"
- "priv/**"
- "**/*.erl"
- "**/*.hrl"
- "**/*.app.src"
- "**/*.app"
documentation:
- changed-files:
- '*.md'
11 changes: 11 additions & 0 deletions guides/multi-app.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,3 +32,14 @@ There's currently two different options available and they works in the same way
]}
...
```

## Pragmatically starting other nova applications

### Starting an application

You can also start other nova applications pragmatically by calling `nova_sup:add_application/2` to add another nova application to your supervision tree. The routes will automatically be added to the routing-module.


## Stopping an application

To stop a nova application you can call `nova_sup:remove_application/1` with the name of the application you want to stop. Use this with caution since calling this method all routes for all other applications will be removed and re-added in order to filter out the one removed.
1 change: 0 additions & 1 deletion rebar.config
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@
{cowboy, "2.13.0"},
{erlydtl, "0.14.0"},
{jhn_stdlib, "5.4.0"},
{routing_tree, "1.0.11"},
{thoas, "1.2.1"}
]}.

Expand Down
143 changes: 143 additions & 0 deletions src/nova_request.erl
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
%%%-------------------------------------------------------------------
%%% @author Niclas Axelsson <burbas@MBPsomtrNiclas3.kgh.local>
%%% @copyright (C) 2024, Niclas Axelsson
%%% @doc
%%%
%%% @end
%%% Created : 22 Dec 2024 by Niclas Axelsson <burbas@MBPsomtrNiclas3.kgh.local>
%%%-------------------------------------------------------------------
-module(nova_request).

-behaviour(gen_server).

%% API
-export([
start_link/0
]).

%% gen_server callbacks
-export([
init/1,
handle_call/3,
handle_cast/2,
handle_info/2,
terminate/2,
code_change/3
]).

-define(SERVER, ?MODULE).

-record(state, {}).

%%%===================================================================
%%% API
%%%===================================================================

%%--------------------------------------------------------------------
%% @doc
%% Starts the server
%% @end
%%--------------------------------------------------------------------
-spec start_link() -> {ok, Pid :: pid()} |
{error, Error :: {already_started, pid()}} |
{error, Error :: term()} |
ignore.
start_link() ->
gen_server:start_link({local, ?SERVER}, ?MODULE, [], []).

%%%===================================================================
%%% gen_server callbacks
%%%===================================================================

%%--------------------------------------------------------------------
%% @private
%% @doc
%% Initializes the server
%% @end
%%--------------------------------------------------------------------
-spec init(Args :: term()) -> {ok, State :: term()} |
{ok, State :: term(), Timeout :: timeout()} |
{ok, State :: term(), hibernate} |
{stop, Reason :: term()} |
ignore.
init([]) ->
process_flag(trap_exit, true),
{ok, #state{}}.

%%--------------------------------------------------------------------
%% @private
%% @doc
%% Handling call messages
%% @end
%%--------------------------------------------------------------------
-spec handle_call(Request :: term(), From :: {pid(), term()}, State :: term()) ->
{reply, Reply :: term(), NewState :: term()} |
{reply, Reply :: term(), NewState :: term(), Timeout :: timeout()} |
{reply, Reply :: term(), NewState :: term(), hibernate} |
{noreply, NewState :: term()} |
{noreply, NewState :: term(), Timeout :: timeout()} |
{noreply, NewState :: term(), hibernate} |
{stop, Reason :: term(), Reply :: term(), NewState :: term()} |
{stop, Reason :: term(), NewState :: term()}.
handle_call(_Request, _From, State) ->
Reply = ok,
{reply, Reply, State}.

%%--------------------------------------------------------------------
%% @private
%% @doc
%% Handling cast messages
%% @end
%%--------------------------------------------------------------------
-spec handle_cast(Request :: term(), State :: term()) ->
{noreply, NewState :: term()} |
{noreply, NewState :: term(), Timeout :: timeout()} |
{noreply, NewState :: term(), hibernate} |
{stop, Reason :: term(), NewState :: term()}.
handle_cast(_Request, State) ->
{noreply, State}.

%%--------------------------------------------------------------------
%% @private
%% @doc
%% Handling all non call/cast messages
%% @end
%%--------------------------------------------------------------------
-spec handle_info(Info :: timeout() | term(), State :: term()) ->
{noreply, NewState :: term()} |
{noreply, NewState :: term(), Timeout :: timeout()} |
{noreply, NewState :: term(), hibernate} |
{stop, Reason :: normal | term(), NewState :: term()}.
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.
%% @end
%%--------------------------------------------------------------------
-spec terminate(Reason :: normal | shutdown | {shutdown, term()} | term(),
State :: term()) -> any().
terminate(_Reason, _State) ->
ok.

%%--------------------------------------------------------------------
%% @private
%% @doc
%% Convert process state when code is changed
%% @end
%%--------------------------------------------------------------------
-spec code_change(OldVsn :: term() | {down, term()},
State :: term(),
Extra :: term()) -> {ok, NewState :: term()} |
{error, Reason :: term()}.
code_change(_OldVsn, State, _Extra) ->
{ok, State}.

%%%===================================================================
%%% Internal functions
%%%===================================================================
94 changes: 68 additions & 26 deletions src/nova_router.erl
Original file line number Diff line number Diff line change
Expand Up @@ -25,15 +25,16 @@
%% Expose the router-callback
routes/1,

%% Modulates the routes-table
add_routes/2,

%% Fetch information about the routing table
plugins/0,
compiled_apps/0
compiled_apps/0,

%% Modulates the routes-table
add_routes/1,
add_routes/2,
remove_application/1
]).

-include_lib("routing_tree/include/routing_tree.hrl").
-include_lib("kernel/include/logger.hrl").
-include("../include/nova_router.hrl").
-include("../include/nova.hrl").
Expand All @@ -56,15 +57,21 @@ compiled_apps() ->
StorageBackend = application:get_env(nova, dispatch_backend, persistent_term),
StorageBackend:get(?NOVA_APPS, []).


%% TODO! We need to implement a way to get and remove plugins for a path
plugins() ->
StorageBackend = application:get_env(nova, dispatch_backend, persistent_term),
StorageBackend:get(?NOVA_PLUGINS, []).

-spec compile(Apps :: [atom() | {atom(), map()}]) -> host_tree().
-spec compile(Apps :: [atom() | {atom(), map()}]) -> nova_routing_tree: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}), #{}),
StorageBackend = application:get_env(nova, dispatch_backend, persistent_term),

StoredDispatch = StorageBackend:get(nova_dispatch,
nova_routing_tree:new(#{options => #{strict => UseStrict}})),
Dispatch = compile(Apps, StoredDispatch, #{}),
%% Write the updated dispatch to storage
StorageBackend:put(nova_dispatch, Dispatch),
Dispatch.

Expand All @@ -74,7 +81,7 @@ compile(Apps) ->
execute(Req = #{host := Host, path := Path, method := Method}, Env) ->
StorageBackend = application:get_env(nova, dispatch_backend, persistent_term),
Dispatch = StorageBackend:get(nova_dispatch),
case routing_tree:lookup(Host, Path, Method, Dispatch) of
case nova_routing_tree:find(Host, Path, Method, Dispatch) of
{error, not_found} ->
logger:debug("Path ~p not found for ~p in ~p", [Path, Method, Host]),
render_status_page('_', 404, #{error => "Not found in path"}, Req, Env);
Expand All @@ -97,18 +104,6 @@ execute(Req = #{host := Host, path := Path, method := Method}, Env) ->
controller_data => #{}
}
};
{ok, Bindings, #nova_handler_value{app = App, callback = Callback,
secure = Secure, plugins = Plugins, extra_state = ExtraState}, Pathinfo} ->
{ok,
Req#{plugins => Plugins,
extra_state => ExtraState#{pathinfo => Pathinfo},
bindings => Bindings},
Env#{app => App,
callback => Callback,
secure => Secure,
controller_data => #{}
}
};
{ok, Bindings, #cowboy_handler_value{app = App, handler = Handler, arguments = Args,
plugins = Plugins, secure = Secure}} ->
{ok,
Expand All @@ -121,7 +116,7 @@ execute(Req = #{host := Host, path := Path, method := Method}, Env) ->
}
};
Error ->
?LOG_ERROR(#{reason => <<"Unexpected return from routing_tree:lookup/4">>,
?LOG_ERROR(#{reason => <<"Unexpected return from nova_routing_tree:find/4">>,
return_object => Error}),
render_status_page(Host, 404, #{error => Error}, Req, Env)
end.
Expand All @@ -138,7 +133,25 @@ lookup_url(Host, Path, Method) ->
lookup_url(Host, Path, Method, Dispatch).

lookup_url(Host, Path, Method, Dispatch) ->
routing_tree:lookup(Host, Path, Method, Dispatch).
nova_routing_tree:find(Host, Path, Method, Dispatch).


%%--------------------------------------------------------------------
%% @doc
%% Works the same way as add_routes/2 but with the exception that you
%% don't need to provide the routes explicitly. When using this it's
%% expected that there's a routing-module associated with the application.
%% Eg. for the application 'test' the corresponding router would then be
%% 'test_router'. Read more about routers in the official documentation.
%% @end
%%--------------------------------------------------------------------
-spec add_routes(App :: atom()) -> ok.
add_routes(App) ->
Router = erlang:list_to_atom(io_lib:format("~s_router", [App])),
Env = nova:get_environment(),
%% Call the router
Routes = Router:routes(Env),
add_routes(App, Routes).

%%--------------------------------------------------------------------
%% @doc
Expand Down Expand Up @@ -177,6 +190,25 @@ add_routes(App, Routes) ->
throw({error, {invalid_routes, App, Routes}}).


%%--------------------------------------------------------------------
%% @doc
%% Remove all routes associated with the given application.
%% @end
%%--------------------------------------------------------------------
-spec remove_application(Application :: atom()) -> ok.
remove_application(Application) when is_atom(Application) ->
StorageBackend = application:get_env(nova, dispatch_backend, persistent_term),
Dispatch = StorageBackend:get(nova_dispatch),
{ok, Dispatch0} =
nova_routing_tree:foldl(Dispatch,
fun(R) ->
[ X || X = {_Host, _Prefix, _Method, #nova_handler_value{app = App}} <- R,
App =/= Application ]
end),
StorageBackend:put(nova_dispatch, Dispatch0),
ok.


%%%%%%%%%%%%%%%%%%%%%%%%
%% INTERNAL FUNCTIONS %%
%%%%%%%%%%%%%%%%%%%%%%%%
Expand All @@ -201,7 +233,7 @@ apply_callback(Module, Function, Args) ->
[]
end.

-spec compile(Apps :: [atom() | {atom(), map()}], Dispatch :: host_tree(), Options :: map()) -> host_tree().
-spec compile(Apps :: [atom() | {atom(), map()}], Dispatch :: nova_routing_tree:tree(), Options :: map()) -> nova_routing_tree:tree().
compile([], Dispatch, _Options) -> Dispatch;
compile([{App, Options}|Tl], Dispatch, GlobalOptions) ->
compile([App|Tl], Dispatch, maps:merge(Options, GlobalOptions));
Expand Down Expand Up @@ -409,7 +441,7 @@ render_status_page(Host, StatusCode, Data, Req, Env) ->
StorageBackend = application:get_env(nova, dispatch_backend, persistent_term),
Dispatch = StorageBackend:get(nova_dispatch),
{Req0, Env0} =
case routing_tree:lookup(Host, StatusCode, '_', Dispatch) of
case nova_routing_tree:find(Host, StatusCode, '_', Dispatch) of
{error, _} ->
%% Render nova page if exists - We need to determine where to find this path?
{Req, Env#{app => nova,
Expand All @@ -433,8 +465,12 @@ render_status_page(Host, StatusCode, Data, Req, Env) ->


insert(Host, Path, Combinator, Value, Tree) ->
try routing_tree:insert(Host, Path, Combinator, Value, Tree) of
Tree0 -> Tree0
try nova_routing_tree:insert(Host, Path, Combinator, Value, Tree) of
{ok, Tree0} -> Tree0;
{error, conflict, Conf} ->
?LOG_ERROR(#{reason => <<"Route conflict">>, route => Path,
combinator => Combinator, conflict => Conf}),
throw({error, conflict, Conf})
catch
throw:Exception ->
?LOG_ERROR(#{reason => <<"Error when inserting route">>, route => Path, combinator => Combinator}),
Expand Down Expand Up @@ -497,4 +533,10 @@ routes(_) ->

-ifdef(TEST).
-compile(export_all).
-include_lib("eunit/include/eunit.hrl").

compile_empty_test() ->
Dispatch = compile([]),
?assertEqual(nova_routing_tree:new(#{options => #{strict => false}}), Dispatch).

-endif.
Loading