Packages

macula

0.20.22
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_utils.erl
Raw

src/macula_utils.erl

%%%-------------------------------------------------------------------
%%% @doc
%%% Common utility functions for Macula.
%%%
%%% This module contains pure utility functions used across the
%%% Macula codebase to improve testability and eliminate duplication.
%%%
%%% All functions in this module are pure (no side effects) and
%%% can be tested independently.
%%% @end
%%%-------------------------------------------------------------------
-module(macula_utils).
%% API
-export([
parse_url/1,
parse_host_port/2,
generate_node_id/0,
next_message_id/1,
ensure_binary/1,
normalize_provider/1,
topic_matches/5,
encode_json/1,
decode_json/1
]).
%%%===================================================================
%%% URL Parsing
%%%===================================================================
%% @doc Parse URL to extract host and port.
-spec parse_url(binary()) -> {string(), inet:port_number()}.
parse_url(Url) when is_binary(Url) ->
parse_url(binary_to_list(Url));
parse_url("https://" ++ Rest) ->
parse_host_port(Rest, 443);
parse_url("http://" ++ Rest) ->
parse_host_port(Rest, 80);
parse_url(Rest) ->
parse_host_port(Rest, 443).
%% @doc Parse host:port string with default port.
-spec parse_host_port(string(), inet:port_number()) -> {string(), inet:port_number()}.
parse_host_port(HostPort, DefaultPort) ->
parse_host_port_parts(string:split(HostPort, ":"), DefaultPort).
parse_host_port_parts([Host], DefaultPort) ->
{Host, DefaultPort};
parse_host_port_parts([Host, PortStr], _DefaultPort) ->
Port = list_to_integer(string:trim(PortStr, trailing, "/")),
{Host, Port}.
%%%===================================================================
%%% ID Generation
%%%===================================================================
%% @doc Generate a random node ID.
-spec generate_node_id() -> binary().
generate_node_id() ->
%% 32-byte random ID
crypto:strong_rand_bytes(32).
%% @doc Get next message ID from counter.
%% Returns {MessageId, NewCounter}.
-spec next_message_id(non_neg_integer()) -> {binary(), non_neg_integer()}.
next_message_id(Counter) ->
NewCounter = Counter + 1,
%% 16-byte message ID from counter
MsgId = <<NewCounter:128>>,
{MsgId, NewCounter}.
%%%===================================================================
%%% Type Conversion
%%%===================================================================
%% @doc Ensure value is binary.
-spec ensure_binary(binary() | list() | atom()) -> binary().
ensure_binary(B) when is_binary(B) -> B;
ensure_binary(L) when is_list(L) -> list_to_binary(L);
ensure_binary(A) when is_atom(A) -> atom_to_binary(A).
%%%===================================================================
%%% Provider Normalization
%%%===================================================================
%% @doc Normalize provider map from binary keys to atom keys.
-spec normalize_provider(map()) -> map().
normalize_provider(Provider) when is_map(Provider) ->
#{
node_id => maps:get(<<"node_id">>, Provider),
endpoint => maps:get(<<"endpoint">>, Provider),
metadata => maps:get(<<"metadata">>, Provider, #{}),
ttl => maps:get(<<"ttl">>, Provider, 300)
}.
%%%===================================================================
%%% Topic Matching
%%%===================================================================
%% @doc Check if a published topic matches a subscription topic pattern.
%% Supports configurable wildcards (defaults: dot-separated with * and **):
%% - WildcardSingle (e.g., '*') matches a single segment
%% - WildcardMulti (e.g., '**') matches multiple segments
%% - Separator (e.g., '.') splits topic into segments
%% - exact match otherwise
-spec topic_matches(binary() | list(), binary() | list(), binary(), binary(), binary()) -> boolean().
topic_matches(Pattern, Topic, Separator, WildcardSingle, WildcardMulti) when is_binary(Pattern), is_binary(Topic) ->
case {binary:match(Pattern, WildcardSingle), binary:match(Pattern, WildcardMulti)} of
{nomatch, nomatch} ->
%% No wildcards - exact match
Pattern =:= Topic;
_ ->
%% Has wildcards - do pattern matching
PatternParts = binary:split(Pattern, Separator, [global]),
TopicParts = binary:split(Topic, Separator, [global]),
match_parts(PatternParts, TopicParts, WildcardSingle, WildcardMulti)
end;
topic_matches(Pattern, Topic, Separator, WildcardSingle, WildcardMulti) when is_list(Pattern) ->
topic_matches(list_to_binary(Pattern), Topic, Separator, WildcardSingle, WildcardMulti);
topic_matches(Pattern, Topic, Separator, WildcardSingle, WildcardMulti) when is_list(Topic) ->
topic_matches(Pattern, list_to_binary(Topic), Separator, WildcardSingle, WildcardMulti).
%% @doc Match topic parts against pattern parts.
-spec match_parts(list(binary()), list(binary()), binary(), binary()) -> boolean().
match_parts([], [], _WildcardSingle, _WildcardMulti) ->
true;
match_parts([WildcardMulti], _TopicParts, _WildcardSingle, WildcardMulti) ->
%% Multi-level wildcard at end matches everything remaining
true;
match_parts([WildcardMulti | PatternRest], TopicParts, WildcardSingle, WildcardMulti) ->
%% Multi-level wildcard in middle - try matching rest of pattern at each position
try_multi_wildcard_match(PatternRest, TopicParts, WildcardSingle, WildcardMulti);
match_parts([WildcardSingle | PatternRest], [_TopicPart | TopicRest], WildcardSingle, WildcardMulti) ->
%% Single-level wildcard matches one segment
match_parts(PatternRest, TopicRest, WildcardSingle, WildcardMulti);
match_parts([PatternPart | PatternRest], [PatternPart | TopicRest], WildcardSingle, WildcardMulti) ->
%% Exact match - continue matching
match_parts(PatternRest, TopicRest, WildcardSingle, WildcardMulti);
match_parts(_Pattern, _Topic, _WildcardSingle, _WildcardMulti) ->
%% Length mismatch
false.
%% @private
%% @doc Try matching pattern against topic at each possible position (for ** wildcard).
-spec try_multi_wildcard_match(list(binary()), list(binary()), binary(), binary()) -> boolean().
try_multi_wildcard_match(Pattern, [], WildcardSingle, WildcardMulti) ->
%% No more topic parts - check if pattern can match empty
match_parts(Pattern, [], WildcardSingle, WildcardMulti);
try_multi_wildcard_match(Pattern, [_TopicPart | TopicRest] = Topic, WildcardSingle, WildcardMulti) ->
%% Try matching at current position or skip topic part and try again
match_parts(Pattern, Topic, WildcardSingle, WildcardMulti) orelse
try_multi_wildcard_match(Pattern, TopicRest, WildcardSingle, WildcardMulti).
%%%===================================================================
%%% JSON Encoding/Decoding
%%%===================================================================
%% @doc Encode map/list to JSON binary.
-spec encode_json(map() | list()) -> binary().
encode_json(Data) ->
iolist_to_binary(json:encode(Data)).
%% @doc Decode JSON binary to map/list.
-spec decode_json(binary()) -> map() | list().
decode_json(Binary) ->
json:decode(Binary).