-
Notifications
You must be signed in to change notification settings - Fork 1.9k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #6699 from hashicorp/f-semver-constraints
Add new "semver" constraint
- Loading branch information
Showing
21 changed files
with
670 additions
and
47 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,147 @@ | ||
// semver is a Semver Constraints package copied from | ||
// github.com/hashicorp/go-version @ 2046c9d0f0b03c779670f5186a2a4b2c85493a71 | ||
// | ||
// Unlike Constraints in go-version, Semver constraints use Semver 2.0 ordering | ||
// rules and only accept properly formatted Semver versions. | ||
package semver | ||
|
||
import ( | ||
"fmt" | ||
"regexp" | ||
"strings" | ||
|
||
"github.com/hashicorp/go-version" | ||
) | ||
|
||
// Constraint represents a single constraint for a version, such as ">= | ||
// 1.0". | ||
type Constraint struct { | ||
f constraintFunc | ||
check *version.Version | ||
original string | ||
} | ||
|
||
// Constraints is a slice of constraints. We make a custom type so that | ||
// we can add methods to it. | ||
type Constraints []*Constraint | ||
|
||
type constraintFunc func(v, c *version.Version) bool | ||
|
||
var constraintOperators map[string]constraintFunc | ||
|
||
var constraintRegexp *regexp.Regexp | ||
|
||
func init() { | ||
constraintOperators = map[string]constraintFunc{ | ||
"": constraintEqual, | ||
"=": constraintEqual, | ||
"!=": constraintNotEqual, | ||
">": constraintGreaterThan, | ||
"<": constraintLessThan, | ||
">=": constraintGreaterThanEqual, | ||
"<=": constraintLessThanEqual, | ||
} | ||
|
||
ops := make([]string, 0, len(constraintOperators)) | ||
for k := range constraintOperators { | ||
ops = append(ops, regexp.QuoteMeta(k)) | ||
} | ||
|
||
constraintRegexp = regexp.MustCompile(fmt.Sprintf( | ||
`^\s*(%s)\s*(%s)\s*$`, | ||
strings.Join(ops, "|"), | ||
version.SemverRegexpRaw)) | ||
} | ||
|
||
// NewConstraint will parse one or more constraints from the given | ||
// constraint string. The string must be a comma-separated list of constraints. | ||
func NewConstraint(v string) (Constraints, error) { | ||
vs := strings.Split(v, ",") | ||
result := make([]*Constraint, len(vs)) | ||
for i, single := range vs { | ||
c, err := parseSingle(single) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
result[i] = c | ||
} | ||
|
||
return Constraints(result), nil | ||
} | ||
|
||
// Check tests if a version satisfies all the constraints. | ||
func (cs Constraints) Check(v *version.Version) bool { | ||
for _, c := range cs { | ||
if !c.Check(v) { | ||
return false | ||
} | ||
} | ||
|
||
return true | ||
} | ||
|
||
// Returns the string format of the constraints | ||
func (cs Constraints) String() string { | ||
csStr := make([]string, len(cs)) | ||
for i, c := range cs { | ||
csStr[i] = c.String() | ||
} | ||
|
||
return strings.Join(csStr, ",") | ||
} | ||
|
||
// Check tests if a constraint is validated by the given version. | ||
func (c *Constraint) Check(v *version.Version) bool { | ||
return c.f(v, c.check) | ||
} | ||
|
||
func (c *Constraint) String() string { | ||
return c.original | ||
} | ||
|
||
func parseSingle(v string) (*Constraint, error) { | ||
matches := constraintRegexp.FindStringSubmatch(v) | ||
if matches == nil { | ||
return nil, fmt.Errorf("Malformed constraint: %s", v) | ||
} | ||
|
||
check, err := version.NewSemver(matches[2]) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
return &Constraint{ | ||
f: constraintOperators[matches[1]], | ||
check: check, | ||
original: v, | ||
}, nil | ||
} | ||
|
||
//------------------------------------------------------------------- | ||
// Constraint functions | ||
//------------------------------------------------------------------- | ||
|
||
func constraintEqual(v, c *version.Version) bool { | ||
return v.Equal(c) | ||
} | ||
|
||
func constraintNotEqual(v, c *version.Version) bool { | ||
return !v.Equal(c) | ||
} | ||
|
||
func constraintGreaterThan(v, c *version.Version) bool { | ||
return v.Compare(c) == 1 | ||
} | ||
|
||
func constraintLessThan(v, c *version.Version) bool { | ||
return v.Compare(c) == -1 | ||
} | ||
|
||
func constraintGreaterThanEqual(v, c *version.Version) bool { | ||
return v.Compare(c) >= 0 | ||
} | ||
|
||
func constraintLessThanEqual(v, c *version.Version) bool { | ||
return v.Compare(c) <= 0 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,128 @@ | ||
package semver | ||
|
||
import ( | ||
"testing" | ||
|
||
"github.com/hashicorp/go-version" | ||
) | ||
|
||
// This file is a copy of github.com/hashicorp/go-version/constraint_test.go | ||
// with minimal changes to demonstrate differences. Diffing the files should | ||
// illustrate behavior differences in Constraint and version.Constraint. | ||
|
||
func TestNewConstraint(t *testing.T) { | ||
cases := []struct { | ||
input string | ||
count int | ||
err bool | ||
}{ | ||
{">= 1.2", 1, false}, | ||
{"1.0", 1, false}, | ||
{">= 1.x", 0, true}, | ||
{">= 1.2, < 1.0", 2, false}, | ||
|
||
// Out of bounds | ||
{"11387778780781445675529500000000000000000", 0, true}, | ||
|
||
// Semver only | ||
{">= 1.0beta1", 0, true}, | ||
|
||
// No pessimistic operator | ||
{"~> 1.0", 0, true}, | ||
} | ||
|
||
for _, tc := range cases { | ||
v, err := NewConstraint(tc.input) | ||
if tc.err && err == nil { | ||
t.Fatalf("expected error for input: %s", tc.input) | ||
} else if !tc.err && err != nil { | ||
t.Fatalf("error for input %s: %s", tc.input, err) | ||
} | ||
|
||
if len(v) != tc.count { | ||
t.Fatalf("input: %s\nexpected len: %d\nactual: %d", | ||
tc.input, tc.count, len(v)) | ||
} | ||
} | ||
} | ||
|
||
func TestConstraintCheck(t *testing.T) { | ||
cases := []struct { | ||
constraint string | ||
version string | ||
check bool | ||
}{ | ||
{">= 1.0, < 1.2", "1.1.5", true}, | ||
{"< 1.0, < 1.2", "1.1.5", false}, | ||
{"= 1.0", "1.1.5", false}, | ||
{"= 1.0", "1.0.0", true}, | ||
{"1.0", "1.0.0", true}, | ||
|
||
// Assert numbers are *not* compared lexically as in #4729 | ||
{"> 10", "8", false}, | ||
|
||
// Pre-releases are ordered according to Semver v2 | ||
{"> 2.0", "2.1.0-beta", true}, | ||
{"> 2.1.0-a", "2.1.0-beta", true}, | ||
{"> 2.1.0-a", "2.1.1-beta", true}, | ||
{"> 2.0.0", "2.1.0-beta", true}, | ||
{"> 2.1.0-a", "2.1.1", true}, | ||
{"> 2.1.0-a", "2.1.1-beta", true}, | ||
{"> 2.1.0-a", "2.1.0", true}, | ||
{"<= 2.1.0-a", "2.0.0", true}, | ||
{">= 0.6.1", "1.3.0-beta1", true}, | ||
{"> 1.0-beta1", "1.0-rc1", true}, | ||
|
||
// Meta components are ignored according to Semver v2 | ||
{">= 0.6.1", "1.3.0-beta1+ent", true}, | ||
{">= 1.3.0-beta1", "1.3.0-beta1+ent", true}, | ||
{"> 1.3.0-beta1+cgo", "1.3.0-beta1+ent", false}, | ||
{"= 1.3.0-beta1+cgo", "1.3.0-beta1+ent", true}, | ||
} | ||
|
||
for _, tc := range cases { | ||
c, err := NewConstraint(tc.constraint) | ||
if err != nil { | ||
t.Fatalf("err: %s", err) | ||
} | ||
|
||
v, err := version.NewSemver(tc.version) | ||
if err != nil { | ||
t.Fatalf("err: %s", err) | ||
} | ||
|
||
actual := c.Check(v) | ||
expected := tc.check | ||
if actual != expected { | ||
t.Fatalf("Version: %s\nConstraint: %s\nExpected: %#v", | ||
tc.version, tc.constraint, expected) | ||
} | ||
} | ||
} | ||
|
||
func TestConstraintsString(t *testing.T) { | ||
cases := []struct { | ||
constraint string | ||
result string | ||
}{ | ||
{">= 1.0, < 1.2", ""}, | ||
} | ||
|
||
for _, tc := range cases { | ||
c, err := NewConstraint(tc.constraint) | ||
if err != nil { | ||
t.Fatalf("err: %s", err) | ||
} | ||
|
||
actual := c.String() | ||
expected := tc.result | ||
if expected == "" { | ||
expected = tc.constraint | ||
} | ||
|
||
if actual != expected { | ||
t.Fatalf("Constraint: %s\nExpected: %#v\nActual: %s", | ||
tc.constraint, expected, actual) | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.