]>
Commit | Line | Data |
---|---|---|
c680a8e1 RS |
1 | package semver |
2 | ||
3 | import ( | |
4 | "errors" | |
5 | "fmt" | |
6 | "strconv" | |
7 | "strings" | |
8 | ) | |
9 | ||
10 | const ( | |
11 | numbers string = "0123456789" | |
12 | alphas = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-" | |
13 | alphanum = alphas + numbers | |
14 | ) | |
15 | ||
16 | // SpecVersion is the latest fully supported spec version of semver | |
17 | var SpecVersion = Version{ | |
18 | Major: 2, | |
19 | Minor: 0, | |
20 | Patch: 0, | |
21 | } | |
22 | ||
23 | // Version represents a semver compatible version | |
24 | type Version struct { | |
25 | Major uint64 | |
26 | Minor uint64 | |
27 | Patch uint64 | |
28 | Pre []PRVersion | |
29 | Build []string //No Precendence | |
30 | } | |
31 | ||
32 | // Version to string | |
33 | func (v Version) String() string { | |
34 | b := make([]byte, 0, 5) | |
35 | b = strconv.AppendUint(b, v.Major, 10) | |
36 | b = append(b, '.') | |
37 | b = strconv.AppendUint(b, v.Minor, 10) | |
38 | b = append(b, '.') | |
39 | b = strconv.AppendUint(b, v.Patch, 10) | |
40 | ||
41 | if len(v.Pre) > 0 { | |
42 | b = append(b, '-') | |
43 | b = append(b, v.Pre[0].String()...) | |
44 | ||
45 | for _, pre := range v.Pre[1:] { | |
46 | b = append(b, '.') | |
47 | b = append(b, pre.String()...) | |
48 | } | |
49 | } | |
50 | ||
51 | if len(v.Build) > 0 { | |
52 | b = append(b, '+') | |
53 | b = append(b, v.Build[0]...) | |
54 | ||
55 | for _, build := range v.Build[1:] { | |
56 | b = append(b, '.') | |
57 | b = append(b, build...) | |
58 | } | |
59 | } | |
60 | ||
61 | return string(b) | |
62 | } | |
63 | ||
64 | // Equals checks if v is equal to o. | |
65 | func (v Version) Equals(o Version) bool { | |
66 | return (v.Compare(o) == 0) | |
67 | } | |
68 | ||
69 | // EQ checks if v is equal to o. | |
70 | func (v Version) EQ(o Version) bool { | |
71 | return (v.Compare(o) == 0) | |
72 | } | |
73 | ||
74 | // NE checks if v is not equal to o. | |
75 | func (v Version) NE(o Version) bool { | |
76 | return (v.Compare(o) != 0) | |
77 | } | |
78 | ||
79 | // GT checks if v is greater than o. | |
80 | func (v Version) GT(o Version) bool { | |
81 | return (v.Compare(o) == 1) | |
82 | } | |
83 | ||
84 | // GTE checks if v is greater than or equal to o. | |
85 | func (v Version) GTE(o Version) bool { | |
86 | return (v.Compare(o) >= 0) | |
87 | } | |
88 | ||
89 | // GE checks if v is greater than or equal to o. | |
90 | func (v Version) GE(o Version) bool { | |
91 | return (v.Compare(o) >= 0) | |
92 | } | |
93 | ||
94 | // LT checks if v is less than o. | |
95 | func (v Version) LT(o Version) bool { | |
96 | return (v.Compare(o) == -1) | |
97 | } | |
98 | ||
99 | // LTE checks if v is less than or equal to o. | |
100 | func (v Version) LTE(o Version) bool { | |
101 | return (v.Compare(o) <= 0) | |
102 | } | |
103 | ||
104 | // LE checks if v is less than or equal to o. | |
105 | func (v Version) LE(o Version) bool { | |
106 | return (v.Compare(o) <= 0) | |
107 | } | |
108 | ||
109 | // Compare compares Versions v to o: | |
110 | // -1 == v is less than o | |
111 | // 0 == v is equal to o | |
112 | // 1 == v is greater than o | |
113 | func (v Version) Compare(o Version) int { | |
114 | if v.Major != o.Major { | |
115 | if v.Major > o.Major { | |
116 | return 1 | |
117 | } | |
118 | return -1 | |
119 | } | |
120 | if v.Minor != o.Minor { | |
121 | if v.Minor > o.Minor { | |
122 | return 1 | |
123 | } | |
124 | return -1 | |
125 | } | |
126 | if v.Patch != o.Patch { | |
127 | if v.Patch > o.Patch { | |
128 | return 1 | |
129 | } | |
130 | return -1 | |
131 | } | |
132 | ||
133 | // Quick comparison if a version has no prerelease versions | |
134 | if len(v.Pre) == 0 && len(o.Pre) == 0 { | |
135 | return 0 | |
136 | } else if len(v.Pre) == 0 && len(o.Pre) > 0 { | |
137 | return 1 | |
138 | } else if len(v.Pre) > 0 && len(o.Pre) == 0 { | |
139 | return -1 | |
140 | } | |
141 | ||
142 | i := 0 | |
143 | for ; i < len(v.Pre) && i < len(o.Pre); i++ { | |
144 | if comp := v.Pre[i].Compare(o.Pre[i]); comp == 0 { | |
145 | continue | |
146 | } else if comp == 1 { | |
147 | return 1 | |
148 | } else { | |
149 | return -1 | |
150 | } | |
151 | } | |
152 | ||
153 | // If all pr versions are the equal but one has further prversion, this one greater | |
154 | if i == len(v.Pre) && i == len(o.Pre) { | |
155 | return 0 | |
156 | } else if i == len(v.Pre) && i < len(o.Pre) { | |
157 | return -1 | |
158 | } else { | |
159 | return 1 | |
160 | } | |
161 | ||
162 | } | |
163 | ||
164 | // Validate validates v and returns error in case | |
165 | func (v Version) Validate() error { | |
166 | // Major, Minor, Patch already validated using uint64 | |
167 | ||
168 | for _, pre := range v.Pre { | |
169 | if !pre.IsNum { //Numeric prerelease versions already uint64 | |
170 | if len(pre.VersionStr) == 0 { | |
171 | return fmt.Errorf("Prerelease can not be empty %q", pre.VersionStr) | |
172 | } | |
173 | if !containsOnly(pre.VersionStr, alphanum) { | |
174 | return fmt.Errorf("Invalid character(s) found in prerelease %q", pre.VersionStr) | |
175 | } | |
176 | } | |
177 | } | |
178 | ||
179 | for _, build := range v.Build { | |
180 | if len(build) == 0 { | |
181 | return fmt.Errorf("Build meta data can not be empty %q", build) | |
182 | } | |
183 | if !containsOnly(build, alphanum) { | |
184 | return fmt.Errorf("Invalid character(s) found in build meta data %q", build) | |
185 | } | |
186 | } | |
187 | ||
188 | return nil | |
189 | } | |
190 | ||
191 | // New is an alias for Parse and returns a pointer, parses version string and returns a validated Version or error | |
192 | func New(s string) (vp *Version, err error) { | |
193 | v, err := Parse(s) | |
194 | vp = &v | |
195 | return | |
196 | } | |
197 | ||
198 | // Make is an alias for Parse, parses version string and returns a validated Version or error | |
199 | func Make(s string) (Version, error) { | |
200 | return Parse(s) | |
201 | } | |
202 | ||
203 | // ParseTolerant allows for certain version specifications that do not strictly adhere to semver | |
204 | // specs to be parsed by this library. It does so by normalizing versions before passing them to | |
205 | // Parse(). It currently trims spaces, removes a "v" prefix, and adds a 0 patch number to versions | |
206 | // with only major and minor components specified | |
207 | func ParseTolerant(s string) (Version, error) { | |
208 | s = strings.TrimSpace(s) | |
209 | s = strings.TrimPrefix(s, "v") | |
210 | ||
211 | // Split into major.minor.(patch+pr+meta) | |
212 | parts := strings.SplitN(s, ".", 3) | |
213 | if len(parts) < 3 { | |
214 | if strings.ContainsAny(parts[len(parts)-1], "+-") { | |
215 | return Version{}, errors.New("Short version cannot contain PreRelease/Build meta data") | |
216 | } | |
217 | for len(parts) < 3 { | |
218 | parts = append(parts, "0") | |
219 | } | |
220 | s = strings.Join(parts, ".") | |
221 | } | |
222 | ||
223 | return Parse(s) | |
224 | } | |
225 | ||
226 | // Parse parses version string and returns a validated Version or error | |
227 | func Parse(s string) (Version, error) { | |
228 | if len(s) == 0 { | |
229 | return Version{}, errors.New("Version string empty") | |
230 | } | |
231 | ||
232 | // Split into major.minor.(patch+pr+meta) | |
233 | parts := strings.SplitN(s, ".", 3) | |
234 | if len(parts) != 3 { | |
235 | return Version{}, errors.New("No Major.Minor.Patch elements found") | |
236 | } | |
237 | ||
238 | // Major | |
239 | if !containsOnly(parts[0], numbers) { | |
240 | return Version{}, fmt.Errorf("Invalid character(s) found in major number %q", parts[0]) | |
241 | } | |
242 | if hasLeadingZeroes(parts[0]) { | |
243 | return Version{}, fmt.Errorf("Major number must not contain leading zeroes %q", parts[0]) | |
244 | } | |
245 | major, err := strconv.ParseUint(parts[0], 10, 64) | |
246 | if err != nil { | |
247 | return Version{}, err | |
248 | } | |
249 | ||
250 | // Minor | |
251 | if !containsOnly(parts[1], numbers) { | |
252 | return Version{}, fmt.Errorf("Invalid character(s) found in minor number %q", parts[1]) | |
253 | } | |
254 | if hasLeadingZeroes(parts[1]) { | |
255 | return Version{}, fmt.Errorf("Minor number must not contain leading zeroes %q", parts[1]) | |
256 | } | |
257 | minor, err := strconv.ParseUint(parts[1], 10, 64) | |
258 | if err != nil { | |
259 | return Version{}, err | |
260 | } | |
261 | ||
262 | v := Version{} | |
263 | v.Major = major | |
264 | v.Minor = minor | |
265 | ||
266 | var build, prerelease []string | |
267 | patchStr := parts[2] | |
268 | ||
269 | if buildIndex := strings.IndexRune(patchStr, '+'); buildIndex != -1 { | |
270 | build = strings.Split(patchStr[buildIndex+1:], ".") | |
271 | patchStr = patchStr[:buildIndex] | |
272 | } | |
273 | ||
274 | if preIndex := strings.IndexRune(patchStr, '-'); preIndex != -1 { | |
275 | prerelease = strings.Split(patchStr[preIndex+1:], ".") | |
276 | patchStr = patchStr[:preIndex] | |
277 | } | |
278 | ||
279 | if !containsOnly(patchStr, numbers) { | |
280 | return Version{}, fmt.Errorf("Invalid character(s) found in patch number %q", patchStr) | |
281 | } | |
282 | if hasLeadingZeroes(patchStr) { | |
283 | return Version{}, fmt.Errorf("Patch number must not contain leading zeroes %q", patchStr) | |
284 | } | |
285 | patch, err := strconv.ParseUint(patchStr, 10, 64) | |
286 | if err != nil { | |
287 | return Version{}, err | |
288 | } | |
289 | ||
290 | v.Patch = patch | |
291 | ||
292 | // Prerelease | |
293 | for _, prstr := range prerelease { | |
294 | parsedPR, err := NewPRVersion(prstr) | |
295 | if err != nil { | |
296 | return Version{}, err | |
297 | } | |
298 | v.Pre = append(v.Pre, parsedPR) | |
299 | } | |
300 | ||
301 | // Build meta data | |
302 | for _, str := range build { | |
303 | if len(str) == 0 { | |
304 | return Version{}, errors.New("Build meta data is empty") | |
305 | } | |
306 | if !containsOnly(str, alphanum) { | |
307 | return Version{}, fmt.Errorf("Invalid character(s) found in build meta data %q", str) | |
308 | } | |
309 | v.Build = append(v.Build, str) | |
310 | } | |
311 | ||
312 | return v, nil | |
313 | } | |
314 | ||
315 | // MustParse is like Parse but panics if the version cannot be parsed. | |
316 | func MustParse(s string) Version { | |
317 | v, err := Parse(s) | |
318 | if err != nil { | |
319 | panic(`semver: Parse(` + s + `): ` + err.Error()) | |
320 | } | |
321 | return v | |
322 | } | |
323 | ||
324 | // PRVersion represents a PreRelease Version | |
325 | type PRVersion struct { | |
326 | VersionStr string | |
327 | VersionNum uint64 | |
328 | IsNum bool | |
329 | } | |
330 | ||
331 | // NewPRVersion creates a new valid prerelease version | |
332 | func NewPRVersion(s string) (PRVersion, error) { | |
333 | if len(s) == 0 { | |
334 | return PRVersion{}, errors.New("Prerelease is empty") | |
335 | } | |
336 | v := PRVersion{} | |
337 | if containsOnly(s, numbers) { | |
338 | if hasLeadingZeroes(s) { | |
339 | return PRVersion{}, fmt.Errorf("Numeric PreRelease version must not contain leading zeroes %q", s) | |
340 | } | |
341 | num, err := strconv.ParseUint(s, 10, 64) | |
342 | ||
343 | // Might never be hit, but just in case | |
344 | if err != nil { | |
345 | return PRVersion{}, err | |
346 | } | |
347 | v.VersionNum = num | |
348 | v.IsNum = true | |
349 | } else if containsOnly(s, alphanum) { | |
350 | v.VersionStr = s | |
351 | v.IsNum = false | |
352 | } else { | |
353 | return PRVersion{}, fmt.Errorf("Invalid character(s) found in prerelease %q", s) | |
354 | } | |
355 | return v, nil | |
356 | } | |
357 | ||
358 | // IsNumeric checks if prerelease-version is numeric | |
359 | func (v PRVersion) IsNumeric() bool { | |
360 | return v.IsNum | |
361 | } | |
362 | ||
363 | // Compare compares two PreRelease Versions v and o: | |
364 | // -1 == v is less than o | |
365 | // 0 == v is equal to o | |
366 | // 1 == v is greater than o | |
367 | func (v PRVersion) Compare(o PRVersion) int { | |
368 | if v.IsNum && !o.IsNum { | |
369 | return -1 | |
370 | } else if !v.IsNum && o.IsNum { | |
371 | return 1 | |
372 | } else if v.IsNum && o.IsNum { | |
373 | if v.VersionNum == o.VersionNum { | |
374 | return 0 | |
375 | } else if v.VersionNum > o.VersionNum { | |
376 | return 1 | |
377 | } else { | |
378 | return -1 | |
379 | } | |
380 | } else { // both are Alphas | |
381 | if v.VersionStr == o.VersionStr { | |
382 | return 0 | |
383 | } else if v.VersionStr > o.VersionStr { | |
384 | return 1 | |
385 | } else { | |
386 | return -1 | |
387 | } | |
388 | } | |
389 | } | |
390 | ||
391 | // PreRelease version to string | |
392 | func (v PRVersion) String() string { | |
393 | if v.IsNum { | |
394 | return strconv.FormatUint(v.VersionNum, 10) | |
395 | } | |
396 | return v.VersionStr | |
397 | } | |
398 | ||
399 | func containsOnly(s string, set string) bool { | |
400 | return strings.IndexFunc(s, func(r rune) bool { | |
401 | return !strings.ContainsRune(set, r) | |
402 | }) == -1 | |
403 | } | |
404 | ||
405 | func hasLeadingZeroes(s string) bool { | |
406 | return len(s) > 1 && s[0] == '0' | |
407 | } | |
408 | ||
409 | // NewBuildVersion creates a new valid build version | |
410 | func NewBuildVersion(s string) (string, error) { | |
411 | if len(s) == 0 { | |
412 | return "", errors.New("Buildversion is empty") | |
413 | } | |
414 | if !containsOnly(s, alphanum) { | |
415 | return "", fmt.Errorf("Invalid character(s) found in build meta data %q", s) | |
416 | } | |
417 | return s, nil | |
418 | } |