blob: d055759611c87a2eea3621d0402691f6023edd2a [file] [log] [blame]
Scott Baker1fe72732019-10-21 10:58:51 -07001package version
2
3import (
4 "fmt"
5 "reflect"
6 "regexp"
7 "strings"
8)
9
10// Constraint represents a single constraint for a version, such as
11// ">= 1.0".
12type Constraint struct {
13 f constraintFunc
14 check *Version
15 original string
16}
17
18// Constraints is a slice of constraints. We make a custom type so that
19// we can add methods to it.
20type Constraints []*Constraint
21
22type constraintFunc func(v, c *Version) bool
23
24var constraintOperators map[string]constraintFunc
25
26var constraintRegexp *regexp.Regexp
27
28func init() {
29 constraintOperators = map[string]constraintFunc{
30 "": constraintEqual,
31 "=": constraintEqual,
32 "!=": constraintNotEqual,
33 ">": constraintGreaterThan,
34 "<": constraintLessThan,
35 ">=": constraintGreaterThanEqual,
36 "<=": constraintLessThanEqual,
37 "~>": constraintPessimistic,
38 }
39
40 ops := make([]string, 0, len(constraintOperators))
41 for k := range constraintOperators {
42 ops = append(ops, regexp.QuoteMeta(k))
43 }
44
45 constraintRegexp = regexp.MustCompile(fmt.Sprintf(
46 `^\s*(%s)\s*(%s)\s*$`,
47 strings.Join(ops, "|"),
48 VersionRegexpRaw))
49}
50
51// NewConstraint will parse one or more constraints from the given
52// constraint string. The string must be a comma-separated list of
53// constraints.
54func NewConstraint(v string) (Constraints, error) {
55 vs := strings.Split(v, ",")
56 result := make([]*Constraint, len(vs))
57 for i, single := range vs {
58 c, err := parseSingle(single)
59 if err != nil {
60 return nil, err
61 }
62
63 result[i] = c
64 }
65
66 return Constraints(result), nil
67}
68
69// Check tests if a version satisfies all the constraints.
70func (cs Constraints) Check(v *Version) bool {
71 for _, c := range cs {
72 if !c.Check(v) {
73 return false
74 }
75 }
76
77 return true
78}
79
80// Returns the string format of the constraints
81func (cs Constraints) String() string {
82 csStr := make([]string, len(cs))
83 for i, c := range cs {
84 csStr[i] = c.String()
85 }
86
87 return strings.Join(csStr, ",")
88}
89
90// Check tests if a constraint is validated by the given version.
91func (c *Constraint) Check(v *Version) bool {
92 return c.f(v, c.check)
93}
94
95func (c *Constraint) String() string {
96 return c.original
97}
98
99func parseSingle(v string) (*Constraint, error) {
100 matches := constraintRegexp.FindStringSubmatch(v)
101 if matches == nil {
102 return nil, fmt.Errorf("Malformed constraint: %s", v)
103 }
104
105 check, err := NewVersion(matches[2])
106 if err != nil {
107 return nil, err
108 }
109
110 return &Constraint{
111 f: constraintOperators[matches[1]],
112 check: check,
113 original: v,
114 }, nil
115}
116
117func prereleaseCheck(v, c *Version) bool {
118 switch vPre, cPre := v.Prerelease() != "", c.Prerelease() != ""; {
119 case cPre && vPre:
120 // A constraint with a pre-release can only match a pre-release version
121 // with the same base segments.
122 return reflect.DeepEqual(c.Segments64(), v.Segments64())
123
124 case !cPre && vPre:
125 // A constraint without a pre-release can only match a version without a
126 // pre-release.
127 return false
128
129 case cPre && !vPre:
130 // OK, except with the pessimistic operator
131 case !cPre && !vPre:
132 // OK
133 }
134 return true
135}
136
137//-------------------------------------------------------------------
138// Constraint functions
139//-------------------------------------------------------------------
140
141func constraintEqual(v, c *Version) bool {
142 return v.Equal(c)
143}
144
145func constraintNotEqual(v, c *Version) bool {
146 return !v.Equal(c)
147}
148
149func constraintGreaterThan(v, c *Version) bool {
150 return prereleaseCheck(v, c) && v.Compare(c) == 1
151}
152
153func constraintLessThan(v, c *Version) bool {
154 return prereleaseCheck(v, c) && v.Compare(c) == -1
155}
156
157func constraintGreaterThanEqual(v, c *Version) bool {
158 return prereleaseCheck(v, c) && v.Compare(c) >= 0
159}
160
161func constraintLessThanEqual(v, c *Version) bool {
162 return prereleaseCheck(v, c) && v.Compare(c) <= 0
163}
164
165func constraintPessimistic(v, c *Version) bool {
166 // Using a pessimistic constraint with a pre-release, restricts versions to pre-releases
167 if !prereleaseCheck(v, c) || (c.Prerelease() != "" && v.Prerelease() == "") {
168 return false
169 }
170
171 // If the version being checked is naturally less than the constraint, then there
172 // is no way for the version to be valid against the constraint
173 if v.LessThan(c) {
174 return false
175 }
176 // We'll use this more than once, so grab the length now so it's a little cleaner
177 // to write the later checks
178 cs := len(c.segments)
179
180 // If the version being checked has less specificity than the constraint, then there
181 // is no way for the version to be valid against the constraint
182 if cs > len(v.segments) {
183 return false
184 }
185
186 // Check the segments in the constraint against those in the version. If the version
187 // being checked, at any point, does not have the same values in each index of the
188 // constraints segments, then it cannot be valid against the constraint.
189 for i := 0; i < c.si-1; i++ {
190 if v.segments[i] != c.segments[i] {
191 return false
192 }
193 }
194
195 // Check the last part of the segment in the constraint. If the version segment at
196 // this index is less than the constraints segment at this index, then it cannot
197 // be valid against the constraint
198 if c.segments[cs-1] > v.segments[cs-1] {
199 return false
200 }
201
202 // If nothing has rejected the version by now, it's valid
203 return true
204}