-module(registry). -behaviour(gen_server). -export([new/0, kinds/0, register/4, lookup/3, list/2]). -export([start_link/0, register/3, lookup/2, list/1, stop/0]). -export([init/1, handle_call/3, handle_cast/2, handle_info/2]). %% Pure-functional registry for the seven bootstrap kinds. %% %% State is a property list keyed by kind atom; each kind's value %% is itself a property list of {Name, Entry} pairs. Entry is %% opaque — typically a proplist with :cid, :schema, :semantics, %% :supersedes fields, but the registry doesn't enforce that here. %% %% A gen_server wrapper (Step 5b) will own the global registry %% process; the pure functions in this module remain the canonical %% API and are usable for tests and for offline projection-replay. %% %% Return shapes: %% new/0 -> State %% kinds/0 -> [Atom, ...] %% register/4 -> {ok, NewState} | {error, unknown_kind} %% lookup/3 -> {ok, Entry} | not_found | {error, unknown_kind} %% list/2 -> [{Name, Entry}, ...] | {error, unknown_kind} new() -> []. kinds() -> [activity_types, object_types, projections, validators, codecs, sig_suites, audience]. register(Kind, Name, Entry, State) -> case is_valid_kind(Kind) of false -> {error, unknown_kind}; true -> Entries = kind_entries(Kind, State), Updated = put_pair(Name, Entry, Entries), {ok, set_kind_entries(Kind, Updated, State)} end. lookup(Kind, Name, State) -> case is_valid_kind(Kind) of false -> {error, unknown_kind}; true -> find_pair(Name, kind_entries(Kind, State)) end. list(Kind, State) -> case is_valid_kind(Kind) of false -> {error, unknown_kind}; true -> kind_entries(Kind, State) end. %% ── Internal ──────────────────────────────────────────────────── is_valid_kind(K) -> lists:member(K, kinds()). kind_entries(Kind, State) -> case find_pair(Kind, State) of not_found -> []; {ok, V} -> V end. set_kind_entries(Kind, Entries, State) -> put_pair(Kind, Entries, State). put_pair(K, V, []) -> [{K, V}]; put_pair(K, V, [{K, _} | Rest]) -> [{K, V} | Rest]; put_pair(K, V, [P | Rest]) -> [P | put_pair(K, V, Rest)]. find_pair(_, []) -> not_found; find_pair(K, [{K, V} | _]) -> {ok, V}; find_pair(K, [_ | Rest]) -> find_pair(K, Rest). %% ── Step 5b: gen_server wrapper ───────────────────────────────── %% %% The named process owns the registry state; concurrent readers %% and writers serialize through gen_server:call. The pure /3 and %% /4 functions remain available for offline projection-replay and %% for tests that don't need a process at all. %% %% Port notes: gen_server:start_link returns the raw Pid (not %% `{ok, Pid}` as in OTP). `?MODULE` macro is unsupported here, so %% the registered name is the literal `registry` atom in every call. start_link() -> Pid = gen_server:start_link(registry, []), erlang:register(registry, Pid), Pid. stop() -> R = gen_server:call(registry, '$gen_stop'), erlang:unregister(registry), R. register(Kind, Name, Entry) -> gen_server:call(registry, {register, Kind, Name, Entry}). lookup(Kind, Name) -> gen_server:call(registry, {lookup, Kind, Name}). list(Kind) -> gen_server:call(registry, {list, Kind}). %% gen_server callbacks init(_) -> {ok, new()}. handle_call({register, Kind, Name, Entry}, _From, State) -> case register(Kind, Name, Entry, State) of {ok, NewState} -> {reply, ok, NewState}; {error, R} -> {reply, {error, R}, State} end; handle_call({lookup, Kind, Name}, _From, State) -> {reply, lookup(Kind, Name, State), State}; handle_call({list, Kind}, _From, State) -> {reply, list(Kind, State), State}. handle_cast(_, S) -> {noreply, S}. handle_info(_, S) -> {noreply, S}.