go get github.com/apparentlymart/go-versions@master

This commit is contained in:
Martin Atkins 2019-12-19 14:57:33 -08:00
parent 18dd1bb4d6
commit eda57670ce
27 changed files with 3095 additions and 0 deletions

1
go.mod
View File

@ -13,6 +13,7 @@ require (
github.com/aliyun/aliyun-tablestore-go-sdk v4.1.2+incompatible
github.com/apparentlymart/go-cidr v1.0.1
github.com/apparentlymart/go-dump v0.0.0-20190214190832-042adf3cf4a0
github.com/apparentlymart/go-versions v0.0.2-0.20180815153302-64b99f7cb171
github.com/armon/circbuf v0.0.0-20190214190532-5111143e8da2
github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da // indirect
github.com/armon/go-radix v1.0.0 // indirect

3
go.sum
View File

@ -69,6 +69,8 @@ github.com/apparentlymart/go-dump v0.0.0-20190214190832-042adf3cf4a0 h1:MzVXffFU
github.com/apparentlymart/go-dump v0.0.0-20190214190832-042adf3cf4a0/go.mod h1:oL81AME2rN47vu18xqj1S1jPIPuN7afo62yKTNn3XMM=
github.com/apparentlymart/go-textseg v1.0.0 h1:rRmlIsPEEhUTIKQb7T++Nz/A5Q6C9IuX2wFoYVvnCs0=
github.com/apparentlymart/go-textseg v1.0.0/go.mod h1:z96Txxhf3xSFMPmb5X/1W05FF/Nj9VFpLOpjS5yuumk=
github.com/apparentlymart/go-versions v0.0.2-0.20180815153302-64b99f7cb171 h1:19Seu/H5gq3Ugtx+CGenwF89SDG3S1REX5i6PJj3RK4=
github.com/apparentlymart/go-versions v0.0.2-0.20180815153302-64b99f7cb171/go.mod h1:JXY95WvQrPJQtudvNARshgWajS7jNNlM90altXIPNyI=
github.com/armon/circbuf v0.0.0-20190214190532-5111143e8da2 h1:7Ip0wMmLHLRJdrloDxZfhMm0xrLXZS8+COSu2bXmEQs=
github.com/armon/circbuf v0.0.0-20190214190532-5111143e8da2/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o=
github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da h1:8GUt8eRujhVEGZFFEjBj46YV4rDjvGrNxb0KMWYkL2I=
@ -131,6 +133,7 @@ github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeME
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/go-test/deep v1.0.1/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA=
github.com/go-test/deep v1.0.3 h1:ZrJSEWsXzPOxaZnFteGEfooLba+ju3FYIbOrS+rQd68=
github.com/go-test/deep v1.0.3/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA=
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=

21
vendor/github.com/apparentlymart/go-versions/LICENSE generated vendored Normal file
View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2018 Martin Atkins
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@ -0,0 +1,352 @@
package constraints
import (
"fmt"
"strings"
)
// Parse parses a constraint string using a syntax similar to that used by
// npm, Go "dep", Rust's "cargo", etc. Exact compatibility with any of these
// systems is not guaranteed, but instead we aim for familiarity in the choice
// of operators and their meanings. The syntax described here is considered the
// canonical syntax for this package, but a Ruby-style syntax is also offered
// via the function "ParseRubyStyle".
//
// A constraint string is a sequence of selection sets delimited by ||, with
// each selection set being a whitespace-delimited sequence of selections.
// Each selection is then the combination of a matching operator and a boundary
// version. The following is an example of a complex constraint string
// illustrating all of these features:
//
// >=1.0.0 <2.0.0 || 1.0.0-beta1 || =2.0.2
//
// In practice constraint strings are usually simpler than this, but this
// complex example allows us to identify each of the parts by example:
//
// Selection Sets: ">=1.0.0 <2.0.0"
// "1.0.0-beta1"
// "=2.0.2"
// Selections: ">=1.0.0"
// "<2.0.0"
// "1.0.0-beta1"
// "=2.0.2"
// Matching Operators: ">=", "<", "=" are explicit operators
// "1.0.0-beta1" has an implicit "=" operator
// Boundary Versions: "1.0.0", "2.0.0", "1.0.0-beta1", "2.0.2"
//
// A constraint string describes the members of a version set by adding exact
// versions or ranges of versions to that set. A version is in the set if
// any one of the selection sets match that version. A selection set matches
// a version if all of its selections match that version. A selection matches
// a version if the version has the indicated relationship with the given
// boundary version.
//
// In the above example, the first selection set matches all released versions
// whose major segment is 1, since both selections must apply. However, the
// remaining two selection sets describe two specific versions outside of that
// range that are also admitted, in addition to those in the indicated range.
//
// The available matching operators are:
//
// < Less than
// <= Less than or equal
// > Greater than
// >= Greater than or equal
// = Equal
// ! Not equal
// ~ Greater than with implied upper limit (described below)
// ^ Greater than excluding new major releases (described below)
//
// If no operator is specified, the operator is implied to be "equal" for a
// full version specification, or a special additional "match" operator for
// a version containing wildcards as described below.
//
// The "~" matching operator is a shorthand for expressing both a lower and
// upper limit within a single expression. The effect of this operator depends
// on how many segments are specified in the boundary version: if only one
// segment is specified then new minor and patch versions are accepted, whereas
// if two or three segments are specified then only patch versions are accepted.
// For example:
//
// ~1 is equivalent to >=1.0.0 <2.0.0
// ~1.0 is equivalent to >=1.0.0 <1.1.0
// ~1.2 is equivalent to >=1.2.0 <1.3.0
// ~1.2.0 is equivalent to >=1.2.0 <1.3.0
// ~1.2.3 is equivalent to >=1.2.3 <1.3.0
//
// The "^" matching operator is similar to "~" except that it always constrains
// only the major version number. It has an additional special behavior for
// when the major version number is zero: in that case, the minor release
// number is constrained, reflecting the common semver convention that initial
// development releases mark breaking changes by incrementing the minor version.
// For example:
//
// ^1 is equivalent to >=1.0.0 <2.0.0
// ^1.2 is equivalent to >=1.2.0 <2.0.0
// ^1.2.3 is equivalent to >=1.2.3 <2.0.0
// ^0.1.0 is equivalent to >=0.1.0 <0.2.0
// ^0.1.2 is equivalent to >=0.1.2 <0.2.0
//
// The boundary version can contain wildcards for the major, minor or patch
// segments, which are specified using the markers "*", "x", or "X". When used
// in a selection with no explicit operator, these specify the implied "match"
// operator and define ranges with similar meaning to the "~" and "^" operators:
//
// 1.* is equivalent to >=1.0.0 <2.0.0
// 1.*.* is equivalent to >=1.0.0 <2.0.0
// 1.0.* is equivalent to >=1.0.0 <1.1.0
//
// When wildcards are used, the first segment specified as a wildcard implies
// that all of the following segments are also wildcards. A version
// specification like "1.*.2" is invalid, because a wildcard minor version
// implies that the patch version must also be a wildcard.
//
// Wildcards have no special meaning when used with explicit operators, and so
// they are merely replaced with zeros in such cases.
//
// Explicit range syntax using a hyphen creates inclusive upper and lower
// bounds:
//
// 1.0.0 - 2.0.0 is equivalent to >=1.0.0 <=2.0.0
// 1.2.3 - 2.3.4 is equivalent to >=1.2.3 <=2.3.4
//
// Requests of exact pre-release versions with the equals operator have
// no special meaning to the constraint parser, but are interpreted as explicit
// requests for those versions when interpreted by the MeetingConstraints
// function (and related functions) in the "versions" package, in the parent
// directory. Pre-release versions that are not explicitly requested are
// excluded from selection so that e.g. "^1.0.0" will not match a version
// "2.0.0-beta.1".
//
// The result is always a UnionSpec, whose members are IntersectionSpecs
// each describing one selection set. In the common case where a string
// contains only one selection, both the UnionSpec and the IntersectionSpec
// will have only one element and can thus be effectively ignored by the
// caller. (Union and intersection of single sets are both no-op.)
// A valid string must contain at least one selection; if an empty selection
// is to be considered as either "no versions" or "all versions" then this
// special case must be handled by the caller prior to calling this function.
//
// If there are syntax errors or ambiguities in the provided string then an
// error is returned. All errors returned by this function are suitable for
// display to English-speaking end-users, and avoid any Go-specific
// terminology.
func Parse(str string) (UnionSpec, error) {
str = strings.TrimSpace(str)
if str == "" {
return nil, fmt.Errorf("empty specification")
}
// Most constraint strings contain only one selection, so we'll
// allocate under that assumption and re-allocate if needed.
uspec := make(UnionSpec, 0, 1)
ispec := make(IntersectionSpec, 0, 1)
remain := str
for {
var selection SelectionSpec
var err error
selection, remain, err = parseSelection(remain)
if err != nil {
return nil, err
}
remain = strings.TrimSpace(remain)
if len(remain) > 0 && remain[0] == '-' {
// Looks like user wants to make a range expression, so we'll
// look for another selection.
remain = strings.TrimSpace(remain[1:])
if remain == "" {
return nil, fmt.Errorf(`operator "-" must be followed by another version selection to specify the upper limit of the range`)
}
var lower, upper SelectionSpec
lower = selection
upper, remain, err = parseSelection(remain)
remain = strings.TrimSpace(remain)
if err != nil {
return nil, err
}
if lower.Operator != OpUnconstrained {
return nil, fmt.Errorf(`lower bound of range specified with "-" operator must be an exact version`)
}
if upper.Operator != OpUnconstrained {
return nil, fmt.Errorf(`upper bound of range specified with "-" operator must be an exact version`)
}
lower.Operator = OpGreaterThanOrEqual
lower.Boundary = lower.Boundary.ConstrainToZero()
if upper.Boundary.IsExact() {
upper.Operator = OpLessThanOrEqual
} else {
upper.Operator = OpLessThan
upper.Boundary = upper.Boundary.ConstrainToUpperBound()
}
ispec = append(ispec, lower, upper)
} else {
if selection.Operator == OpUnconstrained {
// Select a default operator based on whether the version
// specification contains wildcards.
if selection.Boundary.IsExact() {
selection.Operator = OpEqual
} else {
selection.Operator = OpMatch
}
}
if selection.Operator != OpMatch {
switch selection.Operator {
case OpMatch:
// nothing to do
case OpLessThanOrEqual:
if !selection.Boundary.IsExact() {
selection.Operator = OpLessThan
selection.Boundary = selection.Boundary.ConstrainToUpperBound()
}
case OpGreaterThan:
if !selection.Boundary.IsExact() {
// If "greater than" has an imprecise boundary then we'll
// turn it into a "greater than or equal to" and use the
// upper bound of the boundary, so e.g.:
// >1.*.* means >=2.0.0, because that's greater than
// everything matched by 1.*.*.
selection.Operator = OpGreaterThanOrEqual
selection.Boundary = selection.Boundary.ConstrainToUpperBound()
}
default:
selection.Boundary = selection.Boundary.ConstrainToZero()
}
}
ispec = append(ispec, selection)
}
if len(remain) == 0 {
// All done!
break
}
if remain[0] == ',' {
return nil, fmt.Errorf(`commas are not needed to separate version selections; separate with spaces instead`)
}
if remain[0] == '|' {
if !strings.HasPrefix(remain, "||") {
// User was probably trying for "||", so we'll produce a specialized error
return nil, fmt.Errorf(`single "|" is not a valid operator; did you mean "||" to specify an alternative?`)
}
remain = strings.TrimSpace(remain[2:])
if remain == "" {
return nil, fmt.Errorf(`operator "||" must be followed by another version selection`)
}
// Begin a new IntersectionSpec, added to our single UnionSpec
uspec = append(uspec, ispec)
ispec = make(IntersectionSpec, 0, 1)
}
}
uspec = append(uspec, ispec)
return uspec, nil
}
// parseSelection parses one canon-style selection from the prefix of the
// given string, returning the result along with the remaining unconsumed
// string for the caller to use for further processing.
func parseSelection(str string) (SelectionSpec, string, error) {
raw, remain := scanConstraint(str)
var spec SelectionSpec
if len(str) == len(remain) {
if len(remain) > 0 && remain[0] == 'v' {
// User seems to be trying to use a "v" prefix, like "v1.0.0"
return spec, remain, fmt.Errorf(`a "v" prefix should not be used when specifying versions`)
}
// If we made no progress at all then the selection must be entirely invalid.
return spec, remain, fmt.Errorf("the sequence %q is not valid", remain)
}
switch raw.op {
case "":
// We'll deal with this situation in the caller
spec.Operator = OpUnconstrained
case "=":
spec.Operator = OpEqual
case "!":
spec.Operator = OpNotEqual
case ">":
spec.Operator = OpGreaterThan
case ">=":
spec.Operator = OpGreaterThanOrEqual
case "<":
spec.Operator = OpLessThan
case "<=":
spec.Operator = OpLessThanOrEqual
case "~":
if raw.numCt > 1 {
spec.Operator = OpGreaterThanOrEqualPatchOnly
} else {
spec.Operator = OpGreaterThanOrEqualMinorOnly
}
case "^":
if len(raw.nums[0]) > 0 && raw.nums[0][0] == '0' {
// Special case for major version 0, which is initial development:
// we treat the minor number as if it's the major number.
spec.Operator = OpGreaterThanOrEqualPatchOnly
} else {
spec.Operator = OpGreaterThanOrEqualMinorOnly
}
case "=<":
return spec, remain, fmt.Errorf("invalid constraint operator %q; did you mean \"<=\"?", raw.op)
case "=>":
return spec, remain, fmt.Errorf("invalid constraint operator %q; did you mean \">=\"?", raw.op)
default:
return spec, remain, fmt.Errorf("invalid constraint operator %q", raw.op)
}
if raw.sep != "" {
return spec, remain, fmt.Errorf("no spaces allowed after operator %q", raw.op)
}
if raw.numCt > 3 {
return spec, remain, fmt.Errorf("too many numbered portions; only three are allowed (major, minor, patch)")
}
// Unspecified portions are either zero or wildcard depending on whether
// any explicit wildcards are present.
seenWild := false
for i, s := range raw.nums {
switch {
case isWildcardNum(s):
seenWild = true
case i >= raw.numCt:
if seenWild {
raw.nums[i] = "*"
} else {
raw.nums[i] = "0"
}
default:
// If we find a non-wildcard after we've already seen a wildcard
// then this specification is inconsistent, which is an error.
if seenWild {
return spec, remain, fmt.Errorf("can't use exact %s segment after a previous segment was wildcard", rawNumNames[i])
}
}
}
if seenWild {
if raw.pre != "" {
return spec, remain, fmt.Errorf(`can't use prerelease segment (introduced by "-") in a version with wildcards`)
}
if raw.meta != "" {
return spec, remain, fmt.Errorf(`can't use build metadata segment (introduced by "+") in a version with wildcards`)
}
}
spec.Boundary = raw.VersionSpec()
return spec, remain, nil
}

View File

@ -0,0 +1,16 @@
// Code generated by "stringer -type ConstraintDepth"; DO NOT EDIT.
package constraints
import "strconv"
const _ConstraintDepth_name = "UnconstrainedConstrainedMajorConstrainedMinorConstrainedPatch"
var _ConstraintDepth_index = [...]uint8{0, 13, 29, 45, 61}
func (i ConstraintDepth) String() string {
if i < 0 || i >= ConstraintDepth(len(_ConstraintDepth_index)-1) {
return "ConstraintDepth(" + strconv.FormatInt(int64(i), 10) + ")"
}
return _ConstraintDepth_name[_ConstraintDepth_index[i]:_ConstraintDepth_index[i+1]]
}

View File

@ -0,0 +1,13 @@
// Package constraints contains a high-level representation of version
// constraints that retains enough information for direct analysis and
// serialization as a string.
//
// The package also contains parsers to produce that representation from
// various compact constraint specification formats.
//
// The main "versions" package, available in the parent directory, can consume
// the high-level constraint representation from this package to construct
// a version set that contains all versions meeting the given constraints.
// Package "constraints" does not contain any functionalty for checking versions
// against constraints since that is provided by package "versions".
package constraints

View File

@ -0,0 +1,74 @@
package constraints
import (
"strconv"
)
//go:generate ragel -G1 -Z raw_scan.rl
//go:generate gofmt -w raw_scan.go
// rawConstraint is a tokenization of a constraint string, used internally
// as the first layer of parsing.
type rawConstraint struct {
op string
sep string
nums [3]string
numCt int
pre string
meta string
}
// VersionSpec turns the receiver into a VersionSpec in a reasonable
// default way. This method assumes that the raw constraint was already
// validated, and will panic or produce undefined results if it contains
// anything invalid.
//
// In particular, numbers are automatically marked as unconstrained if they
// are omitted or set to wildcards, so the caller must apply any additional
// validation rules on the usage of unconstrained numbers before calling.
func (raw rawConstraint) VersionSpec() VersionSpec {
return VersionSpec{
Major: parseRawNumConstraint(raw.nums[0]),
Minor: parseRawNumConstraint(raw.nums[1]),
Patch: parseRawNumConstraint(raw.nums[2]),
Prerelease: raw.pre,
Metadata: raw.meta,
}
}
var rawNumNames = [...]string{"major", "minor", "patch"}
func isWildcardNum(s string) bool {
switch s {
case "*", "x", "X":
return true
default:
return false
}
}
// parseRawNum parses a raw number string which the caller has already
// determined is non-empty and non-wildcard. If the string is not numeric
// then this function will panic.
func parseRawNum(s string) uint64 {
v, err := strconv.ParseUint(s, 10, 64)
if err != nil {
panic(err)
}
return v
}
// parseRawNumConstraint parses a raw number into a NumConstraint, setting it
// to unconstrained if the value is empty or a wildcard.
func parseRawNumConstraint(s string) NumConstraint {
switch {
case s == "" || isWildcardNum(s):
return NumConstraint{
Unconstrained: true,
}
default:
return NumConstraint{
Num: parseRawNum(s),
}
}
}

View File

@ -0,0 +1,623 @@
// line 1 "raw_scan.rl"
// This file is generated from raw_scan.rl. DO NOT EDIT.
// line 5 "raw_scan.rl"
package constraints
// line 12 "raw_scan.go"
var _scan_eof_actions []byte = []byte{
0, 1, 1, 7, 9, 9, 9, 11,
14, 15, 11,
}
const scan_start int = 1
const scan_first_final int = 7
const scan_error int = 0
const scan_en_main int = 1
// line 11 "raw_scan.rl"
func scanConstraint(data string) (rawConstraint, string) {
var constraint rawConstraint
var numIdx int
var extra string
// Ragel state
p := 0 // "Pointer" into data
pe := len(data) // End-of-data "pointer"
cs := 0 // constraint state (will be initialized by ragel-generated code)
ts := 0
te := 0
eof := pe
// Keep Go compiler happy even if generated code doesn't use these
_ = ts
_ = te
_ = eof
// line 47 "raw_scan.go"
{
cs = scan_start
}
// line 52 "raw_scan.go"
{
if p == pe {
goto _test_eof
}
if cs == 0 {
goto _out
}
_resume:
switch cs {
case 1:
switch data[p] {
case 32:
goto tr1
case 42:
goto tr2
case 46:
goto tr3
case 88:
goto tr2
case 120:
goto tr2
}
switch {
case data[p] < 48:
if 9 <= data[p] && data[p] <= 13 {
goto tr1
}
case data[p] > 57:
switch {
case data[p] > 90:
if 97 <= data[p] && data[p] <= 122 {
goto tr3
}
case data[p] >= 65:
goto tr3
}
default:
goto tr4
}
goto tr0
case 2:
switch data[p] {
case 32:
goto tr6
case 42:
goto tr7
case 46:
goto tr3
case 88:
goto tr7
case 120:
goto tr7
}
switch {
case data[p] < 48:
if 9 <= data[p] && data[p] <= 13 {
goto tr6
}
case data[p] > 57:
switch {
case data[p] > 90:
if 97 <= data[p] && data[p] <= 122 {
goto tr3
}
case data[p] >= 65:
goto tr3
}
default:
goto tr8
}
goto tr5
case 3:
switch data[p] {
case 32:
goto tr10
case 42:
goto tr11
case 88:
goto tr11
case 120:
goto tr11
}
switch {
case data[p] > 13:
if 48 <= data[p] && data[p] <= 57 {
goto tr12
}
case data[p] >= 9:
goto tr10
}
goto tr9
case 0:
goto _out
case 7:
switch data[p] {
case 43:
goto tr19
case 45:
goto tr20
case 46:
goto tr21
}
goto tr18
case 4:
switch {
case data[p] < 48:
if 45 <= data[p] && data[p] <= 46 {
goto tr14
}
case data[p] > 57:
switch {
case data[p] > 90:
if 97 <= data[p] && data[p] <= 122 {
goto tr14
}
case data[p] >= 65:
goto tr14
}
default:
goto tr14
}
goto tr13
case 8:
switch {
case data[p] < 48:
if 45 <= data[p] && data[p] <= 46 {
goto tr14
}
case data[p] > 57:
switch {
case data[p] > 90:
if 97 <= data[p] && data[p] <= 122 {
goto tr14
}
case data[p] >= 65:
goto tr14
}
default:
goto tr14
}
goto tr22
case 5:
switch {
case data[p] < 48:
if 45 <= data[p] && data[p] <= 46 {
goto tr15
}
case data[p] > 57:
switch {
case data[p] > 90:
if 97 <= data[p] && data[p] <= 122 {
goto tr15
}
case data[p] >= 65:
goto tr15
}
default:
goto tr15
}
goto tr13
case 9:
if data[p] == 43 {
goto tr24
}
switch {
case data[p] < 48:
if 45 <= data[p] && data[p] <= 46 {
goto tr15
}
case data[p] > 57:
switch {
case data[p] > 90:
if 97 <= data[p] && data[p] <= 122 {
goto tr15
}
case data[p] >= 65:
goto tr15
}
default:
goto tr15
}
goto tr23
case 6:
switch data[p] {
case 42:
goto tr16
case 88:
goto tr16
case 120:
goto tr16
}
if 48 <= data[p] && data[p] <= 57 {
goto tr17
}
goto tr13
case 10:
switch data[p] {
case 43:
goto tr19
case 45:
goto tr20
case 46:
goto tr21
}
if 48 <= data[p] && data[p] <= 57 {
goto tr25
}
goto tr18
}
tr3:
cs = 0
goto f0
tr9:
cs = 0
goto f6
tr13:
cs = 0
goto f8
tr18:
cs = 0
goto f10
tr22:
cs = 0
goto f13
tr23:
cs = 0
goto f14
tr5:
cs = 2
goto _again
tr0:
cs = 2
goto f1
tr10:
cs = 3
goto _again
tr1:
cs = 3
goto f2
tr6:
cs = 3
goto f4
tr19:
cs = 4
goto f11
tr24:
cs = 4
goto f15
tr20:
cs = 5
goto f11
tr21:
cs = 6
goto f12
tr2:
cs = 7
goto f3
tr7:
cs = 7
goto f5
tr11:
cs = 7
goto f7
tr16:
cs = 7
goto f9
tr14:
cs = 8
goto _again
tr15:
cs = 9
goto _again
tr25:
cs = 10
goto _again
tr4:
cs = 10
goto f3
tr8:
cs = 10
goto f5
tr12:
cs = 10
goto f7
tr17:
cs = 10
goto f9
f9:
// line 38 "raw_scan.rl"
ts = p
goto _again
f12:
// line 52 "raw_scan.rl"
te = p
constraint.numCt++
if numIdx < len(constraint.nums) {
constraint.nums[numIdx] = data[ts:p]
numIdx++
}
goto _again
f8:
// line 71 "raw_scan.rl"
extra = data[p:]
goto _again
f1:
// line 33 "raw_scan.rl"
numIdx = 0
constraint = rawConstraint{}
// line 38 "raw_scan.rl"
ts = p
goto _again
f4:
// line 42 "raw_scan.rl"
te = p
constraint.op = data[ts:p]
// line 38 "raw_scan.rl"
ts = p
goto _again
f7:
// line 47 "raw_scan.rl"
te = p
constraint.sep = data[ts:p]
// line 38 "raw_scan.rl"
ts = p
goto _again
f6:
// line 47 "raw_scan.rl"
te = p
constraint.sep = data[ts:p]
// line 71 "raw_scan.rl"
extra = data[p:]
goto _again
f11:
// line 52 "raw_scan.rl"
te = p
constraint.numCt++
if numIdx < len(constraint.nums) {
constraint.nums[numIdx] = data[ts:p]
numIdx++
}
// line 38 "raw_scan.rl"
ts = p
goto _again
f10:
// line 52 "raw_scan.rl"
te = p
constraint.numCt++
if numIdx < len(constraint.nums) {
constraint.nums[numIdx] = data[ts:p]
numIdx++
}
// line 71 "raw_scan.rl"
extra = data[p:]
goto _again
f15:
// line 61 "raw_scan.rl"
te = p
constraint.pre = data[ts+1 : p]
// line 38 "raw_scan.rl"
ts = p
goto _again
f14:
// line 61 "raw_scan.rl"
te = p
constraint.pre = data[ts+1 : p]
// line 71 "raw_scan.rl"
extra = data[p:]
goto _again
f13:
// line 66 "raw_scan.rl"
te = p
constraint.meta = data[ts+1 : p]
// line 71 "raw_scan.rl"
extra = data[p:]
goto _again
f2:
// line 33 "raw_scan.rl"
numIdx = 0
constraint = rawConstraint{}
// line 38 "raw_scan.rl"
ts = p
// line 42 "raw_scan.rl"
te = p
constraint.op = data[ts:p]
goto _again
f5:
// line 42 "raw_scan.rl"
te = p
constraint.op = data[ts:p]
// line 38 "raw_scan.rl"
ts = p
// line 47 "raw_scan.rl"
te = p
constraint.sep = data[ts:p]
goto _again
f0:
// line 42 "raw_scan.rl"
te = p
constraint.op = data[ts:p]
// line 47 "raw_scan.rl"
te = p
constraint.sep = data[ts:p]
// line 71 "raw_scan.rl"
extra = data[p:]
goto _again
f3:
// line 33 "raw_scan.rl"
numIdx = 0
constraint = rawConstraint{}
// line 38 "raw_scan.rl"
ts = p
// line 42 "raw_scan.rl"
te = p
constraint.op = data[ts:p]
// line 47 "raw_scan.rl"
te = p
constraint.sep = data[ts:p]
goto _again
_again:
if cs == 0 {
goto _out
}
if p++; p != pe {
goto _resume
}
_test_eof:
{
}
if p == eof {
switch _scan_eof_actions[cs] {
case 9:
// line 71 "raw_scan.rl"
extra = data[p:]
case 7:
// line 47 "raw_scan.rl"
te = p
constraint.sep = data[ts:p]
// line 71 "raw_scan.rl"
extra = data[p:]
case 11:
// line 52 "raw_scan.rl"
te = p
constraint.numCt++
if numIdx < len(constraint.nums) {
constraint.nums[numIdx] = data[ts:p]
numIdx++
}
// line 71 "raw_scan.rl"
extra = data[p:]
case 15:
// line 61 "raw_scan.rl"
te = p
constraint.pre = data[ts+1 : p]
// line 71 "raw_scan.rl"
extra = data[p:]
case 14:
// line 66 "raw_scan.rl"
te = p
constraint.meta = data[ts+1 : p]
// line 71 "raw_scan.rl"
extra = data[p:]
case 1:
// line 42 "raw_scan.rl"
te = p
constraint.op = data[ts:p]
// line 47 "raw_scan.rl"
te = p
constraint.sep = data[ts:p]
// line 71 "raw_scan.rl"
extra = data[p:]
// line 610 "raw_scan.go"
}
}
_out:
{
}
}
// line 92 "raw_scan.rl"
return constraint, extra
}

View File

@ -0,0 +1,95 @@
// This file is generated from raw_scan.rl. DO NOT EDIT.
%%{
# (except you are actually in raw_scan.rl here, so edit away!)
machine scan;
}%%
package constraints
%%{
write data;
}%%
func scanConstraint(data string) (rawConstraint, string) {
var constraint rawConstraint
var numIdx int
var extra string
// Ragel state
p := 0 // "Pointer" into data
pe := len(data) // End-of-data "pointer"
cs := 0 // constraint state (will be initialized by ragel-generated code)
ts := 0
te := 0
eof := pe
// Keep Go compiler happy even if generated code doesn't use these
_ = ts
_ = te
_ = eof
%%{
action enterConstraint {
numIdx = 0
constraint = rawConstraint{}
}
action ts {
ts = p
}
action finishOp {
te = p
constraint.op = data[ts:p]
}
action finishSep {
te = p
constraint.sep = data[ts:p]
}
action finishNum {
te = p
constraint.numCt++
if numIdx < len(constraint.nums) {
constraint.nums[numIdx] = data[ts:p]
numIdx++
}
}
action finishPre {
te = p
constraint.pre = data[ts+1:p]
}
action finishMeta {
te = p
constraint.meta = data[ts+1:p]
}
action finishExtra {
extra = data[p:]
}
num = (digit+ | '*' | 'x' | 'X') >ts %finishNum %err(finishNum) %eof(finishNum);
op = ((any - (digit | space | alpha | '.' | '*'))**) >ts %finishOp %err(finishOp) %eof(finishOp);
likelyOp = ('^' | '>' | '<' | '-' | '~' | '!');
sep = (space**) >ts %finishSep %err(finishSep) %eof(finishSep);
nums = (num ('.' num)*);
extraStr = (alnum | '.' | '-')+;
pre = ('-' extraStr) >ts %finishPre %err(finishPre) %eof(finishPre);
meta = ('+' extraStr) >ts %finishMeta %err(finishMeta) %eof(finishMeta);
constraint = (op sep nums pre? meta?) >enterConstraint;
main := (constraint) @/finishExtra %/finishExtra $!finishExtra;
write init;
write exec;
}%%
return constraint, extra
}

View File

@ -0,0 +1,181 @@
package constraints
import (
"fmt"
"strings"
)
// ParseRubyStyle parses a single selection constraint using a syntax similar
// to that used by rubygems and other Ruby tools.
//
// Exact compatibility with rubygems is not guaranteed; "ruby-style" here
// just means that users familiar with rubygems should find familiar the choice
// of operators and their meanings.
//
// ParseRubyStyle parses only a single specification, mimicking the usual
// rubygems approach of providing each selection as a separate string.
// The result can be combined with other results to create an IntersectionSpec
// that describes the effect of multiple such constraints.
func ParseRubyStyle(str string) (SelectionSpec, error) {
if strings.TrimSpace(str) == "" {
return SelectionSpec{}, fmt.Errorf("empty specification")
}
spec, remain, err := parseRubyStyle(str)
if err != nil {
return spec, err
}
if remain != "" {
remain = strings.TrimSpace(remain)
switch {
case remain == "":
return spec, fmt.Errorf("extraneous spaces at end of specification")
case strings.HasPrefix(remain, "v"):
// User seems to be trying to use a "v" prefix, like "v1.0.0"
return spec, fmt.Errorf(`a "v" prefix should not be used`)
case strings.HasPrefix(remain, "||") || strings.HasPrefix(remain, ","):
// User seems to be trying to specify multiple constraints
return spec, fmt.Errorf(`only one constraint may be specified`)
case strings.HasPrefix(remain, "-"):
// User seems to be trying to use npm-style range constraints
return spec, fmt.Errorf(`range constraints are not supported`)
default:
return spec, fmt.Errorf("invalid characters %q", remain)
}
}
return spec, nil
}
// ParseRubyStyleAll is a helper wrapper around ParseRubyStyle that accepts
// multiple selection strings and combines them together into a single
// IntersectionSpec.
func ParseRubyStyleAll(strs ...string) (IntersectionSpec, error) {
spec := make(IntersectionSpec, 0, len(strs))
for _, str := range strs {
subSpec, err := ParseRubyStyle(str)
if err != nil {
return nil, fmt.Errorf("invalid specification %q: %s", str, err)
}
spec = append(spec, subSpec)
}
return spec, nil
}
// ParseRubyStyleMulti is similar to ParseRubyStyle, but rather than parsing
// only a single selection specification it instead expects one or more
// comma-separated specifications, returning the result as an
// IntersectionSpec.
func ParseRubyStyleMulti(str string) (IntersectionSpec, error) {
var spec IntersectionSpec
remain := strings.TrimSpace(str)
for remain != "" {
if strings.TrimSpace(remain) == "" {
break
}
var subSpec SelectionSpec
var err error
var newRemain string
subSpec, newRemain, err = parseRubyStyle(remain)
consumed := remain[:len(remain)-len(newRemain)]
if err != nil {
return nil, fmt.Errorf("invalid specification %q: %s", consumed, err)
}
remain = strings.TrimSpace(newRemain)
if remain != "" {
if !strings.HasPrefix(remain, ",") {
return nil, fmt.Errorf("missing comma after %q", consumed)
}
// Eat the separator comma
remain = strings.TrimSpace(remain[1:])
}
spec = append(spec, subSpec)
}
return spec, nil
}
// parseRubyStyle parses a ruby-style constraint from the prefix of the given
// string and returns the remaining unconsumed string for the caller to use
// for further processing.
func parseRubyStyle(str string) (SelectionSpec, string, error) {
raw, remain := scanConstraint(str)
var spec SelectionSpec
switch raw.op {
case "=", "":
spec.Operator = OpEqual
case "!=":
spec.Operator = OpNotEqual
case ">":
spec.Operator = OpGreaterThan
case ">=":
spec.Operator = OpGreaterThanOrEqual
case "<":
spec.Operator = OpLessThan
case "<=":
spec.Operator = OpLessThanOrEqual
case "~>":
// Ruby-style pessimistic can be either a minor-only or patch-only
// constraint, depending on how many digits were given.
switch raw.numCt {
case 3:
spec.Operator = OpGreaterThanOrEqualPatchOnly
default:
spec.Operator = OpGreaterThanOrEqualMinorOnly
}
case "=<":
return spec, remain, fmt.Errorf("invalid constraint operator %q; did you mean \"<=\"?", raw.op)
case "=>":
return spec, remain, fmt.Errorf("invalid constraint operator %q; did you mean \">=\"?", raw.op)
default:
return spec, remain, fmt.Errorf("invalid constraint operator %q", raw.op)
}
switch raw.sep {
case "":
if raw.op != "" {
return spec, remain, fmt.Errorf("a space separator is required after the operator %q", raw.op)
}
case " ":
if raw.op == "" {
return spec, remain, fmt.Errorf("extraneous spaces at start of specification")
}
default:
if raw.op == "" {
return spec, remain, fmt.Errorf("extraneous spaces at start of specification")
} else {
return spec, remain, fmt.Errorf("only one space is expected after the operator %q", raw.op)
}
}
if raw.numCt > 3 {
return spec, remain, fmt.Errorf("too many numbered portions; only three are allowed (major, minor, patch)")
}
// Ruby-style doesn't use explicit wildcards
for i, s := range raw.nums {
switch {
case isWildcardNum(s):
// Can't use wildcards in an exact specification
return spec, remain, fmt.Errorf("can't use wildcard for %s number; omit segments that should be unconstrained", rawNumNames[i])
}
}
if raw.pre != "" || raw.meta != "" {
// If either the prerelease or meta portions are set then any unconstrained
// segments are implied to be zero in order to guarantee constraint
// consistency.
for i, s := range raw.nums {
if s == "" {
raw.nums[i] = "0"
}
}
}
spec.Boundary = raw.VersionSpec()
return spec, remain, nil
}

View File

@ -0,0 +1,43 @@
// Code generated by "stringer -type SelectionOp"; DO NOT EDIT.
package constraints
import "strconv"
const (
_SelectionOp_name_0 = "OpUnconstrained"
_SelectionOp_name_1 = "OpMatch"
_SelectionOp_name_2 = "OpLessThanOpEqualOpGreaterThan"
_SelectionOp_name_3 = "OpGreaterThanOrEqualMinorOnly"
_SelectionOp_name_4 = "OpGreaterThanOrEqualPatchOnly"
_SelectionOp_name_5 = "OpNotEqual"
_SelectionOp_name_6 = "OpLessThanOrEqualOpGreaterThanOrEqual"
)
var (
_SelectionOp_index_2 = [...]uint8{0, 10, 17, 30}
_SelectionOp_index_6 = [...]uint8{0, 17, 37}
)
func (i SelectionOp) String() string {
switch {
case i == 0:
return _SelectionOp_name_0
case i == 42:
return _SelectionOp_name_1
case 60 <= i && i <= 62:
i -= 60
return _SelectionOp_name_2[_SelectionOp_index_2[i]:_SelectionOp_index_2[i+1]]
case i == 94:
return _SelectionOp_name_3
case i == 126:
return _SelectionOp_name_4
case i == 8800:
return _SelectionOp_name_5
case 8804 <= i && i <= 8805:
i -= 8804
return _SelectionOp_name_6[_SelectionOp_index_6[i]:_SelectionOp_index_6[i+1]]
default:
return "SelectionOp(" + strconv.FormatInt(int64(i), 10) + ")"
}
}

View File

@ -0,0 +1,249 @@
package constraints
import (
"bytes"
"fmt"
"strconv"
)
// Spec is an interface type that UnionSpec, IntersectionSpec, SelectionSpec,
// and VersionSpec all belong to.
//
// It's provided to allow generic code to be written that accepts and operates
// on all specs, but such code must still handle each type separately using
// e.g. a type switch. This is a closed type that will not have any new
// implementations added in future.
type Spec interface {
isSpec()
}
// UnionSpec represents an "or" operation on nested version constraints.
//
// This is not directly representable in all of our supported constraint
// syntaxes.
type UnionSpec []IntersectionSpec
func (s UnionSpec) isSpec() {}
// IntersectionSpec represents an "and" operation on nested version constraints.
type IntersectionSpec []SelectionSpec
func (s IntersectionSpec) isSpec() {}
// SelectionSpec represents applying a single operator to a particular
// "boundary" version.
type SelectionSpec struct {
Boundary VersionSpec
Operator SelectionOp
}
func (s SelectionSpec) isSpec() {}
// VersionSpec represents the boundary within a SelectionSpec.
type VersionSpec struct {
Major NumConstraint
Minor NumConstraint
Patch NumConstraint
Prerelease string
Metadata string
}
func (s VersionSpec) isSpec() {}
// IsExact returns bool if all of the version numbers in the receiver are
// fully-constrained. This is the same as s.ConstraintDepth() == ConstrainedPatch
func (s VersionSpec) IsExact() bool {
return s.ConstraintDepth() == ConstrainedPatch
}
// ConstraintDepth returns the constraint depth of the receiver, which is
// the most specifc version number segment that is exactly constrained.
//
// The constraints must be consistent, which means that if a given segment
// is unconstrained then all of the deeper segments must also be unconstrained.
// If not, this method will panic. Version specs produced by the parsers in
// this package are guaranteed to be consistent.
func (s VersionSpec) ConstraintDepth() ConstraintDepth {
if s == (VersionSpec{}) {
// zero value is a degenerate case meaning completely unconstrained
return Unconstrained
}
switch {
case s.Major.Unconstrained:
if !(s.Minor.Unconstrained && s.Patch.Unconstrained && s.Prerelease == "" && s.Metadata == "") {
panic("inconsistent constraint depth")
}
return Unconstrained
case s.Minor.Unconstrained:
if !(s.Patch.Unconstrained && s.Prerelease == "" && s.Metadata == "") {
panic("inconsistent constraint depth")
}
return ConstrainedMajor
case s.Patch.Unconstrained:
if s.Prerelease != "" || s.Metadata != "" {
panic(fmt.Errorf("inconsistent constraint depth: wildcard major, minor and patch followed by prerelease %q and metadata %q", s.Prerelease, s.Metadata))
}
return ConstrainedMinor
default:
return ConstrainedPatch
}
}
// ConstraintBounds returns two exact VersionSpecs that represent the upper
// and lower bounds of the possibly-inexact receiver. If the receiver
// is already exact then the two bounds are identical and have operator
// OpEqual. If they are different then the lower bound is OpGreaterThanOrEqual
// and the upper bound is OpLessThan.
//
// As a special case, if the version spec is entirely unconstrained the
// two bounds will be identical and the zero value of SelectionSpec. For
// consistency, this result is also returned if the receiver is already
// the zero value of VersionSpec, since a zero spec represents a lack of
// constraint.
//
// The constraints must be consistent as defined by ConstraintDepth, or this
// method will panic.
func (s VersionSpec) ConstraintBounds() (SelectionSpec, SelectionSpec) {
switch s.ConstraintDepth() {
case Unconstrained:
return SelectionSpec{}, SelectionSpec{}
case ConstrainedMajor:
lowerBound := s.ConstrainToZero()
lowerBound.Metadata = ""
upperBound := lowerBound
upperBound.Major.Num++
upperBound.Minor.Num = 0
upperBound.Patch.Num = 0
upperBound.Prerelease = ""
upperBound.Metadata = ""
return SelectionSpec{
Operator: OpGreaterThanOrEqual,
Boundary: lowerBound,
}, SelectionSpec{
Operator: OpLessThan,
Boundary: upperBound,
}
case ConstrainedMinor:
lowerBound := s.ConstrainToZero()
lowerBound.Metadata = ""
upperBound := lowerBound
upperBound.Minor.Num++
upperBound.Patch.Num = 0
upperBound.Metadata = ""
return SelectionSpec{
Operator: OpGreaterThanOrEqual,
Boundary: lowerBound,
}, SelectionSpec{
Operator: OpLessThan,
Boundary: upperBound,
}
default:
eq := SelectionSpec{
Operator: OpEqual,
Boundary: s,
}
return eq, eq
}
}
// ConstrainToZero returns a copy of the receiver with all of its
// unconstrained numeric segments constrained to zero.
func (s VersionSpec) ConstrainToZero() VersionSpec {
switch s.ConstraintDepth() {
case Unconstrained:
s.Major = NumConstraint{Num: 0}
s.Minor = NumConstraint{Num: 0}
s.Patch = NumConstraint{Num: 0}
s.Prerelease = ""
s.Metadata = ""
case ConstrainedMajor:
s.Minor = NumConstraint{Num: 0}
s.Patch = NumConstraint{Num: 0}
s.Prerelease = ""
s.Metadata = ""
case ConstrainedMinor:
s.Patch = NumConstraint{Num: 0}
s.Prerelease = ""
s.Metadata = ""
}
return s
}
// ConstrainToUpperBound returns a copy of the receiver with all of its
// unconstrained numeric segments constrained to zero and its last
// constrained segment increased by one.
//
// This operation is not meaningful for an entirely unconstrained VersionSpec,
// so will return the zero value of the type in that case.
func (s VersionSpec) ConstrainToUpperBound() VersionSpec {
switch s.ConstraintDepth() {
case Unconstrained:
return VersionSpec{}
case ConstrainedMajor:
s.Major.Num++
s.Minor = NumConstraint{Num: 0}
s.Patch = NumConstraint{Num: 0}
s.Prerelease = ""
s.Metadata = ""
case ConstrainedMinor:
s.Minor.Num++
s.Patch = NumConstraint{Num: 0}
s.Prerelease = ""
s.Metadata = ""
}
return s
}
func (s VersionSpec) String() string {
var buf bytes.Buffer
fmt.Fprintf(&buf, "%s.%s.%s", s.Major, s.Minor, s.Patch)
if s.Prerelease != "" {
fmt.Fprintf(&buf, "-%s", s.Prerelease)
}
if s.Metadata != "" {
fmt.Fprintf(&buf, "+%s", s.Metadata)
}
return buf.String()
}
type SelectionOp rune
//go:generate stringer -type SelectionOp
const (
OpUnconstrained SelectionOp = 0
OpGreaterThan SelectionOp = '>'
OpLessThan SelectionOp = '<'
OpGreaterThanOrEqual SelectionOp = '≥'
OpGreaterThanOrEqualPatchOnly SelectionOp = '~'
OpGreaterThanOrEqualMinorOnly SelectionOp = '^'
OpLessThanOrEqual SelectionOp = '≤'
OpEqual SelectionOp = '='
OpNotEqual SelectionOp = '≠'
OpMatch SelectionOp = '*'
)
type NumConstraint struct {
Num uint64
Unconstrained bool
}
func (c NumConstraint) String() string {
if c.Unconstrained {
return "*"
} else {
return strconv.FormatUint(c.Num, 10)
}
}
type ConstraintDepth int
//go:generate stringer -type ConstraintDepth
const (
Unconstrained ConstraintDepth = 0
ConstrainedMajor ConstraintDepth = 1
ConstrainedMinor ConstraintDepth = 2
ConstrainedPatch ConstraintDepth = 3
)

View File

@ -0,0 +1,81 @@
package constraints
import (
"fmt"
"strings"
)
// ParseExactVersion parses a string that must contain the specification of a
// single, exact version, and then returns it as a VersionSpec.
//
// This is primarily here to allow versions.ParseVersion to re-use the
// constraint grammar, and isn't very useful for direct use from calling
// applications.
func ParseExactVersion(vs string) (VersionSpec, error) {
spec := VersionSpec{}
if strings.TrimSpace(vs) == "" {
return spec, fmt.Errorf("empty specification")
}
raw, remain := scanConstraint(vs)
switch strings.TrimSpace(raw.op) {
case ">", ">=", "<", "<=", "!", "!=", "~>", "^", "~":
// If it looks like the user was trying to write a constraint string
// then we'll help them out with a more specialized error.
return spec, fmt.Errorf("can't use constraint operator %q; an exact version is required", raw.op)
case "":
// Empty operator is okay as long as we don't also have separator spaces.
// (Caller can trim off spaces beforehand if they want to tolerate this.)
if raw.sep != "" {
return spec, fmt.Errorf("extraneous spaces at start of specification")
}
default:
return spec, fmt.Errorf("invalid sequence %q at start of specification", raw.op)
}
if remain != "" {
remain = strings.TrimSpace(remain)
switch {
case remain == "":
return spec, fmt.Errorf("extraneous spaces at end of specification")
case strings.HasPrefix(vs, "v"):
// User seems to be trying to use a "v" prefix, like "v1.0.0"
return spec, fmt.Errorf(`a "v" prefix should not be used`)
case strings.HasPrefix(remain, ",") || strings.HasPrefix(remain, "|"):
// User seems to be trying to list/combine multiple versions
return spec, fmt.Errorf("can't specify multiple versions; a single exact version is required")
case strings.HasPrefix(remain, "-"):
// User seems to be trying to use the npm-style range operator
return spec, fmt.Errorf("can't specify version range; a single exact version is required")
case strings.HasPrefix(strings.TrimSpace(vs), remain):
// Whole string is invalid, then.
return spec, fmt.Errorf("invalid specification; required format is three positive integers separated by periods")
default:
return spec, fmt.Errorf("invalid characters %q", remain)
}
}
if raw.numCt > 3 {
return spec, fmt.Errorf("too many numbered portions; only three are allowed (major, minor, patch)")
}
for i := raw.numCt; i < len(raw.nums); i++ {
raw.nums[i] = "0"
}
for i, s := range raw.nums {
switch {
case isWildcardNum(s):
// Can't use wildcards in an exact specification
return spec, fmt.Errorf("can't use wildcard for %s number; an exact version is required", rawNumNames[i])
}
}
// Since we eliminated all of the unconstrained cases above, either by normalizing
// or returning an error, we are guaranteed to get constrained numbers here.
spec = raw.VersionSpec()
return spec, nil
}

View File

@ -0,0 +1,14 @@
// Package versions is a library for wrangling version numbers in Go.
//
// There are many libraries offering some or all of this functionality.
// This package aims to distinguish itself by offering a more convenient and
// ergonomic API than seen in some other libraries. Code that is resolving
// versions and version constraints tends to be hairy and complex already, so
// an expressive API for talking about these concepts will hopefully help to
// make that code more readable.
//
// The version model is based on Semantic Versioning as defined at
// https://semver.org/ . Semantic Versioning does not include any specification
// for constraints, so the constraint model is based on that used by rubygems,
// allowing for upper and lower bounds as well as individual version exclusions.
package versions

View File

@ -0,0 +1,149 @@
package versions
import (
"sort"
)
// List is a slice of Version that implements sort.Interface, and also includes
// some other helper functions.
type List []Version
// Filter removes from the receiver any elements that are not in the given
// set, moving retained elements to lower indices to close any gaps and
// modifying the underlying array in-place. The return value is a slice
// describing the new bounds within the existing backing array. The relative
// ordering of the retained elements is preserved.
//
// The result must always be either the same length or shorter than the
// initial value, so no allocation is required.
//
// As a special case, if the result would be a slice of length zero then a
// nil slice is returned instead, leaving the backing array untouched.
func (l List) Filter(set Set) List {
writeI := 0
for readI := range l {
if set.Has(l[readI]) {
l[writeI] = l[readI]
writeI++
}
}
if writeI == 0 {
return nil
}
return l[:writeI:len(l)]
}
// Newest returns the newest version in the list, or Unspecified if the list
// is empty.
//
// Since build metadata does not participate in precedence, it is possible
// that a given list may have multiple equally-new versions; in that case
// Newest will return an arbitrary version from that subset.
func (l List) Newest() Version {
ret := Unspecified
for i := len(l) - 1; i >= 0; i-- {
if l[i].GreaterThan(ret) {
ret = l[i]
}
}
return ret
}
// NewestInSet is like Filter followed by Newest, except that it does not
// modify the underlying array. This is convenient for the common case of
// selecting the newest version from a set derived from a user-supplied
// constraint.
//
// Similar to Newest, the result is Unspecified if the list is empty or if
// none of the items are in the given set. Also similar to newest, if there
// are multiple newest versions (possibly differentiated only by metadata)
// then one is arbitrarily chosen.
func (l List) NewestInSet(set Set) Version {
ret := Unspecified
for i := len(l) - 1; i >= 0; i-- {
if l[i].GreaterThan(ret) && set.Has(l[i]) {
ret = l[i]
}
}
return ret
}
// NewestList returns a List containing all of the list items that have the
// highest precedence.
//
// For an already-sorted list, the returned slice is a sub-slice of the
// receiver, sharing the same backing array. For an unsorted list, a new
// array is allocated for the result. For an empty list, the result is always
// nil.
//
// Relative ordering of elements in the receiver is preserved in the output.
func (l List) NewestList() List {
if len(l) == 0 {
return nil
}
if l.IsSorted() {
// This is a happy path since we can just count off items from the
// end of our existing list until we find one that is not the same
// as the last.
var i int
n := len(l)
for i = n - 1; i >= 0; i-- {
if !l[i].Same(l[n-1]) {
break
}
}
if i < 0 {
i = 0
}
return l[i:]
}
// For an unsorted list we'll allocate so that we can construct a new,
// filtered slice.
ret := make(List, 0, 1) // one item is the common case, in the absense of build metadata
example := l.Newest()
for _, v := range l {
if v.Same(example) {
ret = append(ret, v)
}
}
return ret
}
// Set returns a finite Set containing the versions in the receiver.
//
// Although it is possible to recover a list from the return value using
// its List method, the result may be in a different order and will have
// any duplicate elements from the receiving list consolidated.
func (l List) Set() Set {
return Selection(l...)
}
func (l List) Len() int {
return len(l)
}
func (l List) Less(i, j int) bool {
return l[i].LessThan(l[j])
}
func (l List) Swap(i, j int) {
l[i], l[j] = l[j], l[i]
}
// Sort applies an in-place sort on the list, preserving the relative order of
// any elements that differ only in build metadata. Earlier versions sort
// first, so the newest versions will be at the highest indices in the list
// once this method returns.
func (l List) Sort() {
sort.Stable(l)
}
// IsSorted returns true if the list is already in ascending order by
// version priority.
func (l List) IsSorted() bool {
return sort.IsSorted(l)
}

View File

@ -0,0 +1,243 @@
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),
}
}

