From 1ea2d28cc8538fad15a7b71814a87e74e23ada03 Mon Sep 17 00:00:00 2001 From: Benedikt Lang Date: Tue, 1 Jul 2014 22:09:37 +0200 Subject: [PATCH 01/42] Add license --- LICENSE | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 LICENSE diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..5ba5c86 --- /dev/null +++ b/LICENSE @@ -0,0 +1,22 @@ +The MIT License + +Copyright (c) 2014 Benedikt Lang + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + From d2e12bf84bd22d2f76ff9af7e6d2f9b13b19bdc1 Mon Sep 17 00:00:00 2001 From: Benedikt Lang Date: Tue, 1 Jul 2014 22:11:26 +0200 Subject: [PATCH 02/42] First draft --- semver.go | 147 +++++++++++++++++++++++++++++++++++++++++++++++++ semver_test.go | 58 +++++++++++++++++++ 2 files changed, 205 insertions(+) create mode 100644 semver.go create mode 100644 semver_test.go diff --git a/semver.go b/semver.go new file mode 100644 index 0000000..69c9810 --- /dev/null +++ b/semver.go @@ -0,0 +1,147 @@ +package semver + +import ( + "bytes" + "errors" + "strconv" +) + +var SEMVER_SPEC_VERSION = Version{ + Major: 2, + Minor: 0, + Patch: 0, +} + +type Version struct { + Major int + Minor int + Patch int + Pre []PRVersion + Build []string //No Precendence +} + +func (v Version) String() string { + var buf bytes.Buffer + var DOT = []byte(".") + var HYPHEN = []byte("-") + var PLUS = []byte("+") + buf.WriteString(strconv.Itoa(v.Major)) + buf.Write(DOT) + buf.WriteString(strconv.Itoa(v.Minor)) + buf.Write(DOT) + buf.WriteString(strconv.Itoa(v.Patch)) + if len(v.Pre) > 0 { + buf.Write(HYPHEN) + for i, pre := range v.Pre { + if i > 0 { + buf.Write(DOT) + } + buf.WriteString(pre.String()) + } + } + if len(v.Build) > 0 { + buf.Write(PLUS) + for i, build := range v.Build { + if i > 0 { + buf.Write(DOT) + } + buf.WriteString(build) + } + } + return buf.String() +} + +func (v *Version) compare(o *Version) int { + if v.Major != o.Major { + if v.Major > o.Major { + return 1 + } else { + return -1 + } + } + if v.Minor != o.Minor { + if v.Minor > o.Minor { + return 1 + } else { + return -1 + } + } + if v.Patch != o.Patch { + if v.Patch > o.Patch { + return 1 + } else { + return -1 + } + } + + if len(v.Pre) == 0 && len(o.Pre) == 0 { + return 0 + } else if len(v.Pre) == 0 && len(o.Pre) > 0 { + return -1 + } else if len(v.Pre) > 0 && len(o.Pre) == 0 { + return 1 + } else { + //Deep PreRelease Version comparison + + return -2 //TODO: Not yet implemented + + } + +} + +func Parse(s string) (*Version, error) { + return nil, errors.New("Not implemented yet") +} + +// PreRelease Version +type PRVersion interface { + compare(*PRVersion) int + String() string //fmt.Stringer + IsNumeric() bool +} + +// Alphabetical PreRelease Version +type AlphaPRVersion struct { + Version string +} + +func (v *AlphaPRVersion) IsNumeric() bool { + return false +} + +func (v *AlphaPRVersion) compare(o *AlphaPRVersion) int { + if v.Version == o.Version { + return 0 + } else if v.Version > o.Version { + return 1 + } else { + return -1 + } +} + +func (v AlphaPRVersion) String() string { + return v.Version +} + +// Numeric PreRelease Version +type NumPRVersion struct { + Version int +} + +func (v *NumPRVersion) compare(o *NumPRVersion) int { + if v.Version == o.Version { + return 0 + } else if v.Version > o.Version { + return 1 + } else { + return -1 + } +} + +func (v NumPRVersion) String() string { + return strconv.Itoa(v.Version) +} + +func (v *NumPRVersion) IsNumeric() bool { + return true +} diff --git a/semver_test.go b/semver_test.go new file mode 100644 index 0000000..37868c1 --- /dev/null +++ b/semver_test.go @@ -0,0 +1,58 @@ +package semver + +import ( + "testing" +) + +func TestParse(t *testing.T) { + +} + +type stringerTest struct { + v Version + result string +} + +var stringerTests = []stringerTest{ + {Version{1, 2, 3, nil, nil}, "1.2.3"}, + {Version{0, 0, 1, nil, nil}, "0.0.1"}, +} + +func TestStringer(t *testing.T) { + for _, test := range stringerTests { + if res := test.v.String(); res != test.result { + t.Errorf("Stringer, expected %q but got %q", test.result, res) + } + } +} + +type compareTest struct { + v1 Version + v2 Version + result int +} + +var compareTests = []compareTest{ + {Version{1, 0, 0, nil, nil}, Version{1, 0, 0, nil, nil}, 0}, + {Version{2, 0, 0, nil, nil}, Version{1, 0, 0, nil, nil}, 1}, + {Version{0, 1, 0, nil, nil}, Version{0, 1, 0, nil, nil}, 0}, + {Version{0, 2, 0, nil, nil}, Version{0, 1, 0, nil, nil}, 1}, + {Version{0, 0, 1, nil, nil}, Version{0, 0, 1, nil, nil}, 0}, + {Version{0, 0, 2, nil, nil}, Version{0, 0, 1, nil, nil}, 1}, + {Version{1, 2, 3, nil, nil}, Version{1, 2, 3, nil, nil}, 0}, + {Version{2, 2, 4, nil, nil}, Version{1, 2, 4, nil, nil}, 1}, + {Version{1, 3, 3, nil, nil}, Version{1, 2, 3, nil, nil}, 1}, + {Version{1, 2, 4, nil, nil}, Version{1, 2, 3, nil, nil}, 1}, +} + +func TestCompare(t *testing.T) { + for _, test := range compareTests { + if res := test.v1.compare(&test.v2); res != test.result { + t.Errorf("Comparing %q : %q, expected %d but got %d", test.v1, test.v2, test.result, res) + } + //Test counterpart + if res := test.v2.compare(&test.v1); res != -test.result { + t.Errorf("Comparing %q : %q, expected %d but got %d", test.v2, test.v1, -test.result, res) + } + } +} From a76e16a4c7c36cbe1ffb0da37f8583e28aa0934a Mon Sep 17 00:00:00 2001 From: Benedikt Lang Date: Wed, 2 Jul 2014 02:15:48 +0200 Subject: [PATCH 03/42] Full working set, missing some tests --- semver.go | 222 ++++++++++++++++++++++++++++++++++++++----------- semver_test.go | 58 +++++++++++-- 2 files changed, 221 insertions(+), 59 deletions(-) diff --git a/semver.go b/semver.go index 69c9810..fc392e9 100644 --- a/semver.go +++ b/semver.go @@ -3,7 +3,9 @@ package semver import ( "bytes" "errors" + "fmt" "strconv" + "strings" ) var SEMVER_SPEC_VERSION = Version{ @@ -13,23 +15,23 @@ var SEMVER_SPEC_VERSION = Version{ } type Version struct { - Major int - Minor int - Patch int - Pre []PRVersion + Major uint64 + Minor uint64 + Patch uint64 + Pre []*PRVersion Build []string //No Precendence } -func (v Version) String() string { +func (v *Version) String() string { var buf bytes.Buffer var DOT = []byte(".") var HYPHEN = []byte("-") var PLUS = []byte("+") - buf.WriteString(strconv.Itoa(v.Major)) + buf.WriteString(strconv.FormatUint(v.Major, 10)) buf.Write(DOT) - buf.WriteString(strconv.Itoa(v.Minor)) + buf.WriteString(strconv.FormatUint(v.Minor, 10)) buf.Write(DOT) - buf.WriteString(strconv.Itoa(v.Patch)) + buf.WriteString(strconv.FormatUint(v.Patch, 10)) if len(v.Pre) > 0 { buf.Write(HYPHEN) for i, pre := range v.Pre { @@ -51,7 +53,7 @@ func (v Version) String() string { return buf.String() } -func (v *Version) compare(o *Version) int { +func (v *Version) Compare(o *Version) int { if v.Major != o.Major { if v.Major > o.Major { return 1 @@ -74,74 +76,194 @@ func (v *Version) compare(o *Version) int { } } + // Quick comparison if a version has no prerelease versions if len(v.Pre) == 0 && len(o.Pre) == 0 { return 0 } else if len(v.Pre) == 0 && len(o.Pre) > 0 { - return -1 - } else if len(v.Pre) > 0 && len(o.Pre) == 0 { return 1 + } else if len(v.Pre) > 0 && len(o.Pre) == 0 { + return -1 } else { - //Deep PreRelease Version comparison - return -2 //TODO: Not yet implemented + i := 0 + for ; i < len(v.Pre) && i < len(o.Pre); i++ { + if comp := v.Pre[i].Compare(o.Pre[i]); comp == 0 { + continue + } else if comp == 1 { + return 1 + } else { + return -1 + } + } - } + // If all pr versions are the equal but one has further prversion, this one greater + if i == len(v.Pre) && i == len(o.Pre) { + return 0 + } else if i == len(v.Pre) && i < len(o.Pre) { + return -1 + } else { + return 1 + } + } } func Parse(s string) (*Version, error) { - return nil, errors.New("Not implemented yet") -} + if len(s) == 0 { + return nil, errors.New("Version string empty") + } -// PreRelease Version -type PRVersion interface { - compare(*PRVersion) int - String() string //fmt.Stringer - IsNumeric() bool -} + parts := strings.SplitN(s, ".", 3) + if len(parts) != 3 { + return nil, errors.New("No Major.Minor.Patch elements found") + } -// Alphabetical PreRelease Version -type AlphaPRVersion struct { - Version string -} + if !containsOnly(parts[0], NUMBERS) { + return nil, fmt.Errorf("Invalid character(s) found in major number %q", parts[0]) + } + major, err := strconv.ParseUint(parts[0], 10, 64) + if err != nil { + return nil, err + } -func (v *AlphaPRVersion) IsNumeric() bool { - return false -} + if !containsOnly(parts[1], NUMBERS) { + return nil, fmt.Errorf("Invalid character(s) found in minor number %q", parts[1]) + } + minor, err := strconv.ParseUint(parts[1], 10, 64) + if err != nil { + return nil, err + } -func (v *AlphaPRVersion) compare(o *AlphaPRVersion) int { - if v.Version == o.Version { - return 0 - } else if v.Version > o.Version { - return 1 + preIndex := strings.Index(parts[2], "-") + buildIndex := strings.Index(parts[2], "+") + + var subVersionIndex int + if preIndex != -1 && buildIndex == -1 { + subVersionIndex = preIndex + } else if preIndex == -1 && buildIndex != -1 { + subVersionIndex = buildIndex + } else if preIndex == -1 && buildIndex == -1 { + subVersionIndex = len(parts[2]) } else { - return -1 + // if there is no actual preIndex but a hyphen inside the build meta data + if buildIndex < preIndex { + subVersionIndex = buildIndex + preIndex = -1 // Build meta data before preIndex found implicates there are no prerelease versions + } else { + subVersionIndex = preIndex + } } -} -func (v AlphaPRVersion) String() string { - return v.Version + if !containsOnly(parts[2][:subVersionIndex], NUMBERS) { + return nil, fmt.Errorf("Invalid character(s) found in patch number %q", parts[2][:subVersionIndex]) + } + patch, err := strconv.ParseUint(parts[2][:subVersionIndex], 10, 64) + if err != nil { + return nil, err + } + v := &Version{} + v.Major = major + v.Minor = minor + v.Patch = patch + + // There are PreRelease versions + if preIndex != -1 { + var preRels string + if buildIndex != -1 { + preRels = parts[2][subVersionIndex+1 : buildIndex] + } else { + preRels = parts[2][subVersionIndex+1:] + } + prparts := strings.Split(preRels, ".") + for _, prstr := range prparts { + parsedPR, err := NewPRVersion(prstr) + if err != nil { + return nil, err + } + v.Pre = append(v.Pre, parsedPR) + } + } + + // There is build meta data + if buildIndex != -1 { + buildStr := parts[2][buildIndex+1:] + buildParts := strings.Split(buildStr, ".") + for _, str := range buildParts { + if !containsOnly(str, ALPHAS+NUMBERS) { + return nil, fmt.Errorf("Invalid character(s) found in build meta data %q", str) + } + v.Build = append(v.Build, str) + } + } + + return v, nil } -// Numeric PreRelease Version -type NumPRVersion struct { - Version int +// PreRelease Version +type PRVersion struct { + VersionStr string + VersionNum uint64 + IsNum bool } -func (v *NumPRVersion) compare(o *NumPRVersion) int { - if v.Version == o.Version { - return 0 - } else if v.Version > o.Version { - return 1 +func NewPRVersion(s string) (*PRVersion, error) { + v := &PRVersion{} + if containsOnly(s, NUMBERS) { + num, err := strconv.ParseUint(s, 10, 64) + if err != nil { + return nil, err + } + v.VersionNum = num + v.IsNum = true + } else if containsOnly(s, ALPHAS+NUMBERS) { + v.VersionStr = s + v.IsNum = false } else { + return nil, fmt.Errorf("Invalid character(s) found in prerelease %q", s) + } + return v, nil +} + +func (v *PRVersion) IsNumeric() bool { + return v.IsNum +} + +func (v *PRVersion) Compare(o *PRVersion) int { + if v.IsNum && !o.IsNum { return -1 + } else if !v.IsNum && o.IsNum { + return 1 + } else if v.IsNum && o.IsNum { + if v.VersionNum == o.VersionNum { + return 0 + } else if v.VersionNum > o.VersionNum { + return 1 + } else { + return -1 + } + } else { // both are Alphas + if v.VersionStr == o.VersionStr { + return 0 + } else if v.VersionStr > o.VersionStr { + return 1 + } else { + return -1 + } } } -func (v NumPRVersion) String() string { - return strconv.Itoa(v.Version) +func (v *PRVersion) String() string { + if v.IsNum { + return strconv.FormatUint(v.VersionNum, 10) + } + return v.VersionStr } -func (v *NumPRVersion) IsNumeric() bool { - return true +const NUMBERS = "0123456789" +const ALPHAS = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-" + +func containsOnly(s string, set string) bool { + return strings.IndexFunc(s, func(r rune) bool { + return !strings.ContainsRune(set, r) + }) == -1 } diff --git a/semver_test.go b/semver_test.go index 37868c1..897d976 100644 --- a/semver_test.go +++ b/semver_test.go @@ -1,31 +1,55 @@ package semver +//TODO: Test incorrect version formats + import ( "testing" ) -func TestParse(t *testing.T) { - -} - -type stringerTest struct { +type formatTest struct { v Version result string } -var stringerTests = []stringerTest{ +var formatTests = []formatTest{ {Version{1, 2, 3, nil, nil}, "1.2.3"}, {Version{0, 0, 1, nil, nil}, "0.0.1"}, + {Version{0, 0, 1, []*PRVersion{prstr("alpha"), prstr("preview")}, []string{"123", "456"}}, "0.0.1-alpha.preview+123.456"}, + {Version{1, 2, 3, []*PRVersion{prstr("alpha"), prnum(1)}, []string{"123", "456"}}, "1.2.3-alpha.1+123.456"}, + {Version{1, 2, 3, []*PRVersion{prstr("alpha"), prnum(1)}, nil}, "1.2.3-alpha.1"}, + {Version{1, 2, 3, nil, []string{"123", "456"}}, "1.2.3+123.456"}, + // Prereleases and build metadata hyphens + {Version{1, 2, 3, []*PRVersion{prstr("alpha"), prstr("b-eta")}, []string{"123", "b-uild"}}, "1.2.3-alpha.b-eta+123.b-uild"}, + {Version{1, 2, 3, nil, []string{"123", "b-uild"}}, "1.2.3+123.b-uild"}, + {Version{1, 2, 3, []*PRVersion{prstr("alpha"), prstr("b-eta")}, nil}, "1.2.3-alpha.b-eta"}, +} + +func prstr(s string) *PRVersion { + return &PRVersion{s, 0, false} +} + +func prnum(i uint64) *PRVersion { + return &PRVersion{"", i, true} } func TestStringer(t *testing.T) { - for _, test := range stringerTests { + for _, test := range formatTests { if res := test.v.String(); res != test.result { t.Errorf("Stringer, expected %q but got %q", test.result, res) } } } +func TestParse(t *testing.T) { + for _, test := range formatTests { + if v, err := Parse(test.result); err != nil { + t.Errorf("Error parsing %q: %q", test.result, err) + } else if comp := v.Compare(&test.v); comp != 0 { + t.Errorf("Parsing, expected %q but got %q, comp: %d ", test.v, v, comp) + } + } +} + type compareTest struct { v1 Version v2 Version @@ -43,15 +67,31 @@ var compareTests = []compareTest{ {Version{2, 2, 4, nil, nil}, Version{1, 2, 4, nil, nil}, 1}, {Version{1, 3, 3, nil, nil}, Version{1, 2, 3, nil, nil}, 1}, {Version{1, 2, 4, nil, nil}, Version{1, 2, 3, nil, nil}, 1}, + + // Spec Examples #11 + {Version{1, 0, 0, nil, nil}, Version{2, 0, 0, nil, nil}, -1}, + {Version{2, 0, 0, nil, nil}, Version{2, 1, 0, nil, nil}, -1}, + {Version{2, 1, 0, nil, nil}, Version{2, 1, 1, nil, nil}, -1}, + + {Version{1, 0, 0, []*PRVersion{prstr("alpha")}, nil}, Version{1, 0, 0, []*PRVersion{prstr("alpha"), prnum(1)}, nil}, -1}, + {Version{1, 0, 0, []*PRVersion{prstr("alpha"), prnum(1)}, nil}, Version{1, 0, 0, []*PRVersion{prstr("alpha"), prstr("beta")}, nil}, -1}, + {Version{1, 0, 0, []*PRVersion{prstr("alpha"), prstr("beta")}, nil}, Version{1, 0, 0, []*PRVersion{prstr("beta")}, nil}, -1}, + {Version{1, 0, 0, []*PRVersion{prstr("beta")}, nil}, Version{1, 0, 0, []*PRVersion{prstr("beta"), prnum(2)}, nil}, -1}, + {Version{1, 0, 0, []*PRVersion{prstr("beta"), prnum(2)}, nil}, Version{1, 0, 0, []*PRVersion{prstr("beta"), prnum(11)}, nil}, -1}, + {Version{1, 0, 0, []*PRVersion{prstr("beta"), prnum(11)}, nil}, Version{1, 0, 0, []*PRVersion{prstr("rc"), prnum(1)}, nil}, -1}, + {Version{1, 0, 0, []*PRVersion{prstr("rc"), prnum(1)}, nil}, Version{1, 0, 0, nil, nil}, -1}, + + // Ignore Build metadata + {Version{1, 0, 0, nil, []string{"1", "2", "3"}}, Version{1, 0, 0, nil, nil}, 0}, } func TestCompare(t *testing.T) { for _, test := range compareTests { - if res := test.v1.compare(&test.v2); res != test.result { + if res := test.v1.Compare(&test.v2); res != test.result { t.Errorf("Comparing %q : %q, expected %d but got %d", test.v1, test.v2, test.result, res) } //Test counterpart - if res := test.v2.compare(&test.v1); res != -test.result { + if res := test.v2.Compare(&test.v1); res != -test.result { t.Errorf("Comparing %q : %q, expected %d but got %d", test.v2, test.v1, -test.result, res) } } From 6bee9cf8c9807748b63e596285ba4fbf4ab256c8 Mon Sep 17 00:00:00 2001 From: Benedikt Lang Date: Wed, 2 Jul 2014 10:38:36 +0200 Subject: [PATCH 04/42] Add validation, further tests, compare helpers --- semver.go | 87 ++++++++++++++++++++++++++++++++--- semver_test.go | 120 +++++++++++++++++++++++++++++++++++++++++++++---- 2 files changed, 194 insertions(+), 13 deletions(-) diff --git a/semver.go b/semver.go index fc392e9..ec2932d 100644 --- a/semver.go +++ b/semver.go @@ -8,7 +8,11 @@ import ( "strings" ) -var SEMVER_SPEC_VERSION = Version{ +const NUMBERS = "0123456789" +const ALPHAS = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-" + +// Latest fully supported spec version +var SPEC_VERSION = Version{ Major: 2, Minor: 0, Patch: 0, @@ -22,6 +26,7 @@ type Version struct { Build []string //No Precendence } +// Version to string func (v *Version) String() string { var buf bytes.Buffer var DOT = []byte(".") @@ -53,6 +58,30 @@ func (v *Version) String() string { return buf.String() } +// Checks if v is greater than o. +func (v *Version) GT(o *Version) bool { + return (v.Compare(o) == 1) +} + +// Checks if v is greater than or equal to o. +func (v *Version) GTE(o *Version) bool { + return (v.Compare(o) >= 0) +} + +// Checks if v is less than o. +func (v *Version) LT(o *Version) bool { + return (v.Compare(o) == -1) +} + +// Checks if v is less than or equal to o. +func (v *Version) LTE(o *Version) bool { + return (v.Compare(o) <= 0) +} + +// Compares Versions v to o: +// -1 == v is less than o +// 0 == v is equal to o +// 1 == v is greater than o func (v *Version) Compare(o *Version) int { if v.Major != o.Major { if v.Major > o.Major { @@ -108,16 +137,50 @@ func (v *Version) Compare(o *Version) int { } } +// Validates v and returns error in case +func (v *Version) Validate() error { + // Major, Minor, Patch already validated using uint64 + + if len(v.Pre) > 0 { + for _, pre := range v.Pre { + if !pre.IsNum { //Numeric prerelease versions already uint64 + if len(pre.VersionStr) == 0 { + return fmt.Errorf("Prerelease can not be empty %q", pre.VersionStr) + } + if !containsOnly(pre.VersionStr, NUMBERS+ALPHAS) { + return fmt.Errorf("Invalid character(s) found in prerelease %q", pre.VersionStr) + } + } + } + } + + if len(v.Build) > 0 { + for _, build := range v.Build { + if len(build) == 0 { + return fmt.Errorf("Build meta data can not be empty %q", build) + } + if !containsOnly(build, ALPHAS+NUMBERS) { + return fmt.Errorf("Invalid character(s) found in build meta data %q", build) + } + } + } + + return nil +} + +// Parses a string to version func Parse(s string) (*Version, error) { if len(s) == 0 { return nil, errors.New("Version string empty") } + // Split into major.minor.(patch+pr+meta) parts := strings.SplitN(s, ".", 3) if len(parts) != 3 { return nil, errors.New("No Major.Minor.Patch elements found") } + // Major if !containsOnly(parts[0], NUMBERS) { return nil, fmt.Errorf("Invalid character(s) found in major number %q", parts[0]) } @@ -126,6 +189,7 @@ func Parse(s string) (*Version, error) { return nil, err } + // Minor if !containsOnly(parts[1], NUMBERS) { return nil, fmt.Errorf("Invalid character(s) found in minor number %q", parts[1]) } @@ -137,6 +201,7 @@ func Parse(s string) (*Version, error) { preIndex := strings.Index(parts[2], "-") buildIndex := strings.Index(parts[2], "+") + // Determine last index of patch version (first of pre or build versions) var subVersionIndex int if preIndex != -1 && buildIndex == -1 { subVersionIndex = preIndex @@ -145,7 +210,7 @@ func Parse(s string) (*Version, error) { } else if preIndex == -1 && buildIndex == -1 { subVersionIndex = len(parts[2]) } else { - // if there is no actual preIndex but a hyphen inside the build meta data + // if there is no actual prversion but a hyphen inside the build meta data if buildIndex < preIndex { subVersionIndex = buildIndex preIndex = -1 // Build meta data before preIndex found implicates there are no prerelease versions @@ -189,6 +254,9 @@ func Parse(s string) (*Version, error) { buildStr := parts[2][buildIndex+1:] buildParts := strings.Split(buildStr, ".") for _, str := range buildParts { + if len(str) == 0 { + return nil, errors.New("Build meta data is empty") + } if !containsOnly(str, ALPHAS+NUMBERS) { return nil, fmt.Errorf("Invalid character(s) found in build meta data %q", str) } @@ -206,10 +274,16 @@ type PRVersion struct { IsNum bool } +// Creates a new valid prerelease version func NewPRVersion(s string) (*PRVersion, error) { + if len(s) == 0 { + return nil, errors.New("Prerelease is empty") + } v := &PRVersion{} if containsOnly(s, NUMBERS) { num, err := strconv.ParseUint(s, 10, 64) + + // Might never be hit, but just in case if err != nil { return nil, err } @@ -224,10 +298,15 @@ func NewPRVersion(s string) (*PRVersion, error) { return v, nil } +// Is pre release version numeric? func (v *PRVersion) IsNumeric() bool { return v.IsNum } +// Compares PreRelease Versions v to o: +// -1 == v is less than o +// 0 == v is equal to o +// 1 == v is greater than o func (v *PRVersion) Compare(o *PRVersion) int { if v.IsNum && !o.IsNum { return -1 @@ -252,6 +331,7 @@ func (v *PRVersion) Compare(o *PRVersion) int { } } +// PreRelease version to string func (v *PRVersion) String() string { if v.IsNum { return strconv.FormatUint(v.VersionNum, 10) @@ -259,9 +339,6 @@ func (v *PRVersion) String() string { return v.VersionStr } -const NUMBERS = "0123456789" -const ALPHAS = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-" - func containsOnly(s string, set string) bool { return strings.IndexFunc(s, func(r rune) bool { return !strings.ContainsRune(set, r) diff --git a/semver_test.go b/semver_test.go index 897d976..9707f51 100644 --- a/semver_test.go +++ b/semver_test.go @@ -6,6 +6,14 @@ import ( "testing" ) +func prstr(s string) *PRVersion { + return &PRVersion{s, 0, false} +} + +func prnum(i uint64) *PRVersion { + return &PRVersion{"", i, true} +} + type formatTest struct { v Version result string @@ -24,14 +32,6 @@ var formatTests = []formatTest{ {Version{1, 2, 3, []*PRVersion{prstr("alpha"), prstr("b-eta")}, nil}, "1.2.3-alpha.b-eta"}, } -func prstr(s string) *PRVersion { - return &PRVersion{s, 0, false} -} - -func prnum(i uint64) *PRVersion { - return &PRVersion{"", i, true} -} - func TestStringer(t *testing.T) { for _, test := range formatTests { if res := test.v.String(); res != test.result { @@ -46,6 +46,16 @@ func TestParse(t *testing.T) { t.Errorf("Error parsing %q: %q", test.result, err) } else if comp := v.Compare(&test.v); comp != 0 { t.Errorf("Parsing, expected %q but got %q, comp: %d ", test.v, v, comp) + } else if err := v.Validate(); err != nil { + t.Errorf("Error validating parsed version %q: %q", test.v, err) + } + } +} + +func TestValidate(t *testing.T) { + for _, test := range formatTests { + if err := test.v.Validate(); err != nil { + t.Errorf("Error validating %q: %q", test.v, err) } } } @@ -96,3 +106,97 @@ func TestCompare(t *testing.T) { } } } + +type wrongformatTest struct { + v *Version + str string +} + +var wrongformatTests = []wrongformatTest{ + {nil, ""}, + {nil, "."}, + {nil, "1."}, + {nil, ".1"}, + {nil, "a.b.c"}, + {nil, "1.a.b"}, + {nil, "1.1.a"}, + {nil, "1.a.1"}, + {nil, "a.1.1"}, + {nil, ".."}, + {nil, "1.."}, + {nil, "1.1."}, + {nil, "1..1"}, + {nil, "1.1.+123"}, + {nil, "1.1.-beta"}, + {nil, "-1.1.1"}, + {nil, "1.-1.1"}, + {nil, "1.1.-1"}, + {&Version{0, 0, 0, []*PRVersion{prstr("!")}, nil}, "0.0.0-!"}, + {&Version{0, 0, 0, nil, []string{"!"}}, "0.0.0+!"}, + // empty prversion + {&Version{0, 0, 0, []*PRVersion{prstr(""), prstr("alpha")}, nil}, "0.0.0-.alpha"}, + // empty build meta data + {&Version{0, 0, 0, []*PRVersion{prstr("alpha")}, []string{""}}, "0.0.0-alpha+"}, + {&Version{0, 0, 0, []*PRVersion{prstr("alpha")}, []string{"test", ""}}, "0.0.0-alpha+test."}, +} + +func TestWrongFormat(t *testing.T) { + for _, test := range wrongformatTests { + + if res, err := Parse(test.str); err == nil { + t.Errorf("Parsing wrong format version %q, expected error but got %q", test.str, res) + } + + if test.v != nil { + if err := test.v.Validate(); err == nil { + t.Errorf("Validating wrong format version %q (%q), expected error", test.v, test.str) + } + } + } +} + +func TestCompareHelper(t *testing.T) { + v := &Version{1, 0, 0, []*PRVersion{prstr("alpha")}, nil} + v1 := &Version{1, 0, 0, nil, nil} + if !v.GTE(v) { + t.Errorf("%q should be greater than or equal to %q", v, v) + } + if !v.LTE(v) { + t.Errorf("%q should be greater than or equal to %q", v, v) + } + if !v.LT(v1) { + t.Errorf("%q should be less than %q", v, v1) + } + if !v.LTE(v1) { + t.Errorf("%q should be less than or equal %q", v, v1) + } + if !v1.GT(v) { + t.Errorf("%q should be less than %q", v1, v) + } + if !v1.GTE(v) { + t.Errorf("%q should be less than or equal %q", v1, v) + } +} + +func TestPreReleaseVersions(t *testing.T) { + p1, err := NewPRVersion("123") + if !p1.IsNumeric() { + t.Errorf("Expected numeric prversion, got %q", p1) + } + if p1.VersionNum != 123 { + t.Error("Wrong prversion number") + } + if err != nil { + t.Errorf("Not expected error %q", err) + } + p2, err := NewPRVersion("alpha") + if p2.IsNumeric() { + t.Errorf("Expected non-numeric prversion, got %q", p2) + } + if p2.VersionStr != "alpha" { + t.Error("Wrong prversion string") + } + if err != nil { + t.Errorf("Not expected error %q", err) + } +} From 58fd7ff9c375ca9b634652d5b9a9b77ab2991dc6 Mon Sep 17 00:00:00 2001 From: Benedikt Lang Date: Wed, 2 Jul 2014 12:29:34 +0200 Subject: [PATCH 05/42] README, examples, benchmarks --- README.md | 126 +++++++++++++++++++++++++++++++++++++++++++++++ examples/main.go | 83 +++++++++++++++++++++++++++++++ semver.go | 16 ++++++ semver_test.go | 110 ++++++++++++++++++++++++++++++++++++++++- 4 files changed, 333 insertions(+), 2 deletions(-) create mode 100644 README.md create mode 100644 examples/main.go diff --git a/README.md b/README.md new file mode 100644 index 0000000..710e362 --- /dev/null +++ b/README.md @@ -0,0 +1,126 @@ +semver for golang +====== + +semver is a Semantic Versioning library written in golang. It fully covers spec version `2.0.0`. + +Usage +----- + + $ go get github.com/blang/semver + + import github.com/blang/semver + v1, err := semver.New("1.0.0-beta") + v2, err := semver.New("2.0.0-beta") + v1.Compare(v2) + + +Why should I use this lib? +----- + +- Fully spec compatible +- No reflection +- No regex +- Fully tested (Coverage >99%) +- Readable parsing/validation errors +- Fast (See [Benchmarks](#benchmarks)) +- Only Stdlib + + +Features +----- + +- Parsing and validation at all levels +- Comparator-like comparisons +- Compare Helper Methods +- InPlace manipulation + + +Example +----- + +Have a look at full examples in [examples/main.go](examples/main.go) + + import github.com/blang/semver + + v, err := semver.New("0.0.1-alpha.preview+123.github") + fmt.Printf("Major: %d\n", v.Major) + fmt.Printf("Minor: %d\n", v.Minor) + fmt.Printf("Patch: %d\n", v.Patch) + fmt.Printf("Pre: %s\n", v.Pre) + fmt.Printf("Build: %s\n", v.Build) + + // Prerelease versions array + if len(v.Pre) > 0 { + fmt.Println("Prerelease versions:") + for i, pre := range v.Pre { + fmt.Printf("%d: %q\n", i, pre) + } + } + + // Build meta data array + if len(v.Build) > 0 { + fmt.Println("Build meta data:") + for i, build := range v.Build { + fmt.Printf("%d: %q\n", i, build) + } + } + + v001, err := semver.New("0.0.1") + // Compare using helpers: v.GT(v2), v.LT, v.GTE, v.LTE + v001.GT(v) == true + v.LT(v001) == true + v.GTE(v) == true + v.LTE(v) == true + + // Or use v.Compare(v2) for comparisons (-1, 0, 1): + v001.Compare(v) == 1 + v.Compare(v001) == -1 + v.Compare(v) == 0 + + // Manipulate Version in place: + v.Pre[0], err = semver.NewPRVersion("beta") + if err != nil { + fmt.Printf("Error parsing pre release version: %q", err) + } + + fmt.Println("\nValidate versions:") + v.Build[0] = "?" + + err = v.Validate() + if err != nil { + fmt.Printf("Validation failed: %s\n", err) + } + + +Benchmarks +----- + + BenchmarkParseSimple 5000000 442 ns/op + BenchmarkParseComplex 1000000 2441 ns/op + BenchmarkParseAverage 1000000 1497 ns/op + BenchmarkValidateSimple 500000000 4.83 ns/op + BenchmarkValidateComplex 1000000 1236 ns/op + BenchmarkValidateAverage 5000000 580 ns/op + BenchmarkCompareSimple 500000000 5.43 ns/op + BenchmarkCompareComplex 100000000 26.3 ns/op + BenchmarkCompareAverage 100000000 29.6 ns/op + +See benchmark cases at [semver_test.go](semver_test.go) + + +Motivation +----- + +I simply couldn't find any lib supporting the full spec. Others were just wrong or used reflection and regex which i don't like. + + +Contribution +----- + +Feel free to make a pull request. For bigger changes create a issue first to discuss about it. + + +License +----- + +See [LICENSE](LICENSE) file. diff --git a/examples/main.go b/examples/main.go new file mode 100644 index 0000000..617d72a --- /dev/null +++ b/examples/main.go @@ -0,0 +1,83 @@ +package main + +import ( + "fmt" + "github.com/blang/semver" +) + +func main() { + v, err := semver.Parse("0.0.1-alpha.preview.222+123.github") + if err != nil { + fmt.Printf("Error while parsing (not valid): %q", err) + } + fmt.Printf("Version to string: %q\n", v) + + fmt.Printf("Major: %d\n", v.Major) + fmt.Printf("Minor: %d\n", v.Minor) + fmt.Printf("Patch: %d\n", v.Patch) + + // Prerelease versions + if len(v.Pre) > 0 { + fmt.Println("Prerelease versions:") + for i, pre := range v.Pre { + fmt.Printf("%d: %q\n", i, pre) + } + } + + // Build meta data + if len(v.Build) > 0 { + fmt.Println("Build meta data:") + for i, build := range v.Build { + fmt.Printf("%d: %q\n", i, build) + } + } + + // New == Parse + v001, err := semver.New("0.0.1") + + fmt.Println("\nUse Version.Compare for comparisons (-1, 0, 1):") + fmt.Printf("%q is greater than %q: Compare == %d\n", v001, v, v001.Compare(v)) + fmt.Printf("%q is less than %q: Compare == %d\n", v, v001, v.Compare(v001)) + fmt.Printf("%q is equal to %q: Compare == %d\n", v, v, v.Compare(v)) + + fmt.Println("\nUse comparison helpers returning booleans:") + fmt.Printf("%q is greater than %q: %t\n", v001, v, v001.GT(v)) + fmt.Printf("%q is greater than equal %q: %t\n", v001, v, v001.GTE(v)) + fmt.Printf("%q is greater than equal %q: %t\n", v, v, v.GTE(v)) + fmt.Printf("%q is less than %q: %t\n", v, v001, v.LT(v001)) + fmt.Printf("%q is less than equal %q: %t\n", v, v001, v.LTE(v001)) + fmt.Printf("%q is less than equal %q: %t\n", v, v, v.LTE(v)) + + fmt.Println("\nManipulate Version in place:") + v.Pre[0], err = semver.NewPRVersion("beta") + if err != nil { + fmt.Printf("Error parsing pre release version: %q", err) + } + fmt.Printf("Version to string: %q\n", v) + + fmt.Println("\nCompare Prerelease versions:") + pre1, _ := semver.NewPRVersion("123") + pre2, _ := semver.NewPRVersion("alpha") + pre3, _ := semver.NewPRVersion("124") + fmt.Printf("%q is less than %q: Compare == %d\n", pre1, pre2, pre1.Compare(pre2)) + fmt.Printf("%q is greater than %q: Compare == %d\n", pre3, pre1, pre3.Compare(pre1)) + fmt.Printf("%q is equal to %q: Compare == %d\n", pre1, pre1, pre1.Compare(pre1)) + + fmt.Println("\nValidate versions:") + v.Build[0] = "?" + + err = v.Validate() + if err != nil { + fmt.Printf("Validation failed: %s\n", err) + } + + fmt.Println("Create valid build meta data:") + b1, _ := semver.NewBuildVersion("build123") + v.Build[0] = b1 + fmt.Printf("Version with new build version %q\n", v) + + _, err = semver.NewBuildVersion("build?123") + if err != nil { + fmt.Printf("Create build version failed: %s\n", err) + } +} diff --git a/semver.go b/semver.go index ec2932d..241890d 100644 --- a/semver.go +++ b/semver.go @@ -168,6 +168,11 @@ func (v *Version) Validate() error { return nil } +// Alias for Parse +func New(s string) (*Version, error) { + return Parse(s) +} + // Parses a string to version func Parse(s string) (*Version, error) { if len(s) == 0 { @@ -344,3 +349,14 @@ func containsOnly(s string, set string) bool { return !strings.ContainsRune(set, r) }) == -1 } + +// Creates a new valid build version +func NewBuildVersion(s string) (string, error) { + if len(s) == 0 { + return "", errors.New("Buildversion is empty") + } + if !containsOnly(s, ALPHAS+NUMBERS) { + return "", fmt.Errorf("Invalid character(s) found in build meta data %q", s) + } + return s, nil +} diff --git a/semver_test.go b/semver_test.go index 9707f51..e7867f4 100644 --- a/semver_test.go +++ b/semver_test.go @@ -1,7 +1,5 @@ package semver -//TODO: Test incorrect version formats - import ( "testing" ) @@ -200,3 +198,111 @@ func TestPreReleaseVersions(t *testing.T) { t.Errorf("Not expected error %q", err) } } + +func TestBuildMetaDataVersions(t *testing.T) { + _, err := NewBuildVersion("123") + if err != nil { + t.Errorf("Unexpected error %q", err) + } + + _, err = NewBuildVersion("build") + if err != nil { + t.Errorf("Unexpected error %q", err) + } + + _, err = NewBuildVersion("test?") + if err == nil { + t.Error("Expected error, got none") + } + + _, err = NewBuildVersion("") + if err == nil { + t.Error("Expected error, got none") + } +} + +func TestNewHelper(t *testing.T) { + v, err := New("1.2.3") + if err != nil { + t.Fatalf("Unexpected error %q", err) + } + if v.Compare(&Version{1, 2, 3, nil, nil}) != 0 { + t.Fatal("Unexpected comparison problem") + } +} + +func BenchmarkParseSimple(b *testing.B) { + const VERSION = "0.0.1" + b.ResetTimer() + for n := 0; n < b.N; n++ { + New(VERSION) + } +} + +func BenchmarkParseComplex(b *testing.B) { + const VERSION = "0.0.1-alpha.preview+123.456" + b.ResetTimer() + for n := 0; n < b.N; n++ { + New(VERSION) + } +} + +func BenchmarkParseAverage(b *testing.B) { + l := len(formatTests) + b.ResetTimer() + for n := 0; n < b.N; n++ { + New(formatTests[n%l].result) + } +} + +func BenchmarkValidateSimple(b *testing.B) { + const VERSION = "0.0.1" + v, _ := New(VERSION) + b.ResetTimer() + for n := 0; n < b.N; n++ { + v.Validate() + } +} + +func BenchmarkValidateComplex(b *testing.B) { + const VERSION = "0.0.1-alpha.preview+123.456" + v, _ := New(VERSION) + b.ResetTimer() + for n := 0; n < b.N; n++ { + v.Validate() + } +} + +func BenchmarkValidateAverage(b *testing.B) { + l := len(formatTests) + b.ResetTimer() + for n := 0; n < b.N; n++ { + formatTests[n%l].v.Validate() + } +} + +func BenchmarkCompareSimple(b *testing.B) { + const VERSION = "0.0.1" + v, _ := New(VERSION) + b.ResetTimer() + for n := 0; n < b.N; n++ { + v.Compare(v) + } +} + +func BenchmarkCompareComplex(b *testing.B) { + const VERSION = "0.0.1-alpha.preview+123.456" + v, _ := New(VERSION) + b.ResetTimer() + for n := 0; n < b.N; n++ { + v.Compare(v) + } +} + +func BenchmarkCompareAverage(b *testing.B) { + l := len(compareTests) + b.ResetTimer() + for n := 0; n < b.N; n++ { + compareTests[n%l].v1.Compare(&(compareTests[n%l].v2)) + } +} From d6107de3e75dd69d76fde6e3ea1647b1b54ba14e Mon Sep 17 00:00:00 2001 From: Benedikt Lang Date: Wed, 2 Jul 2014 12:36:40 +0200 Subject: [PATCH 06/42] Add drone.io build status --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 710e362..2ac9f43 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -semver for golang +semver for golang [![Build Status](https://drone.io/github.com/blang/semver/status.png)](https://drone.io/github.com/blang/semver/latest) ====== semver is a Semantic Versioning library written in golang. It fully covers spec version `2.0.0`. From e030971c62fe97f15360f1d6703b02f4082321c8 Mon Sep 17 00:00:00 2001 From: Benedikt Lang Date: Wed, 2 Jul 2014 12:50:29 +0200 Subject: [PATCH 07/42] Check for leading zeroes --- semver.go | 22 ++++++++++++++++++++++ semver_test.go | 11 +++++++++++ 2 files changed, 33 insertions(+) diff --git a/semver.go b/semver.go index 241890d..82b8223 100644 --- a/semver.go +++ b/semver.go @@ -189,6 +189,9 @@ func Parse(s string) (*Version, error) { if !containsOnly(parts[0], NUMBERS) { return nil, fmt.Errorf("Invalid character(s) found in major number %q", parts[0]) } + if hasLeadingZeroes(parts[0]) { + return nil, fmt.Errorf("Major number must not contain leading zeroes %q", parts[0]) + } major, err := strconv.ParseUint(parts[0], 10, 64) if err != nil { return nil, err @@ -198,6 +201,9 @@ func Parse(s string) (*Version, error) { if !containsOnly(parts[1], NUMBERS) { return nil, fmt.Errorf("Invalid character(s) found in minor number %q", parts[1]) } + if hasLeadingZeroes(parts[1]) { + return nil, fmt.Errorf("Minor number must not contain leading zeroes %q", parts[1]) + } minor, err := strconv.ParseUint(parts[1], 10, 64) if err != nil { return nil, err @@ -227,6 +233,9 @@ func Parse(s string) (*Version, error) { if !containsOnly(parts[2][:subVersionIndex], NUMBERS) { return nil, fmt.Errorf("Invalid character(s) found in patch number %q", parts[2][:subVersionIndex]) } + if hasLeadingZeroes(parts[2][:subVersionIndex]) { + return nil, fmt.Errorf("Patch number must not contain leading zeroes %q", parts[2][:subVersionIndex]) + } patch, err := strconv.ParseUint(parts[2][:subVersionIndex], 10, 64) if err != nil { return nil, err @@ -286,6 +295,9 @@ func NewPRVersion(s string) (*PRVersion, error) { } v := &PRVersion{} if containsOnly(s, NUMBERS) { + if hasLeadingZeroes(s) { + return nil, fmt.Errorf("Numeric PreRelease version must not contain leading zeroes %q", s) + } num, err := strconv.ParseUint(s, 10, 64) // Might never be hit, but just in case @@ -350,6 +362,16 @@ func containsOnly(s string, set string) bool { }) == -1 } +func hasLeadingZeroes(s string) bool { + if len(s) <= 1 { + return false + } + if s[0] == '0' { + return true + } + return false +} + // Creates a new valid build version func NewBuildVersion(s string) (string, error) { if len(s) == 0 { diff --git a/semver_test.go b/semver_test.go index e7867f4..b4dc56b 100644 --- a/semver_test.go +++ b/semver_test.go @@ -129,6 +129,17 @@ var wrongformatTests = []wrongformatTest{ {nil, "-1.1.1"}, {nil, "1.-1.1"}, {nil, "1.1.-1"}, + // Leading zeroes + {nil, "01.1.1"}, + {nil, "001.1.1"}, + {nil, "1.01.1"}, + {nil, "1.001.1"}, + {nil, "1.1.01"}, + {nil, "1.1.001"}, + {nil, "1.1.1-01"}, + {nil, "1.1.1-001"}, + {nil, "1.1.1-beta.01"}, + {nil, "1.1.1-beta.001"}, {&Version{0, 0, 0, []*PRVersion{prstr("!")}, nil}, "0.0.0-!"}, {&Version{0, 0, 0, nil, []string{"!"}}, "0.0.0+!"}, // empty prversion From c2a6207ef944c8f5aeedc764bd0076194892afa6 Mon Sep 17 00:00:00 2001 From: Benedikt Lang Date: Wed, 2 Jul 2014 12:59:19 +0200 Subject: [PATCH 08/42] Add spec #9 test --- semver_test.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/semver_test.go b/semver_test.go index b4dc56b..643bf8d 100644 --- a/semver_test.go +++ b/semver_test.go @@ -81,6 +81,8 @@ var compareTests = []compareTest{ {Version{2, 0, 0, nil, nil}, Version{2, 1, 0, nil, nil}, -1}, {Version{2, 1, 0, nil, nil}, Version{2, 1, 1, nil, nil}, -1}, + // Spec Examples #9 + {Version{1, 0, 0, nil, nil}, Version{1, 0, 0, []*PRVersion{prstr("alpha")}, nil}, 1}, {Version{1, 0, 0, []*PRVersion{prstr("alpha")}, nil}, Version{1, 0, 0, []*PRVersion{prstr("alpha"), prnum(1)}, nil}, -1}, {Version{1, 0, 0, []*PRVersion{prstr("alpha"), prnum(1)}, nil}, Version{1, 0, 0, []*PRVersion{prstr("alpha"), prstr("beta")}, nil}, -1}, {Version{1, 0, 0, []*PRVersion{prstr("alpha"), prstr("beta")}, nil}, Version{1, 0, 0, []*PRVersion{prstr("beta")}, nil}, -1}, From ab6403b31a2b3ca50b96ebdea2dd4ea37de8935a Mon Sep 17 00:00:00 2001 From: Benedikt Lang Date: Wed, 2 Jul 2014 18:25:25 +0200 Subject: [PATCH 09/42] Hide constants --- semver.go | 27 ++++++++++++++------------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/semver.go b/semver.go index 82b8223..ce1c912 100644 --- a/semver.go +++ b/semver.go @@ -8,8 +8,9 @@ import ( "strings" ) -const NUMBERS = "0123456789" -const ALPHAS = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-" +const numbers = "0123456789" +const alphas = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-" +const alphanum = alphas + numbers // Latest fully supported spec version var SPEC_VERSION = Version{ @@ -147,7 +148,7 @@ func (v *Version) Validate() error { if len(pre.VersionStr) == 0 { return fmt.Errorf("Prerelease can not be empty %q", pre.VersionStr) } - if !containsOnly(pre.VersionStr, NUMBERS+ALPHAS) { + if !containsOnly(pre.VersionStr, alphanum) { return fmt.Errorf("Invalid character(s) found in prerelease %q", pre.VersionStr) } } @@ -159,7 +160,7 @@ func (v *Version) Validate() error { if len(build) == 0 { return fmt.Errorf("Build meta data can not be empty %q", build) } - if !containsOnly(build, ALPHAS+NUMBERS) { + if !containsOnly(build, alphanum) { return fmt.Errorf("Invalid character(s) found in build meta data %q", build) } } @@ -168,12 +169,12 @@ func (v *Version) Validate() error { return nil } -// Alias for Parse +// Alias for Parse, parses version string and returns a validated Version or error func New(s string) (*Version, error) { return Parse(s) } -// Parses a string to version +// Parses version string and returns a validated Version or error func Parse(s string) (*Version, error) { if len(s) == 0 { return nil, errors.New("Version string empty") @@ -186,7 +187,7 @@ func Parse(s string) (*Version, error) { } // Major - if !containsOnly(parts[0], NUMBERS) { + if !containsOnly(parts[0], numbers) { return nil, fmt.Errorf("Invalid character(s) found in major number %q", parts[0]) } if hasLeadingZeroes(parts[0]) { @@ -198,7 +199,7 @@ func Parse(s string) (*Version, error) { } // Minor - if !containsOnly(parts[1], NUMBERS) { + if !containsOnly(parts[1], numbers) { return nil, fmt.Errorf("Invalid character(s) found in minor number %q", parts[1]) } if hasLeadingZeroes(parts[1]) { @@ -230,7 +231,7 @@ func Parse(s string) (*Version, error) { } } - if !containsOnly(parts[2][:subVersionIndex], NUMBERS) { + if !containsOnly(parts[2][:subVersionIndex], numbers) { return nil, fmt.Errorf("Invalid character(s) found in patch number %q", parts[2][:subVersionIndex]) } if hasLeadingZeroes(parts[2][:subVersionIndex]) { @@ -271,7 +272,7 @@ func Parse(s string) (*Version, error) { if len(str) == 0 { return nil, errors.New("Build meta data is empty") } - if !containsOnly(str, ALPHAS+NUMBERS) { + if !containsOnly(str, alphanum) { return nil, fmt.Errorf("Invalid character(s) found in build meta data %q", str) } v.Build = append(v.Build, str) @@ -294,7 +295,7 @@ func NewPRVersion(s string) (*PRVersion, error) { return nil, errors.New("Prerelease is empty") } v := &PRVersion{} - if containsOnly(s, NUMBERS) { + if containsOnly(s, numbers) { if hasLeadingZeroes(s) { return nil, fmt.Errorf("Numeric PreRelease version must not contain leading zeroes %q", s) } @@ -306,7 +307,7 @@ func NewPRVersion(s string) (*PRVersion, error) { } v.VersionNum = num v.IsNum = true - } else if containsOnly(s, ALPHAS+NUMBERS) { + } else if containsOnly(s, alphanum) { v.VersionStr = s v.IsNum = false } else { @@ -377,7 +378,7 @@ func NewBuildVersion(s string) (string, error) { if len(s) == 0 { return "", errors.New("Buildversion is empty") } - if !containsOnly(s, ALPHAS+NUMBERS) { + if !containsOnly(s, alphanum) { return "", fmt.Errorf("Invalid character(s) found in build meta data %q", s) } return s, nil From dc253f7f1d64a047a5b1ea5b1e44c4367954319c Mon Sep 17 00:00:00 2001 From: Benedikt Lang Date: Wed, 2 Jul 2014 18:45:10 +0200 Subject: [PATCH 10/42] README: Format, godoc link --- README.md | 115 ++++++++++++++++++++++++++++-------------------------- 1 file changed, 60 insertions(+), 55 deletions(-) diff --git a/README.md b/README.md index 2ac9f43..349b3af 100644 --- a/README.md +++ b/README.md @@ -1,18 +1,22 @@ -semver for golang [![Build Status](https://drone.io/github.com/blang/semver/status.png)](https://drone.io/github.com/blang/semver/latest) +semver for golang [![Build Status](https://drone.io/github.com/blang/semver/status.png)](https://drone.io/github.com/blang/semver/latest) [![GoDoc](https://godoc.org/github.com/blang/semver?status.png)](https://godoc.org/github.com/blang/semver) ====== semver is a Semantic Versioning library written in golang. It fully covers spec version `2.0.0`. Usage ----- +```bash +$ go get github.com/blang/semver +``` - $ go get github.com/blang/semver - - import github.com/blang/semver - v1, err := semver.New("1.0.0-beta") - v2, err := semver.New("2.0.0-beta") - v1.Compare(v2) +```go +import github.com/blang/semver +v1, err := semver.New("1.0.0-beta") +v2, err := semver.New("2.0.0-beta") +v1.Compare(v2) +``` +Also check the [GoDocs](http://godoc.org/github.com/blang/semver). Why should I use this lib? ----- @@ -40,57 +44,58 @@ Example Have a look at full examples in [examples/main.go](examples/main.go) - import github.com/blang/semver - - v, err := semver.New("0.0.1-alpha.preview+123.github") - fmt.Printf("Major: %d\n", v.Major) - fmt.Printf("Minor: %d\n", v.Minor) - fmt.Printf("Patch: %d\n", v.Patch) - fmt.Printf("Pre: %s\n", v.Pre) - fmt.Printf("Build: %s\n", v.Build) - - // Prerelease versions array - if len(v.Pre) > 0 { - fmt.Println("Prerelease versions:") - for i, pre := range v.Pre { - fmt.Printf("%d: %q\n", i, pre) - } - } - - // Build meta data array - if len(v.Build) > 0 { - fmt.Println("Build meta data:") - for i, build := range v.Build { - fmt.Printf("%d: %q\n", i, build) - } +```go +import github.com/blang/semver + +v, err := semver.New("0.0.1-alpha.preview+123.github") +fmt.Printf("Major: %d\n", v.Major) +fmt.Printf("Minor: %d\n", v.Minor) +fmt.Printf("Patch: %d\n", v.Patch) +fmt.Printf("Pre: %s\n", v.Pre) +fmt.Printf("Build: %s\n", v.Build) + +// Prerelease versions array +if len(v.Pre) > 0 { + fmt.Println("Prerelease versions:") + for i, pre := range v.Pre { + fmt.Printf("%d: %q\n", i, pre) } +} - v001, err := semver.New("0.0.1") - // Compare using helpers: v.GT(v2), v.LT, v.GTE, v.LTE - v001.GT(v) == true - v.LT(v001) == true - v.GTE(v) == true - v.LTE(v) == true - - // Or use v.Compare(v2) for comparisons (-1, 0, 1): - v001.Compare(v) == 1 - v.Compare(v001) == -1 - v.Compare(v) == 0 - - // Manipulate Version in place: - v.Pre[0], err = semver.NewPRVersion("beta") - if err != nil { - fmt.Printf("Error parsing pre release version: %q", err) +// Build meta data array +if len(v.Build) > 0 { + fmt.Println("Build meta data:") + for i, build := range v.Build { + fmt.Printf("%d: %q\n", i, build) } - - fmt.Println("\nValidate versions:") - v.Build[0] = "?" - - err = v.Validate() - if err != nil { - fmt.Printf("Validation failed: %s\n", err) - } - +} + +v001, err := semver.New("0.0.1") +// Compare using helpers: v.GT(v2), v.LT, v.GTE, v.LTE +v001.GT(v) == true +v.LT(v001) == true +v.GTE(v) == true +v.LTE(v) == true + +// Or use v.Compare(v2) for comparisons (-1, 0, 1): +v001.Compare(v) == 1 +v.Compare(v001) == -1 +v.Compare(v) == 0 + +// Manipulate Version in place: +v.Pre[0], err = semver.NewPRVersion("beta") +if err != nil { + fmt.Printf("Error parsing pre release version: %q", err) +} + +fmt.Println("\nValidate versions:") +v.Build[0] = "?" + +err = v.Validate() +if err != nil { + fmt.Printf("Validation failed: %s\n", err) +} +``` Benchmarks ----- From ed35585ae3e309b05e7e7024e9ed8161427768cd Mon Sep 17 00:00:00 2001 From: Benedikt Lang Date: Wed, 2 Jul 2014 18:49:12 +0200 Subject: [PATCH 11/42] Add Travis-ci config --- .travis.yml | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 .travis.yml diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..3ece156 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,8 @@ +language: go + +go: + - 1.0 + - 1.1 + - 1.2 + - 1.3 + - tip \ No newline at end of file From a5a18701459ca44f5463b6a809a81b23cc392027 Mon Sep 17 00:00:00 2001 From: Benedikt Lang Date: Wed, 2 Jul 2014 19:06:34 +0200 Subject: [PATCH 12/42] Use drone.io instead of travisci --- .travis.yml | 8 -------- README.md | 2 +- 2 files changed, 1 insertion(+), 9 deletions(-) delete mode 100644 .travis.yml diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 3ece156..0000000 --- a/.travis.yml +++ /dev/null @@ -1,8 +0,0 @@ -language: go - -go: - - 1.0 - - 1.1 - - 1.2 - - 1.3 - - tip \ No newline at end of file diff --git a/README.md b/README.md index 349b3af..16f56e1 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -semver for golang [![Build Status](https://drone.io/github.com/blang/semver/status.png)](https://drone.io/github.com/blang/semver/latest) [![GoDoc](https://godoc.org/github.com/blang/semver?status.png)](https://godoc.org/github.com/blang/semver) +semver for golang [![Build Status](https://drone.io/github.com/blang/semver/status.png)](https://drone.io/github.com/blang/semver/latest) [![GoDoc](https://godoc.org/github.com/blang/semver?status.png)](https://godoc.org/github.com/blang/semver) [![Coverage Status](https://coveralls.io/repos/blang/semver/badge.png)](https://coveralls.io/r/blang/semver) ====== semver is a Semantic Versioning library written in golang. It fully covers spec version `2.0.0`. From f7866e526fce9cdb9de894821bfe6594452fb1e5 Mon Sep 17 00:00:00 2001 From: Benedikt Lang Date: Wed, 2 Jul 2014 19:10:24 +0200 Subject: [PATCH 13/42] README: Coverage status badge upate, semver link --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 16f56e1..c45b6cc 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ -semver for golang [![Build Status](https://drone.io/github.com/blang/semver/status.png)](https://drone.io/github.com/blang/semver/latest) [![GoDoc](https://godoc.org/github.com/blang/semver?status.png)](https://godoc.org/github.com/blang/semver) [![Coverage Status](https://coveralls.io/repos/blang/semver/badge.png)](https://coveralls.io/r/blang/semver) +semver for golang [![Build Status](https://drone.io/github.com/blang/semver/status.png)](https://drone.io/github.com/blang/semver/latest) [![GoDoc](https://godoc.org/github.com/blang/semver?status.png)](https://godoc.org/github.com/blang/semver) [![Coverage Status](https://img.shields.io/coveralls/blang/semver.svg)](https://coveralls.io/r/blang/semver?branch=master) ====== -semver is a Semantic Versioning library written in golang. It fully covers spec version `2.0.0`. +semver is a [Semantic Versioning](http://semver.org/) library written in golang. It fully covers spec version `2.0.0`. Usage ----- From 30bbe0b4a7ad9cbabfdbd7e67fca5ff2aca17fa3 Mon Sep 17 00:00:00 2001 From: Justin LeFebvre Date: Fri, 4 Jul 2014 15:26:00 -0400 Subject: [PATCH 14/42] there is no need to use a bytes buffer when string slices are efficient enough. This is especially true since the version strings are guaranteed to be sufficiently small --- semver.go | 44 ++++++++++++++++++++------------------------ 1 file changed, 20 insertions(+), 24 deletions(-) diff --git a/semver.go b/semver.go index ce1c912..7d5e3c7 100644 --- a/semver.go +++ b/semver.go @@ -1,16 +1,20 @@ package semver import ( - "bytes" "errors" "fmt" "strconv" "strings" ) -const numbers = "0123456789" -const alphas = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-" -const alphanum = alphas + numbers +const ( + numbers string = "0123456789" + alphas string = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-" + alphanum string = alphas + numbers + DOT string = "." + HYPHEN string = "-" + PLUS string = "+" +) // Latest fully supported spec version var SPEC_VERSION = Version{ @@ -29,34 +33,26 @@ type Version struct { // Version to string func (v *Version) String() string { - var buf bytes.Buffer - var DOT = []byte(".") - var HYPHEN = []byte("-") - var PLUS = []byte("+") - buf.WriteString(strconv.FormatUint(v.Major, 10)) - buf.Write(DOT) - buf.WriteString(strconv.FormatUint(v.Minor, 10)) - buf.Write(DOT) - buf.WriteString(strconv.FormatUint(v.Patch, 10)) + versionArray := []string{ + strconv.FormatUint(v.Major, 10), + DOT, + strconv.FormatUint(v.Minor, 10), + DOT, + strconv.FormatUint(v.Patch, 10), + } if len(v.Pre) > 0 { - buf.Write(HYPHEN) + versionArray = append(versionArray, HYPHEN) for i, pre := range v.Pre { if i > 0 { - buf.Write(DOT) + versionArray = append(versionArray, DOT) } - buf.WriteString(pre.String()) + versionArray = append(versionArray, pre.String()) } } if len(v.Build) > 0 { - buf.Write(PLUS) - for i, build := range v.Build { - if i > 0 { - buf.Write(DOT) - } - buf.WriteString(build) - } + versionArray = append(versionArray, PLUS, strings.Join(v.Build, DOT)) } - return buf.String() + return strings.Join(versionArray, "") } // Checks if v is greater than o. From 0f7f01176b4c3f387c9f55a862433debdc3c23b7 Mon Sep 17 00:00:00 2001 From: Benedikt Lang Date: Fri, 4 Jul 2014 22:24:00 +0200 Subject: [PATCH 15/42] Remove constants export --- semver.go | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/semver.go b/semver.go index 7d5e3c7..0238004 100644 --- a/semver.go +++ b/semver.go @@ -9,11 +9,11 @@ import ( const ( numbers string = "0123456789" - alphas string = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-" - alphanum string = alphas + numbers - DOT string = "." - HYPHEN string = "-" - PLUS string = "+" + alphas = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-" + alphanum = alphas + numbers + dot = "." + hyphen = "-" + plus = "+" ) // Latest fully supported spec version @@ -35,22 +35,22 @@ type Version struct { func (v *Version) String() string { versionArray := []string{ strconv.FormatUint(v.Major, 10), - DOT, + dot, strconv.FormatUint(v.Minor, 10), - DOT, + dot, strconv.FormatUint(v.Patch, 10), } if len(v.Pre) > 0 { - versionArray = append(versionArray, HYPHEN) + versionArray = append(versionArray, hyphen) for i, pre := range v.Pre { if i > 0 { - versionArray = append(versionArray, DOT) + versionArray = append(versionArray, dot) } versionArray = append(versionArray, pre.String()) } } if len(v.Build) > 0 { - versionArray = append(versionArray, PLUS, strings.Join(v.Build, DOT)) + versionArray = append(versionArray, plus, strings.Join(v.Build, dot)) } return strings.Join(versionArray, "") } From b5ecf012dba1882025b19d472715d8f6c2a9e85e Mon Sep 17 00:00:00 2001 From: Justin LeFebvre Date: Sun, 6 Jul 2014 18:11:42 -0400 Subject: [PATCH 16/42] really minor change to the logic here --- semver.go | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/semver.go b/semver.go index 0238004..c363131 100644 --- a/semver.go +++ b/semver.go @@ -360,11 +360,8 @@ func containsOnly(s string, set string) bool { } func hasLeadingZeroes(s string) bool { - if len(s) <= 1 { - return false - } - if s[0] == '0' { - return true + if len(s) > 1 { + return strings.HasPrefix(s, "0") } return false } From 50c0e52521a7fc0cf14f1b95948bc8246c97266b Mon Sep 17 00:00:00 2001 From: Justin LeFebvre Date: Wed, 9 Jul 2014 17:35:02 -0400 Subject: [PATCH 17/42] much simpler logic without requiring a branch condition --- semver.go | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/semver.go b/semver.go index c363131..b6bc9bb 100644 --- a/semver.go +++ b/semver.go @@ -360,10 +360,7 @@ func containsOnly(s string, set string) bool { } func hasLeadingZeroes(s string) bool { - if len(s) > 1 { - return strings.HasPrefix(s, "0") - } - return false + return len(s) > 1 && s[0] == '0' } // Creates a new valid build version From 272ed2e7e42c0e3125e3975b738d8f07019b4a76 Mon Sep 17 00:00:00 2001 From: tike Date: Wed, 17 Sep 2014 16:23:31 +0200 Subject: [PATCH 18/42] Added methods to implement sql related interfaces. Version.Scan -> database/sql.Scanner Version.Value -> database/sql/driver.Valuer Closes #4 Closes #5 --- sql.go | 31 +++++++++++++++++++++++++++++++ sql_test.go | 38 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 69 insertions(+) create mode 100644 sql.go create mode 100644 sql_test.go diff --git a/sql.go b/sql.go new file mode 100644 index 0000000..f636f3d --- /dev/null +++ b/sql.go @@ -0,0 +1,31 @@ +package semver + +import ( + "database/sql/driver" + "fmt" +) + +// Scan implements the database/sql.Scanner interface. +func (v *Version) Scan(src interface{}) (err error) { + var strVal string + switch src.(type) { + case string: + strVal = src.(string) + case []byte: + strVal = string(src.([]byte)) + default: + return fmt.Errorf("Version.Scan: cannot convert %T to string.", src) + } + + tmpv, err := Parse(strVal) + if err != nil { + return + } + *v = *tmpv + return +} + +// Value implements the database/sql/driver.Valuer interface. +func (s Version) Value() (driver.Value, error) { + return s.String(), nil +} diff --git a/sql_test.go b/sql_test.go new file mode 100644 index 0000000..7c71e59 --- /dev/null +++ b/sql_test.go @@ -0,0 +1,38 @@ +package semver + +import ( + "testing" +) + +type scanTest struct { + val interface{} + shouldError bool + expected string +} + +var scanTests = []scanTest{ + scanTest{"1.2.3", false, "1.2.3"}, + scanTest{[]byte("1.2.3"), false, "1.2.3"}, + scanTest{7, true, ""}, + scanTest{7e4, true, ""}, + scanTest{true, true, ""}, +} + +func TestScanString(t *testing.T) { + for _, tc := range scanTests { + s := &Version{} + err := s.Scan(tc.val) + if tc.shouldError { + if err == nil { + t.Fatalf("Scan did not return an error on %v (%T)", tc.val, tc.val) + } + } else { + if err != nil { + t.Fatalf("Scan returned an unexpected error: %s (%T) on %v (%T)", tc.val, tc.val, tc.val, tc.val) + } + if val, _ := s.Value(); val != tc.expected { + t.Errorf("Wrong Value returned, expected %q, got %q", tc.expected, val) + } + } + } +} From b1824b988f6f3762cd7e5bc3b3582e743f87597c Mon Sep 17 00:00:00 2001 From: Benedikt Lang Date: Tue, 23 Sep 2014 14:54:00 +0200 Subject: [PATCH 19/42] README: Add database/sql compatibility --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index c45b6cc..d52057f 100644 --- a/README.md +++ b/README.md @@ -37,6 +37,7 @@ Features - Comparator-like comparisons - Compare Helper Methods - InPlace manipulation +- database/sql compatible (sql.Scanner/Valuer) Example From 0d739aeb6a95fcba66f10d2532ec17064088e7af Mon Sep 17 00:00:00 2001 From: Benedikt Lang Date: Mon, 8 Dec 2014 15:58:15 +0100 Subject: [PATCH 20/42] Remove unnecessary pointer arguments Resolves #6 --- semver.go | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/semver.go b/semver.go index b6bc9bb..2c6889a 100644 --- a/semver.go +++ b/semver.go @@ -32,7 +32,7 @@ type Version struct { } // Version to string -func (v *Version) String() string { +func (v Version) String() string { versionArray := []string{ strconv.FormatUint(v.Major, 10), dot, @@ -56,22 +56,22 @@ func (v *Version) String() string { } // Checks if v is greater than o. -func (v *Version) GT(o *Version) bool { +func (v Version) GT(o *Version) bool { return (v.Compare(o) == 1) } // Checks if v is greater than or equal to o. -func (v *Version) GTE(o *Version) bool { +func (v Version) GTE(o *Version) bool { return (v.Compare(o) >= 0) } // Checks if v is less than o. -func (v *Version) LT(o *Version) bool { +func (v Version) LT(o *Version) bool { return (v.Compare(o) == -1) } // Checks if v is less than or equal to o. -func (v *Version) LTE(o *Version) bool { +func (v Version) LTE(o *Version) bool { return (v.Compare(o) <= 0) } @@ -79,7 +79,7 @@ func (v *Version) LTE(o *Version) bool { // -1 == v is less than o // 0 == v is equal to o // 1 == v is greater than o -func (v *Version) Compare(o *Version) int { +func (v Version) Compare(o *Version) int { if v.Major != o.Major { if v.Major > o.Major { return 1 @@ -135,7 +135,7 @@ func (v *Version) Compare(o *Version) int { } // Validates v and returns error in case -func (v *Version) Validate() error { +func (v Version) Validate() error { // Major, Minor, Patch already validated using uint64 if len(v.Pre) > 0 { @@ -313,7 +313,7 @@ func NewPRVersion(s string) (*PRVersion, error) { } // Is pre release version numeric? -func (v *PRVersion) IsNumeric() bool { +func (v PRVersion) IsNumeric() bool { return v.IsNum } @@ -321,7 +321,7 @@ func (v *PRVersion) IsNumeric() bool { // -1 == v is less than o // 0 == v is equal to o // 1 == v is greater than o -func (v *PRVersion) Compare(o *PRVersion) int { +func (v PRVersion) Compare(o *PRVersion) int { if v.IsNum && !o.IsNum { return -1 } else if !v.IsNum && o.IsNum { @@ -346,7 +346,7 @@ func (v *PRVersion) Compare(o *PRVersion) int { } // PreRelease version to string -func (v *PRVersion) String() string { +func (v PRVersion) String() string { if v.IsNum { return strconv.FormatUint(v.VersionNum, 10) } From c8864c2e3e7305b112e67feb844d2d80d4e5e08e Mon Sep 17 00:00:00 2001 From: Benedikt Lang Date: Fri, 12 Dec 2014 21:10:04 +0100 Subject: [PATCH 21/42] Change to value arguments --- semver.go | 60 +++++++++++++++++++++++----------------------- semver_test.go | 65 ++++++++++++++++++++++++++++---------------------- sql.go | 2 +- 3 files changed, 68 insertions(+), 59 deletions(-) diff --git a/semver.go b/semver.go index 2c6889a..752658a 100644 --- a/semver.go +++ b/semver.go @@ -27,7 +27,7 @@ type Version struct { Major uint64 Minor uint64 Patch uint64 - Pre []*PRVersion + Pre []PRVersion Build []string //No Precendence } @@ -56,22 +56,22 @@ func (v Version) String() string { } // Checks if v is greater than o. -func (v Version) GT(o *Version) bool { +func (v Version) GT(o Version) bool { return (v.Compare(o) == 1) } // Checks if v is greater than or equal to o. -func (v Version) GTE(o *Version) bool { +func (v Version) GTE(o Version) bool { return (v.Compare(o) >= 0) } // Checks if v is less than o. -func (v Version) LT(o *Version) bool { +func (v Version) LT(o Version) bool { return (v.Compare(o) == -1) } // Checks if v is less than or equal to o. -func (v Version) LTE(o *Version) bool { +func (v Version) LTE(o Version) bool { return (v.Compare(o) <= 0) } @@ -79,7 +79,7 @@ func (v Version) LTE(o *Version) bool { // -1 == v is less than o // 0 == v is equal to o // 1 == v is greater than o -func (v Version) Compare(o *Version) int { +func (v Version) Compare(o Version) int { if v.Major != o.Major { if v.Major > o.Major { return 1 @@ -166,44 +166,44 @@ func (v Version) Validate() error { } // Alias for Parse, parses version string and returns a validated Version or error -func New(s string) (*Version, error) { +func New(s string) (Version, error) { return Parse(s) } // Parses version string and returns a validated Version or error -func Parse(s string) (*Version, error) { +func Parse(s string) (Version, error) { if len(s) == 0 { - return nil, errors.New("Version string empty") + return Version{}, errors.New("Version string empty") } // Split into major.minor.(patch+pr+meta) parts := strings.SplitN(s, ".", 3) if len(parts) != 3 { - return nil, errors.New("No Major.Minor.Patch elements found") + return Version{}, errors.New("No Major.Minor.Patch elements found") } // Major if !containsOnly(parts[0], numbers) { - return nil, fmt.Errorf("Invalid character(s) found in major number %q", parts[0]) + return Version{}, fmt.Errorf("Invalid character(s) found in major number %q", parts[0]) } if hasLeadingZeroes(parts[0]) { - return nil, fmt.Errorf("Major number must not contain leading zeroes %q", parts[0]) + return Version{}, fmt.Errorf("Major number must not contain leading zeroes %q", parts[0]) } major, err := strconv.ParseUint(parts[0], 10, 64) if err != nil { - return nil, err + return Version{}, err } // Minor if !containsOnly(parts[1], numbers) { - return nil, fmt.Errorf("Invalid character(s) found in minor number %q", parts[1]) + return Version{}, fmt.Errorf("Invalid character(s) found in minor number %q", parts[1]) } if hasLeadingZeroes(parts[1]) { - return nil, fmt.Errorf("Minor number must not contain leading zeroes %q", parts[1]) + return Version{}, fmt.Errorf("Minor number must not contain leading zeroes %q", parts[1]) } minor, err := strconv.ParseUint(parts[1], 10, 64) if err != nil { - return nil, err + return Version{}, err } preIndex := strings.Index(parts[2], "-") @@ -228,16 +228,16 @@ func Parse(s string) (*Version, error) { } if !containsOnly(parts[2][:subVersionIndex], numbers) { - return nil, fmt.Errorf("Invalid character(s) found in patch number %q", parts[2][:subVersionIndex]) + return Version{}, fmt.Errorf("Invalid character(s) found in patch number %q", parts[2][:subVersionIndex]) } if hasLeadingZeroes(parts[2][:subVersionIndex]) { - return nil, fmt.Errorf("Patch number must not contain leading zeroes %q", parts[2][:subVersionIndex]) + return Version{}, fmt.Errorf("Patch number must not contain leading zeroes %q", parts[2][:subVersionIndex]) } patch, err := strconv.ParseUint(parts[2][:subVersionIndex], 10, 64) if err != nil { - return nil, err + return Version{}, err } - v := &Version{} + v := Version{} v.Major = major v.Minor = minor v.Patch = patch @@ -254,7 +254,7 @@ func Parse(s string) (*Version, error) { for _, prstr := range prparts { parsedPR, err := NewPRVersion(prstr) if err != nil { - return nil, err + return Version{}, err } v.Pre = append(v.Pre, parsedPR) } @@ -266,10 +266,10 @@ func Parse(s string) (*Version, error) { buildParts := strings.Split(buildStr, ".") for _, str := range buildParts { if len(str) == 0 { - return nil, errors.New("Build meta data is empty") + return Version{}, errors.New("Build meta data is empty") } if !containsOnly(str, alphanum) { - return nil, fmt.Errorf("Invalid character(s) found in build meta data %q", str) + return Version{}, fmt.Errorf("Invalid character(s) found in build meta data %q", str) } v.Build = append(v.Build, str) } @@ -286,20 +286,20 @@ type PRVersion struct { } // Creates a new valid prerelease version -func NewPRVersion(s string) (*PRVersion, error) { +func NewPRVersion(s string) (PRVersion, error) { if len(s) == 0 { - return nil, errors.New("Prerelease is empty") + return PRVersion{}, errors.New("Prerelease is empty") } - v := &PRVersion{} + v := PRVersion{} if containsOnly(s, numbers) { if hasLeadingZeroes(s) { - return nil, fmt.Errorf("Numeric PreRelease version must not contain leading zeroes %q", s) + return PRVersion{}, fmt.Errorf("Numeric PreRelease version must not contain leading zeroes %q", s) } num, err := strconv.ParseUint(s, 10, 64) // Might never be hit, but just in case if err != nil { - return nil, err + return PRVersion{}, err } v.VersionNum = num v.IsNum = true @@ -307,7 +307,7 @@ func NewPRVersion(s string) (*PRVersion, error) { v.VersionStr = s v.IsNum = false } else { - return nil, fmt.Errorf("Invalid character(s) found in prerelease %q", s) + return PRVersion{}, fmt.Errorf("Invalid character(s) found in prerelease %q", s) } return v, nil } @@ -321,7 +321,7 @@ func (v PRVersion) IsNumeric() bool { // -1 == v is less than o // 0 == v is equal to o // 1 == v is greater than o -func (v PRVersion) Compare(o *PRVersion) int { +func (v PRVersion) Compare(o PRVersion) int { if v.IsNum && !o.IsNum { return -1 } else if !v.IsNum && o.IsNum { diff --git a/semver_test.go b/semver_test.go index 643bf8d..b4af176 100644 --- a/semver_test.go +++ b/semver_test.go @@ -4,12 +4,12 @@ import ( "testing" ) -func prstr(s string) *PRVersion { - return &PRVersion{s, 0, false} +func prstr(s string) PRVersion { + return PRVersion{s, 0, false} } -func prnum(i uint64) *PRVersion { - return &PRVersion{"", i, true} +func prnum(i uint64) PRVersion { + return PRVersion{"", i, true} } type formatTest struct { @@ -20,14 +20,14 @@ type formatTest struct { var formatTests = []formatTest{ {Version{1, 2, 3, nil, nil}, "1.2.3"}, {Version{0, 0, 1, nil, nil}, "0.0.1"}, - {Version{0, 0, 1, []*PRVersion{prstr("alpha"), prstr("preview")}, []string{"123", "456"}}, "0.0.1-alpha.preview+123.456"}, - {Version{1, 2, 3, []*PRVersion{prstr("alpha"), prnum(1)}, []string{"123", "456"}}, "1.2.3-alpha.1+123.456"}, - {Version{1, 2, 3, []*PRVersion{prstr("alpha"), prnum(1)}, nil}, "1.2.3-alpha.1"}, + {Version{0, 0, 1, []PRVersion{prstr("alpha"), prstr("preview")}, []string{"123", "456"}}, "0.0.1-alpha.preview+123.456"}, + {Version{1, 2, 3, []PRVersion{prstr("alpha"), prnum(1)}, []string{"123", "456"}}, "1.2.3-alpha.1+123.456"}, + {Version{1, 2, 3, []PRVersion{prstr("alpha"), prnum(1)}, nil}, "1.2.3-alpha.1"}, {Version{1, 2, 3, nil, []string{"123", "456"}}, "1.2.3+123.456"}, // Prereleases and build metadata hyphens - {Version{1, 2, 3, []*PRVersion{prstr("alpha"), prstr("b-eta")}, []string{"123", "b-uild"}}, "1.2.3-alpha.b-eta+123.b-uild"}, + {Version{1, 2, 3, []PRVersion{prstr("alpha"), prstr("b-eta")}, []string{"123", "b-uild"}}, "1.2.3-alpha.b-eta+123.b-uild"}, {Version{1, 2, 3, nil, []string{"123", "b-uild"}}, "1.2.3+123.b-uild"}, - {Version{1, 2, 3, []*PRVersion{prstr("alpha"), prstr("b-eta")}, nil}, "1.2.3-alpha.b-eta"}, + {Version{1, 2, 3, []PRVersion{prstr("alpha"), prstr("b-eta")}, nil}, "1.2.3-alpha.b-eta"}, } func TestStringer(t *testing.T) { @@ -42,7 +42,7 @@ func TestParse(t *testing.T) { for _, test := range formatTests { if v, err := Parse(test.result); err != nil { t.Errorf("Error parsing %q: %q", test.result, err) - } else if comp := v.Compare(&test.v); comp != 0 { + } else if comp := v.Compare(test.v); comp != 0 { t.Errorf("Parsing, expected %q but got %q, comp: %d ", test.v, v, comp) } else if err := v.Validate(); err != nil { t.Errorf("Error validating parsed version %q: %q", test.v, err) @@ -82,14 +82,14 @@ var compareTests = []compareTest{ {Version{2, 1, 0, nil, nil}, Version{2, 1, 1, nil, nil}, -1}, // Spec Examples #9 - {Version{1, 0, 0, nil, nil}, Version{1, 0, 0, []*PRVersion{prstr("alpha")}, nil}, 1}, - {Version{1, 0, 0, []*PRVersion{prstr("alpha")}, nil}, Version{1, 0, 0, []*PRVersion{prstr("alpha"), prnum(1)}, nil}, -1}, - {Version{1, 0, 0, []*PRVersion{prstr("alpha"), prnum(1)}, nil}, Version{1, 0, 0, []*PRVersion{prstr("alpha"), prstr("beta")}, nil}, -1}, - {Version{1, 0, 0, []*PRVersion{prstr("alpha"), prstr("beta")}, nil}, Version{1, 0, 0, []*PRVersion{prstr("beta")}, nil}, -1}, - {Version{1, 0, 0, []*PRVersion{prstr("beta")}, nil}, Version{1, 0, 0, []*PRVersion{prstr("beta"), prnum(2)}, nil}, -1}, - {Version{1, 0, 0, []*PRVersion{prstr("beta"), prnum(2)}, nil}, Version{1, 0, 0, []*PRVersion{prstr("beta"), prnum(11)}, nil}, -1}, - {Version{1, 0, 0, []*PRVersion{prstr("beta"), prnum(11)}, nil}, Version{1, 0, 0, []*PRVersion{prstr("rc"), prnum(1)}, nil}, -1}, - {Version{1, 0, 0, []*PRVersion{prstr("rc"), prnum(1)}, nil}, Version{1, 0, 0, nil, nil}, -1}, + {Version{1, 0, 0, nil, nil}, Version{1, 0, 0, []PRVersion{prstr("alpha")}, nil}, 1}, + {Version{1, 0, 0, []PRVersion{prstr("alpha")}, nil}, Version{1, 0, 0, []PRVersion{prstr("alpha"), prnum(1)}, nil}, -1}, + {Version{1, 0, 0, []PRVersion{prstr("alpha"), prnum(1)}, nil}, Version{1, 0, 0, []PRVersion{prstr("alpha"), prstr("beta")}, nil}, -1}, + {Version{1, 0, 0, []PRVersion{prstr("alpha"), prstr("beta")}, nil}, Version{1, 0, 0, []PRVersion{prstr("beta")}, nil}, -1}, + {Version{1, 0, 0, []PRVersion{prstr("beta")}, nil}, Version{1, 0, 0, []PRVersion{prstr("beta"), prnum(2)}, nil}, -1}, + {Version{1, 0, 0, []PRVersion{prstr("beta"), prnum(2)}, nil}, Version{1, 0, 0, []PRVersion{prstr("beta"), prnum(11)}, nil}, -1}, + {Version{1, 0, 0, []PRVersion{prstr("beta"), prnum(11)}, nil}, Version{1, 0, 0, []PRVersion{prstr("rc"), prnum(1)}, nil}, -1}, + {Version{1, 0, 0, []PRVersion{prstr("rc"), prnum(1)}, nil}, Version{1, 0, 0, nil, nil}, -1}, // Ignore Build metadata {Version{1, 0, 0, nil, []string{"1", "2", "3"}}, Version{1, 0, 0, nil, nil}, 0}, @@ -97,11 +97,11 @@ var compareTests = []compareTest{ func TestCompare(t *testing.T) { for _, test := range compareTests { - if res := test.v1.Compare(&test.v2); res != test.result { + if res := test.v1.Compare(test.v2); res != test.result { t.Errorf("Comparing %q : %q, expected %d but got %d", test.v1, test.v2, test.result, res) } //Test counterpart - if res := test.v2.Compare(&test.v1); res != -test.result { + if res := test.v2.Compare(test.v1); res != -test.result { t.Errorf("Comparing %q : %q, expected %d but got %d", test.v2, test.v1, -test.result, res) } } @@ -142,13 +142,13 @@ var wrongformatTests = []wrongformatTest{ {nil, "1.1.1-001"}, {nil, "1.1.1-beta.01"}, {nil, "1.1.1-beta.001"}, - {&Version{0, 0, 0, []*PRVersion{prstr("!")}, nil}, "0.0.0-!"}, + {&Version{0, 0, 0, []PRVersion{prstr("!")}, nil}, "0.0.0-!"}, {&Version{0, 0, 0, nil, []string{"!"}}, "0.0.0+!"}, // empty prversion - {&Version{0, 0, 0, []*PRVersion{prstr(""), prstr("alpha")}, nil}, "0.0.0-.alpha"}, + {&Version{0, 0, 0, []PRVersion{prstr(""), prstr("alpha")}, nil}, "0.0.0-.alpha"}, // empty build meta data - {&Version{0, 0, 0, []*PRVersion{prstr("alpha")}, []string{""}}, "0.0.0-alpha+"}, - {&Version{0, 0, 0, []*PRVersion{prstr("alpha")}, []string{"test", ""}}, "0.0.0-alpha+test."}, + {&Version{0, 0, 0, []PRVersion{prstr("alpha")}, []string{""}}, "0.0.0-alpha+"}, + {&Version{0, 0, 0, []PRVersion{prstr("alpha")}, []string{"test", ""}}, "0.0.0-alpha+test."}, } func TestWrongFormat(t *testing.T) { @@ -167,8 +167,8 @@ func TestWrongFormat(t *testing.T) { } func TestCompareHelper(t *testing.T) { - v := &Version{1, 0, 0, []*PRVersion{prstr("alpha")}, nil} - v1 := &Version{1, 0, 0, nil, nil} + v := Version{1, 0, 0, []PRVersion{prstr("alpha")}, nil} + v1 := Version{1, 0, 0, nil, nil} if !v.GTE(v) { t.Errorf("%q should be greater than or equal to %q", v, v) } @@ -239,13 +239,14 @@ func TestNewHelper(t *testing.T) { if err != nil { t.Fatalf("Unexpected error %q", err) } - if v.Compare(&Version{1, 2, 3, nil, nil}) != 0 { + if v.Compare(Version{1, 2, 3, nil, nil}) != 0 { t.Fatal("Unexpected comparison problem") } } func BenchmarkParseSimple(b *testing.B) { const VERSION = "0.0.1" + b.ReportAllocs() b.ResetTimer() for n := 0; n < b.N; n++ { New(VERSION) @@ -254,6 +255,7 @@ func BenchmarkParseSimple(b *testing.B) { func BenchmarkParseComplex(b *testing.B) { const VERSION = "0.0.1-alpha.preview+123.456" + b.ReportAllocs() b.ResetTimer() for n := 0; n < b.N; n++ { New(VERSION) @@ -262,6 +264,7 @@ func BenchmarkParseComplex(b *testing.B) { func BenchmarkParseAverage(b *testing.B) { l := len(formatTests) + b.ReportAllocs() b.ResetTimer() for n := 0; n < b.N; n++ { New(formatTests[n%l].result) @@ -271,6 +274,7 @@ func BenchmarkParseAverage(b *testing.B) { func BenchmarkValidateSimple(b *testing.B) { const VERSION = "0.0.1" v, _ := New(VERSION) + b.ReportAllocs() b.ResetTimer() for n := 0; n < b.N; n++ { v.Validate() @@ -280,6 +284,7 @@ func BenchmarkValidateSimple(b *testing.B) { func BenchmarkValidateComplex(b *testing.B) { const VERSION = "0.0.1-alpha.preview+123.456" v, _ := New(VERSION) + b.ReportAllocs() b.ResetTimer() for n := 0; n < b.N; n++ { v.Validate() @@ -288,6 +293,7 @@ func BenchmarkValidateComplex(b *testing.B) { func BenchmarkValidateAverage(b *testing.B) { l := len(formatTests) + b.ReportAllocs() b.ResetTimer() for n := 0; n < b.N; n++ { formatTests[n%l].v.Validate() @@ -297,6 +303,7 @@ func BenchmarkValidateAverage(b *testing.B) { func BenchmarkCompareSimple(b *testing.B) { const VERSION = "0.0.1" v, _ := New(VERSION) + b.ReportAllocs() b.ResetTimer() for n := 0; n < b.N; n++ { v.Compare(v) @@ -306,6 +313,7 @@ func BenchmarkCompareSimple(b *testing.B) { func BenchmarkCompareComplex(b *testing.B) { const VERSION = "0.0.1-alpha.preview+123.456" v, _ := New(VERSION) + b.ReportAllocs() b.ResetTimer() for n := 0; n < b.N; n++ { v.Compare(v) @@ -314,8 +322,9 @@ func BenchmarkCompareComplex(b *testing.B) { func BenchmarkCompareAverage(b *testing.B) { l := len(compareTests) + b.ReportAllocs() b.ResetTimer() for n := 0; n < b.N; n++ { - compareTests[n%l].v1.Compare(&(compareTests[n%l].v2)) + compareTests[n%l].v1.Compare((compareTests[n%l].v2)) } } diff --git a/sql.go b/sql.go index f636f3d..c56fd16 100644 --- a/sql.go +++ b/sql.go @@ -21,7 +21,7 @@ func (v *Version) Scan(src interface{}) (err error) { if err != nil { return } - *v = *tmpv + *v = tmpv return } From 7f12584be107779f014ee7d6237574185ef73de3 Mon Sep 17 00:00:00 2001 From: Benedikt Lang Date: Fri, 12 Dec 2014 21:13:50 +0100 Subject: [PATCH 22/42] Sortable slice of versions --- sort.go | 24 ++++++++++++++++++++++++ sort_test.go | 30 ++++++++++++++++++++++++++++++ 2 files changed, 54 insertions(+) create mode 100644 sort.go create mode 100644 sort_test.go diff --git a/sort.go b/sort.go new file mode 100644 index 0000000..b250f58 --- /dev/null +++ b/sort.go @@ -0,0 +1,24 @@ +package semver + +import ( + "sort" +) + +type Versions []Version + +func (s Versions) Len() int { + return len(s) +} + +func (s Versions) Swap(i, j int) { + s[i], s[j] = s[j], s[i] +} + +func (s Versions) Less(i, j int) bool { + return s[i].LT(s[j]) +} + +// Sort sorts a slice of versions +func Sort(versions []Version) { + sort.Sort(Versions(versions)) +} diff --git a/sort_test.go b/sort_test.go new file mode 100644 index 0000000..209a6ef --- /dev/null +++ b/sort_test.go @@ -0,0 +1,30 @@ +package semver + +import ( + "reflect" + "testing" +) + +func TestSort(t *testing.T) { + v100, _ := New("1.0.0") + v010, _ := New("0.1.0") + v001, _ := New("0.0.1") + versions := []Version{v010, v100, v001} + Sort(versions) + + correct := []Version{v001, v010, v100} + if !reflect.DeepEqual(versions, correct) { + t.Fatalf("Sort returned wrong order: %s", versions) + } +} + +func BenchmarkSort(b *testing.B) { + v100, _ := New("1.0.0") + v010, _ := New("0.1.0") + v001, _ := New("0.0.1") + b.ReportAllocs() + b.ResetTimer() + for n := 0; n < b.N; n++ { + Sort([]Version{v010, v100, v001}) + } +} From 61fed1c66778e96993807ae3d386b1ad0b3fb564 Mon Sep 17 00:00:00 2001 From: Benedikt Lang Date: Fri, 12 Dec 2014 21:31:07 +0100 Subject: [PATCH 23/42] Add additional compare helpers --- semver.go | 25 +++++++++++++++++++++++++ semver_test.go | 15 +++++++++++++++ 2 files changed, 40 insertions(+) diff --git a/semver.go b/semver.go index 752658a..2ecabe6 100644 --- a/semver.go +++ b/semver.go @@ -55,6 +55,21 @@ func (v Version) String() string { return strings.Join(versionArray, "") } +// Checks if v is equal to o. +func (v Version) Equals(o Version) bool { + return (v.Compare(o) == 0) +} + +// Checks if v is equal to o. +func (v Version) EQ(o Version) bool { + return (v.Compare(o) == 0) +} + +// Checks if v is not equal to o. +func (v Version) NE(o Version) bool { + return (v.Compare(o) != 0) +} + // Checks if v is greater than o. func (v Version) GT(o Version) bool { return (v.Compare(o) == 1) @@ -65,6 +80,11 @@ func (v Version) GTE(o Version) bool { return (v.Compare(o) >= 0) } +// Checks if v is greater than or equal to o. +func (v Version) GE(o Version) bool { + return (v.Compare(o) >= 0) +} + // Checks if v is less than o. func (v Version) LT(o Version) bool { return (v.Compare(o) == -1) @@ -75,6 +95,11 @@ func (v Version) LTE(o Version) bool { return (v.Compare(o) <= 0) } +// Checks if v is less than or equal to o. +func (v Version) LE(o Version) bool { + return (v.Compare(o) <= 0) +} + // Compares Versions v to o: // -1 == v is less than o // 0 == v is equal to o diff --git a/semver_test.go b/semver_test.go index b4af176..858a5b9 100644 --- a/semver_test.go +++ b/semver_test.go @@ -169,6 +169,15 @@ func TestWrongFormat(t *testing.T) { func TestCompareHelper(t *testing.T) { v := Version{1, 0, 0, []PRVersion{prstr("alpha")}, nil} v1 := Version{1, 0, 0, nil, nil} + if !v.EQ(v) { + t.Errorf("%q should be equal to %q", v, v) + } + if !v.Equals(v) { + t.Errorf("%q should be equal to %q", v, v) + } + if !v1.NE(v) { + t.Errorf("%q should not be equal to %q", v, v) + } if !v.GTE(v) { t.Errorf("%q should be greater than or equal to %q", v, v) } @@ -181,12 +190,18 @@ func TestCompareHelper(t *testing.T) { if !v.LTE(v1) { t.Errorf("%q should be less than or equal %q", v, v1) } + if !v.LE(v1) { + t.Errorf("%q should be less than or equal %q", v, v1) + } if !v1.GT(v) { t.Errorf("%q should be less than %q", v1, v) } if !v1.GTE(v) { t.Errorf("%q should be less than or equal %q", v1, v) } + if !v1.GE(v) { + t.Errorf("%q should be less than or equal %q", v1, v) + } } func TestPreReleaseVersions(t *testing.T) { From fa1b4b2f13193b4fba88df0ed90cba7994a59cd0 Mon Sep 17 00:00:00 2001 From: Chris Bandy Date: Thu, 18 Dec 2014 04:24:14 +0000 Subject: [PATCH 24/42] Simplify parsing by using more slices Closes #7 --- semver.go | 65 +++++++++++++++++++++---------------------------------- 1 file changed, 25 insertions(+), 40 deletions(-) diff --git a/semver.go b/semver.go index 2ecabe6..5e95a20 100644 --- a/semver.go +++ b/semver.go @@ -231,52 +231,39 @@ func Parse(s string) (Version, error) { return Version{}, err } - preIndex := strings.Index(parts[2], "-") - buildIndex := strings.Index(parts[2], "+") - - // Determine last index of patch version (first of pre or build versions) - var subVersionIndex int - if preIndex != -1 && buildIndex == -1 { - subVersionIndex = preIndex - } else if preIndex == -1 && buildIndex != -1 { - subVersionIndex = buildIndex - } else if preIndex == -1 && buildIndex == -1 { - subVersionIndex = len(parts[2]) - } else { - // if there is no actual prversion but a hyphen inside the build meta data - if buildIndex < preIndex { - subVersionIndex = buildIndex - preIndex = -1 // Build meta data before preIndex found implicates there are no prerelease versions - } else { - subVersionIndex = preIndex - } + v := Version{} + v.Major = major + v.Minor = minor + + var build, prerelease []string + patchStr := parts[2] + + if buildIndex := strings.IndexRune(patchStr, '+'); buildIndex != -1 { + build = strings.Split(patchStr[buildIndex+1:], ".") + patchStr = patchStr[:buildIndex] } - if !containsOnly(parts[2][:subVersionIndex], numbers) { - return Version{}, fmt.Errorf("Invalid character(s) found in patch number %q", parts[2][:subVersionIndex]) + if preIndex := strings.IndexRune(patchStr, '-'); preIndex != -1 { + prerelease = strings.Split(patchStr[preIndex+1:], ".") + patchStr = patchStr[:preIndex] } - if hasLeadingZeroes(parts[2][:subVersionIndex]) { - return Version{}, fmt.Errorf("Patch number must not contain leading zeroes %q", parts[2][:subVersionIndex]) + + if !containsOnly(patchStr, numbers) { + return Version{}, fmt.Errorf("Invalid character(s) found in patch number %q", patchStr) + } + if hasLeadingZeroes(patchStr) { + return Version{}, fmt.Errorf("Patch number must not contain leading zeroes %q", patchStr) } - patch, err := strconv.ParseUint(parts[2][:subVersionIndex], 10, 64) + patch, err := strconv.ParseUint(patchStr, 10, 64) if err != nil { return Version{}, err } - v := Version{} - v.Major = major - v.Minor = minor + v.Patch = patch // There are PreRelease versions - if preIndex != -1 { - var preRels string - if buildIndex != -1 { - preRels = parts[2][subVersionIndex+1 : buildIndex] - } else { - preRels = parts[2][subVersionIndex+1:] - } - prparts := strings.Split(preRels, ".") - for _, prstr := range prparts { + if len(prerelease) > 0 { + for _, prstr := range prerelease { parsedPR, err := NewPRVersion(prstr) if err != nil { return Version{}, err @@ -286,10 +273,8 @@ func Parse(s string) (Version, error) { } // There is build meta data - if buildIndex != -1 { - buildStr := parts[2][buildIndex+1:] - buildParts := strings.Split(buildStr, ".") - for _, str := range buildParts { + if len(build) > 0 { + for _, str := range build { if len(str) == 0 { return Version{}, errors.New("Build meta data is empty") } From 172168317054cb2b2c41b7686ffd7eb0b3798c33 Mon Sep 17 00:00:00 2001 From: Chris Bandy Date: Thu, 18 Dec 2014 05:48:06 +0000 Subject: [PATCH 25/42] Fix error messages to match assertions Closes #8 --- semver_test.go | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/semver_test.go b/semver_test.go index 858a5b9..0d6b4a6 100644 --- a/semver_test.go +++ b/semver_test.go @@ -176,13 +176,13 @@ func TestCompareHelper(t *testing.T) { t.Errorf("%q should be equal to %q", v, v) } if !v1.NE(v) { - t.Errorf("%q should not be equal to %q", v, v) + t.Errorf("%q should not be equal to %q", v1, v) } if !v.GTE(v) { t.Errorf("%q should be greater than or equal to %q", v, v) } if !v.LTE(v) { - t.Errorf("%q should be greater than or equal to %q", v, v) + t.Errorf("%q should be less than or equal to %q", v, v) } if !v.LT(v1) { t.Errorf("%q should be less than %q", v, v1) @@ -194,13 +194,13 @@ func TestCompareHelper(t *testing.T) { t.Errorf("%q should be less than or equal %q", v, v1) } if !v1.GT(v) { - t.Errorf("%q should be less than %q", v1, v) + t.Errorf("%q should be greater than %q", v1, v) } if !v1.GTE(v) { - t.Errorf("%q should be less than or equal %q", v1, v) + t.Errorf("%q should be greater than or equal %q", v1, v) } if !v1.GE(v) { - t.Errorf("%q should be less than or equal %q", v1, v) + t.Errorf("%q should be greater than or equal %q", v1, v) } } From 58baed933fe4c0405cee0298f5b4311e835b0f12 Mon Sep 17 00:00:00 2001 From: Chris Bandy Date: Thu, 18 Dec 2014 06:17:49 +0000 Subject: [PATCH 26/42] Show that numbers larger than uint64 cannot be parsed Closes #9 --- semver_test.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/semver_test.go b/semver_test.go index 0d6b4a6..46b5d24 100644 --- a/semver_test.go +++ b/semver_test.go @@ -131,6 +131,11 @@ var wrongformatTests = []wrongformatTest{ {nil, "-1.1.1"}, {nil, "1.-1.1"}, {nil, "1.1.-1"}, + // giant numbers + {nil, "20000000000000000000.1.1"}, + {nil, "1.20000000000000000000.1"}, + {nil, "1.1.20000000000000000000"}, + {nil, "1.1.1-20000000000000000000"}, // Leading zeroes {nil, "01.1.1"}, {nil, "001.1.1"}, From f4dbd8b7a4037e5a71587fde22d3d0c8bc376e31 Mon Sep 17 00:00:00 2001 From: Chris Bandy Date: Thu, 18 Dec 2014 06:41:14 +0000 Subject: [PATCH 27/42] Simplify Scan() slightly Closes #10 --- sql.go | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/sql.go b/sql.go index c56fd16..b8d4b6a 100644 --- a/sql.go +++ b/sql.go @@ -7,21 +7,20 @@ import ( // Scan implements the database/sql.Scanner interface. func (v *Version) Scan(src interface{}) (err error) { - var strVal string - switch src.(type) { + var str string + switch src := src.(type) { case string: - strVal = src.(string) + str = src case []byte: - strVal = string(src.([]byte)) + str = string(src) default: return fmt.Errorf("Version.Scan: cannot convert %T to string.", src) } - tmpv, err := Parse(strVal) - if err != nil { - return + if t, err := Parse(str); err == nil { + *v = t } - *v = tmpv + return } From c2ac2cf0fb209db782b0434b3c94c190c8efc043 Mon Sep 17 00:00:00 2001 From: Benedikt Lang Date: Thu, 18 Dec 2014 12:04:15 +0100 Subject: [PATCH 28/42] Push to version 2.0.0 Breaking changes: Changed from pointers to value arguments. If this breaks your code, use latest stable v1 (tag:v1.1.0). You could also use gopkg.in: go get http://gopkg.in/blang/semver.v1 --- README.md | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index d52057f..79b36f4 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,7 @@ Usage ```bash $ go get github.com/blang/semver ``` +Note: Always vendor your dependencies or fix on a specific version tag. ```go import github.com/blang/semver @@ -28,6 +29,8 @@ Why should I use this lib? - Readable parsing/validation errors - Fast (See [Benchmarks](#benchmarks)) - Only Stdlib +- Uses values instead of pointers +- Many features, see below Features @@ -37,6 +40,7 @@ Features - Comparator-like comparisons - Compare Helper Methods - InPlace manipulation +- Sortable (implements sort.Interface) - database/sql compatible (sql.Scanner/Valuer) @@ -101,15 +105,16 @@ if err != nil { Benchmarks ----- - BenchmarkParseSimple 5000000 442 ns/op - BenchmarkParseComplex 1000000 2441 ns/op - BenchmarkParseAverage 1000000 1497 ns/op - BenchmarkValidateSimple 500000000 4.83 ns/op - BenchmarkValidateComplex 1000000 1236 ns/op - BenchmarkValidateAverage 5000000 580 ns/op - BenchmarkCompareSimple 500000000 5.43 ns/op - BenchmarkCompareComplex 100000000 26.3 ns/op - BenchmarkCompareAverage 100000000 29.6 ns/op + BenchmarkParseSimple 5000000 328 ns/op 49 B/op 1 allocs/op + BenchmarkParseComplex 1000000 2105 ns/op 263 B/op 7 allocs/op + BenchmarkParseAverage 1000000 1301 ns/op 168 B/op 4 allocs/op + BenchmarkValidateSimple 500000000 7.92 ns/op 0 B/op 0 allocs/op + BenchmarkValidateComplex 2000000 923 ns/op 0 B/op 0 allocs/op + BenchmarkValidateAverage 5000000 452 ns/op 0 B/op 0 allocs/op + BenchmarkCompareSimple 100000000 11.2 ns/op 0 B/op 0 allocs/op + BenchmarkCompareComplex 50000000 40.9 ns/op 0 B/op 0 allocs/op + BenchmarkCompareAverage 50000000 43.8 ns/op 0 B/op 0 allocs/op + BenchmarkSort 5000000 436 ns/op 259 B/op 2 allocs/op See benchmark cases at [semver_test.go](semver_test.go) From 658dfb434e49242e6cbb2e7f0bd2b34b361c59f4 Mon Sep 17 00:00:00 2001 From: Chris Bandy Date: Fri, 19 Dec 2014 15:21:20 +0000 Subject: [PATCH 29/42] Benchmarks for String() --- semver_test.go | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/semver_test.go b/semver_test.go index 46b5d24..18bb401 100644 --- a/semver_test.go +++ b/semver_test.go @@ -291,6 +291,35 @@ func BenchmarkParseAverage(b *testing.B) { } } +func BenchmarkStringSimple(b *testing.B) { + const VERSION = "0.0.1" + v, _ := New(VERSION) + b.ReportAllocs() + b.ResetTimer() + for n := 0; n < b.N; n++ { + v.String() + } +} + +func BenchmarkStringComplex(b *testing.B) { + const VERSION = "0.0.1-alpha.preview+123.456" + v, _ := New(VERSION) + b.ReportAllocs() + b.ResetTimer() + for n := 0; n < b.N; n++ { + v.String() + } +} + +func BenchmarkStringAverage(b *testing.B) { + l := len(formatTests) + b.ReportAllocs() + b.ResetTimer() + for n := 0; n < b.N; n++ { + formatTests[n%l].v.String() + } +} + func BenchmarkValidateSimple(b *testing.B) { const VERSION = "0.0.1" v, _ := New(VERSION) From 35e41ad4eed241155af9bc76e4e9c32625e8a49b Mon Sep 17 00:00:00 2001 From: Chris Bandy Date: Fri, 19 Dec 2014 18:32:37 +0000 Subject: [PATCH 30/42] Use byte slice to build Version string Closes #11 --- semver.go | 38 +++++++++++++++++++++++--------------- 1 file changed, 23 insertions(+), 15 deletions(-) diff --git a/semver.go b/semver.go index 5e95a20..07d8ed8 100644 --- a/semver.go +++ b/semver.go @@ -33,26 +33,34 @@ type Version struct { // Version to string func (v Version) String() string { - versionArray := []string{ - strconv.FormatUint(v.Major, 10), - dot, - strconv.FormatUint(v.Minor, 10), - dot, - strconv.FormatUint(v.Patch, 10), - } + b := make([]byte, 0) + b = strconv.AppendUint(b, v.Major, 10) + b = append(b, '.') + b = strconv.AppendUint(b, v.Minor, 10) + b = append(b, '.') + b = strconv.AppendUint(b, v.Patch, 10) + if len(v.Pre) > 0 { - versionArray = append(versionArray, hyphen) - for i, pre := range v.Pre { - if i > 0 { - versionArray = append(versionArray, dot) - } - versionArray = append(versionArray, pre.String()) + b = append(b, '-') + b = append(b, v.Pre[0].String()...) + + for _, pre := range v.Pre[1:] { + b = append(b, '.') + b = append(b, pre.String()...) } } + if len(v.Build) > 0 { - versionArray = append(versionArray, plus, strings.Join(v.Build, dot)) + b = append(b, '+') + b = append(b, v.Build[0]...) + + for _, build := range v.Build[1:] { + b = append(b, '.') + b = append(b, build...) + } } - return strings.Join(versionArray, "") + + return string(b) } // Checks if v is equal to o. From 570f3051b58d1999e46715451773187bc4d4bc8c Mon Sep 17 00:00:00 2001 From: Benedikt Lang Date: Fri, 19 Dec 2014 21:56:21 +0100 Subject: [PATCH 31/42] String benchmark results --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index 79b36f4..3a967eb 100644 --- a/README.md +++ b/README.md @@ -108,6 +108,9 @@ Benchmarks BenchmarkParseSimple 5000000 328 ns/op 49 B/op 1 allocs/op BenchmarkParseComplex 1000000 2105 ns/op 263 B/op 7 allocs/op BenchmarkParseAverage 1000000 1301 ns/op 168 B/op 4 allocs/op + BenchmarkStringSimple 10000000 214 ns/op 16 B/op 2 allocs/op + BenchmarkStringComplex 3000000 583 ns/op 88 B/op 4 allocs/op + BenchmarkStringAverage 3000000 461 ns/op 56 B/op 3 allocs/op BenchmarkValidateSimple 500000000 7.92 ns/op 0 B/op 0 allocs/op BenchmarkValidateComplex 2000000 923 ns/op 0 B/op 0 allocs/op BenchmarkValidateAverage 5000000 452 ns/op 0 B/op 0 allocs/op From 907ae8488af3966d893274c8b75173875fc995ba Mon Sep 17 00:00:00 2001 From: Benedikt Lang Date: Fri, 19 Dec 2014 22:12:25 +0100 Subject: [PATCH 32/42] Remove unnecessary constants and range checks --- semver.go | 65 +++++++++++++++++++++++-------------------------------- 1 file changed, 27 insertions(+), 38 deletions(-) diff --git a/semver.go b/semver.go index 07d8ed8..abb7757 100644 --- a/semver.go +++ b/semver.go @@ -11,9 +11,6 @@ const ( numbers string = "0123456789" alphas = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-" alphanum = alphas + numbers - dot = "." - hyphen = "-" - plus = "+" ) // Latest fully supported spec version @@ -171,27 +168,23 @@ func (v Version) Compare(o Version) int { func (v Version) Validate() error { // Major, Minor, Patch already validated using uint64 - if len(v.Pre) > 0 { - for _, pre := range v.Pre { - if !pre.IsNum { //Numeric prerelease versions already uint64 - if len(pre.VersionStr) == 0 { - return fmt.Errorf("Prerelease can not be empty %q", pre.VersionStr) - } - if !containsOnly(pre.VersionStr, alphanum) { - return fmt.Errorf("Invalid character(s) found in prerelease %q", pre.VersionStr) - } + for _, pre := range v.Pre { + if !pre.IsNum { //Numeric prerelease versions already uint64 + if len(pre.VersionStr) == 0 { + return fmt.Errorf("Prerelease can not be empty %q", pre.VersionStr) + } + if !containsOnly(pre.VersionStr, alphanum) { + return fmt.Errorf("Invalid character(s) found in prerelease %q", pre.VersionStr) } } } - if len(v.Build) > 0 { - for _, build := range v.Build { - if len(build) == 0 { - return fmt.Errorf("Build meta data can not be empty %q", build) - } - if !containsOnly(build, alphanum) { - return fmt.Errorf("Invalid character(s) found in build meta data %q", build) - } + for _, build := range v.Build { + if len(build) == 0 { + return fmt.Errorf("Build meta data can not be empty %q", build) + } + if !containsOnly(build, alphanum) { + return fmt.Errorf("Invalid character(s) found in build meta data %q", build) } } @@ -269,28 +262,24 @@ func Parse(s string) (Version, error) { v.Patch = patch - // There are PreRelease versions - if len(prerelease) > 0 { - for _, prstr := range prerelease { - parsedPR, err := NewPRVersion(prstr) - if err != nil { - return Version{}, err - } - v.Pre = append(v.Pre, parsedPR) + // Prerelease + for _, prstr := range prerelease { + parsedPR, err := NewPRVersion(prstr) + if err != nil { + return Version{}, err } + v.Pre = append(v.Pre, parsedPR) } - // There is build meta data - if len(build) > 0 { - for _, str := range build { - if len(str) == 0 { - return Version{}, errors.New("Build meta data is empty") - } - if !containsOnly(str, alphanum) { - return Version{}, fmt.Errorf("Invalid character(s) found in build meta data %q", str) - } - v.Build = append(v.Build, str) + // Build meta data + for _, str := range build { + if len(str) == 0 { + return Version{}, errors.New("Build meta data is empty") + } + if !containsOnly(str, alphanum) { + return Version{}, fmt.Errorf("Invalid character(s) found in build meta data %q", str) } + v.Build = append(v.Build, str) } return v, nil From 0575a3cfd79578c7511f8ff86682491cf70d26b1 Mon Sep 17 00:00:00 2001 From: Chris Bandy Date: Sat, 20 Dec 2014 11:31:21 +0000 Subject: [PATCH 33/42] Start with a slice large enough for the minimum version Closes #12 --- semver.go | 2 +- semver_test.go | 10 ++++++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/semver.go b/semver.go index abb7757..9df310b 100644 --- a/semver.go +++ b/semver.go @@ -30,7 +30,7 @@ type Version struct { // Version to string func (v Version) String() string { - b := make([]byte, 0) + b := make([]byte, 0, 5) b = strconv.AppendUint(b, v.Major, 10) b = append(b, '.') b = strconv.AppendUint(b, v.Minor, 10) diff --git a/semver_test.go b/semver_test.go index 18bb401..90c4545 100644 --- a/semver_test.go +++ b/semver_test.go @@ -301,6 +301,16 @@ func BenchmarkStringSimple(b *testing.B) { } } +func BenchmarkStringLarger(b *testing.B) { + const VERSION = "11.15.2012" + v, _ := New(VERSION) + b.ReportAllocs() + b.ResetTimer() + for n := 0; n < b.N; n++ { + v.String() + } +} + func BenchmarkStringComplex(b *testing.B) { const VERSION = "0.0.1-alpha.preview+123.456" v, _ := New(VERSION) From 42513fed745c7859649fb4a533e768e532eee2df Mon Sep 17 00:00:00 2001 From: Benedikt Lang Date: Sun, 28 Dec 2014 11:28:50 +0100 Subject: [PATCH 34/42] Update benchmarks --- README.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 3a967eb..21b9536 100644 --- a/README.md +++ b/README.md @@ -108,9 +108,10 @@ Benchmarks BenchmarkParseSimple 5000000 328 ns/op 49 B/op 1 allocs/op BenchmarkParseComplex 1000000 2105 ns/op 263 B/op 7 allocs/op BenchmarkParseAverage 1000000 1301 ns/op 168 B/op 4 allocs/op - BenchmarkStringSimple 10000000 214 ns/op 16 B/op 2 allocs/op - BenchmarkStringComplex 3000000 583 ns/op 88 B/op 4 allocs/op - BenchmarkStringAverage 3000000 461 ns/op 56 B/op 3 allocs/op + BenchmarkStringSimple 10000000 130 ns/op 5 B/op 1 allocs/op + BenchmarkStringLarger 5000000 280 ns/op 32 B/op 2 allocs/op + BenchmarkStringComplex 3000000 512 ns/op 80 B/op 3 allocs/op + BenchmarkStringAverage 5000000 387 ns/op 47 B/op 2 allocs/op BenchmarkValidateSimple 500000000 7.92 ns/op 0 B/op 0 allocs/op BenchmarkValidateComplex 2000000 923 ns/op 0 B/op 0 allocs/op BenchmarkValidateAverage 5000000 452 ns/op 0 B/op 0 allocs/op From 5b0d386fa243aaa9c55283ea1cb4a5b7e2fa4160 Mon Sep 17 00:00:00 2001 From: Josh Hawn Date: Tue, 13 Jan 2015 00:41:23 -0800 Subject: [PATCH 35/42] Implements json.Marshaler and json.Unmarshaler Uses simple JSON string (un)marshaling under the hood and Parse() on validating unmarshaled content. Docker-DCO-1.1-Signed-off-by: Josh Hawn (github: jlhawn) --- json.go | 23 +++++++++++++++++++++++ json_test.go | 45 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 68 insertions(+) create mode 100644 json.go create mode 100644 json_test.go diff --git a/json.go b/json.go new file mode 100644 index 0000000..a74bf7c --- /dev/null +++ b/json.go @@ -0,0 +1,23 @@ +package semver + +import ( + "encoding/json" +) + +// MarshalJSON implements the encoding/json.Marshaler interface. +func (v Version) MarshalJSON() ([]byte, error) { + return json.Marshal(v.String()) +} + +// UnmarshalJSON implements the encoding/json.Unmarshaler interface. +func (v *Version) UnmarshalJSON(data []byte) (err error) { + var versionString string + + if err = json.Unmarshal(data, &versionString); err != nil { + return + } + + *v, err = Parse(versionString) + + return +} diff --git a/json_test.go b/json_test.go new file mode 100644 index 0000000..039117d --- /dev/null +++ b/json_test.go @@ -0,0 +1,45 @@ +package semver + +import ( + "encoding/json" + "strconv" + "testing" +) + +func TestJSONMarshal(t *testing.T) { + versionString := "3.1.4-alpha.1.5.9+build.2.6.5" + v, err := Parse(versionString) + if err != nil { + t.Fatal(err) + } + + versionJSON, err := json.Marshal(v) + if err != nil { + t.Fatal(err) + } + + quotedVersionString := strconv.Quote(versionString) + + if string(versionJSON) != quotedVersionString { + t.Fatalf("JSON marshaled semantic version not equal: expected %q, got %q", quotedVersionString, string(versionJSON)) + } +} + +func TestJSONUnmarshal(t *testing.T) { + versionString := "3.1.4-alpha.1.5.9+build.2.6.5" + quotedVersionString := strconv.Quote(versionString) + + var v Version + if err := json.Unmarshal([]byte(quotedVersionString), &v); err != nil { + t.Fatal(err) + } + + if v.String() != versionString { + t.Fatalf("JSON unmarshaled semantic version not equal: expected %q, got %q", versionString, v.String()) + } + + badVersionString := strconv.Quote("3.1.4.1.5.9.2.6.5-other-digits-of-pi") + if err := json.Unmarshal([]byte(badVersionString), &v); err == nil { + t.Fatal("expected JSON unmarshal error, got nil") + } +} From 9bf7bff48b0388cb75991e58c6df7d13e982f1f2 Mon Sep 17 00:00:00 2001 From: Benedikt Lang Date: Tue, 13 Jan 2015 10:22:11 +0100 Subject: [PATCH 36/42] README: Add json feature --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 21b9536..d9c2f98 100644 --- a/README.md +++ b/README.md @@ -42,6 +42,7 @@ Features - InPlace manipulation - Sortable (implements sort.Interface) - database/sql compatible (sql.Scanner/Valuer) +- encoding/json compatible (json.Marshaler/Unmarshaler) Example From fe545f55951c7cffd830784ffa6ac9b23d6b1c6a Mon Sep 17 00:00:00 2001 From: Alexandru Cojocaru Date: Sat, 21 Feb 2015 00:10:26 +0100 Subject: [PATCH 37/42] Add MustParse Closes #14 --- semver.go | 9 +++++++++ semver_test.go | 13 +++++++++++++ 2 files changed, 22 insertions(+) diff --git a/semver.go b/semver.go index 9df310b..7afe611 100644 --- a/semver.go +++ b/semver.go @@ -285,6 +285,15 @@ func Parse(s string) (Version, error) { return v, nil } +// MustParse is like Parse but panics if the version cannot be parsed. +func MustParse(s string) Version { + v, err := Parse(s) + if err != nil { + panic(`semver: Parse(` + s + `): ` + err.Error()) + } + return v +} + // PreRelease Version type PRVersion struct { VersionStr string diff --git a/semver_test.go b/semver_test.go index 90c4545..a6312b8 100644 --- a/semver_test.go +++ b/semver_test.go @@ -50,6 +50,19 @@ func TestParse(t *testing.T) { } } +func TestMustParse(t *testing.T) { + _ = MustParse("32.2.1-alpha") +} + +func TestMustParse_panic(t *testing.T) { + defer func() { + if recover() == nil { + t.Errorf("Should have panicked") + } + }() + _ = MustParse("invalid version") +} + func TestValidate(t *testing.T) { for _, test := range formatTests { if err := test.v.Validate(); err != nil { From 2f3112b6f8f18f9df8743cc75ed51da08094ef6a Mon Sep 17 00:00:00 2001 From: Benedikt Lang Date: Sun, 5 Apr 2015 15:04:57 +0200 Subject: [PATCH 38/42] New() and Make() Introduce Make() which is an alias for Parse() and returns a version as value. New() changed to return a pointer. Breaking changes: Change New() to Make() to fix pointer vs value problem. --- README.md | 8 ++++---- examples/main.go | 4 ++-- semver.go | 13 ++++++++++--- semver_test.go | 35 +++++++++++++++++++++++++---------- sort_test.go | 12 ++++++------ 5 files changed, 47 insertions(+), 25 deletions(-) diff --git a/README.md b/README.md index d9c2f98..5171c5c 100644 --- a/README.md +++ b/README.md @@ -12,8 +12,8 @@ Note: Always vendor your dependencies or fix on a specific version tag. ```go import github.com/blang/semver -v1, err := semver.New("1.0.0-beta") -v2, err := semver.New("2.0.0-beta") +v1, err := semver.Make("1.0.0-beta") +v2, err := semver.Make("2.0.0-beta") v1.Compare(v2) ``` @@ -53,7 +53,7 @@ Have a look at full examples in [examples/main.go](examples/main.go) ```go import github.com/blang/semver -v, err := semver.New("0.0.1-alpha.preview+123.github") +v, err := semver.Make("0.0.1-alpha.preview+123.github") fmt.Printf("Major: %d\n", v.Major) fmt.Printf("Minor: %d\n", v.Minor) fmt.Printf("Patch: %d\n", v.Patch) @@ -76,7 +76,7 @@ if len(v.Build) > 0 { } } -v001, err := semver.New("0.0.1") +v001, err := semver.Make("0.0.1") // Compare using helpers: v.GT(v2), v.LT, v.GTE, v.LTE v001.GT(v) == true v.LT(v001) == true diff --git a/examples/main.go b/examples/main.go index 617d72a..f36c983 100644 --- a/examples/main.go +++ b/examples/main.go @@ -32,8 +32,8 @@ func main() { } } - // New == Parse - v001, err := semver.New("0.0.1") + // Make == Parse (Value), New for Pointer + v001, err := semver.Make("0.0.1") fmt.Println("\nUse Version.Compare for comparisons (-1, 0, 1):") fmt.Printf("%q is greater than %q: Compare == %d\n", v001, v, v001.Compare(v)) diff --git a/semver.go b/semver.go index 7afe611..c82fe66 100644 --- a/semver.go +++ b/semver.go @@ -191,12 +191,19 @@ func (v Version) Validate() error { return nil } -// Alias for Parse, parses version string and returns a validated Version or error -func New(s string) (Version, error) { +// New is an alias for Parse and returns a pointer, parses version string and returns a validated Version or error +func New(s string) (vp *Version, err error) { + v, err := Parse(s) + vp = &v + return +} + +// Make is an alias for Parse, parses version string and returns a validated Version or error +func Make(s string) (Version, error) { return Parse(s) } -// Parses version string and returns a validated Version or error +// Parse parses version string and returns a validated Version or error func Parse(s string) (Version, error) { if len(s) == 0 { return Version{}, errors.New("Version string empty") diff --git a/semver_test.go b/semver_test.go index a6312b8..e56ebce 100644 --- a/semver_test.go +++ b/semver_test.go @@ -272,6 +272,21 @@ func TestNewHelper(t *testing.T) { if err != nil { t.Fatalf("Unexpected error %q", err) } + + // New returns pointer + if v == nil { + t.Fatal("Version is nil") + } + if v.Compare(Version{1, 2, 3, nil, nil}) != 0 { + t.Fatal("Unexpected comparison problem") + } +} + +func TestMakeHelper(t *testing.T) { + v, err := Make("1.2.3") + if err != nil { + t.Fatalf("Unexpected error %q", err) + } if v.Compare(Version{1, 2, 3, nil, nil}) != 0 { t.Fatal("Unexpected comparison problem") } @@ -282,7 +297,7 @@ func BenchmarkParseSimple(b *testing.B) { b.ReportAllocs() b.ResetTimer() for n := 0; n < b.N; n++ { - New(VERSION) + Parse(VERSION) } } @@ -291,7 +306,7 @@ func BenchmarkParseComplex(b *testing.B) { b.ReportAllocs() b.ResetTimer() for n := 0; n < b.N; n++ { - New(VERSION) + Parse(VERSION) } } @@ -300,13 +315,13 @@ func BenchmarkParseAverage(b *testing.B) { b.ReportAllocs() b.ResetTimer() for n := 0; n < b.N; n++ { - New(formatTests[n%l].result) + Parse(formatTests[n%l].result) } } func BenchmarkStringSimple(b *testing.B) { const VERSION = "0.0.1" - v, _ := New(VERSION) + v, _ := Parse(VERSION) b.ReportAllocs() b.ResetTimer() for n := 0; n < b.N; n++ { @@ -316,7 +331,7 @@ func BenchmarkStringSimple(b *testing.B) { func BenchmarkStringLarger(b *testing.B) { const VERSION = "11.15.2012" - v, _ := New(VERSION) + v, _ := Parse(VERSION) b.ReportAllocs() b.ResetTimer() for n := 0; n < b.N; n++ { @@ -326,7 +341,7 @@ func BenchmarkStringLarger(b *testing.B) { func BenchmarkStringComplex(b *testing.B) { const VERSION = "0.0.1-alpha.preview+123.456" - v, _ := New(VERSION) + v, _ := Parse(VERSION) b.ReportAllocs() b.ResetTimer() for n := 0; n < b.N; n++ { @@ -345,7 +360,7 @@ func BenchmarkStringAverage(b *testing.B) { func BenchmarkValidateSimple(b *testing.B) { const VERSION = "0.0.1" - v, _ := New(VERSION) + v, _ := Parse(VERSION) b.ReportAllocs() b.ResetTimer() for n := 0; n < b.N; n++ { @@ -355,7 +370,7 @@ func BenchmarkValidateSimple(b *testing.B) { func BenchmarkValidateComplex(b *testing.B) { const VERSION = "0.0.1-alpha.preview+123.456" - v, _ := New(VERSION) + v, _ := Parse(VERSION) b.ReportAllocs() b.ResetTimer() for n := 0; n < b.N; n++ { @@ -374,7 +389,7 @@ func BenchmarkValidateAverage(b *testing.B) { func BenchmarkCompareSimple(b *testing.B) { const VERSION = "0.0.1" - v, _ := New(VERSION) + v, _ := Parse(VERSION) b.ReportAllocs() b.ResetTimer() for n := 0; n < b.N; n++ { @@ -384,7 +399,7 @@ func BenchmarkCompareSimple(b *testing.B) { func BenchmarkCompareComplex(b *testing.B) { const VERSION = "0.0.1-alpha.preview+123.456" - v, _ := New(VERSION) + v, _ := Parse(VERSION) b.ReportAllocs() b.ResetTimer() for n := 0; n < b.N; n++ { diff --git a/sort_test.go b/sort_test.go index 209a6ef..6889397 100644 --- a/sort_test.go +++ b/sort_test.go @@ -6,9 +6,9 @@ import ( ) func TestSort(t *testing.T) { - v100, _ := New("1.0.0") - v010, _ := New("0.1.0") - v001, _ := New("0.0.1") + v100, _ := Parse("1.0.0") + v010, _ := Parse("0.1.0") + v001, _ := Parse("0.0.1") versions := []Version{v010, v100, v001} Sort(versions) @@ -19,9 +19,9 @@ func TestSort(t *testing.T) { } func BenchmarkSort(b *testing.B) { - v100, _ := New("1.0.0") - v010, _ := New("0.1.0") - v001, _ := New("0.0.1") + v100, _ := Parse("1.0.0") + v010, _ := Parse("0.1.0") + v001, _ := Parse("0.0.1") b.ReportAllocs() b.ResetTimer() for n := 0; n < b.N; n++ { From 31b736133b98f26d5e078ec9eb591666edfd091f Mon Sep 17 00:00:00 2001 From: Benedikt Lang Date: Sat, 20 Jun 2015 10:58:49 +0200 Subject: [PATCH 39/42] Documentation, code cleaning --- semver.go | 81 ++++++++++++++++++++++++++--------------------------- sort.go | 4 +++ sql.go | 4 +-- sql_test.go | 10 +++---- 4 files changed, 50 insertions(+), 49 deletions(-) diff --git a/semver.go b/semver.go index c82fe66..bbf85ce 100644 --- a/semver.go +++ b/semver.go @@ -13,13 +13,14 @@ const ( alphanum = alphas + numbers ) -// Latest fully supported spec version -var SPEC_VERSION = Version{ +// SpecVersion is the latest fully supported spec version of semver +var SpecVersion = Version{ Major: 2, Minor: 0, Patch: 0, } +// Version represents a semver compatible version type Version struct { Major uint64 Minor uint64 @@ -60,52 +61,52 @@ func (v Version) String() string { return string(b) } -// Checks if v is equal to o. +// Equals checks if v is equal to o. func (v Version) Equals(o Version) bool { return (v.Compare(o) == 0) } -// Checks if v is equal to o. +// EQ checks if v is equal to o. func (v Version) EQ(o Version) bool { return (v.Compare(o) == 0) } -// Checks if v is not equal to o. +// NE checks if v is not equal to o. func (v Version) NE(o Version) bool { return (v.Compare(o) != 0) } -// Checks if v is greater than o. +// GT checks if v is greater than o. func (v Version) GT(o Version) bool { return (v.Compare(o) == 1) } -// Checks if v is greater than or equal to o. +// GTE checks if v is greater than or equal to o. func (v Version) GTE(o Version) bool { return (v.Compare(o) >= 0) } -// Checks if v is greater than or equal to o. +// GE checks if v is greater than or equal to o. func (v Version) GE(o Version) bool { return (v.Compare(o) >= 0) } -// Checks if v is less than o. +// LT checks if v is less than o. func (v Version) LT(o Version) bool { return (v.Compare(o) == -1) } -// Checks if v is less than or equal to o. +// LTE checks if v is less than or equal to o. func (v Version) LTE(o Version) bool { return (v.Compare(o) <= 0) } -// Checks if v is less than or equal to o. +// LE checks if v is less than or equal to o. func (v Version) LE(o Version) bool { return (v.Compare(o) <= 0) } -// Compares Versions v to o: +// Compare compares Versions v to o: // -1 == v is less than o // 0 == v is equal to o // 1 == v is greater than o @@ -113,23 +114,20 @@ func (v Version) Compare(o Version) int { if v.Major != o.Major { if v.Major > o.Major { return 1 - } else { - return -1 } + return -1 } if v.Minor != o.Minor { if v.Minor > o.Minor { return 1 - } else { - return -1 } + return -1 } if v.Patch != o.Patch { if v.Patch > o.Patch { return 1 - } else { - return -1 } + return -1 } // Quick comparison if a version has no prerelease versions @@ -139,32 +137,31 @@ func (v Version) Compare(o Version) int { return 1 } else if len(v.Pre) > 0 && len(o.Pre) == 0 { return -1 - } else { - - i := 0 - for ; i < len(v.Pre) && i < len(o.Pre); i++ { - if comp := v.Pre[i].Compare(o.Pre[i]); comp == 0 { - continue - } else if comp == 1 { - return 1 - } else { - return -1 - } - } + } - // If all pr versions are the equal but one has further prversion, this one greater - if i == len(v.Pre) && i == len(o.Pre) { - return 0 - } else if i == len(v.Pre) && i < len(o.Pre) { - return -1 - } else { + i := 0 + for ; i < len(v.Pre) && i < len(o.Pre); i++ { + if comp := v.Pre[i].Compare(o.Pre[i]); comp == 0 { + continue + } else if comp == 1 { return 1 + } else { + return -1 } + } + // If all pr versions are the equal but one has further prversion, this one greater + if i == len(v.Pre) && i == len(o.Pre) { + return 0 + } else if i == len(v.Pre) && i < len(o.Pre) { + return -1 + } else { + return 1 } + } -// Validates v and returns error in case +// Validate validates v and returns error in case func (v Version) Validate() error { // Major, Minor, Patch already validated using uint64 @@ -301,14 +298,14 @@ func MustParse(s string) Version { return v } -// PreRelease Version +// PRVersion represents a PreRelease Version type PRVersion struct { VersionStr string VersionNum uint64 IsNum bool } -// Creates a new valid prerelease version +// NewPRVersion creates a new valid prerelease version func NewPRVersion(s string) (PRVersion, error) { if len(s) == 0 { return PRVersion{}, errors.New("Prerelease is empty") @@ -335,12 +332,12 @@ func NewPRVersion(s string) (PRVersion, error) { return v, nil } -// Is pre release version numeric? +// IsNumeric checks if prerelease-version is numeric func (v PRVersion) IsNumeric() bool { return v.IsNum } -// Compares PreRelease Versions v to o: +// Compare compares two PreRelease Versions v and o: // -1 == v is less than o // 0 == v is equal to o // 1 == v is greater than o @@ -386,7 +383,7 @@ func hasLeadingZeroes(s string) bool { return len(s) > 1 && s[0] == '0' } -// Creates a new valid build version +// NewBuildVersion creates a new valid build version func NewBuildVersion(s string) (string, error) { if len(s) == 0 { return "", errors.New("Buildversion is empty") diff --git a/sort.go b/sort.go index b250f58..e18f880 100644 --- a/sort.go +++ b/sort.go @@ -4,16 +4,20 @@ import ( "sort" ) +// Versions represents multiple versions. type Versions []Version +// Len returns length of version collection func (s Versions) Len() int { return len(s) } +// Swap swaps two versions inside the collection by its indices func (s Versions) Swap(i, j int) { s[i], s[j] = s[j], s[i] } +// Less checks if version at index i is less than version at index j func (s Versions) Less(i, j int) bool { return s[i].LT(s[j]) } diff --git a/sql.go b/sql.go index b8d4b6a..eb4d802 100644 --- a/sql.go +++ b/sql.go @@ -25,6 +25,6 @@ func (v *Version) Scan(src interface{}) (err error) { } // Value implements the database/sql/driver.Valuer interface. -func (s Version) Value() (driver.Value, error) { - return s.String(), nil +func (v Version) Value() (driver.Value, error) { + return v.String(), nil } diff --git a/sql_test.go b/sql_test.go index 7c71e59..ebf48b5 100644 --- a/sql_test.go +++ b/sql_test.go @@ -11,11 +11,11 @@ type scanTest struct { } var scanTests = []scanTest{ - scanTest{"1.2.3", false, "1.2.3"}, - scanTest{[]byte("1.2.3"), false, "1.2.3"}, - scanTest{7, true, ""}, - scanTest{7e4, true, ""}, - scanTest{true, true, ""}, + {"1.2.3", false, "1.2.3"}, + {[]byte("1.2.3"), false, "1.2.3"}, + {7, true, ""}, + {7e4, true, ""}, + {true, true, ""}, } func TestScanString(t *testing.T) { From aea32c919a18e5ef4537bbd283ff29594b1b0165 Mon Sep 17 00:00:00 2001 From: Benedikt Lang Date: Sat, 5 Dec 2015 10:49:59 +0100 Subject: [PATCH 40/42] Range support Added support for version ranges with basic operators: v, err := semver.Parse("1.2.3") range, err := semver.ParseRange(">1.0.0 <2.0.0 || >=3.0.0") if range(v) { //valid } --- README.md | 77 +++++++-- range.go | 224 +++++++++++++++++++++++++ range_test.go | 442 ++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 729 insertions(+), 14 deletions(-) create mode 100644 range.go create mode 100644 range_test.go diff --git a/README.md b/README.md index 5171c5c..4399639 100644 --- a/README.md +++ b/README.md @@ -40,10 +40,52 @@ Features - Comparator-like comparisons - Compare Helper Methods - InPlace manipulation +- Ranges `>=1.0.0 <2.0.0 || >=3.0.0 !3.0.1-beta.1` - Sortable (implements sort.Interface) - database/sql compatible (sql.Scanner/Valuer) - encoding/json compatible (json.Marshaler/Unmarshaler) +Ranges +------ + +A `Range` is a set of conditions which specify which versions satisfy the range. + +A condition is composed of an operator and a version. The supported operators are: + +- `<1.0.0` Less than `1.0.0` +- `<=1.0.0` Less than or equal to `1.0.0` +- `>1.0.0` Greater than `1.0.0` +- `>=1.0.0` Greater than or equal to `1.0.0` +- `1.0.0`, `=1.0.0`, `==1.0.0` Equal to `1.0.0` +- `!1.0.0`, `!=1.0.0` Not equal to `1.0.0`. Excludes version `1.0.0`. + +A `Range` can link multiple `Ranges` separated by space: + +Ranges can be linked by logical AND: + + - `>1.0.0 <2.0.0` would match between both ranges, so `1.1.1` and `1.8.7` but not `1.0.0` or `2.0.0` + - `>1.0.0 <3.0.0 !2.0.3-beta.2` would match every version between `1.0.0` and `3.0.0` except `2.0.3-beta.2` + +Ranges can also be linked by logical OR: + + - `<2.0.0 || >=3.0.0` would match `1.x.x` and `3.x.x` but not `2.x.x` + +AND has a higher precedence than OR. It's not possible to use brackets. + +Ranges can be combined by both AND and OR + + - `>1.0.0 <2.0.0 || >3.0.0 !4.2.1` would match `1.2.3`, `1.9.9`, `3.1.1`, but not `4.2.1`, `2.1.1` + +Range usage: + +``` +v, err := semver.Parse("1.2.3") +range, err := semver.ParseRange(">1.0.0 <2.0.0 || >=3.0.0") +if range(v) { + //valid +} + +``` Example ----- @@ -103,23 +145,30 @@ if err != nil { } ``` + Benchmarks ----- - BenchmarkParseSimple 5000000 328 ns/op 49 B/op 1 allocs/op - BenchmarkParseComplex 1000000 2105 ns/op 263 B/op 7 allocs/op - BenchmarkParseAverage 1000000 1301 ns/op 168 B/op 4 allocs/op - BenchmarkStringSimple 10000000 130 ns/op 5 B/op 1 allocs/op - BenchmarkStringLarger 5000000 280 ns/op 32 B/op 2 allocs/op - BenchmarkStringComplex 3000000 512 ns/op 80 B/op 3 allocs/op - BenchmarkStringAverage 5000000 387 ns/op 47 B/op 2 allocs/op - BenchmarkValidateSimple 500000000 7.92 ns/op 0 B/op 0 allocs/op - BenchmarkValidateComplex 2000000 923 ns/op 0 B/op 0 allocs/op - BenchmarkValidateAverage 5000000 452 ns/op 0 B/op 0 allocs/op - BenchmarkCompareSimple 100000000 11.2 ns/op 0 B/op 0 allocs/op - BenchmarkCompareComplex 50000000 40.9 ns/op 0 B/op 0 allocs/op - BenchmarkCompareAverage 50000000 43.8 ns/op 0 B/op 0 allocs/op - BenchmarkSort 5000000 436 ns/op 259 B/op 2 allocs/op + BenchmarkParseSimple-4 5000000 390 ns/op 48 B/op 1 allocs/op + BenchmarkParseComplex-4 1000000 1813 ns/op 256 B/op 7 allocs/op + BenchmarkParseAverage-4 1000000 1171 ns/op 163 B/op 4 allocs/op + BenchmarkStringSimple-4 20000000 119 ns/op 16 B/op 1 allocs/op + BenchmarkStringLarger-4 10000000 206 ns/op 32 B/op 2 allocs/op + BenchmarkStringComplex-4 5000000 324 ns/op 80 B/op 3 allocs/op + BenchmarkStringAverage-4 5000000 273 ns/op 53 B/op 2 allocs/op + BenchmarkValidateSimple-4 200000000 9.33 ns/op 0 B/op 0 allocs/op + BenchmarkValidateComplex-4 3000000 469 ns/op 0 B/op 0 allocs/op + BenchmarkValidateAverage-4 5000000 256 ns/op 0 B/op 0 allocs/op + BenchmarkCompareSimple-4 100000000 11.8 ns/op 0 B/op 0 allocs/op + BenchmarkCompareComplex-4 50000000 30.8 ns/op 0 B/op 0 allocs/op + BenchmarkCompareAverage-4 30000000 41.5 ns/op 0 B/op 0 allocs/op + BenchmarkSort-4 3000000 419 ns/op 256 B/op 2 allocs/op + BenchmarkRangeParseSimple-4 2000000 850 ns/op 192 B/op 5 allocs/op + BenchmarkRangeParseAverage-4 1000000 1677 ns/op 400 B/op 10 allocs/op + BenchmarkRangeParseComplex-4 300000 5214 ns/op 1440 B/op 30 allocs/op + BenchmarkRangeMatchSimple-4 50000000 25.6 ns/op 0 B/op 0 allocs/op + BenchmarkRangeMatchAverage-4 30000000 56.4 ns/op 0 B/op 0 allocs/op + BenchmarkRangeMatchComplex-4 10000000 153 ns/op 0 B/op 0 allocs/op See benchmark cases at [semver_test.go](semver_test.go) diff --git a/range.go b/range.go new file mode 100644 index 0000000..0a8eaa1 --- /dev/null +++ b/range.go @@ -0,0 +1,224 @@ +package semver + +import ( + "fmt" + "strings" + "unicode" +) + +type comparator func(Version, Version) bool + +var ( + compEQ comparator = func(v1 Version, v2 Version) bool { + return v1.Compare(v2) == 0 + } + compNE = func(v1 Version, v2 Version) bool { + return v1.Compare(v2) != 0 + } + compGT = func(v1 Version, v2 Version) bool { + return v1.Compare(v2) == 1 + } + compGE = func(v1 Version, v2 Version) bool { + return v1.Compare(v2) >= 0 + } + compLT = func(v1 Version, v2 Version) bool { + return v1.Compare(v2) == -1 + } + compLE = func(v1 Version, v2 Version) bool { + return v1.Compare(v2) <= 0 + } +) + +type versionRange struct { + v Version + c comparator +} + +// rangeFunc creates a Range from the given versionRange. +func (vr *versionRange) rangeFunc() Range { + return Range(func(v Version) bool { + return vr.c(v, vr.v) + }) +} + +// Range represents a range of versions. +// A Range can be used to check if a Version satisfies it: +// +// range, err := semver.ParseRange(">1.0.0 <2.0.0") +// range(semver.MustParse("1.1.1") // returns true +type Range func(Version) bool + +// OR combines the existing Range with another Range using logical OR. +func (rf Range) OR(f Range) Range { + return Range(func(v Version) bool { + return rf(v) || f(v) + }) +} + +// AND combines the existing Range with another Range using logical AND. +func (rf Range) AND(f Range) Range { + return Range(func(v Version) bool { + return rf(v) && f(v) + }) +} + +// ParseRange parses a range and returns a Range. +// If the range could not be parsed an error is returned. +// +// Valid ranges are: +// - "<1.0.0" +// - "<=1.0.0" +// - ">1.0.0" +// - ">=1.0.0" +// - "1.0.0", "=1.0.0", "==1.0.0" +// - "!1.0.0", "!=1.0.0" +// +// A Range can consist of multiple ranges separated by space: +// Ranges can be linked by logical AND: +// - ">1.0.0 <2.0.0" would match between both ranges, so "1.1.1" and "1.8.7" but not "1.0.0" or "2.0.0" +// - ">1.0.0 <3.0.0 !2.0.3-beta.2" would match every version between 1.0.0 and 3.0.0 except 2.0.3-beta.2 +// +// Ranges can also be linked by logical OR: +// - "<2.0.0 || >=3.0.0" would match "1.x.x" and "3.x.x" but not "2.x.x" +// +// AND has a higher precedence than OR. It's not possible to use brackets. +// +// Ranges can be combined by both AND and OR +// +// - `>1.0.0 <2.0.0 || >3.0.0 !4.2.1` would match `1.2.3`, `1.9.9`, `3.1.1`, but not `4.2.1`, `2.1.1` +func ParseRange(s string) (Range, error) { + parts := splitAndTrim(s) + orParts, err := splitORParts(parts) + if err != nil { + return nil, err + } + var orFn Range + for _, p := range orParts { + var andFn Range + for _, ap := range p { + opStr, vStr, err := splitComparatorVersion(ap) + if err != nil { + return nil, err + } + vr, err := buildVersionRange(opStr, vStr) + if err != nil { + return nil, fmt.Errorf("Could not parse Range %q: %s", ap, err) + } + rf := vr.rangeFunc() + + // Set function + if andFn == nil { + andFn = rf + } else { // Combine with existing function + andFn = andFn.AND(rf) + } + } + if orFn == nil { + orFn = andFn + } else { + orFn = orFn.OR(andFn) + } + + } + return orFn, nil +} + +// splitORParts splits the already cleaned parts by '||'. +// Checks for invalid positions of the operator and returns an +// error if found. +func splitORParts(parts []string) ([][]string, error) { + var ORparts [][]string + last := 0 + for i, p := range parts { + if p == "||" { + if i == 0 { + return nil, fmt.Errorf("First element in range is '||'") + } + ORparts = append(ORparts, parts[last:i]) + last = i + 1 + } + } + if last == len(parts) { + return nil, fmt.Errorf("Last element in range is '||'") + } + ORparts = append(ORparts, parts[last:]) + return ORparts, nil +} + +// buildVersionRange takes a slice of 2: operator and version +// and builds a versionRange, otherwise an error. +func buildVersionRange(opStr, vStr string) (*versionRange, error) { + c := parseComparator(opStr) + if c == nil { + return nil, fmt.Errorf("Could not parse comparator %q in %q", opStr, strings.Join([]string{opStr, vStr}, "")) + } + v, err := Parse(vStr) + if err != nil { + return nil, fmt.Errorf("Could not parse version %q in %q: %s", vStr, strings.Join([]string{opStr, vStr}, ""), err) + } + + return &versionRange{ + v: v, + c: c, + }, nil + +} + +// splitAndTrim splits a range string by spaces and cleans leading and trailing spaces +func splitAndTrim(s string) (result []string) { + last := 0 + for i := 0; i < len(s); i++ { + if s[i] == ' ' { + if last < i-1 { + result = append(result, s[last:i]) + } + last = i + 1 + } + } + if last < len(s)-1 { + result = append(result, s[last:]) + } + // parts := strings.Split(s, " ") + // for _, x := range parts { + // if s := strings.TrimSpace(x); len(s) != 0 { + // result = append(result, s) + // } + // } + return +} + +// splitComparatorVersion splits the comparator from the version. +// Spaces between the comparator and the version are not allowed. +// Input must be free of leading or trailing spaces. +func splitComparatorVersion(s string) (string, string, error) { + i := strings.IndexFunc(s, unicode.IsDigit) + if i == -1 { + return "", "", fmt.Errorf("Could not get version from string: %q", s) + } + return strings.TrimSpace(s[0:i]), s[i:], nil +} + +func parseComparator(s string) comparator { + switch s { + case "==": + fallthrough + case "": + fallthrough + case "=": + return compEQ + case ">": + return compGT + case ">=": + return compGE + case "<": + return compLT + case "<=": + return compLE + case "!": + fallthrough + case "!=": + return compNE + } + + return nil +} diff --git a/range_test.go b/range_test.go new file mode 100644 index 0000000..5a745a9 --- /dev/null +++ b/range_test.go @@ -0,0 +1,442 @@ +package semver + +import ( + "reflect" + "strings" + "testing" +) + +type comparatorTest struct { + input string + comparator func(comparator) bool +} + +func TestParseComparator(t *testing.T) { + compatorTests := []comparatorTest{ + {">", testGT}, + {">=", testGE}, + {"<", testLT}, + {"<=", testLE}, + {"", testEQ}, + {"=", testEQ}, + {"==", testEQ}, + {"!=", testNE}, + {"!", testNE}, + {"-", nil}, + {"<==", nil}, + {"<<", nil}, + {">>", nil}, + } + + for _, tc := range compatorTests { + if c := parseComparator(tc.input); c == nil { + if tc.comparator != nil { + t.Errorf("Comparator nil for case %q\n", tc.input) + } + } else if !tc.comparator(c) { + t.Errorf("Invalid comparator for case %q\n", tc.input) + } + } +} + +var ( + v1 = MustParse("1.2.2") + v2 = MustParse("1.2.3") + v3 = MustParse("1.2.4") +) + +func testEQ(f comparator) bool { + return f(v1, v1) && !f(v1, v2) +} + +func testNE(f comparator) bool { + return !f(v1, v1) && f(v1, v2) +} + +func testGT(f comparator) bool { + return f(v2, v1) && f(v3, v2) && !f(v1, v2) && !f(v1, v1) +} + +func testGE(f comparator) bool { + return f(v2, v1) && f(v3, v2) && !f(v1, v2) +} + +func testLT(f comparator) bool { + return f(v1, v2) && f(v2, v3) && !f(v2, v1) && !f(v1, v1) +} + +func testLE(f comparator) bool { + return f(v1, v2) && f(v2, v3) && !f(v2, v1) +} + +func TestSplitAndTrim(t *testing.T) { + tests := []struct { + i string + s []string + }{ + {"1.2.3 1.2.3", []string{"1.2.3", "1.2.3"}}, + {" 1.2.3 1.2.3 ", []string{"1.2.3", "1.2.3"}}, // Spaces + {"1.2.3 || >=1.2.3 <1.2.3", []string{"1.2.3", "||", ">=1.2.3", "<1.2.3"}}, + {" 1.2.3 || >=1.2.3 <1.2.3 ", []string{"1.2.3", "||", ">=1.2.3", "<1.2.3"}}, + } + + for _, tc := range tests { + p := splitAndTrim(tc.i) + if !reflect.DeepEqual(p, tc.s) { + t.Errorf("Invalid for case %q: Expected %q, got: %q", tc.i, tc.s, p) + } + } +} + +func TestSplitComparatorVersion(t *testing.T) { + tests := []struct { + i string + p []string + }{ + {">1.2.3", []string{">", "1.2.3"}}, + {">=1.2.3", []string{">=", "1.2.3"}}, + {"<1.2.3", []string{"<", "1.2.3"}}, + {"<=1.2.3", []string{"<=", "1.2.3"}}, + {"1.2.3", []string{"", "1.2.3"}}, + {"=1.2.3", []string{"=", "1.2.3"}}, + {"==1.2.3", []string{"==", "1.2.3"}}, + {"!=1.2.3", []string{"!=", "1.2.3"}}, + {"!1.2.3", []string{"!", "1.2.3"}}, + {"error", nil}, + } + for _, tc := range tests { + if op, v, err := splitComparatorVersion(tc.i); err != nil { + if tc.p != nil { + t.Errorf("Invalid for case %q: Expected %q, got error %q", tc.i, tc.p, err) + } + } else if op != tc.p[0] { + t.Errorf("Invalid operator for case %q: Expected %q, got: %q", tc.i, tc.p[0], op) + } else if v != tc.p[1] { + t.Errorf("Invalid version for case %q: Expected %q, got: %q", tc.i, tc.p[1], v) + } + + } +} + +func TestBuildVersionRange(t *testing.T) { + tests := []struct { + opStr string + vStr string + c func(comparator) bool + v string + }{ + {">", "1.2.3", testGT, "1.2.3"}, + {">=", "1.2.3", testGE, "1.2.3"}, + {"<", "1.2.3", testLT, "1.2.3"}, + {"<=", "1.2.3", testLE, "1.2.3"}, + {"", "1.2.3", testEQ, "1.2.3"}, + {"=", "1.2.3", testEQ, "1.2.3"}, + {"==", "1.2.3", testEQ, "1.2.3"}, + {"!=", "1.2.3", testNE, "1.2.3"}, + {"!", "1.2.3", testNE, "1.2.3"}, + {">>", "1.2.3", nil, ""}, // Invalid comparator + {"=", "invalid", nil, ""}, // Invalid version + } + + for _, tc := range tests { + if r, err := buildVersionRange(tc.opStr, tc.vStr); err != nil { + if tc.c != nil { + t.Errorf("Invalid for case %q: Expected %q, got error %q", strings.Join([]string{tc.opStr, tc.vStr}, ""), tc.v, err) + } + } else if r == nil { + t.Errorf("Invalid for case %q: got nil", strings.Join([]string{tc.opStr, tc.vStr}, "")) + } else { + // test version + if tv := MustParse(tc.v); !r.v.EQ(tv) { + t.Errorf("Invalid for case %q: Expected version %q, got: %q", strings.Join([]string{tc.opStr, tc.vStr}, ""), tv, r.v) + } + // test comparator + if r.c == nil { + t.Errorf("Invalid for case %q: got nil comparator", strings.Join([]string{tc.opStr, tc.vStr}, "")) + continue + } + if !tc.c(r.c) { + t.Errorf("Invalid comparator for case %q\n", strings.Join([]string{tc.opStr, tc.vStr}, "")) + } + } + } + +} + +func TestSplitORParts(t *testing.T) { + tests := []struct { + i []string + o [][]string + }{ + {[]string{">1.2.3", "||", "<1.2.3", "||", "=1.2.3"}, [][]string{ + []string{">1.2.3"}, + []string{"<1.2.3"}, + []string{"=1.2.3"}, + }}, + {[]string{">1.2.3", "<1.2.3", "||", "=1.2.3"}, [][]string{ + []string{">1.2.3", "<1.2.3"}, + []string{"=1.2.3"}, + }}, + {[]string{">1.2.3", "||"}, nil}, + {[]string{"||", ">1.2.3"}, nil}, + } + for _, tc := range tests { + o, err := splitORParts(tc.i) + if err != nil && tc.o != nil { + t.Errorf("Unexpected error for case %q: %s", tc.i, err) + } + if !reflect.DeepEqual(tc.o, o) { + t.Errorf("Invalid for case %q: Expected %q, got: %q", tc.i, tc.o, o) + } + } +} + +func TestVersionRangeToRange(t *testing.T) { + vr := versionRange{ + v: MustParse("1.2.3"), + c: compLT, + } + rf := vr.rangeFunc() + if !rf(MustParse("1.2.2")) || rf(MustParse("1.2.3")) { + t.Errorf("Invalid conversion to range func") + } +} + +func TestRangeAND(t *testing.T) { + v := MustParse("1.2.2") + v1 := MustParse("1.2.1") + v2 := MustParse("1.2.3") + rf1 := Range(func(v Version) bool { + return v.GT(v1) + }) + rf2 := Range(func(v Version) bool { + return v.LT(v2) + }) + rf := rf1.AND(rf2) + if rf(v1) { + t.Errorf("Invalid rangefunc, accepted: %s", v1) + } + if rf(v2) { + t.Errorf("Invalid rangefunc, accepted: %s", v2) + } + if !rf(v) { + t.Errorf("Invalid rangefunc, did not accept: %s", v) + } +} + +func TestRangeOR(t *testing.T) { + tests := []struct { + v Version + b bool + }{ + {MustParse("1.2.0"), true}, + {MustParse("1.2.2"), false}, + {MustParse("1.2.4"), true}, + } + v1 := MustParse("1.2.1") + v2 := MustParse("1.2.3") + rf1 := Range(func(v Version) bool { + return v.LT(v1) + }) + rf2 := Range(func(v Version) bool { + return v.GT(v2) + }) + rf := rf1.OR(rf2) + for _, tc := range tests { + if r := rf(tc.v); r != tc.b { + t.Errorf("Invalid for case %q: Expected %t, got %t", tc.v, tc.b, r) + } + } +} + +func TestParseRange(t *testing.T) { + type tv struct { + v string + b bool + } + tests := []struct { + i string + t []tv + }{ + // Simple expressions + {">1.2.3", []tv{ + {"1.2.2", false}, + {"1.2.3", false}, + {"1.2.4", true}, + }}, + {">=1.2.3", []tv{ + {"1.2.3", true}, + {"1.2.4", true}, + {"1.2.2", false}, + }}, + {"<1.2.3", []tv{ + {"1.2.2", true}, + {"1.2.3", false}, + {"1.2.4", false}, + }}, + {"<=1.2.3", []tv{ + {"1.2.2", true}, + {"1.2.3", true}, + {"1.2.4", false}, + }}, + {"1.2.3", []tv{ + {"1.2.2", false}, + {"1.2.3", true}, + {"1.2.4", false}, + }}, + {"=1.2.3", []tv{ + {"1.2.2", false}, + {"1.2.3", true}, + {"1.2.4", false}, + }}, + {"==1.2.3", []tv{ + {"1.2.2", false}, + {"1.2.3", true}, + {"1.2.4", false}, + }}, + {"!=1.2.3", []tv{ + {"1.2.2", true}, + {"1.2.3", false}, + {"1.2.4", true}, + }}, + {"!1.2.3", []tv{ + {"1.2.2", true}, + {"1.2.3", false}, + {"1.2.4", true}, + }}, + // Simple Expression errors + {">>1.2.3", nil}, + {"!1.2.3", nil}, + {"1.0", nil}, + {"string", nil}, + {"", nil}, + + // AND Expressions + {">1.2.2 <1.2.4", []tv{ + {"1.2.2", false}, + {"1.2.3", true}, + {"1.2.4", false}, + }}, + {"<1.2.2 <1.2.4", []tv{ + {"1.2.1", true}, + {"1.2.2", false}, + {"1.2.3", false}, + {"1.2.4", false}, + }}, + {">1.2.2 <1.2.5 !=1.2.4", []tv{ + {"1.2.2", false}, + {"1.2.3", true}, + {"1.2.4", false}, + {"1.2.5", false}, + }}, + {">1.2.2 <1.2.5 !1.2.4", []tv{ + {"1.2.2", false}, + {"1.2.3", true}, + {"1.2.4", false}, + {"1.2.5", false}, + }}, + // OR Expressions + {">1.2.2 || <1.2.4", []tv{ + {"1.2.2", true}, + {"1.2.3", true}, + {"1.2.4", true}, + }}, + {"<1.2.2 || >1.2.4", []tv{ + {"1.2.2", false}, + {"1.2.3", false}, + {"1.2.4", false}, + }}, + // Combined Expressions + {">1.2.2 <1.2.4 || >=2.0.0", []tv{ + {"1.2.2", false}, + {"1.2.3", true}, + {"1.2.4", false}, + {"2.0.0", true}, + {"2.0.1", true}, + }}, + {">1.2.2 <1.2.4 || >=2.0.0 <3.0.0", []tv{ + {"1.2.2", false}, + {"1.2.3", true}, + {"1.2.4", false}, + {"2.0.0", true}, + {"2.0.1", true}, + {"2.9.9", true}, + {"3.0.0", false}, + }}, + } + + for _, tc := range tests { + r, err := ParseRange(tc.i) + if err != nil && tc.t != nil { + t.Errorf("Error parsing range %q: %s", tc.i, err) + continue + } + for _, tvc := range tc.t { + v := MustParse(tvc.v) + if res := r(v); res != tvc.b { + t.Errorf("Invalid for case %q matching %q: Expected %t, got: %t", tc.i, tvc.v, tvc.b, res) + } + } + + } +} + +func BenchmarkRangeParseSimple(b *testing.B) { + const VERSION = ">1.0.0" + b.ReportAllocs() + b.ResetTimer() + for n := 0; n < b.N; n++ { + ParseRange(VERSION) + } +} + +func BenchmarkRangeParseAverage(b *testing.B) { + const VERSION = ">=1.0.0 <2.0.0" + b.ReportAllocs() + b.ResetTimer() + for n := 0; n < b.N; n++ { + ParseRange(VERSION) + } +} + +func BenchmarkRangeParseComplex(b *testing.B) { + const VERSION = ">=1.0.0 <2.0.0 || >=3.0.1 <4.0.0 !=3.0.3 || >=5.0.0" + b.ReportAllocs() + b.ResetTimer() + for n := 0; n < b.N; n++ { + ParseRange(VERSION) + } +} + +func BenchmarkRangeMatchSimple(b *testing.B) { + const VERSION = ">1.0.0" + r, _ := ParseRange(VERSION) + v := MustParse("2.0.0") + b.ReportAllocs() + b.ResetTimer() + for n := 0; n < b.N; n++ { + r(v) + } +} + +func BenchmarkRangeMatchAverage(b *testing.B) { + const VERSION = ">=1.0.0 <2.0.0" + r, _ := ParseRange(VERSION) + v := MustParse("1.2.3") + b.ReportAllocs() + b.ResetTimer() + for n := 0; n < b.N; n++ { + r(v) + } +} + +func BenchmarkRangeMatchComplex(b *testing.B) { + const VERSION = ">=1.0.0 <2.0.0 || >=3.0.1 <4.0.0 !=3.0.3 || >=5.0.0" + r, _ := ParseRange(VERSION) + v := MustParse("5.0.1") + b.ReportAllocs() + b.ResetTimer() + for n := 0; n < b.N; n++ { + r(v) + } +} From e64c75d1c910cd81c8caa1f2cc2c6409dd65af81 Mon Sep 17 00:00:00 2001 From: kujenga Date: Thu, 16 Jun 2016 18:19:14 -0400 Subject: [PATCH 41/42] add tolerant parsing functionality for non-standard versions --- semver.go | 23 +++++++++++++++++++++++ semver_test.go | 41 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 64 insertions(+) diff --git a/semver.go b/semver.go index bbf85ce..8ee0842 100644 --- a/semver.go +++ b/semver.go @@ -200,6 +200,29 @@ func Make(s string) (Version, error) { return Parse(s) } +// ParseTolerant allows for certain version specifications that do not strictly adhere to semver +// specs to be parsed by this library. It does so by normalizing versions before passing them to +// Parse(). It currently trims spaces, removes a "v" prefix, and adds a 0 patch number to versions +// with only major and minor components specified +func ParseTolerant(s string) (Version, error) { + s = strings.TrimSpace(s) + s = strings.TrimPrefix(s, "v") + + // Split into major.minor.(patch+pr+meta) + parts := strings.SplitN(s, ".", 3) + if len(parts) < 3 { + if strings.ContainsAny(parts[len(parts)-1], "+-") { + return Version{}, errors.New("Short version cannot contain PreRelease/Build meta data") + } + for len(parts) < 3 { + parts = append(parts, "0") + } + s = strings.Join(parts, ".") + } + + return Parse(s) +} + // Parse parses version string and returns a validated Version or error func Parse(s string) (Version, error) { if len(s) == 0 { diff --git a/semver_test.go b/semver_test.go index e56ebce..b3e1fd4 100644 --- a/semver_test.go +++ b/semver_test.go @@ -30,6 +30,13 @@ var formatTests = []formatTest{ {Version{1, 2, 3, []PRVersion{prstr("alpha"), prstr("b-eta")}, nil}, "1.2.3-alpha.b-eta"}, } +var tolerantFormatTests = []formatTest{ + {Version{1, 2, 3, nil, nil}, "v1.2.3"}, + {Version{1, 2, 3, nil, nil}, " 1.2.3 "}, + {Version{1, 2, 0, nil, nil}, "1.2"}, + {Version{1, 0, 0, nil, nil}, "1"}, +} + func TestStringer(t *testing.T) { for _, test := range formatTests { if res := test.v.String(); res != test.result { @@ -50,6 +57,18 @@ func TestParse(t *testing.T) { } } +func TestParseTolerant(t *testing.T) { + for _, test := range tolerantFormatTests { + if v, err := ParseTolerant(test.result); err != nil { + t.Errorf("Error parsing %q: %q", test.result, err) + } else if comp := v.Compare(test.v); comp != 0 { + t.Errorf("Parsing, expected %q but got %q, comp: %d ", test.v, v, comp) + } else if err := v.Validate(); err != nil { + t.Errorf("Error validating parsed version %q: %q", test.v, err) + } + } +} + func TestMustParse(t *testing.T) { _ = MustParse("32.2.1-alpha") } @@ -184,6 +203,19 @@ func TestWrongFormat(t *testing.T) { } } +var wrongTolerantFormatTests = []wrongformatTest{ + {nil, "1.0+abc"}, + {nil, "1.0-rc.1"}, +} + +func TestWrongTolerantFormat(t *testing.T) { + for _, test := range wrongTolerantFormatTests { + if res, err := ParseTolerant(test.str); err == nil { + t.Errorf("Parsing wrong format version %q, expected error but got %q", test.str, res) + } + } +} + func TestCompareHelper(t *testing.T) { v := Version{1, 0, 0, []PRVersion{prstr("alpha")}, nil} v1 := Version{1, 0, 0, nil, nil} @@ -319,6 +351,15 @@ func BenchmarkParseAverage(b *testing.B) { } } +func BenchmarkParseTolerantAverage(b *testing.B) { + l := len(tolerantFormatTests) + b.ReportAllocs() + b.ResetTimer() + for n := 0; n < b.N; n++ { + ParseTolerant(tolerantFormatTests[n%l].result) + } +} + func BenchmarkStringSimple(b *testing.B) { const VERSION = "0.0.1" v, _ := Parse(VERSION) From 60ec3488bfea7cca02b021d106d9911120d25fe9 Mon Sep 17 00:00:00 2001 From: Gabe Rosenhouse Date: Thu, 30 Jun 2016 22:46:53 -0700 Subject: [PATCH 42/42] Adds MustParseRange --- range.go | 9 +++++++++ range_test.go | 17 +++++++++++++++++ 2 files changed, 26 insertions(+) diff --git a/range.go b/range.go index 0a8eaa1..238e131 100644 --- a/range.go +++ b/range.go @@ -222,3 +222,12 @@ func parseComparator(s string) comparator { return nil } + +// MustParseRange is like ParseRange but panics if the range cannot be parsed. +func MustParseRange(s string) Range { + r, err := ParseRange(s) + if err != nil { + panic(`semver: ParseRange(` + s + `): ` + err.Error()) + } + return r +} diff --git a/range_test.go b/range_test.go index 5a745a9..b895bee 100644 --- a/range_test.go +++ b/range_test.go @@ -381,6 +381,23 @@ func TestParseRange(t *testing.T) { } } +func TestMustParseRange(t *testing.T) { + testCase := ">1.2.2 <1.2.4 || >=2.0.0 <3.0.0" + r := MustParseRange(testCase) + if !r(MustParse("1.2.3")) { + t.Errorf("Unexpected range behavior on MustParseRange") + } +} + +func TestMustParseRange_panic(t *testing.T) { + defer func() { + if recover() == nil { + t.Errorf("Should have panicked") + } + }() + _ = MustParseRange("invalid version") +} + func BenchmarkRangeParseSimple(b *testing.B) { const VERSION = ">1.0.0" b.ReportAllocs()