Packages
macula
0.20.21
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_registry_system/macula_registry_manifest.erl
%%%-------------------------------------------------------------------
%%% @doc Macula Registry Package Manifest
%%%
%%% Handles parsing and validation of package manifests:
%%% - Manifest structure validation
%%% - Capability specification parsing
%%% - SemVer version validation
%%% - Dependency resolution
%%%
%%% All functions are stateless.
%%% @end
%%%-------------------------------------------------------------------
-module(macula_registry_manifest).
%% API
-export([parse/1, validate/1]).
-export([get_name/1, get_version/1, get_capabilities/1, get_dependencies/1]).
-export([validate_version/1, compare_versions/2]).
-export([capability_matches/2]).
-export([to_binary/1, from_binary/1]).
%% Types
-export_type([manifest/0, capability/0, dependency/0]).
-type manifest() :: #{
name := binary(),
version := binary(),
otp_release => binary(),
macula_version => binary(),
capabilities := [capability()],
dependencies := [dependency()],
entry_module := atom(),
supervisor := atom(),
description => binary(),
license => binary(),
authors => [binary()]
}.
-type capability() ::
{network, [{connect, binary()}]} |
{pubsub, [{publish | subscribe, binary()}]} |
{rpc, [{register | call, binary()}]} |
{nifs, [binary()]} |
{file_access, [{read | write, binary()}]}.
-type dependency() :: #{
name := binary(),
version := binary(),
registry => binary()
}.
%%%===================================================================
%%% API functions
%%%===================================================================
%% @doc Parse a manifest from a map (with atom or binary keys)
-spec parse(map()) -> {ok, manifest()} | {error, term()}.
parse(Map) when is_map(Map) ->
try
Manifest = #{
name => get_required(Map, name, <<"name">>),
version => get_required(Map, version, <<"version">>),
capabilities => parse_capabilities(get_optional(Map, capabilities, <<"capabilities">>, [])),
dependencies => parse_dependencies(get_optional(Map, dependencies, <<"dependencies">>, [])),
entry_module => get_atom(Map, entry_module, <<"entry_module">>, undefined),
supervisor => get_atom(Map, supervisor, <<"supervisor">>, undefined)
},
%% Add optional fields
Manifest2 = add_optional(Manifest, Map, otp_release, <<"otp_release">>),
Manifest3 = add_optional(Manifest2, Map, macula_version, <<"macula_version">>),
Manifest4 = add_optional(Manifest3, Map, description, <<"description">>),
Manifest5 = add_optional(Manifest4, Map, license, <<"license">>),
Manifest6 = add_optional(Manifest5, Map, authors, <<"authors">>),
{ok, Manifest6}
catch
throw:{missing_field, Field} ->
{error, {missing_required_field, Field}};
throw:{invalid_field, Field, Reason} ->
{error, {invalid_field, Field, Reason}}
end;
parse(_) ->
{error, invalid_manifest_format}.
%% @doc Validate a parsed manifest
-spec validate(manifest()) -> ok | {error, term()}.
validate(Manifest) ->
Validations = [
fun validate_name/1,
fun validate_manifest_version/1,
fun validate_capabilities/1,
fun validate_deps/1
],
run_validations(Validations, Manifest).
%% @doc Get package name from manifest
-spec get_name(manifest()) -> binary().
get_name(#{name := Name}) -> Name.
%% @doc Get package version from manifest
-spec get_version(manifest()) -> binary().
get_version(#{version := Version}) -> Version.
%% @doc Get capabilities from manifest
-spec get_capabilities(manifest()) -> [capability()].
get_capabilities(#{capabilities := Caps}) -> Caps.
%% @doc Get dependencies from manifest
-spec get_dependencies(manifest()) -> [dependency()].
get_dependencies(#{dependencies := Deps}) -> Deps.
%% @doc Validate SemVer version string
-spec validate_version(binary()) -> ok | {error, invalid_version}.
validate_version(Version) when is_binary(Version) ->
case parse_semver(Version) of
{ok, _, _, _} -> ok;
error -> {error, invalid_version}
end;
validate_version(_) ->
{error, invalid_version}.
%% @doc Compare two SemVer versions
%% Returns: lt (less than), eq (equal), gt (greater than)
-spec compare_versions(binary(), binary()) -> lt | eq | gt | {error, term()}.
compare_versions(V1, V2) ->
case {parse_semver(V1), parse_semver(V2)} of
{{ok, Major1, Minor1, Patch1}, {ok, Major2, Minor2, Patch2}} ->
compare_tuples({Major1, Minor1, Patch1}, {Major2, Minor2, Patch2});
_ ->
{error, invalid_version}
end.
%% @doc Check if a capability pattern matches a specific capability
%% Pattern can include wildcards: * matches any single segment, ** matches any segments
-spec capability_matches(capability(), capability()) -> boolean().
capability_matches({Type, PatternSpecs}, {Type, ActualSpecs}) ->
lists:all(fun(PatternSpec) ->
lists:any(fun(ActualSpec) ->
spec_matches(PatternSpec, ActualSpec)
end, ActualSpecs)
end, PatternSpecs);
capability_matches(_, _) ->
false.
%% @doc Serialize manifest to binary (MessagePack)
-spec to_binary(manifest()) -> binary().
to_binary(Manifest) ->
%% Convert atoms to binaries for serialization
SerializableManifest = make_serializable(Manifest),
msgpack:pack(SerializableManifest, [{map_format, map}]).
%% @doc Deserialize manifest from binary (MessagePack)
-spec from_binary(binary()) -> {ok, manifest()} | {error, term()}.
from_binary(Binary) when is_binary(Binary) ->
case msgpack:unpack(Binary, [{map_format, map}]) of
{ok, Map} -> parse(Map);
{error, Reason} -> {error, {decode_error, Reason}}
end;
from_binary(_) ->
{error, invalid_format}.
%%%===================================================================
%%% Internal functions
%%%===================================================================
%% @private Get required field from map with atom or binary key
get_required(Map, AtomKey, BinKey) ->
case maps:get(AtomKey, Map, undefined) of
undefined ->
case maps:get(BinKey, Map, undefined) of
undefined -> throw({missing_field, AtomKey});
Value -> Value
end;
Value -> Value
end.
%% @private Get optional field from map with atom or binary key
get_optional(Map, AtomKey, BinKey, Default) ->
case maps:get(AtomKey, Map, undefined) of
undefined -> maps:get(BinKey, Map, Default);
Value -> Value
end.
%% @private Get atom from map, converting binary if needed
get_atom(Map, AtomKey, BinKey, Default) ->
case get_optional(Map, AtomKey, BinKey, Default) of
undefined -> undefined;
Value when is_atom(Value) -> Value;
Value when is_binary(Value) -> binary_to_atom(Value, utf8);
_ -> throw({invalid_field, AtomKey, expected_atom})
end.
%% @private Add optional field if present
add_optional(Manifest, Map, AtomKey, BinKey) ->
case get_optional(Map, AtomKey, BinKey, undefined) of
undefined -> Manifest;
Value -> maps:put(AtomKey, Value, Manifest)
end.
%% @private Parse capability list
parse_capabilities(Caps) when is_list(Caps) ->
lists:map(fun parse_capability/1, Caps);
parse_capabilities(_) ->
[].
%% @private Parse single capability
parse_capability({Type, Specs}) when is_atom(Type), is_list(Specs) ->
{Type, Specs};
parse_capability(#{<<"type">> := TypeBin} = Map) ->
Type = binary_to_atom(TypeBin, utf8),
Specs = maps:get(<<"specs">>, Map, []),
{Type, parse_cap_specs(Type, Specs)};
parse_capability(Map) when is_map(Map) ->
%% Try atom keys
case maps:get(type, Map, undefined) of
undefined -> throw({invalid_field, capability, missing_type});
Type ->
Specs = maps:get(specs, Map, []),
{Type, parse_cap_specs(Type, Specs)}
end;
parse_capability(_) ->
throw({invalid_field, capability, invalid_format}).
%% @private Parse capability specs based on type
parse_cap_specs(network, Specs) -> parse_network_specs(Specs);
parse_cap_specs(pubsub, Specs) -> parse_pubsub_specs(Specs);
parse_cap_specs(rpc, Specs) -> parse_rpc_specs(Specs);
parse_cap_specs(nifs, Specs) when is_list(Specs) -> Specs;
parse_cap_specs(file_access, Specs) -> parse_file_specs(Specs);
parse_cap_specs(_, Specs) -> Specs.
parse_network_specs(Specs) ->
lists:map(fun
({connect, Pattern}) -> {connect, Pattern};
(#{<<"connect">> := Pattern}) -> {connect, Pattern};
(#{connect := Pattern}) -> {connect, Pattern}
end, Specs).
parse_pubsub_specs(Specs) ->
lists:map(fun
({Op, Pattern}) when Op =:= publish; Op =:= subscribe -> {Op, Pattern};
(#{<<"publish">> := Pattern}) -> {publish, Pattern};
(#{<<"subscribe">> := Pattern}) -> {subscribe, Pattern};
(#{publish := Pattern}) -> {publish, Pattern};
(#{subscribe := Pattern}) -> {subscribe, Pattern}
end, Specs).
parse_rpc_specs(Specs) ->
lists:map(fun
({Op, Pattern}) when Op =:= register; Op =:= call -> {Op, Pattern};
(#{<<"register">> := Pattern}) -> {register, Pattern};
(#{<<"call">> := Pattern}) -> {call, Pattern};
(#{register := Pattern}) -> {register, Pattern};
(#{call := Pattern}) -> {call, Pattern}
end, Specs).
parse_file_specs(Specs) ->
lists:map(fun
({Op, Pattern}) when Op =:= read; Op =:= write -> {Op, Pattern};
(#{<<"read">> := Pattern}) -> {read, Pattern};
(#{<<"write">> := Pattern}) -> {write, Pattern};
(#{read := Pattern}) -> {read, Pattern};
(#{write := Pattern}) -> {write, Pattern}
end, Specs).
%% @private Parse dependency list
parse_dependencies(Deps) when is_list(Deps) ->
lists:map(fun parse_dependency/1, Deps);
parse_dependencies(_) ->
[].
%% @private Parse single dependency
parse_dependency(#{name := Name, version := Version} = Dep) ->
#{name => Name, version => Version, registry => maps:get(registry, Dep, undefined)};
parse_dependency(#{<<"name">> := Name, <<"version">> := Version} = Dep) ->
#{name => Name, version => Version, registry => maps:get(<<"registry">>, Dep, undefined)};
parse_dependency(_) ->
throw({invalid_field, dependency, invalid_format}).
%% @private Validate package name
validate_name(#{name := Name}) when is_binary(Name), byte_size(Name) > 0 ->
%% Name should be lowercase alphanumeric with underscores
case re:run(Name, "^[a-z][a-z0-9_]*$", [{capture, none}]) of
match -> ok;
nomatch -> {error, {invalid_name, Name}}
end;
validate_name(_) ->
{error, {invalid_name, missing}}.
%% @private Validate manifest version field
validate_manifest_version(#{version := Version}) ->
validate_version(Version);
validate_manifest_version(_) ->
{error, {invalid_version, missing}}.
%% @private Validate capabilities format
validate_capabilities(#{capabilities := Caps}) when is_list(Caps) ->
case lists:all(fun is_valid_capability/1, Caps) of
true -> ok;
false -> {error, invalid_capabilities}
end;
validate_capabilities(_) ->
ok.
%% @private Check if capability is valid
is_valid_capability({Type, Specs}) when is_atom(Type), is_list(Specs) -> true;
is_valid_capability(_) -> false.
%% @private Validate dependencies
validate_deps(#{dependencies := Deps}) when is_list(Deps) ->
case lists:all(fun is_valid_dependency/1, Deps) of
true -> ok;
false -> {error, invalid_dependencies}
end;
validate_deps(_) ->
ok.
%% @private Check if dependency is valid
is_valid_dependency(#{name := N, version := V}) when is_binary(N), is_binary(V) -> true;
is_valid_dependency(_) -> false.
%% @private Run all validations
run_validations([], _Manifest) -> ok;
run_validations([Fun | Rest], Manifest) ->
case Fun(Manifest) of
ok -> run_validations(Rest, Manifest);
{error, _} = Error -> Error
end.
%% @private Parse SemVer version string
parse_semver(Version) when is_binary(Version) ->
case binary:split(Version, <<".">>, [global]) of
[MajorBin, MinorBin, PatchBin] ->
parse_semver_parts(MajorBin, MinorBin, PatchBin);
_ -> error
end;
parse_semver(_) ->
error.
%% @private Parse semver major.minor.patch parts
parse_semver_parts(MajorBin, MinorBin, PatchBin) ->
%% Handle pre-release suffix (e.g., "1.0.0-alpha")
PatchStr = case binary:split(PatchBin, <<"-">>) of
[P | _] -> P;
_ -> PatchBin
end,
parse_semver_integers(
catch binary_to_integer(MajorBin),
catch binary_to_integer(MinorBin),
catch binary_to_integer(PatchStr)
).
%% @private Validate parsed integers
parse_semver_integers(Major, Minor, Patch)
when is_integer(Major), is_integer(Minor), is_integer(Patch) ->
{ok, Major, Minor, Patch};
parse_semver_integers(_, _, _) ->
error.
%% @private Compare version tuples
compare_tuples({M1, _, _}, {M2, _, _}) when M1 < M2 -> lt;
compare_tuples({M1, _, _}, {M2, _, _}) when M1 > M2 -> gt;
compare_tuples({_, N1, _}, {_, N2, _}) when N1 < N2 -> lt;
compare_tuples({_, N1, _}, {_, N2, _}) when N1 > N2 -> gt;
compare_tuples({_, _, P1}, {_, _, P2}) when P1 < P2 -> lt;
compare_tuples({_, _, P1}, {_, _, P2}) when P1 > P2 -> gt;
compare_tuples(_, _) -> eq.
%% @private Check if spec matches pattern
spec_matches({Op, Pattern}, {Op, Actual}) ->
pattern_matches(Pattern, Actual);
spec_matches(_, _) ->
false.
%% @private Match pattern with wildcards
pattern_matches(<<"**">>, _) -> true;
pattern_matches(<<"*">>, _) -> true;
pattern_matches(Pattern, Actual) when Pattern =:= Actual -> true;
pattern_matches(Pattern, Actual) ->
%% Handle patterns like "api.*" or "*.example.com"
PatternParts = binary:split(Pattern, <<".">>, [global]),
ActualParts = binary:split(Actual, <<".">>, [global]),
match_parts(PatternParts, ActualParts).
%% @private Match pattern parts
match_parts([], []) -> true;
match_parts([<<"**">> | _], _) -> true;
match_parts([<<"*">> | PRest], [_ | ARest]) -> match_parts(PRest, ARest);
match_parts([P | PRest], [A | ARest]) when P =:= A -> match_parts(PRest, ARest);
match_parts(_, _) -> false.
%% @private Make manifest serializable (atoms to binaries)
make_serializable(Manifest) ->
maps:fold(fun
(entry_module, V, Acc) when is_atom(V) ->
maps:put(<<"entry_module">>, atom_to_binary(V, utf8), Acc);
(supervisor, V, Acc) when is_atom(V) ->
maps:put(<<"supervisor">>, atom_to_binary(V, utf8), Acc);
(capabilities, Caps, Acc) ->
maps:put(<<"capabilities">>, serialize_capabilities(Caps), Acc);
(K, V, Acc) when is_atom(K) ->
maps:put(atom_to_binary(K, utf8), V, Acc);
(K, V, Acc) ->
maps:put(K, V, Acc)
end, #{}, Manifest).
%% @private Serialize capabilities for MessagePack
serialize_capabilities(Caps) ->
lists:map(fun({Type, Specs}) ->
#{<<"type">> => atom_to_binary(Type, utf8),
<<"specs">> => serialize_specs(Specs)}
end, Caps).
serialize_specs(Specs) ->
lists:map(fun
({Op, Pattern}) -> #{atom_to_binary(Op, utf8) => Pattern};
(Other) -> Other
end, Specs).