Skip to content
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
%%%===================================================================
74 changes: 60 additions & 14 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_trie:trie().
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_trie: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_trie: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 Down Expand Up @@ -121,7 +128,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_trie:lookup/4">>,
return_object => Error}),
render_status_page(Host, 404, #{error => Error}, Req, Env)
end.
Expand All @@ -138,7 +145,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_trie:lookup(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 +202,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) ->
Dispatch = persistent_term:get(nova_dispatch),
%% Remove all routes for this application
{ok, Dispatch0} =
nova_routing_trie:foldl(Dispatch,
fun(R) ->
[ X || X = {_Host, _Prefix, #nova_handler_value{app = App}} <- R,
App =/= Application ]
end),
persistent_term:put(nova_dispatch, Dispatch0),
ok.


%%%%%%%%%%%%%%%%%%%%%%%%
%% INTERNAL FUNCTIONS %%
%%%%%%%%%%%%%%%%%%%%%%%%
Expand All @@ -201,7 +245,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_trie:trie(), Options :: map()) -> nova_routing_trie:trie().
compile([], Dispatch, _Options) -> Dispatch;
compile([{App, Options}|Tl], Dispatch, GlobalOptions) ->
compile([App|Tl], Dispatch, maps:merge(Options, GlobalOptions));
Expand Down Expand Up @@ -409,7 +453,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_trie: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,7 +477,7 @@ render_status_page(Host, StatusCode, Data, Req, Env) ->


insert(Host, Path, Combinator, Value, Tree) ->
try routing_tree:insert(Host, Path, Combinator, Value, Tree) of
try nova_routing_trie:insert(Host, Path, Combinator, Value, Tree) of
Tree0 -> Tree0
catch
throw:Exception ->
Expand Down Expand Up @@ -499,6 +543,8 @@ routes(_) ->
-compile(export_all). %% Export all functions for testing purpose
-include_lib("eunit/include/eunit.hrl").


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

-endif.
Loading
Loading