-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").
-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()},
Callback :: function(), Req :: cowboy_req:req()) -> {ok, State :: cowboy_req:req()}.
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'
%%
%% 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()},
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).
%%--------------------------------------------------------------------
%% @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).
%%--------------------------------------------------------------------
%% @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.
%%
%% @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}, _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"
%% @end
%%-----------------------------------------------------------------
-spec handle_redirect({redirect, Route :: list()|binary()}, Callback :: function(),
Req :: cowboy_req:req()) -> {ok, Req :: cowboy_req:req()}.
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.
%% @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, {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
%% @end
%%-----------------------------------------------------------------
-spec handle_websocket({websocket, ControllerData :: any()}, Callback :: function(), Req :: cowboy_req:req()) ->
{ok, Req :: cowboy_req:req()}.
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) ->
{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
?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.
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)].