terraform/vendor/github.com/apparentlymart/go-versions/versions/parse.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),
}
}