View File

@ -0,0 +1,89 @@
package versions
// Set is a set of versions, usually created by parsing a constraint string.
type Set struct {
setI
}
// setI is the private interface implemented by our various constraint
// operators.
type setI interface {
Has(v Version) bool
AllRequested() Set
GoString() string
}
// Has returns true if the given version is a member of the receiving set.
func (s Set) Has(v Version) bool {
// The special Unspecified version is excluded as soon as any sort of
// constraint is applied, and so the only set it is a member of is
// the special All set.
if v == Unspecified {
return s == All
}
return s.setI.Has(v)
}
// Requests returns true if the given version is specifically requested by
// the receiving set.
//
// Requesting is a stronger form of set membership that represents an explicit
// request for a particular version, as opposed to the version just happening
// to match some criteria.
//
// The functions Only and Selection mark their arguments as requested in
// their returned sets. Exact version constraints given in constraint strings
// also mark their versions as requested.
//
// The concept of requesting is intended to help deal with pre-release versions
// in a safe and convenient way. When given generic version constraints like
// ">= 1.0.0" the user generally does not intend to match a pre-release version
// like "2.0.0-beta1", but it is important to stil be able to use that
// version if explicitly requested using the constraint string "2.0.0-beta1".
func (s Set) Requests(v Version) bool {
return s.AllRequested().Has(v)
}
// AllRequested returns a subset of the receiver containing only the requested
// versions, as defined in the documentation for the method Requests.
//
// This can be used in conjunction with the predefined set "Released" to
// include pre-release versions only by explicit request, which is supported
// via the helper method WithoutUnrequestedPrereleases.
//
// The result of AllRequested is always a finite set.
func (s Set) AllRequested() Set {
return s.setI.AllRequested()
}
// WithoutUnrequestedPrereleases returns a new set that includes all released
// versions from the receiving set, plus any explicitly-requested pre-releases,
// but does not include any unrequested pre-releases.
//
// "Requested" here is as defined in the documentation for the "Requests" method.
//
// This method is equivalent to the following set operations:
//
// versions.Union(s.AllRequested(), s.Intersection(versions.Released))
func (s Set) WithoutUnrequestedPrereleases() Set {
return Union(s.AllRequested(), Released.Intersection(s))
}
// UnmarshalText is an implementation of encoding.TextUnmarshaler, allowing
// sets to be automatically unmarshalled from strings in text-based
// serialization formats, including encoding/json.
//
// The format expected is what is accepted by MeetingConstraintsString. Any
// parser errors are passed on verbatim to the caller.
func (s *Set) UnmarshalText(text []byte) error {
str := string(text)
new, err := MeetingConstraintsString(str)
if err != nil {
return err
}
*s = new
return nil
}
var InitialDevelopment Set = OlderThan(MustParseVersion("1.0.0"))

