%%%-------------------------------------------------------------------
%%% @author Niclas Axelsson <niclas@burbas.se>
%%% @copyright (C) 2020, Niclas Axelsson
%%% @doc
%%% This module is responsible for all the different return types a controller have. <i>Nova</i> is constructed
%%% in such way that it's really easy to extend it by using <i>handlers</i>. A handler is basically a module consisting
%%% of a function of arity 4. We will show an example of this.
%%%
%%% If you implement the following module:
%%%
%%% -module(my_handler).
%%% -export([init/0,
%%% handle_console]).
%%%
%%% init() ->
%%% nova_handlers:register_handler(console, {my_handler, handle_console}).
%%%
%%% handle_console({console, Format, Args}, {Module, Function}, State) ->
%%% io:format("~n=====================~n", []).
%%% io:format("~p:~p was called.~n", []),
%%% io:format("State: ~p~n", [State]),
%%% io:format(Format, Args),
%%% io:format("~n=====================~n", []),
%%% {ok, 200, #{}, EmptyBinary}.
%%%
%%% The init/0 should be invoked from your applications <i>supervisor</i> and will register the module
%%% my_handler as handler of the return type {console, Format, Args}. This means that you
%%% can return this tuple in a controller which invokes my_handler:handle_console/4.
%%%
%%% <b>A handler can return two different types</b>
%%%
%%% {ok, StatusCode, Headers, Body} - This will return a proper reply to the requester.
%%%
%%% {error, Reason} - This will render a 500 page to the user.
%%% @end
%%% Created : 12 Feb 2020 by Niclas Axelsson <niclas@burbas.se>
%%%-------------------------------------------------------------------
-module(nova_handlers).
-behaviour(gen_server).
%% API
-export([
start_link/0,
register_handler/2,
unregister_handler/1,
get_handler/1
]).
%% gen_server callbacks
-export([
init/1,
handle_call/3,
handle_cast/2,
handle_info/2,
terminate/2,
code_change/3,
format_status/2
]).
-include_lib("kernel/include/logger.hrl").
-define(SERVER, ?MODULE).
-define(HANDLERS_TABLE, nova_handlers_table).
-type handler_return() :: {ok, State2 :: nova:state()} |
{Module :: atom(), State :: nova:state()} |
{error, Reason :: any()}.
-export_type([handler_return/0]).
-type handler_callback() :: {Module :: atom(), Function :: atom()} |
fun((...) -> handler_return()).
-record(state, {
}).
%%%===================================================================
%%% API
%%%===================================================================
%%--------------------------------------------------------------------
%% @doc
%% Starts the server
%% @hidden
%% @end
%%--------------------------------------------------------------------
-spec start_link() -> {ok, Pid :: pid()} |
{error, Error :: {already_started, pid()}} |
{error, Error :: term()} |
ignore.
start_link() ->
gen_server:start_link({local, ?SERVER}, ?MODULE, [], []).
%%--------------------------------------------------------------------
%% @doc
%% Registers a new handler. This can then be used in a nova controller
%% by returning a tuple where the first element is the name of the handler.
%% @end
%%--------------------------------------------------------------------
-spec register_handler(Handle :: atom(), Callback :: handler_callback()) ->
ok | {error, Reason :: atom()}.
register_handler(Handle, Callback) ->
gen_server:cast(?SERVER, {register_handler, Handle, Callback}).
%%--------------------------------------------------------------------
%% @doc
%% Unregisters a handler and makes it unavailable for all controllers.
%% @end
%%--------------------------------------------------------------------
-spec unregister_handler(Handle :: atom()) -> ok.
unregister_handler(Handle) ->
gen_server:call(?SERVER, {unregister_handler, Handle}).
%%--------------------------------------------------------------------
%% @doc
%% Fetches the handler identified with 'Handle' and returns the callback
%% function for it.
%% @end
%%--------------------------------------------------------------------
-spec get_handler(Handle :: atom()) -> {ok, Callback :: handler_callback()} |
{error, not_found}.
get_handler(Handle) ->
case ets:lookup(?HANDLERS_TABLE, Handle) of
[] ->
{error, not_found};
[{Handle, Callback}] ->
{ok, Callback}
end.
%%%===================================================================
%%% gen_server callbacks
%%%===================================================================
%%--------------------------------------------------------------------
%% @private
%% @doc
%% Initializes the server
%% @end
%%--------------------------------------------------------------------
-spec init(Args :: term()) -> {ok, State :: term()} |
{ok, State :: term(), Timeout :: timeout()} |
{ok, State :: term(), hibernate} |
{stop, Reason :: term()} |
ignore.
init([]) ->
process_flag(trap_exit, true),
ets:new(?HANDLERS_TABLE, [named_table, set, protected]),
register_handler(json, fun nova_basic_handler:handle_json/3),
register_handler(ok, fun nova_basic_handler:handle_ok/3),
register_handler(status, fun nova_basic_handler:handle_status/3),
register_handler(redirect, fun nova_basic_handler:handle_redirect/3),
register_handler(sendfile, fun nova_basic_handler:handle_sendfile/3),
register_handler(ws, fun nova_basic_handler:handle_ws/2),
register_handler(view, fun nova_basic_handler:handle_view/3),
{ok, #state{}}.
%%--------------------------------------------------------------------
%% @private
%% @doc
%% Handling call messages
%% @end
%%--------------------------------------------------------------------
-spec handle_call(Request :: term(), From :: {pid(), term()}, State :: term()) ->
{reply, Reply :: term(), NewState :: term()} |
{reply, Reply :: term(), NewState :: term(), Timeout :: timeout()} |
{reply, Reply :: term(), NewState :: term(), hibernate} |
{noreply, NewState :: term()} |
{noreply, NewState :: term(), Timeout :: timeout()} |
{noreply, NewState :: term(), hibernate} |
{stop, Reason :: term(), Reply :: term(), NewState :: term()} |
{stop, Reason :: term(), NewState :: term()}.
handle_call({unregister_handler, Handle}, _From, State) ->
ets:delete(?HANDLERS_TABLE, Handle),
?LOG_DEBUG(#{action => <<"Removed handler">>, handler => Handle}),
{reply, ok, State};
handle_call(_Request, _From, State) ->
Reply = ok,
{reply, Reply, State}.
%%--------------------------------------------------------------------
%% @private
%% @doc
%% Handling cast messages
%% @end
%%--------------------------------------------------------------------
-spec handle_cast(Request :: term(), State :: term()) ->
{noreply, NewState :: term()} |
{noreply, NewState :: term(), Timeout :: timeout()} |
{noreply, NewState :: term(), hibernate} |
{stop, Reason :: term(), NewState :: term()}.
handle_cast({register_handler, Handle, Callback}, State) ->
Callback0 =
case Callback of
Callback when is_function(Callback) -> Callback;
{Module, Function} -> fun Module:Function/4
end,
case ets:lookup(?HANDLERS_TABLE, Handle) of
[] ->
?LOG_DEBUG(#{action => <<"Registered handler">>, handler => Handle}),
ets:insert(?HANDLERS_TABLE, {Handle, Callback0}),
{noreply, State};
_ ->
?LOG_ERROR(#{msg => <<"Another handler is already registered on that name">>, handler => Handle}),
{noreply, State}
end;
handle_cast(_Request, State) ->
{noreply, State}.
%%--------------------------------------------------------------------
%% @private
%% @doc
%% Handling all non call/cast messages
%% @end
%%--------------------------------------------------------------------
-spec handle_info(Info :: timeout() | term(), State :: term()) ->
{noreply, NewState :: term()} |
{noreply, NewState :: term(), Timeout :: timeout()} |
{noreply, NewState :: term(), hibernate} |
{stop, Reason :: normal | term(), NewState :: term()}.
handle_info(_Info, State) ->
{noreply, State}.
%%--------------------------------------------------------------------
%% @private
%% @doc
%% This function is called by a gen_server when it is about to
%% terminate. It should be the opposite of Module:init/1 and do any
%% necessary cleaning up. When it returns, the gen_server terminates
%% with Reason. The return value is ignored.
%% @end
%%--------------------------------------------------------------------
-spec terminate(Reason :: normal | shutdown | {shutdown, term()} | term(),
State :: term()) -> any().
terminate(_Reason, _State) ->
ok.
%%--------------------------------------------------------------------
%% @private
%% @doc
%% Convert process state when code is changed
%% @end
%%--------------------------------------------------------------------
-spec code_change(OldVsn :: term() | {down, term()},
State :: term(),
Extra :: term()) -> {ok, NewState :: term()} |
{error, Reason :: term()}.
code_change(_OldVsn, State, _Extra) ->
{ok, State}.
%%--------------------------------------------------------------------
%% @private
%% @doc
%% This function is called for changing the form and appearance
%% of gen_server status when it is returned from sys:get_status/1,2
%% or when it appears in termination error logs.
%% @end
%%--------------------------------------------------------------------
-spec format_status(Opt :: normal | terminate,
Status :: list()) -> Status :: term().
format_status(_Opt, Status) ->
Status.
%%%===================================================================
%%% Internal functions
%%%===================================================================