Nanyx

Semantic version parsing

Parse a version string into a record with optional tag and build metadata.

module semanticVersion

type SemanticVersion = (
  major: int
  minor: int
  patch: int
  tag?: string
  buildMetadata?: string
)

-- Split a string at the first occurrence of a separator, if there is one
def splitOnce: (string, string) -> (string, #some(string) | #none) = { s, separator ->
  match s \indexOf(separator)
    | #some(i) -> (s \take(i), #some(s \drop(i + separator.length)))
    | #notFound -> (s, #none)
}

def parseNumber: string -> #some(int) | #error(#invalidVersionNumber) = { s ->
  Int.parse(s) \Result.mapError { _ -> #invalidVersionNumber }
}

-- "1.2.3-beta+build42" has core "1.2.3-beta" and build metadata "build42";
-- the core has numbers "1.2.3" and tag "beta"
export def parse: string -> #some(SemanticVersion) | #error(#invalidVersionNumber) = { value ->
  def (core, buildMetadata) = splitOnce(value, "+")
  def (numbers, tag) = splitOnce(core, "-")

  match numbers \split(".")
    | [major, minor, patch] -> proc {
        return SemanticVersion(
          major = try parseNumber(major)
          minor = try parseNumber(minor)
          patch = try parseNumber(patch)
          tag = tag
          buildMetadata = buildMetadata
        )
      }
    | _ -> #error(#invalidVersionNumber)
}

@entry
def main = {
  for ["1.2.3", "1.2.3-beta+build42", "1.2", "x.y.z"] | input ->
    match parse(input)
      | #some(v) -> println("{input}: major {v.major}, minor {v.minor}, patch {v.patch}")
      | #error(#invalidVersionNumber) -> println("{input}: not a version number")
}

This program lives in the repository as examples/semanticVersionParsing.nyx.