Current section

Files

Jump to
purl src purl_special_case.erl
Raw

src/purl_special_case.erl

-module(purl_special_case).
-feature(maybe_expr, enable).
-include("internal/doc.hrl").
-include("purl.hrl").
?MODULEDOC(false).
-export([apply/1]).
-define(RULES, #{
<<"bitbucket">> => #{
name_case_sensitive => false,
name_normalize => hyphen_case,
namespace_case_sensitive => false
},
<<"cran">> => #{version_required => true},
<<"composer">> => #{
name_case_sensitive => false,
namespace_case_sensitive => false
},
<<"github">> => #{
name_case_sensitive => false,
namespace_case_sensitive => false
},
<<"huggingface">> => #{version_case_sensitive => false},
<<"pypi">> => #{
name_case_sensitive => false,
name_normalize => hyphen_case
},
<<"swift">> => #{
version_required => true,
namespace_min_length => 1
}
}).
-spec apply(Purl) -> {ok, Purl} | purl:parse_error() when Purl :: purl:t().
apply(Purl) ->
maybe
{ok, Purl1} ?= downcase_namespace(Purl),
{ok, Purl2} ?= enforce_namespace_length(Purl1),
{ok, Purl3} ?= downcase_name(Purl2),
{ok, Purl4} ?= normalize_name(Purl3),
{ok, Purl5} ?= downcase_version(Purl4),
{ok, Purl6} ?= version_required(Purl5),
{ok, Purl7} ?= conan_enforce_channel(Purl6),
cpan_verify_distribution(Purl7)
end.
-spec downcase_namespace(Purl) -> {ok, Purl} when Purl :: purl:t().
downcase_namespace(#purl{type = Type, namespace = Namespace} = Purl) ->
case maps:get(Type, ?RULES, #{}) of
#{namespace_case_sensitive := false} ->
{ok, Purl#purl{namespace = lists:map(fun string:lowercase/1, Namespace)}};
_ ->
{ok, Purl}
end.
-spec enforce_namespace_length(Purl) -> {ok, Purl} | purl:parse_error() when Purl :: purl:t().
enforce_namespace_length(#purl{type = Type, namespace = Namespace} = Purl) ->
case maps:get(Type, ?RULES, #{}) of
#{namespace_min_length := MinLength} ->
case length(Namespace) >= MinLength of
true -> {ok, Purl};
false -> {error, {special_case_failed, <<"namespace missing">>}}
end;
_ ->
{ok, Purl}
end.
-spec downcase_name(Purl) -> {ok, Purl} when Purl :: purl:t().
downcase_name(#purl{type = Type, name = Name, qualifiers = Qualifiers} = Purl) ->
case maps:get(Type, ?RULES, #{}) of
#{name_case_sensitive := false} ->
{ok, Purl#purl{name = string:lowercase(Name)}};
_ when Type =:= <<"mlflow">>, is_map_key(<<"repository_url">>, Qualifiers) ->
case uri_string:parse(maps:get(<<"repository_url">>, Qualifiers)) of
#{host := Host} ->
case
binary:part(
Host,
byte_size(Host) - byte_size(<<"azuredatabricks.net">>),
byte_size(<<"azuredatabricks.net">>)
) =:= <<"azuredatabricks.net">>
of
true -> {ok, Purl#purl{name = string:lowercase(Name)}};
false -> {ok, Purl}
end;
_ ->
{ok, Purl}
end;
_ ->
{ok, Purl}
end.
-spec normalize_name(Purl) -> {ok, Purl} when Purl :: purl:t().
normalize_name(#purl{type = Type, name = Name} = Purl) ->
case maps:get(Type, ?RULES, #{}) of
#{name_normalize := hyphen_case} ->
{ok, Purl#purl{name = iolist_to_binary(string:replace(Name, "_", "-", all))}};
_ ->
{ok, Purl}
end.
-spec downcase_version(Purl) -> {ok, Purl} when Purl :: purl:t().
downcase_version(#purl{type = Type, version = Version} = Purl) ->
case maps:get(Type, ?RULES, #{}) of
#{version_case_sensitive := false} when is_binary(Version) ->
{ok, Purl#purl{version = string:lowercase(Version)}};
_ ->
{ok, Purl}
end.
-spec version_required(Purl) -> {ok, Purl} | purl:parse_error() when Purl :: purl:t().
version_required(#purl{type = Type, version = Version} = Purl) ->
case maps:get(Type, ?RULES, #{}) of
#{version_required := true} when Version =:= undefined ->
{error, {special_case_failed, <<"version missing">>}};
_ ->
{ok, Purl}
end.
-spec conan_enforce_channel(Purl) -> {ok, Purl} | purl:parse_error() when Purl :: purl:t().
conan_enforce_channel(
#purl{type = <<"conan">>, namespace = Namespace, qualifiers = Qualifiers} = Purl
) ->
case {Namespace, maps:is_key(<<"channel">>, Qualifiers)} of
{[_ | _], true} ->
{ok, Purl};
{[], false} ->
{ok, Purl};
_ ->
{error,
{special_case_failed,
<<"either namespace & channel must both be present or both be absent">>}}
end;
conan_enforce_channel(Purl) ->
{ok, Purl}.
-spec cpan_verify_distribution(Purl) -> {ok, Purl} | purl:parse_error() when Purl :: purl:t().
cpan_verify_distribution(#purl{type = <<"cpan">>, namespace = [], name = Name} = Purl) ->
case binary:match(Name, <<"-">>) of
{_, _} -> {error, {special_case_failed, <<"cpan modules must not contain \"-\"">>}};
nomatch -> {ok, Purl}
end;
cpan_verify_distribution(#purl{type = <<"cpan">>, namespace = Namespace, name = Name} = Purl) ->
case binary:match(Name, <<"::">>) of
{_, _} ->
{error, {special_case_failed, <<"cpan distribution name must not contain \"::\"">>}};
nomatch ->
{ok, Purl#purl{namespace = lists:map(fun string:uppercase/1, Namespace)}}
end;
cpan_verify_distribution(Purl) ->
{ok, Purl}.