package semver import ( "errors" "fmt" "strconv" "strings" ) const ( numbers string = "0123456789" alphas = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-" alphanum = alphas + numbers dot = "." hyphen = "-" plus = "+" ) // Latest fully supported spec version var SPEC_VERSION = Version{ Major: 2, Minor: 0, Patch: 0, } type Version struct { Major uint64 Minor uint64 Patch uint64 Pre []*PRVersion Build []string //No Precendence } // 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), } 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()) } } if len(v.Build) > 0 { versionArray = append(versionArray, plus, strings.Join(v.Build, dot)) } return strings.Join(versionArray, "") } // 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 { 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 } } // 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 { 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 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) } } } } 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) } } } return nil } // Alias for Parse, parses version string and returns a validated Version or 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) { 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]) } 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 } // Minor 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 } 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 } } 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 } 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 len(str) == 0 { return nil, 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) } v.Build = append(v.Build, str) } } return v, nil } // PreRelease Version type PRVersion struct { VersionStr string VersionNum uint64 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) { 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 if err != nil { return nil, err } v.VersionNum = num v.IsNum = true } else if containsOnly(s, alphanum) { v.VersionStr = s v.IsNum = false } else { return nil, fmt.Errorf("Invalid character(s) found in prerelease %q", s) } 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 } 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 } } } // PreRelease version to string func (v PRVersion) String() string { if v.IsNum { return strconv.FormatUint(v.VersionNum, 10) } return v.VersionStr } func containsOnly(s string, set string) bool { return strings.IndexFunc(s, func(r rune) bool { return !strings.ContainsRune(set, r) }) == -1 } func hasLeadingZeroes(s string) bool { return len(s) > 1 && s[0] == '0' } // 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, alphanum) { return "", fmt.Errorf("Invalid character(s) found in build meta data %q", s) } return s, nil }