Packages
macula
4.2.9
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/mri/macula_mri.erl
%%%-------------------------------------------------------------------
%%% @doc Macula Resource Identifier (MRI) - Core Module
%%%
%%% Provides parsing, validation, formatting, and manipulation of MRIs.
%%% MRI format: `mri:{type}:{realm}/{path}'
%%%
%%% Example:
%%% {ok, Parsed} = macula_mri:parse(MRI),
%%% app = macula_mri:type(Parsed),
%%% Realm = macula_mri:realm(Parsed),
%%% Path = macula_mri:path(Parsed).
%%%
%%% @end
%%%-------------------------------------------------------------------
-module(macula_mri).
%% Parsing and Formatting
-export([parse/1, format/1, validate/1, is_valid/1]).
%% Accessors
-export([type/1, realm/1, path/1, path_string/1]).
%% Hierarchy
-export([parent/1, parent_type/1, ancestors/1, is_ancestor/2, depth/1]).
%% Construction
-export([new/1, new/3]).
-export([new_realm/1, new_org/2, new_user/3, new_app/3, new_service/4]).
-export([new_instance/4, new_instance_qualified/5]).
%% Path manipulation
-export([append_segment/2, join_path/1, split_path/1]).
%% DHT topic/procedure derivation (for instances)
-export([derive_topic/2, derive_procedure/2, to_topic_prefix/1]).
%% Trie index operations (NIF-accelerated, O(d) queries)
-export([build_index/1, index_children/3, index_descendants/3]).
-export([index_insert/4, index_remove/3, index_size/1]).
%% Types
-export_type([mri/0, mri_map/0, mri_type/0, realm/0, path_segment/0]).
-type mri() :: binary().
-type mri_map() :: #{
type := mri_type(),
realm := realm(),
path := [path_segment()]
}.
-type mri_type() :: atom() | {custom, binary()}.
-type realm() :: binary().
-type path_segment() :: binary().
-define(MRI_PREFIX, <<"mri:">>).
-define(MAX_PATH_DEPTH, 10).
-define(MAX_SEGMENT_LENGTH, 63).
%%===================================================================
%% Parsing and Formatting
%%===================================================================
%% @doc Parse an MRI binary into a map.
-spec parse(mri()) -> {ok, mri_map()} | {error, term()}.
parse(<<>>) ->
{error, empty_mri};
parse(MRI) when is_binary(MRI) ->
case binary:split(MRI, <<":">>, [global]) of
[<<"mri">>, TypeBin, RealmAndPath] ->
parse_type_and_path(TypeBin, RealmAndPath);
_ ->
{error, invalid_format}
end;
parse(_) ->
{error, not_binary}.
%% @doc Format an MRI map back to binary.
-spec format(mri_map()) -> mri().
format(#{type := Type, realm := Realm, path := []}) ->
TypeBin = type_to_binary(Type),
<<"mri:", TypeBin/binary, ":", Realm/binary>>;
format(#{type := Type, realm := Realm, path := Path}) ->
TypeBin = type_to_binary(Type),
PathBin = join_path(Path),
<<"mri:", TypeBin/binary, ":", Realm/binary, "/", PathBin/binary>>.
%% @doc Validate an MRI, returning ok or error with reason.
-spec validate(mri() | mri_map()) -> ok | {error, term()}.
validate(MRI) when is_binary(MRI) ->
case parse(MRI) of
{ok, Parsed} -> validate_parsed(Parsed);
{error, _} = Err -> Err
end;
validate(#{type := _, realm := _, path := _} = Parsed) ->
validate_parsed(Parsed);
validate(_) ->
{error, invalid_input}.
%% @doc Check if an MRI is valid.
-spec is_valid(mri() | mri_map()) -> boolean().
is_valid(MRI) ->
validate(MRI) =:= ok.
%%===================================================================
%% Accessors
%%===================================================================
%% @doc Get the type from an MRI (binary or parsed).
-spec type(mri() | mri_map()) -> mri_type().
type(#{type := Type}) ->
Type;
type(MRI) when is_binary(MRI) ->
{ok, #{type := Type}} = parse(MRI),
Type.
%% @doc Get the realm from an MRI.
-spec realm(mri() | mri_map()) -> realm().
realm(#{realm := Realm}) ->
Realm;
realm(MRI) when is_binary(MRI) ->
{ok, #{realm := Realm}} = parse(MRI),
Realm.
%% @doc Get the path segments from an MRI.
-spec path(mri() | mri_map()) -> [path_segment()].
path(#{path := Path}) ->
Path;
path(MRI) when is_binary(MRI) ->
{ok, #{path := Path}} = parse(MRI),
Path.
%% @doc Get the path as a single joined string.
-spec path_string(mri() | mri_map()) -> binary().
path_string(MRI) ->
join_path(path(MRI)).
%%===================================================================
%% Hierarchy
%%===================================================================
%% @doc Get the parent MRI, or undefined for realm-level MRIs.
-spec parent(mri() | mri_map()) -> mri() | undefined.
parent(MRI) when is_binary(MRI) ->
{ok, Parsed} = parse(MRI),
parent(Parsed);
parent(#{type := realm}) ->
undefined;
parent(#{type := Type, realm := Realm, path := []}) ->
%% No path segments - parent is realm
case parent_type(Type) of
undefined -> undefined;
ParentType -> format(#{type => ParentType, realm => Realm, path => []})
end;
parent(#{type := Type, realm := Realm, path := Path}) ->
case drop_last(Path) of
[] ->
%% Path becomes empty - check if parent type exists
case parent_type(Type) of
undefined -> format(#{type => realm, realm => Realm, path => []});
ParentType -> format(#{type => ParentType, realm => Realm, path => []})
end;
ShorterPath ->
case parent_type(Type) of
undefined -> format(#{type => Type, realm => Realm, path => ShorterPath});
ParentType -> format(#{type => ParentType, realm => Realm, path => ShorterPath})
end
end.
%% @doc Get the parent type for a given type.
-spec parent_type(mri_type()) -> mri_type() | undefined.
parent_type(realm) -> undefined;
parent_type(org) -> realm;
parent_type(user) -> org;
parent_type(app) -> org;
parent_type(service) -> app;
parent_type(artifact) -> org;
parent_type(license) -> app;
parent_type(cert) -> org;
parent_type(key) -> org;
parent_type(topic) -> org;
parent_type(proc) -> org;
parent_type(content) -> org;
parent_type(device) -> org;
parent_type(cluster) -> org;
parent_type(location) -> org;
parent_type(zone) -> org;
parent_type(network) -> org;
parent_type(model) -> org;
parent_type(dataset) -> org;
parent_type(config) -> org;
parent_type(class) -> undefined; %% Classes have their own hierarchy
parent_type(taxonomy) -> org;
parent_type({custom, _}) -> undefined;
parent_type(_) -> undefined.
%% @doc Get all ancestors of an MRI, from immediate parent to root.
-spec ancestors(mri() | mri_map()) -> [mri()].
ancestors(MRI) ->
ancestors(MRI, []).
ancestors(MRI, Acc) ->
case parent(MRI) of
undefined -> lists:reverse(Acc);
Parent -> ancestors(Parent, [Parent | Acc])
end.
%% @doc Check if PotentialAncestor is an ancestor of MRI.
-spec is_ancestor(mri(), mri()) -> boolean().
is_ancestor(PotentialAncestor, MRI) when is_binary(PotentialAncestor), is_binary(MRI) ->
lists:member(PotentialAncestor, ancestors(MRI)).
%% @doc Get the depth of an MRI (0 for realm, 1 for org, etc).
-spec depth(mri() | mri_map()) -> non_neg_integer().
depth(#{path := Path}) ->
length(Path);
depth(MRI) when is_binary(MRI) ->
{ok, Parsed} = parse(MRI),
depth(Parsed).
%%===================================================================
%% Construction
%%===================================================================
%% @doc Create a new MRI from a map.
-spec new(map()) -> {ok, mri()} | {error, term()}.
new(#{type := Type, realm := Realm} = Opts) ->
Path = maps:get(path, Opts, []),
MRI = format(#{type => Type, realm => Realm, path => Path}),
case validate(MRI) of
ok -> {ok, MRI};
{error, _} = Err -> Err
end.
%% @doc Create a new MRI from type, realm, and path.
-spec new(mri_type(), realm(), [path_segment()]) -> {ok, mri()} | {error, term()}.
new(Type, Realm, Path) ->
new(#{type => Type, realm => Realm, path => Path}).
%% @doc Create a realm MRI.
-spec new_realm(realm()) -> mri().
new_realm(Realm) ->
format(#{type => realm, realm => Realm, path => []}).
%% @doc Create an org MRI.
-spec new_org(realm(), binary()) -> mri().
new_org(Realm, Org) ->
format(#{type => org, realm => Realm, path => [Org]}).
%% @doc Create a user MRI.
-spec new_user(realm(), binary(), binary()) -> mri().
new_user(Realm, Org, User) ->
format(#{type => user, realm => Realm, path => [Org, User]}).
%% @doc Create an app MRI.
-spec new_app(realm(), binary(), binary()) -> mri().
new_app(Realm, Org, App) ->
format(#{type => app, realm => Realm, path => [Org, App]}).
%% @doc Create a service MRI.
-spec new_service(realm(), binary(), binary(), binary()) -> mri().
new_service(Realm, Org, App, Service) ->
format(#{type => service, realm => Realm, path => [Org, App, Service]}).
%% @doc Create an instance MRI (running installation of an artifact).
%% Instance path: {org}/{device}/{instance_name}
%% Example: mri:instance:io.macula/acme/edge-01/counter
-spec new_instance(realm(), binary(), binary(), binary()) -> mri().
new_instance(Realm, Org, Device, InstanceName) ->
format(#{type => instance, realm => Realm, path => [Org, Device, InstanceName]}).
%% @doc Create an instance MRI with qualifier (environment or replica index).
%% Example: mri:instance:io.macula/acme/edge-01/counter.prod
-spec new_instance_qualified(realm(), binary(), binary(), binary(), binary()) -> mri().
new_instance_qualified(Realm, Org, Device, ArtifactName, Qualifier) ->
InstanceName = <<ArtifactName/binary, ".", Qualifier/binary>>,
format(#{type => instance, realm => Realm, path => [Org, Device, InstanceName]}).
%%===================================================================
%% Path Manipulation
%%===================================================================
%% @doc Append a segment to an MRI's path.
-spec append_segment(mri() | mri_map(), path_segment()) -> mri().
append_segment(#{type := Type, realm := Realm, path := Path}, Segment) ->
format(#{type => Type, realm => Realm, path => Path ++ [Segment]});
append_segment(MRI, Segment) when is_binary(MRI) ->
{ok, Parsed} = parse(MRI),
append_segment(Parsed, Segment).
%% @doc Join path segments with slashes.
-spec join_path([path_segment()]) -> binary().
join_path([]) ->
<<>>;
join_path([Segment]) ->
Segment;
join_path(Segments) ->
lists:foldl(
fun(Seg, <<>>) -> Seg;
(Seg, Acc) -> <<Acc/binary, "/", Seg/binary>>
end,
<<>>,
Segments
).
%% @doc Split a path string into segments.
-spec split_path(binary()) -> [path_segment()].
split_path(<<>>) ->
[];
split_path(PathBin) ->
binary:split(PathBin, <<"/">>, [global]).
%%===================================================================
%% DHT Topic/Procedure Derivation (for instances)
%%===================================================================
%% @doc Derive a DHT topic from an instance MRI and a declared topic name.
%%
%% Takes an instance MRI and a declared topic name, returns the full DHT topic.
%% The instance path becomes a prefix: realm/org/device/instance.declared_topic
%%
%% Example: InstanceMRI = "mri:instance:io.macula/acme/edge-01/counter.prod"
%% DeclaredTopic = "orders.created"
%% Result = "io.macula/acme/edge-01/counter.prod.orders.created"
%% @end
-spec derive_topic(mri(), binary()) -> binary().
derive_topic(InstanceMRI, DeclaredTopic) ->
InstancePath = to_topic_prefix(InstanceMRI),
<<InstancePath/binary, ".", DeclaredTopic/binary>>.
%% @doc Derive a DHT procedure from an instance MRI and a declared procedure name.
%%
%% Takes an instance MRI and a declared procedure name, returns the full DHT procedure.
%% The instance path becomes a prefix: realm/org/device/instance.declared_procedure
%%
%% Example: InstanceMRI = "mri:instance:io.macula/acme/edge-01/counter.prod"
%% DeclaredProcedure = "place_order"
%% Result = "io.macula/acme/edge-01/counter.prod.place_order"
%% @end
-spec derive_procedure(mri(), binary()) -> binary().
derive_procedure(InstanceMRI, DeclaredProcedure) ->
InstancePath = to_topic_prefix(InstanceMRI),
<<InstancePath/binary, ".", DeclaredProcedure/binary>>.
%% @doc Convert an MRI to a topic/procedure prefix (realm/path segments).
%%
%% Takes any MRI and extracts the realm and path as a topic-compatible prefix.
%% Format: realm/path_segment1/path_segment2/...
%%
%% Example: MRI = "mri:instance:io.macula/acme/edge-01/counter.prod"
%% Result = "io.macula/acme/edge-01/counter.prod"
%% @end
-spec to_topic_prefix(mri()) -> binary().
to_topic_prefix(MRI) ->
{ok, #{realm := Realm, path := Path}} = parse(MRI),
PathBin = join_path(Path),
<<Realm/binary, "/", PathBin/binary>>.
%%===================================================================
%% Internal Functions
%%===================================================================
parse_type_and_path(TypeBin, RealmAndPath) ->
case binary:split(RealmAndPath, <<"/">>) of
[Realm] ->
%% No path (e.g., mri:realm:io.macula)
parse_with_type(TypeBin, Realm, []);
[Realm, PathBin] ->
Path = split_path(PathBin),
parse_with_type(TypeBin, Realm, Path)
end.
parse_with_type(TypeBin, Realm, Path) ->
case binary_to_type(TypeBin) of
{ok, Type} ->
{ok, #{type => Type, realm => Realm, path => Path}};
{error, _} = Err ->
Err
end.
binary_to_type(<<"realm">>) -> {ok, realm};
binary_to_type(<<"org">>) -> {ok, org};
binary_to_type(<<"user">>) -> {ok, user};
binary_to_type(<<"app">>) -> {ok, app};
binary_to_type(<<"service">>) -> {ok, service};
binary_to_type(<<"artifact">>) -> {ok, artifact};
binary_to_type(<<"instance">>) -> {ok, instance};
binary_to_type(<<"license">>) -> {ok, license};
binary_to_type(<<"cert">>) -> {ok, cert};
binary_to_type(<<"key">>) -> {ok, key};
binary_to_type(<<"topic">>) -> {ok, topic};
binary_to_type(<<"proc">>) -> {ok, proc};
binary_to_type(<<"content">>) -> {ok, content};
binary_to_type(<<"device">>) -> {ok, device};
binary_to_type(<<"cluster">>) -> {ok, cluster};
binary_to_type(<<"location">>) -> {ok, location};
binary_to_type(<<"zone">>) -> {ok, zone};
binary_to_type(<<"network">>) -> {ok, network};
binary_to_type(<<"model">>) -> {ok, model};
binary_to_type(<<"dataset">>) -> {ok, dataset};
binary_to_type(<<"config">>) -> {ok, config};
binary_to_type(<<"class">>) -> {ok, class};
binary_to_type(<<"taxonomy">>) -> {ok, taxonomy};
binary_to_type(TypeBin) ->
%% Check if it's a registered custom type
case macula_mri_registry:is_valid_type(TypeBin) of
true -> {ok, {custom, TypeBin}};
false -> {error, {unknown_type, TypeBin}}
end.
type_to_binary(realm) -> <<"realm">>;
type_to_binary(org) -> <<"org">>;
type_to_binary(user) -> <<"user">>;
type_to_binary(app) -> <<"app">>;
type_to_binary(service) -> <<"service">>;
type_to_binary(artifact) -> <<"artifact">>;
type_to_binary(instance) -> <<"instance">>;
type_to_binary(license) -> <<"license">>;
type_to_binary(cert) -> <<"cert">>;
type_to_binary(key) -> <<"key">>;
type_to_binary(topic) -> <<"topic">>;
type_to_binary(proc) -> <<"proc">>;
type_to_binary(content) -> <<"content">>;
type_to_binary(device) -> <<"device">>;
type_to_binary(cluster) -> <<"cluster">>;
type_to_binary(location) -> <<"location">>;
type_to_binary(zone) -> <<"zone">>;
type_to_binary(network) -> <<"network">>;
type_to_binary(model) -> <<"model">>;
type_to_binary(dataset) -> <<"dataset">>;
type_to_binary(config) -> <<"config">>;
type_to_binary(class) -> <<"class">>;
type_to_binary(taxonomy) -> <<"taxonomy">>;
type_to_binary({custom, Bin}) -> Bin;
type_to_binary(Atom) when is_atom(Atom) -> atom_to_binary(Atom, utf8).
validate_parsed(#{type := Type, realm := Realm, path := Path}) ->
validate_type(Type, Realm, Path).
validate_type(Type, Realm, Path) ->
case macula_mri_registry:is_valid_type(Type) of
true -> validate_realm(Realm, Path);
false -> {error, {invalid_type, Type}}
end.
validate_realm(Realm, Path) when is_binary(Realm), byte_size(Realm) > 0 ->
validate_realm_format(Realm, Path);
validate_realm(_, _) ->
{error, empty_realm}.
validate_realm_format(Realm, Path) ->
%% Realm should be reverse-domain notation (e.g., io.macula)
case is_valid_realm_format(Realm) of
true -> validate_path(Path);
false -> {error, {invalid_realm_format, Realm}}
end.
is_valid_realm_format(Realm) ->
%% Use NIF when available (avoids binary_to_list conversion)
case nif_available() of
true -> macula_mri_nif:validate_realm_format(Realm);
false ->
case binary:match(Realm, <<".">>) of
nomatch -> false;
_ ->
lists:all(
fun(C) ->
(C >= $a andalso C =< $z) orelse
(C >= $0 andalso C =< $9) orelse
C =:= $.
end,
binary_to_list(Realm)
)
end
end.
validate_path(Path) when length(Path) > ?MAX_PATH_DEPTH ->
{error, {path_too_deep, length(Path)}};
validate_path(Path) ->
validate_path_segments(Path).
validate_path_segments([]) ->
ok;
validate_path_segments([Segment | Rest]) ->
case validate_segment(Segment) of
ok -> validate_path_segments(Rest);
{error, _} = Err -> Err
end.
validate_segment(Segment) when byte_size(Segment) > ?MAX_SEGMENT_LENGTH ->
{error, {segment_too_long, Segment}};
validate_segment(Segment) when byte_size(Segment) =:= 0 ->
{error, empty_segment};
validate_segment(Segment) ->
%% Lowercase alphanumeric, hyphen, underscore
%% No leading/trailing hyphen
case is_valid_segment_format(Segment) of
true -> ok;
false -> {error, {invalid_segment_format, Segment}}
end.
is_valid_segment_format(<<$-, _/binary>>) ->
false; %% No leading hyphen
is_valid_segment_format(Segment) ->
case binary:last(Segment) of
$- -> false; %% No trailing hyphen
_ -> validate_segment_chars(Segment)
end.
validate_segment_chars(Segment) ->
case nif_available() of
true -> macula_mri_nif:validate_segment_chars(Segment);
false ->
lists:all(
fun(C) ->
(C >= $a andalso C =< $z) orelse
(C >= $0 andalso C =< $9) orelse
C =:= $- orelse
C =:= $_
end,
binary_to_list(Segment)
)
end.
drop_last([]) -> [];
drop_last([_]) -> [];
drop_last(List) -> lists:droplast(List).
%% @private Check if the MRI NIF module is available.
nif_available() ->
case code:ensure_loaded(macula_mri_nif) of
{module, macula_mri_nif} -> macula_mri_nif:is_nif_loaded();
_ -> false
end.
%%===================================================================
%% Trie Index Operations (NIF-accelerated)
%%===================================================================
%% @doc Build a trie index from a list of MRI tuples.
%% Input: list of `{Realm, PathSegments, FullMRI}' tuples.
%% Returns an opaque index reference (NIF) or map (Erlang fallback).
%% Enables O(d) hierarchy queries instead of O(n) table scans.
-spec build_index([{realm(), [path_segment()], mri()}]) -> {ok, term()}.
build_index(MRIs) ->
macula_mri_nif:build_path_index(MRIs).
%% @doc Find direct children of a parent using a trie index.
%% O(d) complexity where d is path depth.
-spec index_children(term(), realm(), [path_segment()]) ->
{ok, [mri()]} | {error, term()}.
index_children(Index, Realm, Path) ->
macula_mri_nif:index_find_children(Index, Realm, Path).
%% @doc Find all descendants of a parent using a trie index.
%% O(d+m) complexity where d is path depth and m is descendant count.
-spec index_descendants(term(), realm(), [path_segment()]) ->
{ok, [mri()]} | {error, term()}.
index_descendants(Index, Realm, Path) ->
macula_mri_nif:index_find_descendants(Index, Realm, Path).
%% @doc Insert a single MRI into an existing trie index.
-spec index_insert(term(), realm(), [path_segment()], mri()) -> ok | {error, term()}.
index_insert(Index, Realm, Path, MRI) ->
macula_mri_nif:index_insert(Index, Realm, Path, MRI).
%% @doc Remove a single MRI from an existing trie index.
-spec index_remove(term(), realm(), [path_segment()]) -> ok | {error, term()}.
index_remove(Index, Realm, Path) ->
macula_mri_nif:index_remove(Index, Realm, Path).
%% @doc Get the number of MRIs in a trie index.
-spec index_size(term()) -> {ok, non_neg_integer()}.
index_size(Index) ->
macula_mri_nif:index_size(Index).