View File

@ -0,0 +1,98 @@
package versions
import (
"fmt"
)
type setBound struct {
v Version
op setBoundOp
}
func (s setBound) Has(v Version) bool {
switch s.op {
case setBoundGT:
return v.GreaterThan(s.v)
case setBoundGTE:
return v.GreaterThan(s.v) || v.Same(s.v)
case setBoundLT:
return v.LessThan(s.v)
case setBoundLTE:
return v.LessThan(s.v) || v.Same(s.v)
default:
// Should never happen because the above is exhaustive
panic("invalid setBound operator")
}
}
func (s setBound) AllRequested() Set {
// Inequalities request nothing.
return None
}
func (s setBound) GoString() string {
switch s.op {
case setBoundGT:
return fmt.Sprintf("versions.NewerThan(%#v)", s.v)
case setBoundGTE:
return fmt.Sprintf("versions.AtLeast(%#v)", s.v)
case setBoundLT:
return fmt.Sprintf("versions.OlderThan(%#v)", s.v)
case setBoundLTE:
return fmt.Sprintf("versions.AtMost(%#v)", s.v)
default:
// Should never happen because the above is exhaustive
return fmt.Sprintf("versions.Set{versions.setBound{v:%#v,op:%#v}}", s.v, s.op)
}
}
// NewerThan returns a set containing all versions greater than the given
// version, non-inclusive.
func NewerThan(v Version) Set {
return Set{
setI: setBound{
v: v,
op: setBoundGT,
},
}
}
// OlderThan returns a set containing all versions lower than the given
// version, non-inclusive.
func OlderThan(v Version) Set {
return Set{
setI: setBound{
v: v,
op: setBoundLT,
},
}
}
// AtLeast returns a set containing all versions greater than or equal to
// the given version.
func AtLeast(v Version) Set {
return Set{
setI: setBound{
v: v,
op: setBoundGTE,
},
}
}
// AtMost returns a set containing all versions less than or equal to the given
// version, non-inclusive.
func AtMost(v Version) Set {
return Set{
setI: setBound{
v: v,
op: setBoundLTE,
},
}
}
type setBoundOp rune
const setBoundGT = '>'
const setBoundGTE = '≥'
const setBoundLT = '<'
const setBoundLTE = '≤'

