Current section

Files

Jump to
verl src verl.erl
Raw

src/verl.erl

-module(verl).
-export([
compare/2,
is_match/2,
is_match/3,
parse/1,
parse_requirement/1,
compile_requirement/1
]).
-type version() :: binary().
-type requirement() :: binary().
-type major() :: non_neg_integer().
-type minor() :: non_neg_integer().
-type patch() :: non_neg_integer().
-type pre() :: [binary() | non_neg_integer()].
-type build() :: binary() | undefined.
-type version_t() :: #{
major => major(),
minor => minor(),
patch => patch(),
pre => pre(),
build => build()
}.
-type requirement_t() :: #{
string => requirement(),
matchspec => list(),
compiled => boolean()
}.
-type compiled_requirement() :: #{
compiled => true,
matchspec => ets:comp_match_spec(),
string => requirement()
}.
-type match_opts() :: [allow_pre | {allow_pre, true}].
-export_type([
version/0,
requirement/0,
major/0,
minor/0,
patch/0,
pre/0,
build/0,
version_t/0,
requirement_t/0,
compiled_requirement/0
]).
%%% @doc
%%% Compares two versions, returning whether the first argument is greater, equal, or
%%% less than the second argument.
%%% @end
-spec compare(version(), version()) -> gt | eq | lt | {error, invalid_version}.
compare(Version1, Version2) ->
ver_cmp(to_matchable(Version1, true), to_matchable(Version2, true)).
%%% @doc
%%% Parses a semantic version, returning {ok, version_t()} or {error, invalid_version}
%%% @end
-spec parse(version()) -> {ok, version_t()} | {error, invalid_version}.
parse(Str) ->
build_version(Str).
%%% @doc
%%% Parses a semantic version requirement, returning {ok, requirement_t()} or
%%% {error, invalid_requirement}
%%% @end
-spec parse_requirement(requirement()) -> {ok, requirement_t()} | {error, invalid_requirement}.
parse_requirement(Str) ->
case verl_parser:parse_requirement(Str) of
{ok, Spec} ->
{ok, #{string => Str, matchspec => Spec, compiled => false}};
{error, invalid_requirement} ->
{error, invalid_requirement}
end.
%%% @doc
%%% Compiles a version requirement as returned by `parse_requirement' for faster
%%% matches.
%%% @end
-spec compile_requirement(requirement_t()) -> compiled_requirement().
compile_requirement(Req) when is_map(Req) ->
Ms = ets:match_spec_compile(maps:get(matchspec, Req)),
maps:put(compiled, true, maps:put(matchspec, Ms, Req)).
%%% @doc
%%% Returns `true' if the dependency is in range of the requirement, otherwise
%%% `false', or an error.
%%% @end
-spec is_match(version() | version_t(), requirement() | requirement_t()) ->
boolean() | {error, badarg | invalid_requirement | invalid_version}.
is_match(Version, Requirement) ->
is_match(Version, Requirement, []).
%%% @doc
%%% Works like `is_match/2' but takes extra options as an argument.
%%% @end
-spec is_match(version() | version_t(), requirement() | requirement_t(), match_opts()) ->
boolean() | {error, badarg | invalid_requirement | invalid_version}.
is_match(Version, Requirement, Opts) when is_binary(Version) andalso is_binary(Requirement) ->
case build_version(Version) of
{ok, Ver} ->
case build_requirement(Requirement) of
{ok, Req} ->
is_match(Ver, Req, Opts);
{error, invalid_requirement} ->
{error, invalid_requirement}
end;
{error, invalid_version} ->
{error, invalid_version}
end;
is_match(Version, Requirement, Opts) when is_binary(Version) andalso is_map(Requirement) ->
case build_version(Version) of
{ok, Ver} ->
is_match(Ver, Requirement, Opts);
{error, invalid_version} ->
{error, invalid_version}
end;
is_match(Version, Requirement, Opts) when is_map(Version) andalso is_binary(Requirement) ->
case build_requirement(Requirement) of
{ok, Req} ->
is_match(Version, Req, Opts);
{error, invalid_requirement} ->
{error, invalid_requirement}
end;
is_match(Version, #{matchspec := Spec, compiled := false} = R, Opts) when is_map(R) ->
AllowPre = proplists:get_value(allow_pre, Opts, true),
{ok, Result} = ets:test_ms(to_matchable(Version, AllowPre), Spec),
Result /= false;
is_match(Version, #{matchspec := Spec, compiled := true} = R, Opts) when
is_map(Version) andalso is_map(R)
->
AllowPre = proplists:get_value(allow_pre, Opts, true),
ets:match_spec_run([to_matchable(Version, AllowPre)], Spec) /= [].
to_matchable(#{major := Major, minor := Minor, patch := Patch, pre := Pre}, AllowPre) ->
{Major, Minor, Patch, Pre, AllowPre};
to_matchable(String, AllowPre) when is_binary(String) ->
case verl_parser:parse_version(String) of
{ok, {Major, Minor, Patch, Pre, _Build}} ->
{Major, Minor, Patch, Pre, AllowPre};
{error, invalid_version} ->
{error, invalid_version}
end.
%% private
%%
build_version(Version) ->
case verl_parser:parse_version(Version) of
{ok, {Major, Minor, Patch, Pre, Build}} ->
{ok, #{
major => Major,
minor => Minor,
patch => Patch,
pre => Pre,
build => build_string(Build)
}};
{error, invalid_version} ->
{error, invalid_version}
end.
build_requirement(Str) ->
case verl_parser:parse_requirement(Str) of
{ok, Spec} ->
{ok, #{string => Str, matchspec => Spec, compiled => false}};
{error, invalid_requirement} ->
{error, invalid_requirement}
end.
build_string(Build) ->
case Build of
[] -> undefined;
_ -> binary:list_to_bin(Build)
end.
ver_cmp({Maj1, Min1, Patch1, Pre1, _}, {Maj2, Min2, Patch2, Pre2, _}) ->
case {Maj1, Min1, Patch1} > {Maj2, Min2, Patch2} of
true ->
gt;
false ->
case {Maj1, Min1, Patch1} < {Maj2, Min2, Patch2} of
true ->
lt;
false ->
test_pre(Pre1, Pre2)
end
end;
ver_cmp(_, _) ->
{error, invalid_version}.
test_pre(Pre1, Pre2) ->
case pre_is_eq(Pre1, Pre2) of
true ->
gt;
false ->
case pre_is_eq(Pre2, Pre1) of
true ->
lt;
false ->
pre_cmp(Pre1, Pre2)
end
end.
pre_cmp(Pre1, Pre2) ->
case Pre1 > Pre2 of
true ->
gt;
false ->
case Pre1 < Pre2 of
true ->
lt;
false ->
eq
end
end.
pre_is_eq(Pre1, Pre2) ->
case Pre1 == [] of
false -> false;
true -> Pre2 /= []
end.