-module(version_bump@semver).
-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]).
-define(FILEPATH, "src/version_bump/semver.gleam").
-export([release_type_rank/1, release_type_to_string/1, parse/1, to_string/1, compare/2, bump/2, effective_release_type/3, bump_with_prerelease/3, max/1]).
-export_type([release_type/0, version/0, versioning_mode/0]).
-if(?OTP_RELEASE >= 27).
-define(MODULEDOC(Str), -moduledoc(Str)).
-define(DOC(Str), -doc(Str)).
-else.
-define(MODULEDOC(Str), -compile([])).
-define(DOC(Str), -compile([])).
-endif.
?MODULEDOC(
" Semantic Versioning (SemVer 2.0.0) over the foundation `Version` type.\n"
"\n"
" This module is pure: parsing a string into a `Version`, rendering it back,\n"
" comparing two versions per the SemVer precedence rules (build metadata\n"
" ignored, prerelease identifiers compared field-by-field), and bumping a\n"
" version by a `ReleaseType`.\n"
).
-type release_type() :: patch | minor | major.
-type version() :: {version,
integer(),
integer(),
integer(),
list(binary()),
list(binary())}.
-type versioning_mode() :: initial_development | stable.
-file("src/version_bump/semver.gleam", 23).
-spec release_type_rank(release_type()) -> integer().
release_type_rank(T) ->
case T of
patch ->
1;
minor ->
2;
major ->
3
end.
-file("src/version_bump/semver.gleam", 31).
-spec release_type_to_string(release_type()) -> binary().
release_type_to_string(T) ->
case T of
patch ->
<<"patch"/utf8>>;
minor ->
<<"minor"/utf8>>;
major ->
<<"major"/utf8>>
end.
-file("src/version_bump/semver.gleam", 334).
-spec is_letter(binary()) -> boolean().
is_letter(C) ->
case string:lowercase(C) of
<<"a"/utf8>> ->
true;
<<"b"/utf8>> ->
true;
<<"c"/utf8>> ->
true;
<<"d"/utf8>> ->
true;
<<"e"/utf8>> ->
true;
<<"f"/utf8>> ->
true;
<<"g"/utf8>> ->
true;
<<"h"/utf8>> ->
true;
<<"i"/utf8>> ->
true;
<<"j"/utf8>> ->
true;
<<"k"/utf8>> ->
true;
<<"l"/utf8>> ->
true;
<<"m"/utf8>> ->
true;
<<"n"/utf8>> ->
true;
<<"o"/utf8>> ->
true;
<<"p"/utf8>> ->
true;
<<"q"/utf8>> ->
true;
<<"r"/utf8>> ->
true;
<<"s"/utf8>> ->
true;
<<"t"/utf8>> ->
true;
<<"u"/utf8>> ->
true;
<<"v"/utf8>> ->
true;
<<"w"/utf8>> ->
true;
<<"x"/utf8>> ->
true;
<<"y"/utf8>> ->
true;
<<"z"/utf8>> ->
true;
_ ->
false
end.
-file("src/version_bump/semver.gleam", 327).
-spec is_digit(binary()) -> boolean().
is_digit(C) ->
case C of
<<"0"/utf8>> ->
true;
<<"1"/utf8>> ->
true;
<<"2"/utf8>> ->
true;
<<"3"/utf8>> ->
true;
<<"4"/utf8>> ->
true;
<<"5"/utf8>> ->
true;
<<"6"/utf8>> ->
true;
<<"7"/utf8>> ->
true;
<<"8"/utf8>> ->
true;
<<"9"/utf8>> ->
true;
_ ->
false
end.
-file("src/version_bump/semver.gleam", 323).
-spec is_id_char(binary()) -> boolean().
is_id_char(C) ->
(is_digit(C) orelse is_letter(C)) orelse (C =:= <<"-"/utf8>>).
-file("src/version_bump/semver.gleam", 317).
?DOC(
" A string is a valid SemVer identifier character set: ASCII alphanumerics\n"
" and hyphens only.\n"
).
-spec is_valid_alnum_id(binary()) -> boolean().
is_valid_alnum_id(S) ->
_pipe = S,
_pipe@1 = gleam@string:to_graphemes(_pipe),
gleam@list:all(_pipe@1, fun is_id_char/1).
-file("src/version_bump/semver.gleam", 302).
?DOC(
" Validate build identifiers: each must be non-empty and contain only\n"
" [0-9A-Za-z-]. Leading zeros are allowed in build metadata.\n"
).
-spec validate_build(list(binary())) -> {ok, nil} |
{error, version_bump@error:release_error()}.
validate_build(Ids) ->
gleam@list:try_each(Ids, fun(Id) -> case Id of
<<""/utf8>> ->
{error, {version_error, <<"Empty build identifier"/utf8>>}};
_ ->
case is_valid_alnum_id(Id) of
false ->
{error,
{version_error,
<<"Invalid build identifier: "/utf8,
Id/binary>>}};
true ->
{ok, nil}
end
end end).
-file("src/version_bump/semver.gleam", 377).
-spec has_leading_zero(binary()) -> boolean().
has_leading_zero(S) ->
(S /= <<"0"/utf8>>) andalso gleam_stdlib:string_starts_with(S, <<"0"/utf8>>).
-file("src/version_bump/semver.gleam", 367).
?DOC(" True when every character of a non-empty string is a digit.\n").
-spec is_numeric(binary()) -> boolean().
is_numeric(S) ->
case S of
<<""/utf8>> ->
false;
_ ->
_pipe = S,
_pipe@1 = gleam@string:to_graphemes(_pipe),
gleam@list:all(_pipe@1, fun is_digit/1)
end.
-file("src/version_bump/semver.gleam", 276).
?DOC(
" Validate prerelease identifiers: each must be non-empty, contain only\n"
" [0-9A-Za-z-], and numeric identifiers must not have leading zeros.\n"
).
-spec validate_prerelease(list(binary())) -> {ok, nil} |
{error, version_bump@error:release_error()}.
validate_prerelease(Ids) ->
gleam@list:try_each(Ids, fun(Id) -> case Id of
<<""/utf8>> ->
{error,
{version_error, <<"Empty prerelease identifier"/utf8>>}};
_ ->
case is_valid_alnum_id(Id) of
false ->
{error,
{version_error,
<<"Invalid prerelease identifier: "/utf8,
Id/binary>>}};
true ->
case is_numeric(Id) of
true ->
case has_leading_zero(Id) of
true ->
{error,
{version_error,
<<"Numeric prerelease identifier has leading zero: "/utf8,
Id/binary>>}};
false ->
{ok, nil}
end;
false ->
{ok, nil}
end
end
end end).
-file("src/version_bump/semver.gleam", 230).
?DOC(
" Split a dot-separated section into identifiers, treating the empty string as\n"
" \"no section\" (an empty list) rather than a single empty identifier.\n"
).
-spec split_dot_section(binary()) -> list(binary()).
split_dot_section(S) ->
case S of
<<""/utf8>> ->
[];
_ ->
gleam@string:split(S, <<"."/utf8>>)
end.
-file("src/version_bump/semver.gleam", 258).
?DOC(
" Parse a non-negative integer that has no leading zeros (per SemVer the core\n"
" and numeric prerelease identifiers must not have leading zeros). `0` itself\n"
" is allowed.\n"
).
-spec parse_numeric_id(binary()) -> {ok, integer()} | {error, nil}.
parse_numeric_id(S) ->
case S of
<<""/utf8>> ->
{error, nil};
<<"0"/utf8>> ->
{ok, 0};
_ ->
case gleam_stdlib:string_starts_with(S, <<"0"/utf8>>) of
true ->
{error, nil};
false ->
case gleam_stdlib:parse_int(S) of
{ok, N} when N >= 0 ->
{ok, N};
_ ->
{error, nil}
end
end
end.
-file("src/version_bump/semver.gleam", 238).
?DOC(" Parse the `major.minor.patch` core into a triple of ints.\n").
-spec parse_core(binary()) -> {ok, {integer(), integer(), integer()}} |
{error, version_bump@error:release_error()}.
parse_core(Core) ->
case gleam@string:split(Core, <<"."/utf8>>) of
[Maj, Min, Pat] ->
case {parse_numeric_id(Maj),
parse_numeric_id(Min),
parse_numeric_id(Pat)} of
{{ok, A}, {ok, B}, {ok, C}} ->
{ok, {A, B, C}};
{_, _, _} ->
{error,
{version_error,
<<"Invalid version core, expected numeric major.minor.patch: "/utf8,
Core/binary>>}}
end;
_ ->
{error,
{version_error,
<<"Invalid version, expected major.minor.patch: "/utf8,
Core/binary>>}}
end.
-file("src/version_bump/semver.gleam", 221).
?DOC(
" Split a string into the part before the first occurrence of `sep` and the\n"
" part after it. If `sep` is not present, the second element is the empty\n"
" string. The separator itself is dropped.\n"
).
-spec split_once(binary(), binary()) -> {binary(), binary()}.
split_once(S, Sep) ->
case gleam@string:split_once(S, Sep) of
{ok, {Before, After}} ->
{Before, After};
{error, _} ->
{S, <<""/utf8>>}
end.
-file("src/version_bump/semver.gleam", 211).
-spec strip_leading_v(binary()) -> binary().
strip_leading_v(S) ->
case gleam_stdlib:string_starts_with(S, <<"v"/utf8>>) orelse gleam_stdlib:string_starts_with(
S,
<<"V"/utf8>>
) of
true ->
gleam@string:drop_start(S, 1);
false ->
S
end.
-file("src/version_bump/semver.gleam", 58).
?DOC(
" Parse a SemVer string into a `Version`.\n"
"\n"
" Accepts an optional leading `v` (e.g. `v1.2.3`), an optional `-prerelease`\n"
" section of dot-separated identifiers, and an optional `+build` metadata\n"
" section of dot-separated identifiers. The order of the optional sections is\n"
" `core[-prerelease][+build]` per the spec.\n"
"\n"
" Returns a `VersionError` when the string is not a valid version.\n"
).
-spec parse(binary()) -> {ok, version()} |
{error, version_bump@error:release_error()}.
parse(S) ->
Trimmed = gleam@string:trim(S),
Without_v = strip_leading_v(Trimmed),
{Core_and_pre, Build} = split_once(Without_v, <<"+"/utf8>>),
{Core, Prerelease, Has_pre} = case gleam@string:split_once(
Core_and_pre,
<<"-"/utf8>>
) of
{ok, {Before, After}} ->
{Before, After, true};
{error, _} ->
{Core_and_pre, <<""/utf8>>, false}
end,
case parse_core(Core) of
{error, E} ->
{error, E};
{ok, {Major, Minor, Patch}} ->
Pre_ids = case Has_pre of
true ->
gleam@string:split(Prerelease, <<"."/utf8>>);
false ->
[]
end,
Build_ids = split_dot_section(Build),
case {validate_prerelease(Pre_ids), validate_build(Build_ids)} of
{{ok, _}, {ok, _}} ->
{ok, {version, Major, Minor, Patch, Pre_ids, Build_ids}};
{{error, E@1}, _} ->
{error, E@1};
{_, {error, E@2}} ->
{error, E@2}
end
end.
-file("src/version_bump/semver.gleam", 99).
?DOC(" Render a `Version` back to its canonical SemVer string (no leading `v`).\n").
-spec to_string(version()) -> binary().
to_string(V) ->
Core = <<<<<<<<(erlang:integer_to_binary(erlang:element(2, V)))/binary,
"."/utf8>>/binary,
(erlang:integer_to_binary(erlang:element(3, V)))/binary>>/binary,
"."/utf8>>/binary,
(erlang:integer_to_binary(erlang:element(4, V)))/binary>>,
With_pre = case erlang:element(5, V) of
[] ->
Core;
Ids ->
<<<<Core/binary, "-"/utf8>>/binary,
(gleam@string:join(Ids, <<"."/utf8>>))/binary>>
end,
case erlang:element(6, V) of
[] ->
With_pre;
Ids@1 ->
<<<<With_pre/binary, "+"/utf8>>/binary,
(gleam@string:join(Ids@1, <<"."/utf8>>))/binary>>
end.
-file("src/version_bump/semver.gleam", 415).
?DOC(
" Compare two individual prerelease identifiers. Numeric identifiers compare\n"
" numerically and rank lower than alphanumeric identifiers.\n"
).
-spec compare_identifier(binary(), binary()) -> gleam@order:order().
compare_identifier(A, B) ->
case {is_numeric(A), is_numeric(B)} of
{true, true} ->
case {gleam_stdlib:parse_int(A), gleam_stdlib:parse_int(B)} of
{{ok, Na}, {ok, Nb}} ->
gleam@int:compare(Na, Nb);
{_, _} ->
gleam@string:compare(A, B)
end;
{true, false} ->
lt;
{false, true} ->
gt;
{false, false} ->
gleam@string:compare(A, B)
end.
-file("src/version_bump/semver.gleam", 400).
?DOC(
" Compare two non-empty prerelease identifier lists field-by-field. When all\n"
" compared fields are equal, the list with MORE fields has higher precedence.\n"
).
-spec compare_pre_fields(list(binary()), list(binary())) -> gleam@order:order().
compare_pre_fields(A, B) ->
case {A, B} of
{[], []} ->
eq;
{[], _} ->
lt;
{_, []} ->
gt;
{[X | Xs], [Y | Ys]} ->
case compare_identifier(X, Y) of
eq ->
compare_pre_fields(Xs, Ys);
Other ->
Other
end
end.
-file("src/version_bump/semver.gleam", 388).
?DOC(
" Compare two prerelease identifier lists per SemVer precedence rules.\n"
"\n"
" An empty list means \"no prerelease\" (a normal release), which outranks any\n"
" prerelease. This release-vs-prerelease distinction only applies at the top\n"
" level; once both versions are known to have prerelease identifiers, the\n"
" lists are compared field-by-field where a larger set of fields wins when all\n"
" preceding fields are equal (see `compare_pre_fields`).\n"
).
-spec compare_prerelease(list(binary()), list(binary())) -> gleam@order:order().
compare_prerelease(A, B) ->
case {A, B} of
{[], []} ->
eq;
{[], _} ->
gt;
{_, []} ->
lt;
{_, _} ->
compare_pre_fields(A, B)
end.
-file("src/version_bump/semver.gleam", 123).
?DOC(
" Compare two versions per SemVer 2.0.0 precedence.\n"
"\n"
" Core version (major, minor, patch) is compared numerically. Build metadata\n"
" is ignored. A version WITH a prerelease has LOWER precedence than the same\n"
" version WITHOUT one. Prerelease identifiers are compared field-by-field:\n"
" numeric identifiers compare numerically and always rank below alphanumeric\n"
" ones; a longer set of identifiers wins when all preceding fields are equal.\n"
).
-spec compare(version(), version()) -> gleam@order:order().
compare(A, B) ->
case gleam@int:compare(erlang:element(2, A), erlang:element(2, B)) of
eq ->
case gleam@int:compare(erlang:element(3, A), erlang:element(3, B)) of
eq ->
case gleam@int:compare(
erlang:element(4, A),
erlang:element(4, B)
) of
eq ->
compare_prerelease(
erlang:element(5, A),
erlang:element(5, B)
);
Other ->
Other
end;
Other@1 ->
Other@1
end;
Other@2 ->
Other@2
end.
-file("src/version_bump/semver.gleam", 143).
?DOC(
" Bump a version by a release type.\n"
"\n"
" Clears any prerelease and build metadata. `Major` increments major and\n"
" resets minor and patch to 0. `Minor` increments minor and resets patch to 0.\n"
" `Patch` increments patch.\n"
).
-spec bump(version(), release_type()) -> version().
bump(V, T) ->
case T of
major ->
{version, erlang:element(2, V) + 1, 0, 0, [], []};
minor ->
{version, erlang:element(2, V), erlang:element(3, V) + 1, 0, [], []};
patch ->
{version,
erlang:element(2, V),
erlang:element(3, V),
erlang:element(4, V) + 1,
[],
[]}
end.
-file("src/version_bump/semver.gleam", 163).
?DOC(
" The release type to actually apply, given the versioning mode. In\n"
" `InitialDevelopment` while the major version is 0, a breaking change is\n"
" downshifted to a *minor* bump so it stays in `0.x` instead of jumping to\n"
" `1.0.0`; features and fixes are unaffected, and once major >= 1 the mode has\n"
" no effect.\n"
).
-spec effective_release_type(version(), release_type(), versioning_mode()) -> release_type().
effective_release_type(Version, T, Mode) ->
case Mode of
stable ->
T;
initial_development ->
case erlang:element(2, Version) =:= 0 of
false ->
T;
true ->
case T of
major ->
minor;
minor ->
minor;
patch ->
patch
end
end
end.
-file("src/version_bump/semver.gleam", 188).
?DOC(
" Bump a version by a release type, attaching a prerelease identifier.\n"
"\n"
" The core version is bumped exactly as `bump` does, then a prerelease of\n"
" `[id, \"1\"]` is attached, e.g. bumping `1.1.0` by `Minor` with `\"beta\"`\n"
" yields `1.2.0-beta.1`.\n"
).
-spec bump_with_prerelease(version(), release_type(), binary()) -> version().
bump_with_prerelease(V, T, Id) ->
Bumped = bump(V, T),
{version,
erlang:element(2, Bumped),
erlang:element(3, Bumped),
erlang:element(4, Bumped),
[Id, <<"1"/utf8>>],
erlang:element(6, Bumped)}.
-file("src/version_bump/semver.gleam", 194).
?DOC(" The greatest version in a list per `compare`, or `None` if the list is empty.\n").
-spec max(list(version())) -> gleam@option:option(version()).
max(Versions) ->
case Versions of
[] ->
none;
[First | Rest] ->
{some,
gleam@list:fold(
Rest,
First,
fun(Acc, V) -> case compare(V, Acc) of
gt ->
V;
_ ->
Acc
end end
)}
end.