View File

@ -0,0 +1,103 @@
package versions
import (
"bytes"
"fmt"
)
type setExact map[Version]struct{}
func (s setExact) Has(v Version) bool {
_, has := s[v]
return has
}
func (s setExact) AllRequested() Set {
// We just return the receiver verbatim here, because everything in it
// is explicitly requested.
return Set{setI: s}
}
func (s setExact) GoString() string {
if len(s) == 0 {
// Degenerate case; caller should use None instead
return "versions.Set{setExact{}}"
}
if len(s) == 1 {
var first Version
for v := range s {
first = v
break
}
return fmt.Sprintf("versions.Only(%#v)", first)
}
var buf bytes.Buffer
fmt.Fprint(&buf, "versions.Selection(")
versions := s.listVersions()
versions.Sort()
for i, version := range versions {
if i == 0 {
fmt.Fprint(&buf, version.GoString())
} else {
fmt.Fprintf(&buf, ", %#v", version)
}
}
fmt.Fprint(&buf, ")")
return buf.String()
}
// Only returns a version set containing only the given version.
//
// This function is guaranteed to produce a finite set.
func Only(v Version) Set {
return Set{
setI: setExact{v: struct{}{}},
}
}
// Selection returns a version set containing only the versions given
// as arguments.
//
// This function is guaranteed to produce a finite set.
func Selection(vs ...Version) Set {
if len(vs) == 0 {
return None
}
ret := make(setExact)
for _, v := range vs {
ret[v] = struct{}{}
}
return Set{setI: ret}
}
// Exactly returns true if and only if the receiving set is finite and
// contains only a single version that is the same as the version given.
func (s Set) Exactly(v Version) bool {
if !s.IsFinite() {
return false
}
l := s.List()
if len(l) != 1 {
return false
}
return v.Same(l[0])
}
var _ setFinite = setExact(nil)
func (s setExact) isFinite() bool {
return true
}
func (s setExact) listVersions() List {
if len(s) == 0 {
return nil
}
ret := make(List, 0, len(s))
for v := range s {
ret = append(ret, v)
}
return ret
}

