-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 -> <<<>/binary, (gleam@string:join(Ids, <<"."/utf8>>))/binary>> end, case erlang:element(6, V) of [] -> With_pre; Ids@1 -> <<<>/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.