diff --git a/constraints.go b/constraints.go index 8461c7e..a99e0eb 100644 --- a/constraints.go +++ b/constraints.go @@ -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 diff --git a/constraints_test.go b/constraints_test.go index c720797..bd6d86f 100644 --- a/constraints_test.go +++ b/constraints_test.go @@ -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()) + } + }) + } +}