View File

@ -0,0 +1,49 @@
package versions
// All is an infinite set containing all possible versions.
var All Set
// None is a finite set containing no versions.
var None Set
type setExtreme bool
func (s setExtreme) Has(v Version) bool {
return bool(s)
}
func (s setExtreme) AllRequested() Set {
// The extreme sets request nothing.
return None
}
func (s setExtreme) GoString() string {
switch bool(s) {
case true:
return "versions.All"
case false:
return "versions.None"
default:
panic("strange new boolean value")
}
}
var _ setFinite = setExtreme(false)
func (s setExtreme) isFinite() bool {
// Only None is finite
return !bool(s)
}
func (s setExtreme) listVersions() List {
return nil
}
func init() {
All = Set{
setI: setExtreme(true),
}
None = Set{
setI: setExtreme(false),
}
}

View File

@ -0,0 +1,34 @@
package versions
// setFinite is the interface implemented by set implementations that
// represent a finite number of versions, and can thus list those versions.
type setFinite interface {
isFinite() bool
listVersions() List
}
// IsFinite returns true if the set represents a finite number of versions,
// and can thus support List without panicking.
func (s Set) IsFinite() bool {
return isFinite(s.setI)
}
// List returns the specific versions represented by a finite list, in an
// undefined order. If desired, the caller can sort the resulting list
// using its Sort method.
//
// If the set is not finite, this method will panic. Use IsFinite to check
// unless a finite set was guaranteed by whatever operation(s) constructed
// the set.
func (s Set) List() List {
finite, ok := s.setI.(setFinite)
if !ok || !finite.isFinite() {
panic("List called on infinite set")
}
return finite.listVersions()
}
func isFinite(s setI) bool {
finite, ok := s.(setFinite)
return ok && finite.isFinite()
}

