Skip to content

Commit

Permalink
feat: Implement MinVersion() for Constraints.
Browse files Browse the repository at this point in the history
MinVersion() returns return the lowest version that can possibly match the given constraint.

For examples:

  *  `MinVersion("1.0.1") = "1.0.1"`
  *  `MinVersion("=1.0.1") = "1.0.1"`
  *  `MinVersion("~1.0.1") = "1.0.1"`
  *  `MinVersion(">1.0.1") = "1.0.2"`
  *  `MinVersion(">=1.0.1") = "1.0.1"`
  *  `MinVersion("<2.0.0 >1.0.1") = "1.0.2"`

etc.

The implementation is based on node-semver:

  https://github.com/npm/node-semver/blob/main/ranges/min-version.js

One minor difference is how prerelease versions are treated given `>`:

  * In node-semver, `MinVersion(">1.0.0-beta') = "1.0.0-beta.0"`
  * In the proposed impl, `MinVersion(">1.0.0-beta') = "1.0.0"`

This behavior made more sense to me given how Version("1.0.0-beta").IncPatch() behaves.
  • Loading branch information
taeold committed Dec 16, 2023
1 parent 98fc853 commit 4bf0515
Show file tree
Hide file tree
Showing 2 changed files with 134 additions and 0 deletions.
42 changes: 42 additions & 0 deletions constraints.go
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,48 @@ func (cs Constraints) Validate(v *Version) (bool, []error) {
return false, e
}

// MinVersion return the lowest version that can possibly match the given constraints.
func (cs Constraints) MinVersion() (*Version, error) {
minVer, _ := NewVersion("0.0.0")
if cs.Check(minVer) {
return minVer, nil
}

minVer, _ = NewVersion("0.0.0-0")
if cs.Check(minVer) {
return minVer, nil
}

minVer = nil
for _, constraintSet := range cs.constraints {
var minCandidate *Version
for _, c := range constraintSet {
switch c.origfunc {
case "", "=":
minCandidate = c.con
case ">":
newV := c.con.IncPatch()
minCandidate = &newV
case ">=", "=>", "^", "~", "~>":
if minCandidate == nil || c.con.GreaterThan(minCandidate) {
minCandidate = c.con
}
case "<", "<=", "!=", "=<":
// (ignored for minimum version calculation)
default:
return nil, fmt.Errorf("unexpected operator: %s", c.origfunc)
}
}
if minCandidate != nil && (minVer == nil || minCandidate.LessThan(minVer)) {
minVer = minCandidate
}
}
if minVer == nil || !cs.Check(minVer) {
return nil, fmt.Errorf("no valid version found that satisfies all constraints")
}
return minVer, nil
}

func (cs Constraints) String() string {
buf := make([]string, len(cs.constraints))
var tmp bytes.Buffer
Expand Down
92 changes: 92 additions & 0 deletions constraints_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -809,3 +809,95 @@ func FuzzNewConstraint(f *testing.F) {
_, _ = NewConstraint(a)
})
}

func TestConstraintMinVersion(t *testing.T) {
tests := []struct {
constraint string
minVersion string
}{
{"*", "0.0.0"},
{"* || >=2", "0.0.0"},
{">=2 || *", "0.0.0"},
{">2 || *", "0.0.0"},
{"1.0.0", "1.0.0"},
{"1.0", "1.0.0"},
{"1.0.x", "1.0.0"},
{"1.0.*", "1.0.0"},
{"1", "1.0.0"},
{"1.x.x", "1.0.0"},
{"1.x.x", "1.0.0"},
{"1.*.x", "1.0.0"},
{"1.x.*", "1.0.0"},
{"1.x", "1.0.0"},
{"1.*", "1.0.0"},
{"=1.0.0", "1.0.0"},
{"~1.1.1", "1.1.1"},
{"~1.1.1-beta", "1.1.1-beta"},
{"~1.1.1 || >=2", "1.1.1"},
{"^1.1.1", "1.1.1"},
{"~>1.1.1", "1.1.1"},
{"^1.1.1-beta", "1.1.1-beta"},
{"^1.1.1 || >=2", "1.1.1"},
{"^2.16.2 ^2.16", "2.16.2"},
{"1.1.1 - 1.8.0", "1.1.1"},
{"1.1 - 1.8.0", "1.1.0"},
{"<2", "0.0.0"},
{"<0.0.0-beta", "0.0.0-0"},
{"<0.0.1-beta", "0.0.0"},
{"<2 || >4", "0.0.0"},
{">4 || <2", "0.0.0"},
{"<=2 || >=4", "0.0.0"},
{">=4 || <=2", "0.0.0"},
{"=>4 || <=2", "0.0.0"},
{"=>4 || =<2", "0.0.0"},
{">=1.1.1 <2 || >=2.2.2 <2", "1.1.1"},
{">=2.2.2 <2 || >=1.1.1 <2", "1.1.1"},
{">1.0.0", "1.0.1"},
{">2 || >1.0.0", "1.0.1"},
{">1.2.3-0", "1.2.3"},
}

for _, tc := range tests {
t.Run(tc.constraint, func(t *testing.T) {
c, err := NewConstraint(tc.constraint)
if err != nil {
t.Fatalf("err: %s", err)
}

want, err := NewVersion(tc.minVersion)
if err != nil {
t.Fatalf("err: %s", err)
}

got, err := c.MinVersion()
if err != nil {
t.Fatalf("err: %s", err)
}
if !want.Equal(got) {
t.Errorf("Unexpected min version for constraint %v: want %v, got %v", tc.constraint, tc.minVersion, got.String())
}
})
}
}

func TestConstraintMinVersionError(t *testing.T) {
tests := []struct {
constraint string
}{
{">=2 <1"},
}

for _, tc := range tests {
t.Run(tc.constraint, func(t *testing.T) {
c, err := NewConstraint(tc.constraint)
if err != nil {
t.Fatalf("err: %s", err)
}

got, err := c.MinVersion()
if err == nil {
t.Fatalf("MinVersion(%s) unexpectedly returned a valid min version of %s", tc.constraint, got.String())
}
})
}
}

0 comments on commit 4bf0515

Please sign in to comment.