Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[FEAT] Functionality to dynamically add routes #274

Merged
merged 4 commits into from
May 3, 2024
Merged
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
20 changes: 20 additions & 0 deletions guides/routing.md
Original file line number Diff line number Diff line change
Expand Up @@ -105,3 +105,23 @@ It's possible to configure a small set of endpoints with a specific plugin. This

In the example above we have enabled the *pre-request*-plugin `nova_json_schemas` for all routes under the `/admin` prefix. This will cause all requests to be validated against the JSON schema defined in the `nova_json_schemas` plugin.
You can also include *post-request*-plugins in the same way.


## Adding routes programatically

You can also add routes programatically by calling `nova_router:add_route/2`. This is useful if you want to add routes dynamically. The spec for it is:

```erlang
%% nova_router:add_route/2 specification
-spec add_route(App :: atom(), Routes :: map() | [map()]) -> ok.
```

First argument is the application you want to add the route to. The second argument is the route or a list of routes you want to add - it uses the same structure as in the regular routers.

```erlang
nova_router:add_route(my_app, #{prefix => "/admin", routes => [{"/", {my_controller, main}, #{methods => [get]}}]}).
```

This will add the routes defined in the second argument to the `my_app` application.

**Note**: If a route already exists it will be overwritten.
File renamed without changes.
7 changes: 6 additions & 1 deletion src/controllers/nova_error_controller.erl
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,12 @@ status_code(Req) ->
not_found(Req) ->
%% Check the accept-headers
Accept = cowboy_req:header(<<"accept">>, Req),
AcceptList = binary:split(Accept, <<",">>, [global]),
AcceptList = case Accept of
undefined ->
[<<"application/json">>];
_ ->
binary:split(Accept, <<",">>, [global])
end,

case {lists:member(<<"application/json">>, AcceptList),
lists:member(<<"text/html">>, AcceptList)} of
Expand Down
2 changes: 1 addition & 1 deletion src/nova_handler.erl
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
]).

-include_lib("kernel/include/logger.hrl").
-include("nova_router.hrl").
-include("../include/nova_router.hrl").

-callback init(Req, any()) -> {ok | module(), Req, any()}
| {module(), Req, any(), any()}
Expand Down
44 changes: 42 additions & 2 deletions src/nova_router.erl
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,15 @@
render_status_page/5,

%% Expose the router-callback
routes/1
routes/1,

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

-include_lib("routing_tree/include/routing_tree.hrl").
-include_lib("kernel/include/logger.hrl").
-include("nova_router.hrl").
-include("../include/nova_router.hrl").

-type bindings() :: #{binary() := binary()}.
-export_type([bindings/0]).
Expand Down Expand Up @@ -118,6 +121,42 @@ lookup_url(Host, Path, Method) ->
lookup_url(Host, Path, Method, Dispatch) ->
routing_tree:lookup(Host, Path, Method, Dispatch).

%%--------------------------------------------------------------------
%% @doc
%% Add routes to the dispatch-table for the given app. The routes
%% can be either a list of maps or a map. It use the same structure as
%% the routes-callback in the router-module.
%% @end
%%--------------------------------------------------------------------
-spec add_routes(App :: atom(), Routes :: [map()] | map()) -> ok.
add_routes(_App, []) -> ok;
add_routes(App, [Routes|Tl]) when is_list(Routes) ->
Options = #{},
StorageBackend = application:get_env(nova, dispatch_backend, persistent_term),
Dispatch = StorageBackend:get(nova_dispatch),

%% Take out the prefix for the app and store it in the persistent store
CompiledApps = StorageBackend:get(?NOVA_APPS, []),
CompiledApps0 =
case lists:keyfind(App, 1, CompiledApps) of
false ->
[{App, maps:get(prefix, Options, "/")}|CompiledApps];
_StoredApp ->
CompiledApps
end,

Options1 = Options#{app => App},

{ok, Dispatch1, _Options2} = compile_paths(Routes, Dispatch, Options1),

StorageBackend:put(?NOVA_APPS, CompiledApps0),
StorageBackend:put(nova_dispatch, Dispatch1),

add_routes(App, Tl);
add_routes(App, Routes) ->
add_routes(App, [Routes]).


%%%%%%%%%%%%%%%%%%%%%%%%
%% INTERNAL FUNCTIONS %%
%%%%%%%%%%%%%%%%%%%%%%%%
Expand Down Expand Up @@ -275,6 +314,7 @@ parse_url(Host, [{Path, {Mod, Func}, Options}|Tl], Prefix,

RealPath = case Path of
_ when is_list(Path) -> string:concat(Prefix, Path);
_ when is_binary(Path) -> string:concat(Prefix, binary_to_list(Path));
_ when is_integer(Path) -> Path;
_ -> throw({unknown_path, Path})
end,
Expand Down