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)

    Returned only by parse when the Minor part of a SemVer is missing. I.e. when there is no Minor part like 1..3 or 1-pre+build.

  • MissingPatch(msg: String)

    Returned only by parse when the Patch part of a SemVer is missing. I.e. when there is no Patch part like 1.2.+build or 1.2-pre+build.

  • MissingPreRelease(msg: String)

    Returned only by parse when the Pre-release part of a SemVer is missing despite it having its leading hyphen present. E.g: 1.2.3- or 1.2.3-+build.

  • MissingBuild(msg: String)

    Returned only by parse when the Build part of a SemVer is missing despite it having its leading plus present. E.g: 1.2.3+ or 1.2.3-rc0+.

  • MissingPreOrBuildSeparator(msg: String)

    Returned when there is no Pre-release or Build separators (- and +) between the Major.Minor.Patch core part of the SemVer and the rest. E.g: 1.2.3rc0 or 1.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)

    Returned only by parse when the Pre-release tag part of a SemVer contains unacceptable characters.

  • InvalidBuild(msg: String)

    Returned only by parse when the Build tag part of a SemVer contains unacceptable characters.

  • 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 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"
Search Document