semver.go

  1package semver
  2
  3import (
  4	"errors"
  5	"fmt"
  6	"strconv"
  7	"strings"
  8)
  9
 10const (
 11	numbers  string = "0123456789"
 12	alphas          = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-"
 13	alphanum        = alphas + numbers
 14)
 15
 16// SpecVersion is the latest fully supported spec version of semver
 17var SpecVersion = Version{
 18	Major: 2,
 19	Minor: 0,
 20	Patch: 0,
 21}
 22
 23// Version represents a semver compatible version
 24type 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
 33func (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.
 65func (v Version) Equals(o Version) bool {
 66	return (v.Compare(o) == 0)
 67}
 68
 69// EQ checks if v is equal to o.
 70func (v Version) EQ(o Version) bool {
 71	return (v.Compare(o) == 0)
 72}
 73
 74// NE checks if v is not equal to o.
 75func (v Version) NE(o Version) bool {
 76	return (v.Compare(o) != 0)
 77}
 78
 79// GT checks if v is greater than o.
 80func (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.
 85func (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.
 90func (v Version) GE(o Version) bool {
 91	return (v.Compare(o) >= 0)
 92}
 93
 94// LT checks if v is less than o.
 95func (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.
100func (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.
105func (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
113func (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// IncrementPatch increments the patch version
165func (v *Version) IncrementPatch() error {
166	if v.Major == 0 {
167		return fmt.Errorf("Patch version can not be incremented for %q", v.String())
168	}
169	v.Patch += 1
170	return nil
171}
172
173// IncrementMinor increments the minor version
174func (v *Version) IncrementMinor() error {
175	if v.Major == 0 {
176		return fmt.Errorf("Minor version can not be incremented for %q", v.String())
177	}
178	v.Minor += 1
179	v.Patch = 0
180	return nil
181}
182
183// IncrementMajor increments the major version
184func (v *Version) IncrementMajor() error {
185	if v.Major == 0 {
186		return fmt.Errorf("Major version can not be incremented for %q", v.String())
187	}
188	v.Major += 1
189	v.Minor = 0
190	v.Patch = 0
191	return nil
192}
193
194// Validate validates v and returns error in case
195func (v Version) Validate() error {
196	// Major, Minor, Patch already validated using uint64
197
198	for _, pre := range v.Pre {
199		if !pre.IsNum { //Numeric prerelease versions already uint64
200			if len(pre.VersionStr) == 0 {
201				return fmt.Errorf("Prerelease can not be empty %q", pre.VersionStr)
202			}
203			if !containsOnly(pre.VersionStr, alphanum) {
204				return fmt.Errorf("Invalid character(s) found in prerelease %q", pre.VersionStr)
205			}
206		}
207	}
208
209	for _, build := range v.Build {
210		if len(build) == 0 {
211			return fmt.Errorf("Build meta data can not be empty %q", build)
212		}
213		if !containsOnly(build, alphanum) {
214			return fmt.Errorf("Invalid character(s) found in build meta data %q", build)
215		}
216	}
217
218	return nil
219}
220
221// New is an alias for Parse and returns a pointer, parses version string and returns a validated Version or error
222func New(s string) (vp *Version, err error) {
223	v, err := Parse(s)
224	vp = &v
225	return
226}
227
228// Make is an alias for Parse, parses version string and returns a validated Version or error
229func Make(s string) (Version, error) {
230	return Parse(s)
231}
232
233// ParseTolerant allows for certain version specifications that do not strictly adhere to semver
234// specs to be parsed by this library. It does so by normalizing versions before passing them to
235// Parse(). It currently trims spaces, removes a "v" prefix, adds a 0 patch number to versions
236// with only major and minor components specified, and removes leading 0s.
237func ParseTolerant(s string) (Version, error) {
238	s = strings.TrimSpace(s)
239	s = strings.TrimPrefix(s, "v")
240
241	// Split into major.minor.(patch+pr+meta)
242	parts := strings.SplitN(s, ".", 3)
243	// Remove leading zeros.
244	for i, p := range parts {
245		if len(p) > 1 {
246			parts[i] = strings.TrimPrefix(p, "0")
247		}
248	}
249	// Fill up shortened versions.
250	if len(parts) < 3 {
251		if strings.ContainsAny(parts[len(parts)-1], "+-") {
252			return Version{}, errors.New("Short version cannot contain PreRelease/Build meta data")
253		}
254		for len(parts) < 3 {
255			parts = append(parts, "0")
256		}
257	}
258	s = strings.Join(parts, ".")
259
260	return Parse(s)
261}
262
263// Parse parses version string and returns a validated Version or error
264func Parse(s string) (Version, error) {
265	if len(s) == 0 {
266		return Version{}, errors.New("Version string empty")
267	}
268
269	// Split into major.minor.(patch+pr+meta)
270	parts := strings.SplitN(s, ".", 3)
271	if len(parts) != 3 {
272		return Version{}, errors.New("No Major.Minor.Patch elements found")
273	}
274
275	// Major
276	if !containsOnly(parts[0], numbers) {
277		return Version{}, fmt.Errorf("Invalid character(s) found in major number %q", parts[0])
278	}
279	if hasLeadingZeroes(parts[0]) {
280		return Version{}, fmt.Errorf("Major number must not contain leading zeroes %q", parts[0])
281	}
282	major, err := strconv.ParseUint(parts[0], 10, 64)
283	if err != nil {
284		return Version{}, err
285	}
286
287	// Minor
288	if !containsOnly(parts[1], numbers) {
289		return Version{}, fmt.Errorf("Invalid character(s) found in minor number %q", parts[1])
290	}
291	if hasLeadingZeroes(parts[1]) {
292		return Version{}, fmt.Errorf("Minor number must not contain leading zeroes %q", parts[1])
293	}
294	minor, err := strconv.ParseUint(parts[1], 10, 64)
295	if err != nil {
296		return Version{}, err
297	}
298
299	v := Version{}
300	v.Major = major
301	v.Minor = minor
302
303	var build, prerelease []string
304	patchStr := parts[2]
305
306	if buildIndex := strings.IndexRune(patchStr, '+'); buildIndex != -1 {
307		build = strings.Split(patchStr[buildIndex+1:], ".")
308		patchStr = patchStr[:buildIndex]
309	}
310
311	if preIndex := strings.IndexRune(patchStr, '-'); preIndex != -1 {
312		prerelease = strings.Split(patchStr[preIndex+1:], ".")
313		patchStr = patchStr[:preIndex]
314	}
315
316	if !containsOnly(patchStr, numbers) {
317		return Version{}, fmt.Errorf("Invalid character(s) found in patch number %q", patchStr)
318	}
319	if hasLeadingZeroes(patchStr) {
320		return Version{}, fmt.Errorf("Patch number must not contain leading zeroes %q", patchStr)
321	}
322	patch, err := strconv.ParseUint(patchStr, 10, 64)
323	if err != nil {
324		return Version{}, err
325	}
326
327	v.Patch = patch
328
329	// Prerelease
330	for _, prstr := range prerelease {
331		parsedPR, err := NewPRVersion(prstr)
332		if err != nil {
333			return Version{}, err
334		}
335		v.Pre = append(v.Pre, parsedPR)
336	}
337
338	// Build meta data
339	for _, str := range build {
340		if len(str) == 0 {
341			return Version{}, errors.New("Build meta data is empty")
342		}
343		if !containsOnly(str, alphanum) {
344			return Version{}, fmt.Errorf("Invalid character(s) found in build meta data %q", str)
345		}
346		v.Build = append(v.Build, str)
347	}
348
349	return v, nil
350}
351
352// MustParse is like Parse but panics if the version cannot be parsed.
353func MustParse(s string) Version {
354	v, err := Parse(s)
355	if err != nil {
356		panic(`semver: Parse(` + s + `): ` + err.Error())
357	}
358	return v
359}
360
361// PRVersion represents a PreRelease Version
362type PRVersion struct {
363	VersionStr string
364	VersionNum uint64
365	IsNum      bool
366}
367
368// NewPRVersion creates a new valid prerelease version
369func NewPRVersion(s string) (PRVersion, error) {
370	if len(s) == 0 {
371		return PRVersion{}, errors.New("Prerelease is empty")
372	}
373	v := PRVersion{}
374	if containsOnly(s, numbers) {
375		if hasLeadingZeroes(s) {
376			return PRVersion{}, fmt.Errorf("Numeric PreRelease version must not contain leading zeroes %q", s)
377		}
378		num, err := strconv.ParseUint(s, 10, 64)
379
380		// Might never be hit, but just in case
381		if err != nil {
382			return PRVersion{}, err
383		}
384		v.VersionNum = num
385		v.IsNum = true
386	} else if containsOnly(s, alphanum) {
387		v.VersionStr = s
388		v.IsNum = false
389	} else {
390		return PRVersion{}, fmt.Errorf("Invalid character(s) found in prerelease %q", s)
391	}
392	return v, nil
393}
394
395// IsNumeric checks if prerelease-version is numeric
396func (v PRVersion) IsNumeric() bool {
397	return v.IsNum
398}
399
400// Compare compares two PreRelease Versions v and o:
401// -1 == v is less than o
402// 0 == v is equal to o
403// 1 == v is greater than o
404func (v PRVersion) Compare(o PRVersion) int {
405	if v.IsNum && !o.IsNum {
406		return -1
407	} else if !v.IsNum && o.IsNum {
408		return 1
409	} else if v.IsNum && o.IsNum {
410		if v.VersionNum == o.VersionNum {
411			return 0
412		} else if v.VersionNum > o.VersionNum {
413			return 1
414		} else {
415			return -1
416		}
417	} else { // both are Alphas
418		if v.VersionStr == o.VersionStr {
419			return 0
420		} else if v.VersionStr > o.VersionStr {
421			return 1
422		} else {
423			return -1
424		}
425	}
426}
427
428// PreRelease version to string
429func (v PRVersion) String() string {
430	if v.IsNum {
431		return strconv.FormatUint(v.VersionNum, 10)
432	}
433	return v.VersionStr
434}
435
436func containsOnly(s string, set string) bool {
437	return strings.IndexFunc(s, func(r rune) bool {
438		return !strings.ContainsRune(set, r)
439	}) == -1
440}
441
442func hasLeadingZeroes(s string) bool {
443	return len(s) > 1 && s[0] == '0'
444}
445
446// NewBuildVersion creates a new valid build version
447func NewBuildVersion(s string) (string, error) {
448	if len(s) == 0 {
449		return "", errors.New("Buildversion is empty")
450	}
451	if !containsOnly(s, alphanum) {
452		return "", fmt.Errorf("Invalid character(s) found in build meta data %q", s)
453	}
454	return s, nil
455}