Fosi Fossil Service
Small teams. Open source. Built on Fossil.

CaaS / Lamina

Unified Management for Fossil Repositories

Public | Owner: CaaS | Default branch: trunk | Last update: 2026-10-25 00:41

Artifact a09bc0998f58f3f2876a06a87f6bd56fe7f53b779a658ffdcbc1468d2a954ab0:


%%%-------------------------------------------------------------------
%%% 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
%%%===================================================================

Clone

You can clone this repository with the following command:

fossil clone https://fosi.dev/CaaS/Lamina

Contribute

The best way to contribute is through e-mail. See the project documentation for more details.