Skip to main content

src/hecate_om.erl

%%% @doc Public facade for hecate-om.
%%%
%%% Services typically only need a handful of these:
%%%
%%%   hecate_om:boot(MyServiceMod)         %% one-call lifecycle wiring
%%%   hecate_om:advertise_capabilities()   %% (re-)publish my caps
%%%   hecate_om:health()                   %% snapshot for /health
%%%   hecate_om:service_cert()             %% load my service-principal cert
%%%   hecate_om:macula_client()            %% returns the SDK client handle
-module(hecate_om).

-export([
    boot/1,
    boot/2,
    advertise_capabilities/0,
    health/0,
    service_cert/0,
    macula_client/0,
    service_module/0
]).

-define(SERVICE_MODULE_KEY, hecate_om_service_module).

%% @doc Wire a service module into hecate_om and start it.
%%
%% Typical call from the hosting service's `_app:start/2':
%%
%%   start(_, _) ->
%%       hecate_om:boot(my_service).
-spec boot(module()) -> {ok, pid()} | {error, term()}.
boot(ServiceMod) ->
    boot(ServiceMod, #{}).

-spec boot(module(), map()) -> {ok, pid()} | {error, term()}.
boot(ServiceMod, Opts) when is_atom(ServiceMod), is_map(Opts) ->
    persistent_term:put(?SERVICE_MODULE_KEY, ServiceMod),
    ok = maybe_wire_store(ServiceMod),
    ok = hecate_om_capabilities:register(ServiceMod:capabilities()),
    ok = hecate_om_health:register(ServiceMod),
    ServiceMod:start(Opts).

%% @private When the service module exports both `store_id/0' and
%% `data_dir/0', treat it as a CMD/PRJ service that owns a reckon-db
%% store. Wire the canonical pattern before the service's own
%% start/1 runs. Producer-only services omit the callbacks and pay
%% nothing.
maybe_wire_store(ServiceMod) ->
    _ = code:ensure_loaded(ServiceMod),
    Has = erlang:function_exported(ServiceMod, store_id, 0) andalso
          erlang:function_exported(ServiceMod, data_dir, 0),
    case Has of
        false -> ok;
        true ->
            StoreId = ServiceMod:store_id(),
            DataDir = ServiceMod:data_dir(),
            Indexes = store_indexes(ServiceMod),
            case hecate_om_store:ensure(StoreId, DataDir, Indexes) of
                ok           -> ok;
                {error, Why} -> error({hecate_om_store_failed, ServiceMod, Why})
            end
    end.

%% Optional store_indexes/0 callback: the service's declared secondary
%% index list. Defaults to [] (no indexes) when the service doesn't
%% export it.
store_indexes(ServiceMod) ->
    case erlang:function_exported(ServiceMod, store_indexes, 0) of
        true  -> ServiceMod:store_indexes();
        false -> []
    end.

-spec service_module() -> module() | undefined.
service_module() ->
    persistent_term:get(?SERVICE_MODULE_KEY, undefined).

%% @doc (Re-)publish this service's capabilities onto the mesh.
%% Typically called once at boot; call again when the capability
%% set changes.
-spec advertise_capabilities() -> ok.
advertise_capabilities() ->
    hecate_om_capabilities:publish().

%% @doc Snapshot of this service's health. Used by /health handler.
-spec health() -> hecate_om_service:health().
health() ->
    hecate_om_health:snapshot().

-spec service_cert() -> {ok, binary()} | {error, term()}.
service_cert() ->
    hecate_om_identity:service_cert().

-spec macula_client() -> {ok, term()} | {error, term()}.
macula_client() ->
    hecate_om_identity:macula_client().