]> git.lizzy.rs Git - micro.git/blob - tools/semver/semver.go
Make XML highlighting more fine-grained
[micro.git] / tools / semver / semver.go
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 Precedence
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 }