github.com/verrazzano/verrazzano@v1.7.0/pkg/semver/semver.go (about)

     1  // Copyright (c) 2020, 2023, Oracle and/or its affiliates.
     2  // Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl.
     3  
     4  package semver
     5  
     6  import (
     7  	"errors"
     8  	"fmt"
     9  	"regexp"
    10  	"strconv"
    11  	"strings"
    12  
    13  	"go.uber.org/zap"
    14  )
    15  
    16  const semverRegex = "^[v|V](0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)(?:-((?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\\.(?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\\+([0-9a-zA-Z-]+(?:\\.[0-9a-zA-Z-]+)*))?$"
    17  
    18  // SemVersion Implements a basic notion a semantic version (see https://semver.org/, test page: https://regex101.com/r/vkijKf/1/)
    19  type SemVersion struct {
    20  	Major      int64
    21  	Minor      int64
    22  	Patch      int64
    23  	Prerelease string
    24  	Build      string
    25  }
    26  
    27  var compiledRegEx *regexp.Regexp
    28  
    29  func getRegex() (*regexp.Regexp, error) {
    30  	if compiledRegEx != nil {
    31  		return compiledRegEx, nil
    32  	}
    33  	var err error
    34  	compiledRegEx, err = regexp.Compile(semverRegex)
    35  	if err != nil {
    36  		return nil, err
    37  	}
    38  	return compiledRegEx, nil
    39  }
    40  
    41  // NewSemVersion Create an instance of a SemVersion
    42  func NewSemVersion(version string) (*SemVersion, error) {
    43  	if len(version) == 0 {
    44  		return nil, errors.New("SemVersion string cannot be empty")
    45  	}
    46  	savedVersion := version
    47  	if !strings.HasPrefix(version, "v") && !strings.HasPrefix(version, "V") {
    48  		if version[0] >= '0' && version[0] <= '9' {
    49  			version = "v" + version
    50  		} else {
    51  			return nil, invalidVersionError(savedVersion)
    52  		}
    53  	}
    54  	regex, err := getRegex()
    55  	if err != nil {
    56  		return nil, err
    57  	}
    58  
    59  	allMatches := regex.FindAllStringSubmatch(version, -1)
    60  	zap.S().Debugf("allMatches: %v", allMatches)
    61  	if len(allMatches) == 0 {
    62  		return nil, invalidVersionError(savedVersion)
    63  	}
    64  
    65  	versionComponents := allMatches[0]
    66  	zap.S().Debugf("components: %v", versionComponents)
    67  	numComponents := len(versionComponents)
    68  	if numComponents < 3 {
    69  		return nil, invalidVersionError(savedVersion)
    70  	}
    71  	majorVer, err := strconv.ParseInt(versionComponents[1], 10, 64)
    72  	if err != nil {
    73  		return nil, err
    74  	}
    75  	minorVer, err := strconv.ParseInt(versionComponents[2], 10, 64)
    76  	if err != nil {
    77  		return nil, err
    78  	}
    79  
    80  	var patchVer int64
    81  	if numComponents > 3 {
    82  		patchVer, err = strconv.ParseInt(versionComponents[3], 10, 64)
    83  		if err != nil {
    84  			return nil, err
    85  		}
    86  	}
    87  
    88  	var prereleaseVer string
    89  	if numComponents > 4 {
    90  		prereleaseVer = versionComponents[4]
    91  	}
    92  
    93  	var buildVer string
    94  	if numComponents > 5 {
    95  		buildVer = versionComponents[5]
    96  	}
    97  	semVersion := SemVersion{
    98  		Major:      majorVer,
    99  		Minor:      minorVer,
   100  		Patch:      patchVer,
   101  		Prerelease: prereleaseVer,
   102  		Build:      buildVer,
   103  	}
   104  	return &semVersion, nil
   105  }
   106  
   107  // CompareTo Compares the current version to another version
   108  // - if from > this, -1 is returned
   109  // - if from < this, 1 is returned
   110  // - if they are equal, 0 is returned
   111  func (v *SemVersion) CompareTo(from *SemVersion) int {
   112  	var result int
   113  	if result = compareVersion(from.Major, v.Major); result == 0 {
   114  		if result = compareVersion(from.Minor, v.Minor); result == 0 {
   115  			if result = compareVersion(from.Patch, v.Patch); result == 0 {
   116  				if result = compareVersionSubstring(from.Prerelease, v.Prerelease); result == 0 {
   117  					result = compareVersionSubstring(from.Build, v.Build)
   118  				}
   119  			}
   120  		}
   121  	}
   122  	return result
   123  }
   124  
   125  // CompareToPrereleaseInts Compares the current version to another version treating the Prerelease field as an int
   126  // - if from > this, -1 is returned
   127  // - if from < this, 1 is returned
   128  // - if they are equal, 0 is returned
   129  // - if the prerelease field can not be converted to an int, it will return an error
   130  func (v *SemVersion) CompareToPrereleaseInts(from *SemVersion) (int, error) {
   131  	var result int
   132  	var err error
   133  	if result = compareVersion(from.Major, v.Major); result == 0 {
   134  		if result = compareVersion(from.Minor, v.Minor); result == 0 {
   135  			if result = compareVersion(from.Patch, v.Patch); result == 0 {
   136  				fromPrereleaseStr := from.Prerelease
   137  				if fromPrereleaseStr == "" {
   138  					fromPrereleaseStr = "0"
   139  				}
   140  				vPrereleaseStr := v.Prerelease
   141  				if vPrereleaseStr == "" {
   142  					vPrereleaseStr = "0"
   143  				}
   144  				fromPrerelease, err := strconv.Atoi(fromPrereleaseStr)
   145  				if err != nil {
   146  					return 0, err
   147  				}
   148  				vPrerelease, err := strconv.Atoi(vPrereleaseStr)
   149  				if err != nil {
   150  					return 0, err
   151  				}
   152  				if result = compareVersion(int64(fromPrerelease), int64(vPrerelease)); result == 0 {
   153  					result = compareVersionSubstring(from.Build, v.Build)
   154  				}
   155  			}
   156  		}
   157  	}
   158  	return result, err
   159  }
   160  
   161  // IsEqualTo Returns true if to == from
   162  func (v *SemVersion) IsEqualTo(from *SemVersion) bool {
   163  	return v.CompareTo(from) == 0
   164  }
   165  
   166  // IsGreatherThan Returns true if to > from
   167  func (v *SemVersion) IsGreatherThan(from *SemVersion) bool {
   168  	return v.CompareTo(from) > 0
   169  }
   170  
   171  // IsLessThan Returns true if to < from
   172  func (v *SemVersion) IsLessThan(from *SemVersion) bool {
   173  	return v.CompareTo(from) < 0
   174  }
   175  
   176  // ToString Convert to a valid semver string representation
   177  func (v *SemVersion) ToString() string {
   178  	if v.Build != "" && v.Prerelease != "" {
   179  		return fmt.Sprintf("%v.%v.%v-%v+%v", v.Major, v.Minor, v.Patch, v.Prerelease, v.Build)
   180  	} else if v.Build == "" && v.Prerelease != "" {
   181  		return fmt.Sprintf("%v.%v.%v-%v", v.Major, v.Minor, v.Patch, v.Prerelease)
   182  	} else if v.Build != "" && v.Prerelease == "" {
   183  		return fmt.Sprintf("%v.%v.%v+%v", v.Major, v.Minor, v.Patch, v.Build)
   184  	} else {
   185  		return fmt.Sprintf("%v.%v.%v", v.Major, v.Minor, v.Patch)
   186  	}
   187  }
   188  
   189  // ToStringWithoutBuildAndPrerelease Convert to a valid semver string representation without the build and pre-release fields
   190  func (v *SemVersion) ToStringWithoutBuildAndPrerelease() string {
   191  	return fmt.Sprintf("%v.%v.%v", v.Major, v.Minor, v.Patch)
   192  }
   193  
   194  // invalidVersionError returns an invalid version error message
   195  func invalidVersionError(version string) error {
   196  	return fmt.Errorf("Invalid version string: %s (valid format is vn.n.n or n.n.n)", version)
   197  }
   198  
   199  // Returns
   200  // - 1 if v2 > v1
   201  // - -1 if v1 > v2
   202  // - 0 of v1 == v2
   203  func compareVersion(v1 int64, v2 int64) int {
   204  	if v1 < v2 {
   205  		return 1
   206  	}
   207  	if v1 > v2 {
   208  		return -1
   209  	}
   210  	return 0
   211  }
   212  
   213  // Returns 0 if the strings are equal, or 1 if not
   214  func compareVersionSubstring(v1 string, v2 string) int {
   215  	if strings.Compare(v1, v2) == 0 {
   216  		return 0
   217  	}
   218  	return 1
   219  }
   220  
   221  // IsGreaterThanOrEqualTo Returns true if to >= from
   222  func (v *SemVersion) IsGreaterThanOrEqualTo(from *SemVersion) bool {
   223  	return v.IsGreatherThan(from) || v.IsEqualTo(from)
   224  }
   225  
   226  // IsEqualOrPatchVersionOf Returns true if to == from or to is a patch version of from
   227  func (v *SemVersion) IsEqualToOrPatchVersionOf(from *SemVersion) bool {
   228  	return v.IsEqualTo(from) || (from.Patch == int64(0) && v.Major == from.Major && v.Minor == from.Minor)
   229  }