gleamsver
Gleam utilities for parsing, comparing, and encoding SemVer versions.
This package aims to respect the specifications of the Semantic Versioning 2.0.0 standard as described on semver.org.
import gleam/io
import gleamsver
fn compress_message(message: String) -> String {
message <> ", but compressed ;)"
}
pub fn main() {
// Parse correct SemVer 2.0.0 strings using `parse()`:
let assert Ok(server_version_with_compression) = gleamsver.parse("1.3.7-rc0")
io.debug(server_version_with_compression) // SemVer(1, 3, 7, "rc0", "")
// Parse loose SemVer strings using `parse_loosely()`:
let assert Ok(current_server_version) = gleamsver.parse_loosely("v1.4")
// Convert back to SemVer strings using `to_string()`:
let uncompressed_message =
"Hello, server version " <> gleamsver.to_string(current_server_version)
let message = {
// Guard against version mismatches using `guard_version_*()` functions:
use <- gleamsver.guard_version_compatible(
version: server_version_with_compression,
compatible_with: current_server_version,
else_return: uncompressed_message,
)
// Compression will only occur if the above guard succeeds:
compress_message(uncompressed_message)
}
// Prints "Hello, server version 1.4.0, but compressed ;)"
io.println(message)
}
Types
SemVer represents all the constituent parts of a Semantic Versioning (or SemVer) 2.0.0 definition as described on semver.org.
pub type SemVer {
SemVer(
major: Int,
minor: Int,
patch: Int,
pre: String,
build: String,
)
}
Constructors
-
SemVer( major: Int, minor: Int, patch: Int, pre: String, build: String, )
Arguments
-
major
Leading Major Integer version.
-
minor
Middle Minor Integer version.
-
patch
Third Patch Integer version.
-
pre
String Pre-build tag(s) of the version.
-
build
String Build tag(s) of the version.
-
Type whose variants can possibly be returned by parse
on invalid
inputs, and a subset of them can be returned by parse_loosely
.
To get the error string from within it, simply use the_error.msg
,
or call string_from_parsing_error(the_error)
.
pub type SemVerParseError {
EmptyInput(msg: String)
MissingMajor(msg: String)
MissingMinor(msg: String)
MissingPatch(msg: String)
MissingPreRelease(msg: String)
MissingBuild(msg: String)
MissingPreOrBuildSeparator(msg: String)
InvalidMajor(msg: String)
InvalidMinor(msg: String)
InvalidPatch(msg: String)
InvalidPreRelease(msg: String)
InvalidBuild(msg: String)
InternalCodePointError(msg: String)
InternalError(msg: String)
}
Constructors
-
EmptyInput(msg: String)
Returned only by
parse
when its input is empty. -
MissingMajor(msg: String)
Returned when the Major part of a
SemVer
is missing. I.e. there are no leading numeric digits to the input String. -
MissingMinor(msg: String)
-
MissingPatch(msg: String)
-
MissingPreRelease(msg: String)
-
MissingBuild(msg: String)
-
MissingPreOrBuildSeparator(msg: String)
Returned when there is no Pre-release or Build separators (
-
and+
) between the Major.Minor.Patch core part of theSemVer
and the rest. E.g:1.2.3rc0
or1.2.3build5
. -
InvalidMajor(msg: String)
Returned when the Integer Major part of a
SemVer
cannot be parsed. -
InvalidMinor(msg: String)
Returned when the Integer Minor part of a
SemVer
cannot be parsed. -
InvalidPatch(msg: String)
Returned when the Integer Patch part of a
SemVer
cannot be parsed. -
InvalidPreRelease(msg: String)
-
InvalidBuild(msg: String)
-
InternalCodePointError(msg: String)
Internal UTF conversion error which you should never get.
-
InternalError(msg: String)
Internal error variant only used for testing which you should never get.
Constants
pub const empty_semver: SemVer
Constant representing an empty SemVer
(0.0.0
with no pre-release or build tags).
Functions
pub fn are_compatible(v1: SemVer, with v2: SemVer) -> Bool
Checks whether the first given SemVer
is compatible with
the second based on the
compatibility rules described on semver.org.
In order for a version to count as compatible with another, the Major part of the versions must be exactly equal, and the Minor, Patch and Pre-release parts of the first must be less than or equal to the second’s.
If you would like to compare versions in general, use compare
or compare_core.
Examples
are_compatible(SemVer(1, 2, 3, "", ""), SemVer(1, 2, 4, "", ""))
-> True
are_compatible(SemVer(2, 0, 0, "", ""), SemVer(1, 2, 3, "", ""))
-> False
// NOTE: Any Pre-release tags have lower precedence than full release:
are_compatible(SemVer(1, 2, 3, "", ""), SemVer(1, 2, 3, "alpha.1", ""))
-> False
// NOTE: Build tags are **not** compared:
are_compatible(SemVer(1, 2, 3, "", "20240505"), SemVer(1, 2, 3, "", ""))
-> True
pub fn are_equal(v1: SemVer, with v2: SemVer) -> Bool
Compares the core Major.Minor.Patch
and the Pre-release and Build tags
of the two given SemVer
s, returning True
only if they
are exactly equal.
Examples
are_equal(SemVer(1, 2, 3, "rc5", ""), with: SemVer(1, 2, 3, "rc5", ""))
// -> True
// NOTE: Pre-release and Build parts must be exactly equal too!
are_equal(SemVer(1, 2, 3, "", ""), with: SemVer(1, 2, 3, "rc0", "20250505"))
// -> False
pub fn are_equal_core(v1: SemVer, with v2: SemVer) -> Bool
Compares only the core Major.Minor.Patch
of the two given
SemVer
s, returning True
only if they are exactly equal.
If you would like an exact equality check of the Pre-release and Build
parts as well, please use are_equal
.
Examples
are_equal_core(SemVer(1, 2, 3, "", ""), with: SemVer(1, 2, 3, "", ""))
// -> True
// NOTE: Pre-release and Build parts are **not** compared!
are_equal_core(SemVer(1, 2, 3, "", ""), with: SemVer(1, 2, 3, "rc0", "20250505"))
// -> True
are_equal_core(SemVer(1, 2, 3, "", ""), with: SemVer(4, 5, 6, "", ""))
// -> False
pub fn compare(v1: SemVer, with v2: SemVer) -> Order
Compares the core Major.Minor.Patch
versions and Pre-release tags of the
two given SemVer
s, returning the gleam/order.Order
of the resulting
comparisons as described on semver.org.
If you would like to only compare core versions, use
compare_core
.
If you want to check for exact equality, use are_equal
.
Build tag(s) are never compared.
Examples:
compare(SemVer(1, 2, 3, "", ""), with: SemVer(5, 6, 7, "", ""))
// -> Lt
compare(SemVer(1, 2, 3, "rc0", ""), with: SemVer(1, 2, 3, "rc0", ""))
// -> Eq
compare(SemVer(5, 6, 7, "rc0", "20240505"), with: SemVer(1, 2, 3, "rc5", "30240505"))
// -> Gt
// NOTE: pre-release tags are compared:
compare(SemVer(1, 2, 3, "alpha", ""), with: SemVer(1, 2, 3, "alpha.1", ""))
// -> Lt
compare(SemVer(1, 2, 3, "alpha.12", ""), with: SemVer(1, 2, 3, "alpha.5", ""))
// -> Gt
// NOTE: will **not** compare Build tags at all!
compare(SemVer(1, 2, 3, "rc5", "20240505"), with: SemVer(1, 2, 3, "rc5", "30240505"))
// -> Eq
pub fn compare_core(v1: SemVer, with v2: SemVer) -> Order
Compares only the core Major.Minor.Patch
versions of the two given
SemVer
s, returning the gleam/order.Order
of the resulting
comparisons.
It does not compare the Pre-release or Build tags in any way!
If you would like to compare Pre-release tags too, use compare
.
If you want to check for exact equality, use are_equal
.
Examples:
compare_core(SemVer(1, 2, 3, "", ""), with: SemVer(5, 6, 7, "", ""))
// -> Lt
compare_core(SemVer(1, 2, 3, "", ""), with: SemVer(1, 2, 3, "", ""))
// -> Eq
compare_core(SemVer(5, 6, 7, "rc0", "20240505"), with: SemVer(1, 2, 3, "rc5", "30240505"))
// -> Gt
// NOTE: will **not** compare Pre-release and Build tags at all!
compare_core(SemVer(1, 2, 3, "rc0", "20240505"), with: SemVer(1, 2, 3, "rc5", "30240505"))
// -> Eq
pub fn compare_pre_release_strings(
pre1: String,
with pre2: String,
) -> Order
Compares two given pre-release Strings based on the set of rules described in point 11 of semver.org.
Examples.
// NOTE: empty pre-release tags always count as larger:
compare_pre_release_strings("", "any.thing")
// -> Gt
compare_pre_release_strings("any.thing", "")
// -> Lt
// Integer parts are compared as integers:
compare_pre_release_strings("12.thing.A", "13.thing.B")
// -> Lt
// Non-integer parts are compared lexicographically:
compare_pre_release_strings("12.thing.A", "12.thing.B")
// -> Lt
// NOTE: integer parts always have lower precedence over non-integer ones:
compare_pre_release_strings("12.thing.1", "12.thing.rc0")
// -> Lt
// NOTE: 'B' comes before 'a' in the ASCII table:
compare_pre_release_strings("12.thing.B", "12.thing.a")
// -> Lt
// NOTE: '0' comes before '6' in the ASCII table:
compare_pre_release_strings("rc07", "rc6")
// -> Lt
pub fn guard_version_compatible(
version v1: SemVer,
compatible_with v2: SemVer,
else_return default_value: a,
if_compatible operation_if_compatible: fn() -> a,
) -> a
Guards that the given first SemVer
is compatible with
the second SemVer
, running and returning the result
of the if_compatible
callback function if so,
or returning the else_return
value if not.
The semantics of the compatibility check are as dictated by the
are_compatible
function.
Examples
let uncompressed_message = "Hello!"
let message = {
use <- gleamsver.guard_version_compatible(
version: server_version_with_compression,
compatible_with: current_server_version,
else_return: uncompressed_message,
)
// Compression will only occur if the above guard succeeds:
uncompressed_message <> ", but compressed ;)"
}
io.println(message) // compression depends on version compatibility
pub fn guard_version_eq(
version v1: SemVer,
equal_to v2: SemVer,
else_return default_value: a,
if_compatible operation_if_eq: fn() -> a,
) -> a
Guards that the given first SemVer
is equal to
the second SemVer
, running and returning the
result of the if_compatible
callback function if so, or
returning the else_return
default value if not.
The semantics of the comparison check are as dictated by the
compare
function.
Examples
let uncompressed_message = "Hello!"
let message = {
use <- gleamsver.guard_version_eq(
version: server_version_with_compression,
equal_to: current_server_version,
else_return: uncompressed_message,
)
// Compression will only occur if the above guard succeeds:
uncompressed_message <> ", but compressed ;)"
}
io.println(message) // compression depends on version equality
pub fn guard_version_gt(
version v1: SemVer,
greater_than v2: SemVer,
else_return default_value: a,
if_compatible operation_if_gt: fn() -> a,
) -> a
Guards that the given first SemVer
is greater than
the second SemVer
, running and returning the result
of the if_compatible
callback function if so, or
returning the else_return
default value if not.
The semantics of the comparison check are as dictated by the
compare
function.
Examples
let uncompressed_message = "Hello!"
let message = {
use <- gleamsver.guard_version_gt(
version: current_server_version,
greater_than: server_version_with_compression,
else_return: uncompressed_message,
)
// Compression will only occur if the above guard succeeds:
uncompressed_message <> ", but compressed ;)"
}
io.println(message) // compression depends on version ordering
pub fn guard_version_gte(
version v1: SemVer,
greater_than_or_equal v2: SemVer,
else_return default_value: a,
if_compatible operation_if_gte: fn() -> a,
) -> a
Guards that the given first SemVer
is greater than
or equal to the second SemVer
, running and returning
the result of the if_compatible
callback function if so, or
returning the else_return
default value if not.
The semantics of the comparison check are as dictated by the
compare
function.
Examples
let uncompressed_message = "Hello!"
let message = {
use <- gleamsver.guard_version_gte(
version: current_server_version,
greater_than_or_equal: server_version_with_compression,
else_return: uncompressed_message,
)
// Compression will only occur if the above guard succeeds:
uncompressed_message <> ", but compressed ;)"
}
io.println(message) // compression depends on version ordering
pub fn guard_version_lt(
version v1: SemVer,
less_than v2: SemVer,
else_return default_value: a,
if_compatible operation_if_lt: fn() -> a,
) -> a
Guards that the given first SemVer
is less than
the second SemVer
, running and returning the result
of the if_compatible
callback function if so, or
returning the else_return
default value if not.
The semantics of the comparison check are as dictated by the
compare
function.
Examples
let uncompressed_message = "Hello!"
let message = {
use <- gleamsver.guard_version_lt(
version: server_version_with_compression,
less_that: current_server_version,
else_return: uncompressed_message,
)
// Compression will only occur if the above guard succeeds:
uncompressed_message <> ", but compressed ;)"
}
io.println(message) // compression depends on version ordering
pub fn guard_version_lte(
version v1: SemVer,
less_than_or_equal v2: SemVer,
else_return default_value: a,
if_compatible operation_if_lte: fn() -> a,
) -> a
Guards that the given first SemVer
is less than or equal
to the second SemVer
, running and returning the result
of the if_compatible
callback function if so, or
returning the else_return
default value if not.
The semantics of the comparison check are as dictated by the
compare
function.
Examples
let uncompressed_message = "Hello!"
let message = {
use <- gleamsver.guard_version_lte(
version: server_version_with_compression,
less_that_or_equal: current_server_version,
else_return: uncompressed_message,
)
// Compression will only occur if the above guard succeeds:
uncompressed_message <> ", but compressed ;)"
}
io.println(message) // compression depends on version ordering
pub fn parse(version: String) -> Result(SemVer, SemVerParseError)
Parses the given string into a SemVer
.
Parsing rules are exactly based on the rules defined on semver.org.
If you would prefer some leniency when parsing, see parse_loosely
.
See SemVerParseError
for possible error variants
returned by parse
.
Examples
parse("1.2.3-rc0+20240505")
// -> Ok(SemVer(major: 1, minor; 2, patch: 3, pre: "rc0", build: "20240505"))
Both the Pre-release (-rc0
) and Build (+20240505
) parts are optional:
parse("4.5.6-rc0")
// -> Ok(SemVer(major: 4, minor; 5, patch: 6, pre: "rc0", build: ""))
parse("4.5.6+20240505")
// -> Ok(SemVer(major: 4, minor; 5, patch: 6, pre: "", build: "20240505"))
parse("4.5.6-rc0+20240505")
// -> Ok(SemVer(major: 4, minor; 5, patch: 6, pre: "rc0", build: "20240505"))
// NOTE: the Pre-release should always come *before* the Build,
// otherwise, it will get included as part of the Build:
parse("6.7.8+20240505-rc0")
// -> Ok(SemVer(major: 4, minor; 5, patch: 6, pre: "", build: "20240505-rc0"))
Possible parsing errors
The parse
function aims to return a relevant error variant
and accompanying helpful String
message on parsing failures.
Please see type SemVerParseError
and
string_from_parsing_error
.
parse("abc")
// -> MissingMajor("Leading Major SemVer Integer part is missing.")
// To get the error String directly, simply:
parse("abc") |> result.map_error(string_from_parsing_error)
// -> Error("Leading Major SemVer Integer part is missing.")
pub fn parse_loosely(
version: String,
) -> Result(SemVer, SemVerParseError)
Parse the given string into a SemVer
more loosely than
parse
.
Please see parse
for a baseline on how this function works, as
all inputs accepted by parse
are also accepted by
parse_loosely
.
The main additions over the behavior of parse
are as follows:
- will also accept a single leading
v
in the input (e.g.v1.2.3-pre+build
) - will accept missing Minor and/or Patch versions (e.g.
1-pre+build
) - will accept any non-alphanumeric character in the Pre-release and Build
parts as long as they are still prefixed by the usual
-
or+
.
See SemVerParseError
for possible error variants
returned by parse_loosely
.
Examples
parse_loosely("")
// -> Ok(SemVer(major: 0, minor; 0, patch: 0, pre: "", build: ""))
parse_loosely("v1-rc0")
// -> Ok(SemVer(major: 1, minor; 0, patch: 0, pre: "rc0", build: ""))
parse_loosely("v1..3")
// -> Ok(SemVer(major: 1, minor; 0, patch: 3, pre: "", build: ""))
parse_loosely("v1.2+2024~05~05")
// -> Ok(SemVer(major: 1, minor; 2, patch: 0, pre: "", build: "v1.2+2024~05~05"))
pub fn string_from_parsing_error(
error: SemVerParseError,
) -> String
Returns the inner String
from any SemVerParseError
type variant.
This is equivalent to simply using the_error.msg
.
Examples
string_from_parsing_error(EmptyInput("Input SemVer string is empty."))
// -> "Input SemVer string is empty."
pub fn to_string(ver: SemVer) -> String
Converts a SemVer
into a String
as defined on
semver.org.
Examples
to_string(SemVer(1, 2, 3, "rc0", "20240505"))
// -> "1.2.3-rc0+20240505"
Both the Pre-release (“-rc0”) and Build (“+20240505”) parts are optional:
to_string(SemVer(1, 2, 3, "rc0", ""))
// -> "1.2.3-rc0"
to_string(SemVer(1, 2, 3, "", "20240505"))
// -> "1.2.3+20240505"
pub fn to_string_concise(ver: SemVer) -> String
Converts a SemVer
into a String
as defined on semver.org.
Will omit the Minor.Patch
(second and third parts) if they are 0
.
Although its output will not be re-parseable using parse
,
it is still compatible with parse_loosely
.
Examples
to_string_concise(SemVer(1, 0, 0, "rc0", "20240505"))
// -> "1-rc0+20240505"
to_string_concise(SemVer(1, 2, 0, "rc0", "20240505"))
// -> "1.2-rc0+20240505"