-module(nova_basic_handler).
-export([
handle_json/3,
handle_ok/3,
handle_status/3,
handle_redirect/3,
handle_sendfile/3,
handle_websocket/3
]).
-include_lib("kernel/include/logger.hrl").
-type mod_fun() :: {Module :: atom(), Function :: atom()} | undefined.
-type erlydtl_vars() :: map() | [{Key :: atom() | binary() | string(), Value :: any()}].
%%--------------------------------------------------------------------
%% @doc
%% Handler for JSON. It takes two 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.
%% @end
%%--------------------------------------------------------------------
-spec handle_json({json, JSON :: map()} | {json, StatusCode :: integer(), Headers :: map(), JSON :: map()},
ModFun :: mod_fun(), Req :: cowboy_req:req()) -> {ok, State :: cowboy_req:req()} |
{Module :: atom(), Req :: cowboy_req:req()}.
handle_json({json, StatusCode, Headers, JSON}, _ModFun, Req) ->
try json:encode(JSON, [binary, maps]) of
EncodedJSON ->
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}
catch
Class:Reason:Stacktrace ->
logger:error(#{msg => "Error while processing JSON", class => Class,
reason => Reason, stacktrace => Stacktrace}),
handle_status({status, 500}, undefined, Req)
end;
handle_json({json, JSON}, ModFun, Req = #{method := Method}) ->
case Method of
<<"POST">> ->
handle_json({json, 201, #{}, JSON}, ModFun, Req);
_ ->
handle_json({json, 200, #{}, JSON}, ModFun, 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.
%%
%%
%% -module(my_first_controller).
%% -compile(export_all).
%%
%% my_function(_Req) ->
%% {ok, []}.
%%
%% The example above will then render the view named 'app_main.dtl'
%%
%% Options can be specified as follows:
%%
%% - view - Specifies if another view should be rendered instead of default one
%%
%% - headers - Custom headers
%% @end
%%--------------------------------------------------------------------
-spec handle_ok({ok, Variables :: erlydtl_vars()} | {ok, Variables :: erlydtl_vars(), Options :: map()},
ModFun :: mod_fun(), Req :: cowboy_req:req()) -> {ok, cowboy_req:req()}.
handle_ok({ok, Variables}, {Mod, _Func}, Req) ->
%% Derive the view from module
ViewNameAtom = get_view_name(Mod),
handle_view(ViewNameAtom, Variables, #{}, Req);
handle_ok({ok, Variables, Options}, {Mod, _Func}, Req) ->
View =
case maps:get(view, Options, undefined) of
undefined ->
ViewName = atom_to_list(Mod) ++ "_dtl",
list_to_atom(ViewName);
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).
%%--------------------------------------------------------------------
%% @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 concidered a JSON-structure and encoded.
%%
%% @end
%%--------------------------------------------------------------------
-spec handle_status({status, StatusCode :: integer()} |
{status, StatusCode :: integer(), ExtraHeaders :: map()} |
{status, StatusCode :: integer(), ExtraHeaders :: map(), Body :: binary() | map()},
ModFun :: mod_fun(), Req :: cowboy_req:req()) -> {ok, Req :: cowboy_req:req()}.
handle_status({status, Status, ExtraHeaders, JSON}, _ModFun, Req) when is_map(JSON) ->
%% We do not need to render a status page since we just return a JSON structure
Headers0 = maps:merge(#{<<"content-type">> => <<"application/json">>}, ExtraHeaders),
Req0 = cowboy_req:set_resp_headers(Headers0, Req),
Req1 = Req0#{resp_status_code => Status},
Req2 = cowboy_req:set_resp_body(json:encode(JSON, [binary]), Req1),
{ok, Req2};
handle_status({status, Status, ExtraHeaders, Body}, _ModFun, 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}, _ModFun, 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}, ModFun, State) when is_integer(Status) ->
handle_status({status, Status, #{}}, ModFun, 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"
%% @end
%%-----------------------------------------------------------------
-spec handle_redirect({redirect, Route :: list()}, ModFun :: mod_fun(),
Req :: cowboy_req:req()) -> {ok, Req :: cowboy_req:req()}.
handle_redirect({redirect, Route}, _ModFun, Req) ->
Headers = #{<<"Location">> => list_to_binary(Route)},
Req0 = cowboy_req:set_resp_headers(Headers, Req),
Req1 = Req0#{resp_status_code => 302},
{ok, Req1}.
%%--------------------------------------------------------------------
%% @doc
%% Handles sendfile.
%% @end
%%-----------------------------------------------------------------
-spec handle_sendfile({sendfile, StatusCode :: integer(), Headers :: map(), {Offset :: integer(),
Length :: integer(),
Path :: list()}, Mime :: binary()},
ModFun :: mod_fun(), Req) -> {ok, Req} when Req :: cowboy_req:req().
handle_sendfile({sendfile, StatusCode, Headers, {Offset, Length, Path}, Mime}, _ModFun, 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
%% @end
%%-----------------------------------------------------------------
-spec handle_websocket({websocket, ControllerData :: any()}, ModFun :: mod_fun(), Req :: cowboy_req:req()) ->
{ok, Req :: cowboy_req:req()}.
handle_websocket({websocket, ControllerData}, {Module, _Fun}, Req) ->
case Module:init(ControllerData) of
{ok, NewControllerData} ->
{cowboy_websocket, Req#{controller_data => NewControllerData}, #{}};
Error ->
logger:error(#{msg => "Handler returned unsupported result", handler => Module, return_obj => Error}),
%% Render 500
{ok, Req}
end.
%%%===================================================================
%%% Internal functions
%%%===================================================================
handle_view(View, Variables, Options, Req) ->
{ok, HTML} = render_dtl(View, Variables, []),
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
logger:error(#{msg => "Nova could not render template", template => View, reason => Reason}),
throw({error, template_not_found});
_ ->
View:render(Variables, Options)
end;
_ ->
View:render(Variables, Options)
end.
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([H|T]) ->
[H|get_view_name(T)].