%%%-------------------------------------------------------------------
%%% Copyright (C) 2020, Niclas Axelsson
%%% Created : 12 Feb 2020 by Niclas Axelsson <niclas@burbas.se>
%%%-------------------------------------------------------------------
-module(nova_handlers).
-moduledoc """
This module is responsible for all the different return types a controller have. *Nova* is constructed in such way that it's really easy to extend it by using *handlers*. 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 *supervisor* 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.
__A handler can return two different types__
\{ok, StatusCode, Headers, Body\} - This will return a proper reply to the requester.
\{error, Reason\} - This will render a 500 page to the user.
""".
-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/1
]).
-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
%%%===================================================================
%%--------------------------------------------------------------------
%% Starts the server
-doc false.
-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.".
-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.".
-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.".
-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
%%%===================================================================
%%--------------------------------------------------------------------
%% Initializes the server
-doc false.
-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]),
%% Inserted directly rather than cast to ourselves: register_handler/2 is
%% asynchronous, so the defaults would not be guaranteed to exist when
%% start_link/0 returns.
ets:insert(?HANDLERS_TABLE, [{json, fun nova_basic_handler:handle_json/3},
{ok, fun nova_basic_handler:handle_ok/3},
{status, fun nova_basic_handler:handle_status/3},
{redirect, fun nova_basic_handler:handle_redirect/3},
{sendfile, fun nova_basic_handler:handle_sendfile/3},
{ws, fun nova_basic_handler:handle_ws/2},
{view, fun nova_basic_handler:handle_view/3}]),
{ok, #state{}}.
%%--------------------------------------------------------------------
%% Handling call messages
-doc false.
-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}.
%%--------------------------------------------------------------------
%% Handling cast messages
-doc false.
-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}.
%%--------------------------------------------------------------------
%% Handling all non call/cast messages
-doc false.
-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}.
%%--------------------------------------------------------------------
%% 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.
-doc false.
-spec terminate(Reason :: normal | shutdown | {shutdown, term()} | term(),
State :: term()) -> any().
terminate(_Reason, _State) ->
ok.
%%--------------------------------------------------------------------
%% Convert process state when code is changed
-doc false.
-spec code_change(OldVsn :: term() | {down, term()},
State :: term(),
Extra :: term()) -> {ok, NewState :: term()} |
{error, Reason :: term()}.
code_change(_OldVsn, State, _Extra) ->
{ok, State}.
%%--------------------------------------------------------------------
%% 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.
-doc false.
-spec format_status(Status) -> NewStatus when Status :: #{'log'=>[any()], 'message'=>_, 'reason'=>_, 'state'=>_},
NewStatus :: #{'log'=>[any()], 'message'=>_, 'reason'=>_, 'state'=>_}.
format_status(Status) ->
Status.
%%%===================================================================
%%% Internal functions
%%%===================================================================