View File

@ -0,0 +1,132 @@
package versions
import (
"bytes"
"fmt"
)
type setIntersection []setI
func (s setIntersection) Has(v Version) bool {
if len(s) == 0 {
// Weird to have an intersection with no elements, but we'll
// allow it and return something sensible.
return false
}
for _, ss := range s {
if !ss.Has(v) {
return false
}
}
return true
}
func (s setIntersection) AllRequested() Set {
// The requested set for an intersection is the union of all of its
// members requested sets intersection the receiver. Therefore we'll
// borrow the same logic from setUnion's implementation here but
// then wrap it up in a setIntersection before we return.
asUnion := setUnion(s)
ar := asUnion.AllRequested()
si := make(setIntersection, len(s)+1)
si[0] = ar.setI
copy(si[1:], s)
return Set{setI: si}
}
func (s setIntersection) GoString() string {
var buf bytes.Buffer
fmt.Fprint(&buf, "versions.Intersection(")
for i, ss := range s {
if i == 0 {
fmt.Fprint(&buf, ss.GoString())
} else {
fmt.Fprintf(&buf, ", %#v", ss)
}
}
fmt.Fprint(&buf, ")")
return buf.String()
}
// Intersection creates a new set that contains the versions that all of the
// given sets have in common.
//
// The result is finite if any of the given sets are finite.
func Intersection(sets ...Set) Set {
if len(sets) == 0 {
return None
}
r := make(setIntersection, 0, len(sets))
for _, set := range sets {
if set == All {
continue
}
if set == None {
return None
}
if su, ok := set.setI.(setIntersection); ok {
r = append(r, su...)
} else {
r = append(r, set.setI)
}
}
if len(r) == 1 {
return Set{setI: r[0]}
}
return Set{setI: r}
}
// Intersection returns a new set that contains all of the versions that
// the receiver and the given sets have in common.
//
// The result is a finite set if the receiver or any of the given sets are
// finite.
func (s Set) Intersection(others ...Set) Set {
r := make(setIntersection, 1, len(others)+1)
r[0] = s.setI
for _, ss := range others {
if ss == All {
continue
}
if ss == None {
return None
}
if su, ok := ss.setI.(setIntersection); ok {
r = append(r, su...)
} else {
r = append(r, ss.setI)
}
}
if len(r) == 1 {
return Set{setI: r[0]}
}
return Set{setI: r}
}
var _ setFinite = setIntersection{}
func (s setIntersection) isFinite() bool {
// intersection is finite if any of its members are, or if it is empty
if len(s) == 0 {
return true
}
for _, ss := range s {
if isFinite(ss) {
return true
}
}
return false
}
func (s setIntersection) listVersions() List {
var ret List
for _, ss := range s {
if isFinite(ss) {
ret = append(ret, ss.(setFinite).listVersions()...)
}
}
ret.Filter(Set{setI: s})
return ret
}

