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
21 changes: 21 additions & 0 deletions guides/routing.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,27 @@ As you saw in the initial example in the [Basic components](#basic-components) s
>
> One needs to define `protocol => ws` in the options-map in order to enable websocket communications.

### Native cowboy handlers ###

```
{Route :: list(), Handler :: atom(), Options :: map()}
```

By defining `protocol => cowboy` in the options-map, the handler module is invoked
directly with `Handler:init/2` the same way cowboy itself would do it. This means that
plain cowboy handlers, as well as handlers using cowboy's sub-protocols such as
`cowboy_rest` and `cowboy_loop`, can be routed through nova without any wrapping -
while still passing through nova's plugin- and security-handling.

The arguments given to `Handler:init/2` can be set with the `arguments`-option
(defaults to `#{}`), and the route can be restricted with the `methods`-option
just like regular routes.

Example:
```erlang
{"/my-rest-api", my_rest_handler, #{protocol => cowboy}},
{"/long-poll", my_loop_handler, #{protocol => cowboy, arguments => #{timeout => 30000}}}
```

### Static files

Expand Down
20 changes: 20 additions & 0 deletions src/nova_router.erl
Original file line number Diff line number Diff line change
Expand Up @@ -369,6 +369,26 @@ parse_url(Host, [{Path, Callback, Options}|Tl], T = #{prefix := Prefix}, Value =
router_file => maps:get(router_file, Options, undefined)}),
parse_url(Host, Tl, T, Value, Tree)
end;
parse_url(Host, [{Path, Handler, Options = #{protocol := cowboy}}|Tl], T = #{prefix := Prefix},
Value = #nova_handler_value{app = App, secure = Secure}, Tree) when is_atom(Handler) ->
%% Plain cowboy handler (eg cowboy_rest, cowboy_loop or a basic
%% cowboy_handler). The handler module is invoked with Handler:init/2
%% and any sub-protocol upgrade it returns is honored by nova_handler.
Value0 = #cowboy_handler_value{
app = App,
handler = Handler,
arguments = maps:get(arguments, Options, #{}),
plugins = Value#nova_handler_value.plugins,
secure = Secure},
?LOG_DEBUG(#{action => <<"Adding route">>, protocol => <<"cowboy">>, route => Path, app => App,
router_file => maps:get(router_file, T, undefined)}),
RealPath = concat_strings(Prefix, Path),
CompiledPaths =
lists:foldl(
fun(Method, Tree0) ->
insert(Host, RealPath, method_to_binary(Method), Value0, Tree0)
end, Tree, maps:get(methods, Options, ['_'])),
parse_url(Host, Tl, T, Value, CompiledPaths);
parse_url(Host,
[{Path, Mod, #{protocol := ws}} | Tl],
T = #{prefix := Prefix}, #nova_handler_value{app = App, secure = Secure} = Value,
Expand Down
19 changes: 19 additions & 0 deletions test/base_app_router.erl
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
%%% Router fixture for nova_cowboy_handler_SUITE.
-module(base_app_router).

-export([routes/1, hello/1]).

routes(_Env) ->
[#{prefix => "",
security => false,
routes => [
{"/base", fun base_app_router:hello/1, #{methods => [get]}},
{"/plain", test_plain_handler, #{protocol => cowboy, methods => [get]}},
{"/loop", test_loop_handler, #{protocol => cowboy}},
{"/loop-args", test_loop_handler, #{protocol => cowboy,
arguments => #{body => <<"loop-args-ok">>}}},
{"/stream", test_stream_handler, #{protocol => cowboy}}
]}].

hello(_Req) ->
{json, #{app => <<"base">>}}.
80 changes: 80 additions & 0 deletions test/nova_cowboy_handler_SUITE.erl
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
%%% Tests that plain cowboy handlers, as well as handlers using cowboy's
%%% sub-protocols (eg cowboy_loop), can be routed through nova with
%%% protocol => cowboy.
-module(nova_cowboy_handler_SUITE).

-include_lib("common_test/include/ct.hrl").

-compile(export_all).
-compile(nowarn_export_all).

-define(PORT, 10080).

all() ->
[regular_route_still_works,
plain_cowboy_handler,
loop_cowboy_handler,
handler_arguments,
streaming_handler,
method_restriction].

init_per_suite(Config) ->
application:load(nova),
application:set_env(nova, bootstrap_application, base_app),
application:set_env(nova, cowboy_configuration, #{port => ?PORT}),
{ok, _} = application:ensure_all_started(nova),
{ok, _} = application:ensure_all_started(inets),
Config.

end_per_suite(_Config) ->
application:stop(nova),
ok.

%% Sanity check - ordinary nova routes are unaffected.
regular_route_still_works(_Config) ->
{200, Body} = request(get, "/base"),
{ok, #{<<"app">> := <<"base">>}} = thoas:decode(list_to_binary(Body)),
ok.

%% A basic cowboy handler replying directly from init/2.
plain_cowboy_handler(_Config) ->
{200, "plain-ok"} = request(get, "/plain"),
ok.

%% A cowboy_loop handler - the sub-protocol upgrade returned from
%% init/2 is honored.
loop_cowboy_handler(_Config) ->
{200, "loop-ok"} = request(get, "/loop"),
ok.

%% The arguments-option is passed as state to Handler:init/2.
handler_arguments(_Config) ->
{200, "loop-args-ok"} = request(get, "/loop-args"),
ok.

%% A handler streaming its response in several chunks with
%% cowboy_req:stream_reply/3 and cowboy_req:stream_body/3.
streaming_handler(_Config) ->
{200, "chunk1chunk2chunk3done"} = request(get, "/stream"),
ok.

%% The methods-option restricts cowboy-routes just like regular routes.
method_restriction(_Config) ->
{405, _} = request(post, "/plain"),
ok.

%%%%%%%%%%%%%%%%%%%%%%%%
%% Helpers %%
%%%%%%%%%%%%%%%%%%%%%%%%

request(get, Path) ->
do_request(get, {url(Path), []});
request(post, Path) ->
do_request(post, {url(Path), [], "text/plain", ""}).

do_request(Method, Request) ->
{ok, {{_, Status, _}, _Headers, Body}} = httpc:request(Method, Request, [], []),
{Status, Body}.

url(Path) ->
"http://127.0.0.1:" ++ integer_to_list(?PORT) ++ Path.
14 changes: 14 additions & 0 deletions test/test_loop_handler.erl
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
%%% A plain cowboy_loop handler, routed through nova with protocol => cowboy.
-module(test_loop_handler).
-behaviour(cowboy_loop).

-export([init/2, info/3]).

init(Req, State) ->
erlang:send_after(50, self(), reply),
{cowboy_loop, Req, State}.

info(reply, Req, State) ->
Body = maps:get(body, State, <<"loop-ok">>),
Req1 = cowboy_req:reply(200, #{<<"content-type">> => <<"text/plain">>}, Body, Req),
{stop, Req1, State}.
10 changes: 10 additions & 0 deletions test/test_plain_handler.erl
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
%%% A plain cowboy handler (no sub-protocol), routed through nova with
%%% protocol => cowboy.
-module(test_plain_handler).
-behaviour(cowboy_handler).

-export([init/2]).

init(Req0, State) ->
Req = cowboy_req:reply(200, #{<<"content-type">> => <<"text/plain">>}, <<"plain-ok">>, Req0),
{ok, Req, State}.
19 changes: 19 additions & 0 deletions test/test_stream_handler.erl
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
%%% A cowboy_loop handler streaming its response with
%%% cowboy_req:stream_reply/3 and cowboy_req:stream_body/3.
-module(test_stream_handler).
-behaviour(cowboy_loop).

-export([init/2, info/3]).

init(Req0, State) ->
Req = cowboy_req:stream_reply(200, #{<<"content-type">> => <<"text/plain">>}, Req0),
self() ! {chunk, 1},
{cowboy_loop, Req, State}.

info({chunk, N}, Req, State) when N =< 3 ->
ok = cowboy_req:stream_body(io_lib:format("chunk~B", [N]), nofin, Req),
erlang:send_after(20, self(), {chunk, N + 1}),
{ok, Req, State};
info({chunk, _}, Req, State) ->
ok = cowboy_req:stream_body(<<"done">>, fin, Req),
{stop, Req, State}.
Loading