ecluse:ecluse-core
Safe HaskellNone
LanguageGHC2021

Ecluse.Core.Version

Description

Version identity and ordering.

A Version carries the raw text verbatim (version strings are embedded in artifact URLs and re-served, so fidelity matters) alongside a parsed, canonical VersionKey -- present only when the raw text parses for its ecosystem. Ordering goes through compareVersions, which is defined only on parsed keys, so non-canonical text can never reach the comparator (parse, don't validate).

Parsing is per-ecosystem and selected by the Ecosystem tag from Ecluse.Core.Ecosystem: semver for npm (Ecluse.Core.Version.Semver), PEP 440 for PyPI (Ecluse.Core.Version.Pep440), Gem::Version for RubyGems (Ecluse.Core.Version.Gem). Each grammar and its ordering rules live in its own module; this module is the agnostic abstraction that dispatches to them on the Ecosystem tag. The grammar modules are kept private -- callers build with mkVersion (total) or parseVersionKey (reports the parse error) and compare with compareVersions.

This vocabulary is consumed by Ecluse.Core.Package (PackageDetails holds a Version) and the rules engine (Ecluse.Core.Rules). See docs/architecture/domain-model.mdVersion.

Synopsis

Versions

data Version Source #

A package version.

The raw text is kept verbatim for faithful round-trip (version strings are embedded in artifact URLs and re-served), while a parsed, canonical VersionKey -- present only when the raw text parses for its ecosystem -- is what ordering uses. Build with mkVersion (total: an unparseable version is still represented, just with no key, so a proxy never drops a version over a parser gap) or parseVersionKey when you want the parse error.

There is deliberately no Ord on Version: comparison goes through compareVersions, which is defined only on parsed keys, so non-canonical text can never reach the comparator.

Instances

Instances details
Show Version Source # 
Instance details

Defined in Ecluse.Core.Version

Eq Version Source # 
Instance details

Defined in Ecluse.Core.Version

Methods

(==) :: Version -> Version -> Bool #

(/=) :: Version -> Version -> Bool #

versionKey :: Version -> Maybe VersionKey Source #

The parsed, canonical ordering key; Nothing if the raw text could not be parsed for its ecosystem (ordering rules then abstain).

mkVersion :: Ecosystem -> Text -> Version Source #

Build a Version, parsing the raw text into a canonical key when possible. Total: a version that does not parse is still represented (with no key) rather than rejected, so a proxy never drops a version over a parser gap.

unVersion :: Version -> Text Source #

The raw version text.

renderVersion :: Version -> Text Source #

Render a version in wire form (the raw text).

compareVersions :: Version -> Version -> Maybe Ordering Source #

Compare two versions by their canonical keys. Nothing if either version did not parse (its key is absent) -- an ordering-based rule should then abstain, mirroring the other "unknown signal" cases (CodeExecUnknown, TrustUnknown).

Canonical ordering keys

data VersionKey Source #

The parsed, canonical, comparable form of a version. Opaque: the only way to obtain one is parseVersionKey, so a VersionKey always holds a well-formed, normalised version -- the comparator structurally cannot see non-canonical input (parse, don't validate). Its Ord is meaningful only within a single ecosystem, which is the only case that ever arises (one compares versions of one package).

parseVersionKey :: Ecosystem -> Text -> Either VersionError VersionKey Source #

Parse raw version text into a canonical VersionKey for its ecosystem, or report why it could not be parsed. This is the parsing boundary: downstream code holds a VersionKey and relies on it being valid.

newtype VersionError Source #

Why a version string failed to parse.

Constructors

VersionError 

Instances

Instances details
Show VersionError Source # 
Instance details

Defined in Ecluse.Core.Version

Eq VersionError Source # 
Instance details

Defined in Ecluse.Core.Version

isStable :: VersionKey -> Bool Source #

Whether a parsed version is a stable (final, non-prerelease) release. The notion is ecosystem-specific, dispatched on the key's constructor:

  • semver (npm) -- stable iff there is no -prerelease component (the prerelease is SemverFinal). So 1.0.0 is stable; 1.0.0-rc.1 and 2.0.0-beta are not.
  • PEP 440 (PyPI) -- stable iff it is neither a pre-release (a/b/rc) nor a dev release. Post-releases are stable. So 1.0 and 1.0.post1 are stable; 1.0a1, 1.0rc1, 1.0.dev1 and 1.0a1.dev2 are not.
  • RubyGems -- stable iff no segment contains a letter (the version is all-numeric). So 1.0.0 is stable; 1.0.0.pre and 1.2.0.rc1 are not.

Used by selectLatest to prefer a stable release when dist-tags.latest must be repointed.

>>> isStable <$> parseVersionKey Npm "1.0.0"
Right True
>>> isStable <$> parseVersionKey Npm "1.0.0-rc.1"
Right False
>>> isStable <$> parseVersionKey PyPI "1.0.post1"
Right True
>>> isStable <$> parseVersionKey PyPI "1.0a1.dev2"
Right False
>>> isStable <$> parseVersionKey RubyGems "1.0.0.pre"
Right False

Resolving dist-tags.latest

selectLatest :: Maybe Version -> [Version] -> Maybe Version Source #

Resolve dist-tags.latest for a packument after denied/undecidable versions have been filtered out -- the keep-unless-denied, stable-preferring rule from docs/architecture/rules-engine.md ("Applying verdicts to a packument"). chosen is the source's currently-tagged latest (if any); survivors is the surviving versions. The result, when present, is always one of survivors, so the caller can use its unVersion as the tag string.

The resolution, in order:

  • If survivors is empty, there is nothing to point at -- Nothing.
  • Keep: if chosen survives (by raw text), return it unchanged. This is the identity on a single-input packument and never promotes a prerelease over a maintainer's chosen stable latest.
  • Repoint (only when the chosen latest did not survive): among survivors with a parseable key, prefer the maximum stable one; if none are stable, the maximum prerelease one. (Within one ecosystem parseable keys are totally ordered, so compareVersions is total over them.)
  • No parseable survivor: to keep the result naming a present version, fall back to the lexicographically-smallest survivor by unVersion. An unparseable version never outranks a parseable one.