View File

@ -0,0 +1,30 @@
package versions
type setReleased struct{}
func (s setReleased) Has(v Version) bool {
return v.Prerelease == ""
}
func (s setReleased) AllRequested() Set {
// The set of all released versions requests nothing.
return None
}
func (s setReleased) GoString() string {
return "versions.Released"
}
// Released is a set containing all versions that have an empty prerelease
// string.
var Released Set
// Prerelease is a set containing all versions that have a prerelease marker.
// This is the complement of Released, or in other words it is
// All.Subtract(Released).
var Prerelease Set
func init() {
Released = Set{setI: setReleased{}}
Prerelease = All.Subtract(Released)
}

View File

@ -0,0 +1,56 @@
package versions
import "fmt"
type setSubtract struct {
from setI
sub setI
}
func (s setSubtract) Has(v Version) bool {
return s.from.Has(v) && !s.sub.Has(v)
}
func (s setSubtract) AllRequested() Set {
// Our set requests anything that is requested by "from", unless it'd
// be excluded by "sub". Notice that the whole of "sub" is used, rather
// than just the requested parts, because requesting is a positive
// action only.
return Set{setI: s.from}.AllRequested().Subtract(Set{setI: s.sub})
}
func (s setSubtract) GoString() string {
return fmt.Sprintf("(%#v).Subtract(%#v)", s.from, s.sub)
}
// Subtract returns a new set that has all of the versions from the receiver
// except for any versions in the other given set.
//
// If the receiver is finite then the returned set is also finite.
func (s Set) Subtract(other Set) Set {
if other == None || s == None {
return s
}
if other == All {
return None
}
return Set{
setI: setSubtract{
from: s.setI,
sub: other.setI,
},
}
}
var _ setFinite = setSubtract{}
func (s setSubtract) isFinite() bool {
// subtract is finite if its "from" is finite
return isFinite(s.from)
}
func (s setSubtract) listVersions() List {
ret := s.from.(setFinite).listVersions()
ret = ret.Filter(Set{setI: s.sub})
return ret
}

