Packages
macula
2.1.1
7.1.0
7.0.0
6.0.0
5.2.2
5.2.1
5.2.0
5.1.0
5.0.0
4.8.0
4.7.1
4.7.0
4.6.0
4.5.0
4.4.10
4.4.9
4.4.8
4.4.7
4.4.6
4.4.5
4.4.4
4.4.3
4.4.2
4.4.1
4.4.0
4.3.1
4.3.0
4.2.9
4.2.8
4.2.7
4.2.6
4.2.5
4.2.4
4.2.3
4.2.2
4.2.1
4.2.0
4.1.1
4.1.0
4.0.0
3.16.0
3.15.3
3.15.2
3.15.1
3.14.0
3.13.0
3.12.1
3.12.0
3.11.1
3.11.0
3.10.3
3.10.2
3.10.1
3.9.0
3.8.0
3.7.0
3.5.0
3.4.0
3.3.0
3.2.0
3.1.0
3.0.0
2.1.1
2.1.0
2.0.0
1.5.2
1.5.1
1.4.30
1.4.29
1.4.28
1.4.27
1.4.26
1.4.25
1.4.24
1.4.23
1.4.22
1.4.21
1.4.20
1.4.19
1.4.18
1.4.17
1.4.16
1.4.15
1.4.14
1.4.13
1.4.11
1.4.10
1.4.9
1.4.8
1.4.7
1.4.6
1.4.5
1.4.4
1.4.3
1.4.2
1.4.1
1.4.0
1.3.1
1.3.0
1.2.0
1.1.0
1.0.10
1.0.9
1.0.8
1.0.7
1.0.6
1.0.5
1.0.4
1.0.3
1.0.2
1.0.1
1.0.0
0.48.6
0.48.5
0.48.4
0.48.3
0.48.2
0.48.1
0.48.0
0.47.1
0.47.0
0.46.3
0.46.1
0.46.0
0.45.3
0.45.2
0.45.1
0.45.0
0.44.2
0.44.1
0.44.0
0.43.3
0.43.2
0.43.1
0.43.0
0.42.9
0.42.8
0.42.7
0.42.6
0.42.5
0.42.4
0.42.3
0.42.2
0.42.1
0.42.0
0.41.1
0.41.0
0.40.1
0.40.0
0.39.9
0.39.8
0.39.7
0.39.6
0.39.5
0.39.4
0.39.3
0.39.2
0.39.1
0.39.0
0.38.8
0.38.7
0.38.6
0.38.5
0.38.4
0.38.3
0.38.2
0.38.1
0.38.0
0.37.7
0.37.6
0.37.5
0.37.4
0.37.3
0.37.2
0.37.1
0.37.0
0.36.6
0.36.5
0.36.4
0.36.3
0.36.2
0.36.1
0.36.0
0.35.4
0.35.3
0.35.2
0.35.1
0.35.0
0.34.1
0.34.0
0.33.1
0.33.0
0.32.5
0.32.4
0.32.3
0.32.2
0.32.1
0.32.0
0.31.9
0.31.8
0.31.7
0.31.6
0.31.5
0.31.4
0.31.3
0.31.2
0.31.1
0.31.0
0.30.10
0.30.9
0.30.8
0.30.7
0.30.6
0.30.5
0.30.4
0.30.3
0.30.2
0.30.1
0.30.0
0.29.0
0.28.3
0.28.2
0.28.1
0.28.0
0.27.1
0.27.0
0.26.1
0.26.0
0.25.6
0.25.5
0.25.4
0.25.3
0.25.2
0.25.1
0.25.0
0.24.6
0.24.5
0.24.4
0.24.3
0.24.2
0.24.1
0.24.0
0.23.3
0.23.2
0.23.1
0.23.0
0.22.12
0.22.11
0.22.10
0.22.9
0.22.8
0.22.7
0.22.6
0.22.5
0.22.4
0.22.3
0.22.2
0.22.1
0.22.0
0.21.7
0.21.6
0.21.5
0.21.4
0.21.2
0.21.1
0.21.0
0.20.25
0.20.24
0.20.23
0.20.22
0.20.21
0.20.20
0.20.19
0.20.18
0.20.17
0.20.16
0.20.15
0.20.14
0.20.13
0.20.12
0.20.11
0.20.10
0.20.9
0.20.8
0.20.7
0.20.6
0.20.5
0.20.3
0.20.2
0.20.1
0.20.0
0.19.2
0.19.1
0.19.0
0.18.1
0.18.0
0.17.4
0.17.3
0.17.2
0.17.1
0.17.0
0.16.6
0.16.5
0.16.4
0.16.3
0.16.2
0.16.1
0.16.0
0.15.1
0.15.0
0.14.3
0.14.2
0.14.1
0.14.0
0.12.6
0.12.5
0.12.3
0.11.3
0.10.2
0.10.1
0.10.0
0.9.2
0.9.1
0.9.0
0.8.25
0.8.24
0.8.23
0.8.22
0.8.21
0.8.20
0.8.19
0.8.18
0.8.17
0.8.16
0.8.15
0.8.14
0.8.13
0.8.12
0.8.11
0.8.10
0.8.9
0.8.8
0.8.7
0.8.6
0.8.5
0.8.4
0.8.3
0.8.2
0.8.1
0.8.0
0.7.30
0.7.29
0.7.28
0.7.27
0.7.26
0.7.25
0.7.24
0.7.23
0.7.22
0.7.21
0.7.20
0.7.19
0.7.18
0.7.17
0.7.16
0.7.15
0.7.14
0.7.13
0.7.12
0.7.11
0.7.10
0.7.9
0.7.8
0.7.7
0.7.6
0.7.5
0.7.4
0.7.3
0.7.2
0.7.1
0.7.0
0.6.7
0.6.6
0.6.5
0.6.4
0.6.3
0.6.2
0.6.1
0.6.0
0.5.0
0.4.4
0.4.3
0.4.2
0.4.1
0.4.0
0.3.4
0.3.3
0.3.2
0.3.1
Macula HTTP/3 Mesh SDK — connect, subscribe, publish, call, advertise
Current section
Files
Jump to
Current section
Files
src/macula_mri_registry.erl
%%%-------------------------------------------------------------------
%%% @doc MRI Type Registry
%%%
%%% Manages the registry of valid MRI types with their schemas.
%%% Provides both built-in types and runtime registration of custom types.
%%%
%%% Built-in types are always valid. Custom types can be registered
%%% at runtime and optionally scoped to specific realms.
%%%
%%% @end
%%%-------------------------------------------------------------------
-module(macula_mri_registry).
-behaviour(gen_server).
%% API
-export([start_link/0, start_link/1]).
-export([is_valid_type/1, is_valid_type/2]).
-export([register_type/2, unregister_type/1]).
-export([get_type_schema/1, list_types/0, list_custom_types/0]).
-export([path_schema/1, validate_path_for_type/2]).
%% gen_server callbacks
-export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2]).
-define(SERVER, ?MODULE).
-define(TABLE, macula_mri_types).
%% Built-in types that are always valid
-define(BUILTIN_TYPES, [
realm, org, user, app, service,
artifact, instance, license, cert, key,
topic, proc, content,
device, cluster, location, zone, network,
model, dataset, config,
class, taxonomy
]).
-record(state, {}).
-record(type_schema, {
name :: binary() | atom(),
description :: binary(),
path_schema :: [atom()],
parent_type :: atom() | undefined,
realm :: binary() | undefined %% nil means global
}).
%%===================================================================
%% API
%%===================================================================
%% @doc Start the registry as a linked process.
-spec start_link() -> {ok, pid()} | {error, term()}.
start_link() ->
start_link([]).
%% @doc Start the registry with options.
-spec start_link(list()) -> {ok, pid()} | {error, term()}.
start_link(Opts) ->
gen_server:start_link({local, ?SERVER}, ?MODULE, Opts, []).
%% @doc Check if a type is valid (built-in or registered).
-spec is_valid_type(atom() | binary()) -> boolean().
is_valid_type(Type) when is_atom(Type) ->
is_builtin_type(Type) orelse is_registered_type(atom_to_binary(Type, utf8));
is_valid_type(TypeBin) when is_binary(TypeBin) ->
is_builtin_type_bin(TypeBin) orelse is_registered_type(TypeBin);
is_valid_type({custom, TypeBin}) when is_binary(TypeBin) ->
is_registered_type(TypeBin);
is_valid_type(_) ->
false.
%% @doc Check if a type is valid for a specific realm.
-spec is_valid_type(atom() | binary(), binary()) -> boolean().
is_valid_type(Type, Realm) when is_atom(Type) ->
is_builtin_type(Type) orelse is_registered_type_for_realm(atom_to_binary(Type, utf8), Realm);
is_valid_type(TypeBin, Realm) when is_binary(TypeBin) ->
is_builtin_type_bin(TypeBin) orelse is_registered_type_for_realm(TypeBin, Realm);
is_valid_type({custom, TypeBin}, Realm) when is_binary(TypeBin) ->
is_registered_type_for_realm(TypeBin, Realm);
is_valid_type(_, _) ->
false.
%% @doc Register a custom type with schema.
%% Schema map can contain:
%% - description: binary() - Human-readable description
%% - path_schema: [atom()] - Path segment roles (e.g., [org, device_id])
%% - parent_type: atom() - Type of the parent MRI
%% - realm: binary() - Restrict to this realm (undefined = global)
-spec register_type(binary(), map()) -> ok | {error, term()}.
register_type(TypeBin, Schema) when is_binary(TypeBin), is_map(Schema) ->
case is_builtin_type_bin(TypeBin) of
true -> {error, cannot_override_builtin};
false -> gen_server:call(?SERVER, {register_type, TypeBin, Schema})
end;
register_type(_, _) ->
{error, invalid_arguments}.
%% @doc Unregister a custom type.
-spec unregister_type(binary()) -> ok | {error, term()}.
unregister_type(TypeBin) when is_binary(TypeBin) ->
case is_builtin_type_bin(TypeBin) of
true -> {error, cannot_remove_builtin};
false -> gen_server:call(?SERVER, {unregister_type, TypeBin})
end;
unregister_type(_) ->
{error, invalid_arguments}.
%% @doc Get the schema for a type.
-spec get_type_schema(atom() | binary()) -> {ok, map()} | {error, not_found}.
get_type_schema(Type) when is_atom(Type) ->
get_type_schema(atom_to_binary(Type, utf8));
get_type_schema(TypeBin) when is_binary(TypeBin) ->
case is_builtin_type_bin(TypeBin) of
true -> {ok, builtin_schema(binary_to_atom(TypeBin, utf8))};
false -> lookup_custom_schema(TypeBin)
end;
get_type_schema(_) ->
{error, invalid_type}.
%% @doc List all valid types (built-in + custom).
-spec list_types() -> [atom() | binary()].
list_types() ->
Builtin = ?BUILTIN_TYPES,
Custom = list_custom_types(),
Builtin ++ Custom.
%% @doc List only custom registered types.
-spec list_custom_types() -> [binary()].
list_custom_types() ->
case ets:info(?TABLE) of
undefined -> [];
_ -> [Name || #type_schema{name = Name} <- ets:tab2list(?TABLE)]
end.
%% @doc Get the path schema for a type.
-spec path_schema(atom() | binary()) -> {ok, [atom()]} | {error, not_found}.
path_schema(Type) ->
case get_type_schema(Type) of
{ok, #{path_schema := Schema}} -> {ok, Schema};
{ok, _} -> {ok, []};
{error, _} = Err -> Err
end.
%% @doc Validate that a path matches the expected schema for a type.
-spec validate_path_for_type(atom() | binary(), [binary()]) -> ok | {error, term()}.
validate_path_for_type(Type, Path) ->
case path_schema(Type) of
{ok, []} -> ok; %% No schema means any path is valid
{ok, Schema} -> validate_path_against_schema(Path, Schema);
{error, _} = Err -> Err
end.
%%===================================================================
%% gen_server callbacks
%%===================================================================
init(_Opts) ->
%% Create ETS table for custom types
?TABLE = ets:new(?TABLE, [
named_table,
set,
public,
{keypos, #type_schema.name},
{read_concurrency, true}
]),
{ok, #state{}}.
handle_call({register_type, TypeBin, Schema}, _From, State) ->
TypeSchema = #type_schema{
name = TypeBin,
description = maps:get(description, Schema, <<>>),
path_schema = maps:get(path_schema, Schema, []),
parent_type = maps:get(parent_type, Schema, undefined),
realm = maps:get(realm, Schema, undefined)
},
true = ets:insert(?TABLE, TypeSchema),
{reply, ok, State};
handle_call({unregister_type, TypeBin}, _From, State) ->
true = ets:delete(?TABLE, TypeBin),
{reply, ok, State};
handle_call(_Request, _From, State) ->
{reply, {error, unknown_request}, State}.
handle_cast(_Msg, State) ->
{noreply, State}.
handle_info(_Info, State) ->
{noreply, State}.
terminate(_Reason, _State) ->
ok.
%%===================================================================
%% Internal Functions
%%===================================================================
is_builtin_type(Type) when is_atom(Type) ->
lists:member(Type, ?BUILTIN_TYPES).
is_builtin_type_bin(<<"realm">>) -> true;
is_builtin_type_bin(<<"org">>) -> true;
is_builtin_type_bin(<<"user">>) -> true;
is_builtin_type_bin(<<"app">>) -> true;
is_builtin_type_bin(<<"service">>) -> true;
is_builtin_type_bin(<<"artifact">>) -> true;
is_builtin_type_bin(<<"instance">>) -> true;
is_builtin_type_bin(<<"license">>) -> true;
is_builtin_type_bin(<<"cert">>) -> true;
is_builtin_type_bin(<<"key">>) -> true;
is_builtin_type_bin(<<"topic">>) -> true;
is_builtin_type_bin(<<"proc">>) -> true;
is_builtin_type_bin(<<"content">>) -> true;
is_builtin_type_bin(<<"device">>) -> true;
is_builtin_type_bin(<<"cluster">>) -> true;
is_builtin_type_bin(<<"location">>) -> true;
is_builtin_type_bin(<<"zone">>) -> true;
is_builtin_type_bin(<<"network">>) -> true;
is_builtin_type_bin(<<"model">>) -> true;
is_builtin_type_bin(<<"dataset">>) -> true;
is_builtin_type_bin(<<"config">>) -> true;
is_builtin_type_bin(<<"class">>) -> true;
is_builtin_type_bin(<<"taxonomy">>) -> true;
is_builtin_type_bin(_) -> false.
is_registered_type(TypeBin) ->
case ets:info(?TABLE) of
undefined -> false;
_ -> ets:member(?TABLE, TypeBin)
end.
is_registered_type_for_realm(TypeBin, Realm) ->
case ets:info(?TABLE) of
undefined ->
false;
_ ->
case ets:lookup(?TABLE, TypeBin) of
[#type_schema{realm = undefined}] -> true; %% Global type
[#type_schema{realm = Realm}] -> true; %% Realm-specific match
_ -> false
end
end.
lookup_custom_schema(TypeBin) ->
case ets:info(?TABLE) of
undefined ->
{error, not_found};
_ ->
case ets:lookup(?TABLE, TypeBin) of
[#type_schema{} = Schema] ->
{ok, schema_to_map(Schema)};
[] ->
{error, not_found}
end
end.
schema_to_map(#type_schema{
name = Name,
description = Desc,
path_schema = PathSchema,
parent_type = ParentType,
realm = Realm
}) ->
#{
name => Name,
description => Desc,
path_schema => PathSchema,
parent_type => ParentType,
realm => Realm
}.
builtin_schema(realm) ->
#{name => realm, description => <<"Top-level mesh domain">>, path_schema => []};
builtin_schema(org) ->
#{name => org, description => <<"Organization within realm">>, path_schema => [org]};
builtin_schema(user) ->
#{name => user, description => <<"User within organization">>, path_schema => [org, user]};
builtin_schema(app) ->
#{name => app, description => <<"Application">>, path_schema => [org, app]};
builtin_schema(service) ->
#{name => service, description => <<"Service within application">>, path_schema => [org, app, service]};
builtin_schema(artifact) ->
#{name => artifact, description => <<"Published artifact">>, path_schema => [org, artifact_id]};
builtin_schema(instance) ->
#{name => instance, description => <<"Running application instance">>, path_schema => [org, device, instance_name]};
builtin_schema(license) ->
#{name => license, description => <<"License grant">>, path_schema => [org, app, license_id]};
builtin_schema(cert) ->
#{name => cert, description => <<"Certificate">>, path_schema => [org, cert_id]};
builtin_schema(key) ->
#{name => key, description => <<"Signing key">>, path_schema => [org, key_id]};
builtin_schema(topic) ->
#{name => topic, description => <<"PubSub topic">>, path_schema => [org, topic_path]};
builtin_schema(proc) ->
#{name => proc, description => <<"RPC procedure">>, path_schema => [org, proc_path]};
builtin_schema(content) ->
#{name => content, description => <<"Content-addressed blob">>, path_schema => [mcid]};
builtin_schema(device) ->
#{name => device, description => <<"Physical device">>, path_schema => [org, device_id]};
builtin_schema(cluster) ->
#{name => cluster, description => <<"Group of nodes">>, path_schema => [org, cluster_name]};
builtin_schema(location) ->
#{name => location, description => <<"Geographical location">>, path_schema => [org, location_path]};
builtin_schema(zone) ->
#{name => zone, description => <<"Logical/physical zone">>, path_schema => [org, zone_name]};
builtin_schema(network) ->
#{name => network, description => <<"Network segment">>, path_schema => [org, network_name]};
builtin_schema(model) ->
#{name => model, description => <<"AI/ML model">>, path_schema => [org, model_name]};
builtin_schema(dataset) ->
#{name => dataset, description => <<"Training/inference dataset">>, path_schema => [org, dataset_name]};
builtin_schema(config) ->
#{name => config, description => <<"Configuration resource">>, path_schema => [org, config_path]};
builtin_schema(class) ->
#{name => class, description => <<"Classification class">>, path_schema => [class_path]};
builtin_schema(taxonomy) ->
#{name => taxonomy, description => <<"Taxonomy definition">>, path_schema => [org, taxonomy_name]};
builtin_schema(_) ->
#{}.
validate_path_against_schema(Path, Schema) when length(Path) =:= length(Schema) ->
ok;
validate_path_against_schema(Path, Schema) when length(Path) < length(Schema) ->
{error, {path_too_short, expected, length(Schema), got, length(Path)}};
validate_path_against_schema(Path, Schema) ->
{error, {path_too_long, expected, length(Schema), got, length(Path)}}.