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
parsewhen its input is empty. -
MissingMajor(msg: String)Returned when the Major part of a
SemVeris 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 theSemVerand the rest. E.g:1.2.3rc0or1.2.3build5. -
InvalidMajor(msg: String)Returned when the Integer Major part of a
SemVercannot be parsed. -
InvalidMinor(msg: String)Returned when the Integer Minor part of a
SemVercannot be parsed. -
InvalidPatch(msg: String)Returned when the Integer Patch part of a
SemVercannot 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 SemVers, 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
SemVers, 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 SemVers, 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
SemVers, 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
vin 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"