Packages
nova
0.14.3
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_basic_handler.erl
-module(nova_basic_handler).
-export([
handle_json/3,
handle_ok/3,
handle_view/3,
handle_status/3,
handle_redirect/3,
handle_sendfile/3,
handle_websocket/3,
handle_ws/2
]).
-include_lib("kernel/include/logger.hrl").
-ifdef(TEST).
-export([maybe_inject_csrf_token/2]).
-endif.
-type erlydtl_vars() :: map() | [{Key :: atom() | binary() | string(), Value :: any()}].
%%--------------------------------------------------------------------
%% @doc
%% Handler for JSON. It can take one of three different return objects:
%%
%% {json, JSON :: map()} returns the JSON encoded to the user.
%% If the operation was a POST the HTTP-status code will be 201, otherwise
%% 200.
%%
%% {json, StatusCode :: integer(), Headers :: map(), JSON :: map()} Same
%% operation as the above except you can set custom status code and custom
%% headers.
%%
%% {json, StatusCode :: integer(), Headers :: map(), Req0 :: cowboy_req:req(), JSON :: map()}
%% This is the same as the above but you can also return the request object. This is particularly
%% useful if you want to set cookies or other headers that are not supported by the handler.
%% @end
%%--------------------------------------------------------------------
-spec handle_json({json, JSON :: map()} | {json, StatusCode :: integer(), Headers :: map(), JSON :: map()} |
{json, StatusCode :: integer(), Headers :: map(), JSON :: map(), Req0 :: cowboy_req:req()},
Callback :: function(), Req :: cowboy_req:req()) -> {ok, State :: cowboy_req:req()}.
handle_json({json, StatusCode, Headers, Req0, JSON}, Callback, _Req) ->
handle_json({json, StatusCode, Headers, JSON}, Callback, Req0);
handle_json({json, StatusCode, Headers, JSON}, _Callback, Req) ->
JsonLib = nova:get_env(json_lib, thoas),
EncodedJSON = JsonLib:encode(JSON),
Headers0 = maps:merge(#{<<"content-type">> => <<"application/json">>}, Headers),
Req0 = cowboy_req:set_resp_headers(Headers0, Req),
Req1 = cowboy_req:set_resp_body(EncodedJSON, Req0),
Req2 = Req1#{resp_status_code => StatusCode},
{ok, Req2};
handle_json({json, JSON}, Callback, Req = #{method := Method}) ->
case Method of
<<"POST">> ->
handle_json({json, 201, #{}, JSON}, Callback, Req);
_ ->
handle_json({json, 200, #{}, JSON}, Callback, Req)
end.
%%--------------------------------------------------------------------
%% @doc
%% Handler for regular views. This will render a template with given variables.
%% If not another view is specified in options a view that corresponds to the controller will be
%% rendered. The first element of the returned tuple could be either ok or view - they are
%% identical in their functionality.
%%
%%
%% -module(my_first_controller).
%% -compile(export_all).
%%
%% my_function(_Req) ->
%% {ok, []}.
%%
%% The example above will then render the view named 'app_main.dtl'
%%
%% The tuple can have three different forms:
%%
%% {ok, Variables} - This will render the default view with the given variables
%%
%% {ok, Variables, Options} - This will render the default view with the given variables and options
%% Options can be specified as follows:
%%
%% - view - Specifies if another view should be rendered instead of default one
%%
%% - headers - Custom headers
%%
%% {ok, Variables, Options, Req0} - This will render the default view with the given variables and options
%% but also takes a request object. This is useful if you want to set cookies or other headers that are not
%% supported by the handler.
%% @end
%%--------------------------------------------------------------------
-spec handle_ok({ok, Variables :: erlydtl_vars()} | {ok, Variables :: erlydtl_vars(), Options :: map()},
Callback :: function(), Req :: cowboy_req:req()) -> {ok, cowboy_req:req()}.
handle_ok({ok, Variables}, Callback, Req) ->
%% Derive the view from module
{module, Module} = erlang:fun_info(Callback, module),
ViewNameAtom = get_view_name(Module),
handle_view(ViewNameAtom, Variables, #{}, Req);
handle_ok({ok, Variables, Options}, Callback, Req) ->
{module, Module} = erlang:fun_info(Callback, module),
View =
case maps:get(view, Options, undefined) of
undefined ->
get_view_name(Module);
CustomView when is_atom(CustomView) ->
ViewName = atom_to_list(CustomView) ++ "_dtl",
list_to_atom(ViewName);
CustomView ->
list_to_atom(CustomView ++ "_dtl")
end,
handle_view(View, Variables, Options, Req);
handle_ok({ok, Variables, Options, Req0}, Callback, _Req) ->
handle_ok({ok, Variables, Options}, Callback, Req0).
%%--------------------------------------------------------------------
%% @doc
%% Handler for regular views and uses the ok-handler. For more info see
%% handle_ok/3.
%% @end
%%--------------------------------------------------------------------
handle_view({view, Variables}, Callback, Req) ->
handle_ok({ok, Variables}, Callback, Req);
handle_view({view, Variables, Options}, Callback, Req) ->
handle_ok({ok, Variables, Options}, Callback, Req);
handle_view({view, Variables, Options, Req0}, Callback, _Req) ->
handle_ok({ok, Variables, Options}, Callback, Req0).
%%--------------------------------------------------------------------
%% @doc
%% Handler for returning http status codes. There's three different ways one can
%% return status code. The most basic case is {status, Status} where Status is
%% the code that should be returned.
%%
%% If there's a need for additional headers to be sent along with the http code one can specify
%% a third argument that is a map with header-fields.
%%
%% One can also send in a body as a fourth argument in the tuple. It can either be a binary or
%% a map. If it's a map it will be considered a JSON-structure and encoded.
%%
%% And if you want to modulate the request object you can send it in as the fifth argument.
%%
%% @end
%%--------------------------------------------------------------------
-spec handle_status({status, StatusCode :: integer()} |
{status, StatusCode :: integer(), ExtraHeaders :: map()} |
{status, StatusCode :: integer(), ExtraHeaders :: map(), Body :: binary() | map()},
Callback :: function(), Req :: cowboy_req:req()) -> {ok, Req :: cowboy_req:req()}.
handle_status({status, Status, ExtraHeaders, JSON, Req0}, Callback, _Req) ->
handle_status({status, Status, ExtraHeaders, JSON}, Callback, Req0);
handle_status({status, Status, ExtraHeaders, JSON}, _Callback, Req) when is_map(JSON) ->
%% We do not need to render a status page since we just return a JSON structure
JsonLib = nova:get_env(json_lib, thoas),
Headers0 = maps:merge(#{<<"content-type">> => <<"application/json">>}, ExtraHeaders),
Req0 = cowboy_req:set_resp_headers(Headers0, Req),
Req1 = Req0#{resp_status_code => Status},
JSONStr = JsonLib:encode(JSON),
Req2 = cowboy_req:set_resp_body(JSONStr, Req1),
{ok, Req2};
handle_status({status, Status, ExtraHeaders, Body}, _Callback, Req) ->
%% Body is a binary - just send it out
Req0 = cowboy_req:set_resp_headers(ExtraHeaders, Req),
Req1 = Req0#{resp_status_code => Status},
Req2 = cowboy_req:set_resp_body(Body, Req1),
{ok, Req2};
handle_status({status, Status, ExtraHeaders}, _Callback, Req) ->
Req0 = cowboy_req:set_resp_headers(ExtraHeaders, Req),
Req1 = Req0#{resp_status_code => Status},
{ok, Req2, _Env} = nova_router:render_status_page(Status, #{}, Req1),
{ok, Req2};
handle_status({status, Status}, Callback, State) when is_integer(Status) ->
handle_status({status, Status, #{}}, Callback, State).
%%--------------------------------------------------------------------
%% @doc
%% Handles redirects. This will return a 302-status code with a location given
%% by the user. Something like {redirect, "/login"} will send a
%% 302 with location set to "/login"
%%
%% Optionally you can attach the request object as the third argument. This is useful
%% if you want to set cookies or other headers that are not supported by the handler.
%% @end
%%-----------------------------------------------------------------
-spec handle_redirect({redirect, Route :: list()|binary()}, Callback :: function(),
Req :: cowboy_req:req()) -> {ok, Req :: cowboy_req:req()}.
handle_redirect({redirect, Route, Req0}, Callback, _Req) ->
handle_redirect({redirect, Route}, Callback, Req0);
handle_redirect({redirect, Route}, Callback, Req) when is_list(Route) ->
handle_redirect({redirect, list_to_binary(Route)}, Callback, Req);
handle_redirect({redirect, Route}, _Callback, Req) ->
Headers = #{<<"location">> => Route},
Req0 = cowboy_req:set_resp_headers(Headers, Req),
Req1 = Req0#{resp_status_code => 302},
{ok, Req1}.
%%--------------------------------------------------------------------
%% @doc
%% Handles sendfile.
%%
%% The tuple have the following structure:
%% {sendfile, StatusCode, Headers, {Offset, Length, Path}, Mime}
%%
%% - sendfile is the atom that tells the handler to send a file.
%%
%% - StatusCode is the HTTP status code that should be returned.
%%
%% - Headers is a map with headers that should be sent along with the file.
%%
%% - Offset is the offset in the file where the sending should start.
%%
%% - Length is the length of the file that should be sent.
%%
%% - Path is the path to the file that should be sent.
%%
%% - Mime is the mime-type of the file.
%%
%%
%% Optionally a sixth element can be added to the tuple. This is the request object. This is useful
%% if you want to set cookies or other headers that are not supported by the handler.
%% @end
%%-----------------------------------------------------------------
-spec handle_sendfile({sendfile, StatusCode :: integer(), Headers :: map(), {Offset :: integer(),
Length :: integer(),
Path :: list()}, Mime :: binary()},
Callback :: function(), Req) -> {ok, Req} when Req :: cowboy_req:req().
handle_sendfile({sendfile, StatusCode, Headers, FileInfo, Mime, Req0}, Callback, _Req) ->
handle_sendfile({sendfile, StatusCode, Headers, FileInfo, Mime}, Callback, Req0);
handle_sendfile({sendfile, StatusCode, Headers, {Offset, Length, Path}, Mime}, _Callback, Req) ->
Headers0 = maps:merge(#{<<"content-type">> => Mime}, Headers),
Req0 = cowboy_req:set_resp_headers(Headers0, Req),
Req1 = cowboy_req:set_resp_body({sendfile, Offset, Length, Path}, Req0),
Req2 = Req1#{resp_status_code => StatusCode},
{ok, Req2}.
%%--------------------------------------------------------------------
%% @doc
%% Handles upgrading to websocket. This is a special handler in regards to
%% arguments. The tuple-object for websocket only takes two arguments; What the initial state
%% of the websocket handler should be and the request object.
%% @end
%%-----------------------------------------------------------------
-spec handle_websocket({websocket, ControllerData :: any()}, Callback :: function(), Req :: cowboy_req:req()) ->
{ok, Req :: cowboy_req:req()}.
handle_websocket({websocket, ControllerData, Req0}, Callback, _Req) ->
handle_websocket({websocket, ControllerData}, Callback, Req0);
handle_websocket({websocket, ControllerData}, Callback, Req) ->
{module, Module} = erlang:fun_info(Callback, module),
case Module:init(ControllerData) of
{ok, NewControllerData} ->
{cowboy_websocket, Req#{controller_data => NewControllerData}, #{}};
Error ->
?LOG_ERROR(#{msg => <<"Handler returned unsupported result">>, handler => Module, return_obj => Error}),
%% Render 500
{ok, Req}
end.
%%--------------------------------------------------------------------
%% @doc
%% Handles basic websocket operations. This is a special handler in regards to
%% arguments. Handlers for websocket only takes two arguments; What the controller
%% returned and the state. And the handler should return what cowboy expects.
%%
%% Example of a valid return value is {reply, Frame, State}
%% @end
%%-----------------------------------------------------------------
handle_ws({reply, Frame, NewControllerData}, State = #{commands := Commands}) ->
State#{controller_data => NewControllerData,
commands => [Frame|Commands]};
handle_ws({reply, Frame, NewControllerData, hibernate}, State = #{commands := Commands}) ->
State#{controller_data => NewControllerData,
commands => [Frame|Commands],
hibernate => true};
handle_ws({ok, NewControllerData}, State) ->
State#{controller_data => NewControllerData};
handle_ws({ok, NewControllerData, hibernate}, State) ->
State#{controller_data => NewControllerData,
hibernate => true};
handle_ws({stop, NewControllerData}, State) ->
{stop, State#{controller_data => NewControllerData}};
handle_ws(ok, State) ->
State.
%%%===================================================================
%%% Internal functions
%%%===================================================================
handle_view(View, Variables, Options, Req) ->
Variables1 = maybe_inject_csrf_token(Variables, Req),
{ok, HTML} = render_dtl(View, Variables1, []),
Headers =
case maps:get(headers, Options, undefined) of
undefined ->
#{<<"content-type">> => <<"text/html">>};
UserHeaders ->
UserHeaders
end,
StatusCode = maps:get(status_code, Options, 200),
Req0 = cowboy_req:set_resp_headers(Headers, Req),
Req1 = cowboy_req:set_resp_body(HTML, Req0),
Req2 = Req1#{resp_status_code => StatusCode},
{ok, Req2}.
render_dtl(View, Variables, Options) ->
case code:is_loaded(View) of
false ->
case code:load_file(View) of
{error, Reason} ->
%% Cast a warning since the module could not be found
?LOG_ERROR(#{msg => <<"Nova could not render template">>, template => View, reason => Reason}),
throw({404, {template_not_found, View}});
_ ->
View:render(Variables, Options)
end;
_ ->
View:render(Variables, Options)
end.
maybe_inject_csrf_token(Variables, #{csrf_token := Token}) when is_list(Variables) ->
[{csrf_token, Token} | Variables];
maybe_inject_csrf_token(Variables, #{csrf_token := Token}) when is_map(Variables) ->
Variables#{csrf_token => Token};
maybe_inject_csrf_token(Variables, _Req) ->
Variables.
get_view_name({Mod, _Opts}) -> get_view_name(Mod);
get_view_name(Mod) when is_atom(Mod) ->
StrName = get_view_name(erlang:atom_to_list(Mod)),
erlang:list_to_atom(StrName);
get_view_name([$_, $c, $o, $n, $t, $r, $o, $l, $l, $e, $r]) ->
"_dtl";
get_view_name([]) ->
"_dtl";
get_view_name([H|T]) ->
[H|get_view_name(T)].
-ifdef(TEST).
-compile(export_all).
-endif.