1package version 2 3import ( 4 "errors" 5 "fmt" 6 "regexp" 7) 8 9var ( 10 versionRegexp = matchingRegexp{regexp.MustCompile(`\A(?P<release>[0-9A-Za-z_\.]+)(\-(?P<pre_release>[0-9A-Za-z_\-\.]+))?(\+(?P<post_release>[0-9A-Za-z_\-\.]+))?\z`)} 11) 12 13type matchingRegexp struct { 14 *regexp.Regexp 15} 16 17type Version struct { 18 Release, PreRelease, PostRelease VersionSegment 19 20 Segments []VersionSegment 21} 22 23func MustNewVersionFromString(v string) Version { 24 ver, err := NewVersionFromString(v) 25 if err != nil { 26 panic(fmt.Sprintf("Invalid version '%s': %s", v, err)) 27 } 28 29 return ver 30} 31 32func NewVersionFromString(v string) (Version, error) { 33 var err error 34 35 if len(v) == 0 { 36 return Version{}, errors.New("Expected version to be non-empty string") 37 } 38 39 captures := versionRegexp.FindStringSubmatchMap(v) 40 if len(captures) == 0 { 41 errMsg := fmt.Sprintf("Expected version '%s' to match version format", v) 42 return Version{}, errors.New(errMsg) 43 } 44 45 release := VersionSegment{} 46 preRelease := VersionSegment{} 47 postRelease := VersionSegment{} 48 49 if releaseStr, ok := captures["release"]; ok { 50 release, err = NewVersionSegmentFromString(releaseStr) 51 if err != nil { 52 return Version{}, err 53 } 54 } 55 56 if preReleaseStr, ok := captures["pre_release"]; ok { 57 preRelease, err = NewVersionSegmentFromString(preReleaseStr) 58 if err != nil { 59 return Version{}, err 60 } 61 } 62 63 if postReleaseStr, ok := captures["post_release"]; ok { 64 postRelease, err = NewVersionSegmentFromString(postReleaseStr) 65 if err != nil { 66 return Version{}, err 67 } 68 } 69 70 return NewVersion(release, preRelease, postRelease) 71} 72 73func NewVersion(release, preRelease, postRelease VersionSegment) (Version, error) { 74 if release.Empty() { 75 return Version{}, errors.New("Expected to non-empty release segment for constructing version") 76 } 77 78 version := Version{ 79 Release: release, 80 PreRelease: preRelease, 81 PostRelease: postRelease, 82 Segments: []VersionSegment{release, preRelease, postRelease}, 83 } 84 85 return version, nil 86} 87 88func (v Version) IncrementRelease() (Version, error) { 89 incRelease, err := v.Release.Increment() 90 if err != nil { 91 return Version{}, err 92 } 93 94 return NewVersion(incRelease, VersionSegment{}, VersionSegment{}) 95} 96 97func (v Version) IncrementPostRelease(defaultPostRelease VersionSegment) (Version, error) { 98 var newPostRelease VersionSegment 99 var err error 100 101 if defaultPostRelease.Empty() { 102 return Version{}, errors.New("Expected default post relase to be non-empty") 103 } 104 105 if v.PostRelease.Empty() { 106 newPostRelease = defaultPostRelease.Copy() 107 } else { 108 newPostRelease, err = v.PostRelease.Increment() 109 if err != nil { 110 return Version{}, err 111 } 112 } 113 114 return NewVersion(v.Release.Copy(), v.PreRelease.Copy(), newPostRelease) 115} 116 117func (v Version) Empty() bool { return len(v.Segments) == 0 } 118 119func (v Version) String() string { return v.AsString() } 120 121func (v Version) AsString() string { 122 result := v.Release.AsString() 123 124 if !v.PreRelease.Empty() { 125 result += "-" + v.PreRelease.AsString() 126 } 127 128 if !v.PostRelease.Empty() { 129 result += "+" + v.PostRelease.AsString() 130 } 131 132 return result 133} 134 135func (v Version) Compare(other Version) int { 136 result := v.Release.Compare(other.Release) 137 if result != 0 { 138 return result 139 } 140 141 if !v.PreRelease.Empty() || !other.PreRelease.Empty() { 142 if v.PreRelease.Empty() { 143 return 1 144 } 145 if other.PreRelease.Empty() { 146 return -1 147 } 148 result = v.PreRelease.Compare(other.PreRelease) 149 if result != 0 { 150 return result 151 } 152 } 153 154 if !v.PostRelease.Empty() || !other.PostRelease.Empty() { 155 if v.PostRelease.Empty() { 156 return -1 157 } 158 if other.PostRelease.Empty() { 159 return 1 160 } 161 result = v.PostRelease.Compare(other.PostRelease) 162 if result != 0 { 163 return result 164 } 165 } 166 167 return 0 168} 169 170func (v Version) IsEq(other Version) bool { return v.Compare(other) == 0 } 171func (v Version) IsGt(other Version) bool { return v.Compare(other) == 1 } 172func (v Version) IsLt(other Version) bool { return v.Compare(other) == -1 } 173 174func (r *matchingRegexp) FindStringSubmatchMap(s string) map[string]string { 175 captures := map[string]string{} 176 177 match := r.FindStringSubmatch(s) 178 if match == nil { 179 return captures 180 } 181 182 for i, name := range r.SubexpNames() { 183 // 0 is a whole regex 184 if i == 0 || name == "" || match[i] == "" { 185 continue 186 } 187 188 captures[name] = match[i] 189 } 190 191 return captures 192} 193