Packages

macula

1.4.23
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
macula src macula_topic.erl
Raw

src/macula_topic.erl

%%%-------------------------------------------------------------------
%%% @doc Mesh topic construction and validation.
%%%
%%% Every mesh topic follows a strict 5-segment structure:
%%%
%%% {realm}/{org}/{app}/{domain}/{name}_v{N}
%%%
%%% Facts (past tense) are published via pub/sub.
%%% Hopes (present tense) are advertised via RPC.
%%%
%%% The verb tense determines intent:
%%% lobby_opened_v1 → fact (something happened)
%%% join_game_v1 → hope (we want something to happen)
%%%
%%% System topics (_mesh.node.up) are exempt from this structure.
%%% @end
%%%-------------------------------------------------------------------
-module(macula_topic).
-export([
fact/5,
hope/5,
build/5,
parse/1,
validate/1,
is_system_topic/1
]).
-type realm() :: binary().
-type app_id() :: binary(). %% "org/app" format
-type domain() :: binary().
-type name() :: binary().
-type version() :: pos_integer().
-export_type([realm/0, app_id/0, domain/0, name/0, version/0]).
%%%===================================================================
%%% API
%%%===================================================================
%% @doc Build a fact topic (pub/sub — something that happened).
%% Name should be past tense: subject_past_verb.
-spec fact(realm(), app_id(), domain(), name(), version()) -> binary().
fact(Realm, AppId, Domain, Name, Version) ->
build(Realm, AppId, Domain, Name, Version).
%% @doc Build a hope topic (RPC — something we want to happen).
%% Name should be present tense: verb_subject.
-spec hope(realm(), app_id(), domain(), name(), version()) -> binary().
hope(Realm, AppId, Domain, Name, Version) ->
build(Realm, AppId, Domain, Name, Version).
%% @doc Build a topic from parts.
%% AppId must be "org/app" format (exactly one slash).
-spec build(realm(), app_id(), domain(), name(), version()) -> binary().
build(Realm, AppId, Domain, Name, Version)
when is_binary(Realm), is_binary(AppId),
is_binary(Domain), is_binary(Name),
is_integer(Version), Version > 0 ->
validate_segment(realm, Realm),
validate_app_id(AppId),
validate_segment(domain, Domain),
validate_segment(name, Name),
Vsn = integer_to_binary(Version),
<<Realm/binary, "/", AppId/binary, "/",
Domain/binary, "/", Name/binary, "_v", Vsn/binary>>.
%% @doc Parse a topic into its constituent parts.
-spec parse(binary()) -> {ok, map()} | {error, term()}.
parse(Topic) when is_binary(Topic) ->
case binary:split(Topic, <<"/">>, [global]) of
[Realm, Org, App, Domain, NameVsn] ->
parse_name_version(Realm, Org, App, Domain, NameVsn);
_ ->
{error, {invalid_structure, Topic}}
end.
%% @doc Validate a topic string.
-spec validate(binary()) -> ok | {error, term()}.
validate(Topic) ->
case is_system_topic(Topic) of
true -> ok;
false ->
case parse(Topic) of
{ok, _} -> ok;
{error, _} = Err -> Err
end
end.
%% @doc Check if a topic is a system topic (underscore prefix).
-spec is_system_topic(binary()) -> boolean().
is_system_topic(<<"_", _/binary>>) -> true;
is_system_topic(_) -> false.
%%%===================================================================
%%% Internal
%%%===================================================================
parse_name_version(Realm, Org, App, Domain, NameVsn) ->
case parse_version_suffix(NameVsn) of
{ok, Name, Version} ->
{ok, #{
realm => Realm,
org => Org,
app => App,
app_id => <<Org/binary, "/", App/binary>>,
domain => Domain,
name => Name,
version => Version,
topic => <<Realm/binary, "/", Org/binary, "/", App/binary, "/",
Domain/binary, "/", NameVsn/binary>>
}};
{error, _} = Err ->
Err
end.
parse_version_suffix(NameVsn) ->
case re:run(NameVsn, <<"^(.+)_v([0-9]+)$">>, [{capture, [1, 2], binary}]) of
{match, [Name, VsnBin]} ->
{ok, Name, binary_to_integer(VsnBin)};
nomatch ->
{error, {missing_version_suffix, NameVsn}}
end.
validate_app_id(AppId) ->
case binary:split(AppId, <<"/">>) of
[Org, App] when byte_size(Org) > 0, byte_size(App) > 0 ->
validate_segment(org, Org),
validate_segment(app, App),
ok;
_ ->
error({invalid_app_id, AppId})
end.
validate_segment(Label, Segment) ->
case re:run(Segment, <<"^[a-z0-9][a-z0-9._-]*$">>) of
{match, _} -> ok;
nomatch -> error({invalid_segment, Label, Segment})
end.