View File

@ -0,0 +1,121 @@
package versions
import (
"bytes"
"fmt"
)
type setUnion []setI
func (s setUnion) Has(v Version) bool {
for _, ss := range s {
if ss.Has(v) {
return true
}
}
return false
}
func (s setUnion) AllRequested() Set {
// Since a union includes everything from its members, it includes all
// of the requested versions from its members too.
if len(s) == 0 {
return None
}
si := make(setUnion, 0, len(s))
for _, ss := range s {
ar := ss.AllRequested()
if ar == None {
continue
}
si = append(si, ar.setI)
}
if len(si) == 1 {
return Set{setI: si[0]}
}
return Set{setI: si}
}
func (s setUnion) GoString() string {
var buf bytes.Buffer
fmt.Fprint(&buf, "versions.Union(")
for i, ss := range s {
if i == 0 {
fmt.Fprint(&buf, ss.GoString())
} else {
fmt.Fprintf(&buf, ", %#v", ss)
}
}
fmt.Fprint(&buf, ")")
return buf.String()
}
// Union creates a new set that contains all of the given versions.
//
// The result is finite only if the receiver and all of the other given sets
// are finite.
func Union(sets ...Set) Set {
if len(sets) == 0 {
return None
}
r := make(setUnion, 0, len(sets))
for _, set := range sets {
if set == None {
continue
}
if su, ok := set.setI.(setUnion); ok {
r = append(r, su...)
} else {
r = append(r, set.setI)
}
}
if len(r) == 1 {
return Set{setI: r[0]}
}
return Set{setI: r}
}
// Union returns a new set that contains all of the versions from the
// receiver and all of the versions from each of the other given sets.
//
// The result is finite only if the receiver and all of the other given sets
// are finite.
func (s Set) Union(others ...Set) Set {
r := make(setUnion, 1, len(others)+1)
r[0] = s.setI
for _, ss := range others {
if ss == None {
continue
}
if su, ok := ss.setI.(setUnion); ok {
r = append(r, su...)
} else {
r = append(r, ss.setI)
}
}
if len(r) == 1 {
return Set{setI: r[0]}
}
return Set{setI: r}
}
var _ setFinite = setUnion{}
func (s setUnion) isFinite() bool {
// union is finite only if all of its members are finite
for _, ss := range s {
if !isFinite(ss) {
return false
}
}
return true
}
func (s setUnion) listVersions() List {
var ret List
for _, ss := range s {
ret = append(ret, ss.(setFinite).listVersions()...)
}
return ret
}

View File

@ -0,0 +1,222 @@
package versions
import (
"fmt"
"strings"
)
// Version represents a single version.
type Version struct {
Major uint64
Minor uint64
Patch uint64
Prerelease VersionExtra
Metadata VersionExtra
}
// Unspecified is the zero value of Version and represents the absense of a
// version number.
//
// Note that this is indistinguishable from the explicit version that
// results from parsing the string "0.0.0".
var Unspecified Version
// Same returns true if the receiver has the same precedence as the other
// given version. In other words, it has the same major, minor and patch
// version number and an identical prerelease portion. The Metadata, if
// any, is not considered.
func (v Version) Same(other Version) bool {
return (v.Major == other.Major &&
v.Minor == other.Minor &&
v.Patch == other.Patch &&
v.Prerelease == other.Prerelease)
}
// Comparable returns a version that is the same as the receiver but its
// metadata is the empty string. For Comparable versions, the standard
// equality operator == is equivalent to method Same.
func (v Version) Comparable() Version {
v.Metadata = ""
return v
}
// String is an implementation of fmt.Stringer that returns the receiver
// in the canonical "semver" format.
func (v Version) String() string {
s := fmt.Sprintf("%d.%d.%d", v.Major, v.Minor, v.Patch)
if v.Prerelease != "" {
s = fmt.Sprintf("%s-%s", s, v.Prerelease)
}
if v.Metadata != "" {
s = fmt.Sprintf("%s+%s", s, v.Metadata)
}
return s
}
func (v Version) GoString() string {
return fmt.Sprintf("versions.MustParseVersion(%q)", v.String())
}
// LessThan returns true if the receiver has a lower precedence than the
// other given version, as defined by the semantic versioning specification.
func (v Version) LessThan(other Version) bool {
switch {
case v.Major != other.Major:
return v.Major < other.Major
case v.Minor != other.Minor:
return v.Minor < other.Minor
case v.Patch != other.Patch:
return v.Patch < other.Patch
case v.Prerelease != other.Prerelease:
if v.Prerelease == "" {
return false
}
if other.Prerelease == "" {
return true
}
return v.Prerelease.LessThan(other.Prerelease)
default:
return false
}
}
// GreaterThan returns true if the receiver has a higher precedence than the
// other given version, as defined by the semantic versioning specification.
func (v Version) GreaterThan(other Version) bool {
switch {
case v.Major != other.Major:
return v.Major > other.Major
case v.Minor != other.Minor:
return v.Minor > other.Minor
case v.Patch != other.Patch:
return v.Patch > other.Patch
case v.Prerelease != other.Prerelease:
if v.Prerelease == "" {
return true
}
if other.Prerelease == "" {
return false
}
return !v.Prerelease.LessThan(other.Prerelease)
default:
return false
}
}
// MarshalText is an implementation of encoding.TextMarshaler, allowing versions
// to be automatically marshalled for text-based serialization formats,
// including encoding/json.
//
// The format used is that returned by String, which can be parsed using
// ParseVersion.
func (v Version) MarshalText() (text []byte, err error) {
return []byte(v.String()), nil
}
// UnmarshalText is an implementation of encoding.TextUnmarshaler, allowing
// versions to be automatically unmarshalled from strings in text-based
// serialization formats, including encoding/json.
//
// The format expected is what is accepted by ParseVersion. Any parser errors
// are passed on verbatim to the caller.
func (v *Version) UnmarshalText(text []byte) error {
str := string(text)
new, err := ParseVersion(str)
if err != nil {
return err
}
*v = new
return nil
}
// VersionExtra represents a string containing dot-delimited tokens, as used
// in the pre-release and build metadata portions of a Semantic Versioning
// version expression.
type VersionExtra string
// Parts tokenizes the string into its separate parts by splitting on dots.
//
// The result is undefined if the receiver is not valid per the semver spec,
func (e VersionExtra) Parts() []string {
return strings.Split(string(e), ".")
}
func (e VersionExtra) Raw() string {
return string(e)
}
// LessThan returns true if the receiever has lower precedence than the
// other given VersionExtra string, per the rules defined in the semver
// spec for pre-release versions.
//
// Build metadata has no defined precedence rules, so it is not meaningful
// to call this method on a VersionExtra representing build metadata.
func (e VersionExtra) LessThan(other VersionExtra) bool {
if e == other {
// Easy path
return false
}
s1 := string(e)
s2 := string(other)
for {
d1 := strings.IndexByte(s1, '.')
d2 := strings.IndexByte(s2, '.')
switch {
case d1 == -1 && d2 != -1:
// s1 has fewer parts, so it precedes s2
return true
case d2 == -1 && d1 != -1:
// s1 has more parts, so it succeeds s2
return false
case d1 == -1: // d2 must be -1 too, because of the above
// this is our last portion to compare
return lessThanStr(s1, s2)
default:
s1s := s1[:d1]
s2s := s2[:d2]
if s1s != s2s {
return lessThanStr(s1s, s2s)
}
s1 = s1[d1+1:]
s2 = s2[d2+1:]
}
}
}
func lessThanStr(s1, s2 string) bool {
// How we compare here depends on whether the string is entirely consistent of digits
s1Numeric := true
s2Numeric := true
for _, c := range s1 {
if c < '0' || c > '9' {
s1Numeric = false
break
}
}
for _, c := range s2 {
if c < '0' || c > '9' {
s2Numeric = false
break
}
}
switch {
case s1Numeric && !s2Numeric:
return true
case s2Numeric && !s1Numeric:
return false
case s1Numeric: // s2Numeric must also be true
switch {
case len(s1) < len(s2):
return true
case len(s2) < len(s1):
return false
default:
return s1 < s2
}
default:
return s1 < s2
}
}

3
vendor/modules.txt vendored
View File

@ -84,6 +84,9 @@ github.com/apparentlymart/go-cidr/cidr
github.com/apparentlymart/go-dump/dump
# github.com/apparentlymart/go-textseg v1.0.0
github.com/apparentlymart/go-textseg/textseg
# github.com/apparentlymart/go-versions v0.0.2-0.20180815153302-64b99f7cb171
github.com/apparentlymart/go-versions/versions
github.com/apparentlymart/go-versions/versions/constraints
# github.com/armon/circbuf v0.0.0-20190214190532-5111143e8da2
github.com/armon/circbuf
# github.com/armon/go-radix v1.0.0