Current section

Files

Jump to
macula_tweann src morphology_registry.erl
Raw

src/morphology_registry.erl

%% @doc Registry for morphology modules.
%%
%% Provides runtime registration of morphology implementations. Applications
%% can register custom morphologies without modifying the macula-tweann library.
%%
%% Usage:
%%
%% Start registry (done automatically by application):
%% morphology_registry:start_link().
%%
%% Register a morphology:
%% ok = morphology_registry:register(my_problem, my_morphology_module).
%%
%% Get registered module:
%% {ok, my_morphology_module} = morphology_registry:get(my_problem).
%%
%% List all registered:
%% [my_problem, xor_mimic] = morphology_registry:list_all().
%%
%% @author Macula.io
%% @copyright 2025 Macula.io, Apache-2.0
-module(morphology_registry).
-behaviour(gen_server).
-compile({no_auto_import, [get/1]}).
%% API
-export([
start_link/0,
register/2,
unregister/1,
get/1,
list_all/0,
is_registered/1
]).
%% gen_server callbacks
-export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]).
-define(SERVER, ?MODULE).
-define(TABLE, morphology_registry_table).
-record(state, {}).
%%==============================================================================
%% API Functions
%%==============================================================================
%% @doc Start the morphology registry server.
%%
%% Creates ETS table for storing morphology registrations.
%%
%% @returns {ok, Pid} | {error, Reason}
-spec start_link() -> {ok, pid()} | {error, term()}.
start_link() ->
gen_server:start_link({local, ?SERVER}, ?MODULE, [], []).
%% @doc Register a morphology module.
%%
%% Verifies that the module implements morphology_behaviour before registering.
%%
%% Example:
%%
%% ok = morphology_registry:register(xor_mimic, morphology_xor).
%%
%% @param MorphologyName Atom identifying the morphology (e.g., xor_mimic)
%% @param Module Module implementing morphology_behaviour
%% @returns ok | {error, Reason}
-spec register(atom(), module()) -> ok | {error, term()}.
register(MorphologyName, Module) when is_atom(MorphologyName), is_atom(Module) ->
gen_server:call(?SERVER, {register, MorphologyName, Module}).
%% @doc Unregister a morphology.
%%
%% @param MorphologyName The morphology to remove
%% @returns ok
-spec unregister(atom()) -> ok.
unregister(MorphologyName) when is_atom(MorphologyName) ->
gen_server:call(?SERVER, {unregister, MorphologyName}).
%% @doc Get the module for a registered morphology.
%%
%% @param MorphologyName The morphology to look up
%% @returns {ok, Module} | {error, not_found}
-spec get(atom()) -> {ok, module()} | {error, not_found}.
get(MorphologyName) when is_atom(MorphologyName) ->
case ets:lookup(?TABLE, MorphologyName) of
[{MorphologyName, Module}] -> {ok, Module};
[] -> {error, not_found}
end.
%% @doc List all registered morphologies.
%%
%% @returns List of morphology names
-spec list_all() -> [atom()].
list_all() ->
[Name || {Name, _Module} <- ets:tab2list(?TABLE)].
%% @doc Check if a morphology is registered.
%%
%% @param MorphologyName The morphology to check
%% @returns boolean()
-spec is_registered(atom()) -> boolean().
is_registered(MorphologyName) when is_atom(MorphologyName) ->
case get(MorphologyName) of
{ok, _} -> true;
{error, not_found} -> false
end.
%%==============================================================================
%% gen_server Callbacks
%%==============================================================================
%% @private
init([]) ->
% Create ETS table for morphology storage
_Tab = ets:new(?TABLE, [named_table, set, public, {read_concurrency, true}]),
{ok, #state{}}.
%% @private
handle_call({register, MorphologyName, Module}, _From, State) ->
% Verify module is loaded
case code:ensure_loaded(Module) of
{module, Module} ->
% Verify module exports required callbacks
case verify_morphology_module(Module) of
ok ->
ets:insert(?TABLE, {MorphologyName, Module}),
{reply, ok, State};
{error, Reason} ->
{reply, {error, Reason}, State}
end;
{error, Reason} ->
{reply, {error, {module_not_loaded, Reason}}, State}
end;
handle_call({unregister, MorphologyName}, _From, State) ->
ets:delete(?TABLE, MorphologyName),
{reply, ok, State};
handle_call(_Request, _From, State) ->
{reply, {error, unknown_request}, State}.
%% @private
handle_cast(_Msg, State) ->
{noreply, State}.
%% @private
handle_info(_Info, State) ->
{noreply, State}.
%% @private
terminate(_Reason, _State) ->
ok.
%% @private
code_change(_OldVsn, State, _Extra) ->
{ok, State}.
%%==============================================================================
%% Internal Functions
%%==============================================================================
%% @private
%% @doc Verify that a module implements morphology_behaviour.
%%
%% Checks that the module exports get_sensors/1 and get_actuators/1.
%%
%% @param Module The module to verify
%% @returns ok | {error, Reason}
-spec verify_morphology_module(module()) ->
ok | {error, {missing_callback, get_sensors | get_actuators, 1}}.
verify_morphology_module(Module) ->
Exports = Module:module_info(exports),
HasGetSensors = lists:member({get_sensors, 1}, Exports),
HasGetActuators = lists:member({get_actuators, 1}, Exports),
if
HasGetSensors andalso HasGetActuators ->
ok;
not HasGetSensors ->
{error, {missing_callback, get_sensors, 1}};
not HasGetActuators ->
{error, {missing_callback, get_actuators, 1}}
end.