244 lines
8.2 KiB
Go
244 lines
8.2 KiB
Go
package versions
|
|
|
|
import (
|
|
"fmt"
|
|
|
|
"github.com/apparentlymart/go-versions/versions/constraints"
|
|
)
|
|
|
|
// ParseVersion attempts to parse the given string as a semantic version
|
|
// specification, and returns the result if successful.
|
|
//
|
|
// If the given string is not parseable then an error is returned that is
|
|
// suitable for display directly to a hypothetical end-user that provided this
|
|
// version string, as long as they can read English.
|
|
func ParseVersion(s string) (Version, error) {
|
|
spec, err := constraints.ParseExactVersion(s)
|
|
if err != nil {
|
|
return Unspecified, err
|
|
}
|
|
return versionFromExactVersionSpec(spec), nil
|
|
}
|
|
|
|
// MustParseVersion is the same as ParseVersion except that it will panic
|
|
// instead of returning an error.
|
|
func MustParseVersion(s string) Version {
|
|
v, err := ParseVersion(s)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
return v
|
|
}
|
|
|
|
// MeetingConstraints returns a version set that contains all of the versions
|
|
// that meet the given constraints, specified using the Spec type from the
|
|
// constraints package.
|
|
//
|
|
// The resulting Set has all pre-release versions excluded, except any that
|
|
// are explicitly mentioned as exact selections. For example, the constraint
|
|
// "2.0.0-beta1 || >2" contains 2.0.0-beta1 but not 2.0.0-beta2 or 3.0.0-beta1.
|
|
// This additional constraint on pre-releases can be avoided by calling
|
|
// MeetingConstraintsExact instead, at which point the caller can apply other
|
|
// logic to deal with prereleases.
|
|
//
|
|
// This function expects an internally-consistent Spec like what would be
|
|
// generated by that package's constraint parsers. Behavior is undefined --
|
|
// including the possibility of panics -- if specs are hand-created and the
|
|
// expected invariants aren't met.
|
|
func MeetingConstraints(spec constraints.Spec) Set {
|
|
exact := MeetingConstraintsExact(spec)
|
|
reqd := exact.AllRequested().List()
|
|
set := Intersection(Released, exact)
|
|
reqd = reqd.Filter(Prerelease)
|
|
if len(reqd) != 0 {
|
|
set = Union(Selection(reqd...), set)
|
|
}
|
|
return set
|
|
}
|
|
|
|
// MeetingConstraintsExact is like MeetingConstraints except that it doesn't
|
|
// apply the extra rules to exclude pre-release versions that are not
|
|
// explicitly requested.
|
|
//
|
|
// This means that given a constraint ">=1.0.0 <2.0.0" a hypothetical version
|
|
// 2.0.0-beta1 _is_ in the returned set, because prerelease versions have
|
|
// lower precedence than their corresponding release.
|
|
//
|
|
// A caller can use this to implement its own specialized handling of
|
|
// pre-release versions by applying additional set operations to the result,
|
|
// such as intersecting it with the predefined set versions.Released to
|
|
// remove prerelease versions altogether.
|
|
func MeetingConstraintsExact(spec constraints.Spec) Set {
|
|
if spec == nil {
|
|
return All
|
|
}
|
|
|
|
switch ts := spec.(type) {
|
|
|
|
case constraints.VersionSpec:
|
|
lowerBound, upperBound := ts.ConstraintBounds()
|
|
switch lowerBound.Operator {
|
|
case constraints.OpUnconstrained:
|
|
return All
|
|
case constraints.OpEqual:
|
|
return Only(versionFromExactVersionSpec(lowerBound.Boundary))
|
|
default:
|
|
return AtLeast(
|
|
versionFromExactVersionSpec(lowerBound.Boundary),
|
|
).Intersection(
|
|
OlderThan(versionFromExactVersionSpec(upperBound.Boundary)))
|
|
}
|
|
|
|
case constraints.SelectionSpec:
|
|
lower := ts.Boundary.ConstrainToZero()
|
|
if ts.Operator != constraints.OpEqual && ts.Operator != constraints.OpNotEqual {
|
|
lower.Metadata = "" // metadata is only considered for exact matches
|
|
}
|
|
|
|
switch ts.Operator {
|
|
case constraints.OpUnconstrained:
|
|
// Degenerate case, but we'll allow it.
|
|
return All
|
|
case constraints.OpMatch:
|
|
// The match operator uses the constraints implied by the
|
|
// Boundary version spec as the specification.
|
|
// Note that we discard "lower" in this case, because we do want
|
|
// to match our metadata if it's specified.
|
|
return MeetingConstraintsExact(ts.Boundary)
|
|
case constraints.OpEqual, constraints.OpNotEqual:
|
|
set := Only(versionFromExactVersionSpec(lower))
|
|
if ts.Operator == constraints.OpNotEqual {
|
|
// We want everything _except_ what's in our set, then.
|
|
set = All.Subtract(set)
|
|
}
|
|
return set
|
|
case constraints.OpGreaterThan:
|
|
return NewerThan(versionFromExactVersionSpec(lower))
|
|
case constraints.OpGreaterThanOrEqual:
|
|
return AtLeast(versionFromExactVersionSpec(lower))
|
|
case constraints.OpLessThan:
|
|
return OlderThan(versionFromExactVersionSpec(lower))
|
|
case constraints.OpLessThanOrEqual:
|
|
return AtMost(versionFromExactVersionSpec(lower))
|
|
case constraints.OpGreaterThanOrEqualMinorOnly:
|
|
upper := lower
|
|
upper.Major.Num++
|
|
upper.Minor.Num = 0
|
|
upper.Patch.Num = 0
|
|
upper.Prerelease = ""
|
|
return AtLeast(
|
|
versionFromExactVersionSpec(lower),
|
|
).Intersection(
|
|
OlderThan(versionFromExactVersionSpec(upper)))
|
|
case constraints.OpGreaterThanOrEqualPatchOnly:
|
|
upper := lower
|
|
upper.Minor.Num++
|
|
upper.Patch.Num = 0
|
|
upper.Prerelease = ""
|
|
return AtLeast(
|
|
versionFromExactVersionSpec(lower),
|
|
).Intersection(
|
|
OlderThan(versionFromExactVersionSpec(upper)))
|
|
default:
|
|
panic(fmt.Errorf("unsupported constraints.SelectionOp %s", ts.Operator))
|
|
}
|
|
|
|
case constraints.UnionSpec:
|
|
if len(ts) == 0 {
|
|
return All
|
|
}
|
|
if len(ts) == 1 {
|
|
return MeetingConstraintsExact(ts[0])
|
|
}
|
|
union := make(setUnion, len(ts))
|
|
for i, subSpec := range ts {
|
|
union[i] = MeetingConstraintsExact(subSpec).setI
|
|
}
|
|
return Set{setI: union}
|
|
|
|
case constraints.IntersectionSpec:
|
|
if len(ts) == 0 {
|
|
return All
|
|
}
|
|
if len(ts) == 1 {
|
|
return MeetingConstraintsExact(ts[0])
|
|
}
|
|
intersection := make(setIntersection, len(ts))
|
|
for i, subSpec := range ts {
|
|
intersection[i] = MeetingConstraintsExact(subSpec).setI
|
|
}
|
|
return Set{setI: intersection}
|
|
|
|
default:
|
|
// should never happen because the above cases are exhaustive for
|
|
// all valid constraint implementations.
|
|
panic(fmt.Errorf("unsupported constraints.Spec implementation %T", spec))
|
|
}
|
|
}
|
|
|
|
// MeetingConstraintsString attempts to parse the given spec as a constraints
|
|
// string in our canonical format, which is most similar to the syntax used by
|
|
// npm, Go's "dep" tool, Rust's "cargo", etc.
|
|
//
|
|
// This is a covenience wrapper around calling constraints.Parse and then
|
|
// passing the result to MeetingConstraints. Call into the constraints package
|
|
// yourself for access to the constraint tree.
|
|
//
|
|
// If unsuccessful, the error from the underlying parser is returned verbatim.
|
|
// Parser errors are suitable for showing to an end-user in situations where
|
|
// the given spec came from user input.
|
|
func MeetingConstraintsString(spec string) (Set, error) {
|
|
s, err := constraints.Parse(spec)
|
|
if err != nil {
|
|
return None, err
|
|
}
|
|
return MeetingConstraints(s), nil
|
|
}
|
|
|
|
// MeetingConstraintsStringRuby attempts to parse the given spec as a
|
|
// "Ruby-style" version constraint string, and returns the set of versions
|
|
// that match the constraint if successful.
|
|
//
|
|
// If unsuccessful, the error from the underlying parser is returned verbatim.
|
|
// Parser errors are suitable for showing to an end-user in situations where
|
|
// the given spec came from user input.
|
|
//
|
|
// "Ruby-style" here is not a promise of exact compatibility with rubygems
|
|
// or any other Ruby tools. Rather, it refers to this parser using a syntax
|
|
// that is intended to feel familiar to those who are familiar with rubygems
|
|
// syntax.
|
|
//
|
|
// Constraints are parsed in "multi" mode, allowing multiple comma-separated
|
|
// constraints that are combined with the Intersection operator. For more
|
|
// control over the parsing process, use the constraints package API directly
|
|
// and then call MeetingConstraints.
|
|
func MeetingConstraintsStringRuby(spec string) (Set, error) {
|
|
s, err := constraints.ParseRubyStyleMulti(spec)
|
|
if err != nil {
|
|
return None, err
|
|
}
|
|
return MeetingConstraints(s), nil
|
|
}
|
|
|
|
// MustMakeSet can be used to wrap any function that returns a set and an error
|
|
// to make it panic if an error occurs and return the set otherwise.
|
|
//
|
|
// This is intended for tests and other situations where input is from
|
|
// known-good constants.
|
|
func MustMakeSet(set Set, err error) Set {
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
return set
|
|
}
|
|
|
|
func versionFromExactVersionSpec(spec constraints.VersionSpec) Version {
|
|
return Version{
|
|
Major: spec.Major.Num,
|
|
Minor: spec.Minor.Num,
|
|
Patch: spec.Patch.Num,
|
|
Prerelease: VersionExtra(spec.Prerelease),
|
|
Metadata: VersionExtra(spec.Metadata),
|
|
}
|
|
}
|