github.com/sudo-bmitch/version-bump@v0.0.0-20240503123857-70b0e3f646dd/internal/source/source.go (about) 1 // Package source is used to fetch the latest version information from upstream 2 package source 3 4 import ( 5 "fmt" 6 "regexp" 7 "sort" 8 "strconv" 9 "sync" 10 11 "github.com/Masterminds/semver/v3" 12 "github.com/sudo-bmitch/version-bump/internal/config" 13 "github.com/sudo-bmitch/version-bump/internal/template" 14 ) 15 16 type Source interface { 17 // Get returns the version from upstream 18 Get(data config.SourceTmplData) (string, error) 19 Key(data config.SourceTmplData) (string, error) 20 } 21 22 var sourceTypes map[string]func(config.Source) Source = map[string]func(config.Source) Source{ 23 "custom": newCustom, 24 "git": newGit, 25 "manual": newManual, 26 "registry": newRegistry, 27 "gh-release": newGHRelease, 28 // TODO: add url (headers, parse json/yaml, parse regex), github release 29 } 30 31 var mu sync.Mutex 32 var sourceCache map[string]Source = map[string]Source{} 33 34 // Get a named source 35 func Get(confSrc config.Source) (Source, error) { 36 mu.Lock() 37 defer mu.Unlock() 38 if s, ok := sourceCache[confSrc.Name]; ok { 39 return s, nil 40 } 41 if newFn, ok := sourceTypes[confSrc.Type]; ok { 42 s := newFn(confSrc) 43 sourceCache[confSrc.Name] = s 44 return s, nil 45 } 46 return nil, fmt.Errorf("source type not known: %s", confSrc.Type) 47 } 48 49 // VersionTmplData is used by source to output the version 50 type VersionTmplData struct { 51 VerMap map[string]string // map of all matching versions, key is for sorting, value is the returned version 52 VerList []string // sorted list of valid keys in VerMap, generated by procResult 53 Version string // selected version after sorting and offset, this is overwritten if len(Versions) > 0 54 VerMeta map[string]interface{} // additional metadata 55 } 56 57 type FilterTmplData struct { 58 Key string 59 Value string 60 Meta interface{} 61 } 62 63 func procResult(confExp config.Source, data VersionTmplData) (string, error) { 64 // filter 65 if confExp.Filter.Expr != "" && len(data.VerMap) > 0 { 66 filterExp, err := regexp.Compile(confExp.Filter.Expr) 67 if err != nil { 68 return "", fmt.Errorf("failed to compile filter expr \"%s\": %w", confExp.Filter.Expr, err) 69 } 70 for k, v := range data.VerMap { 71 filterStr := k 72 if confExp.Filter.Template != "" { 73 filterStr, err = template.String(confExp.Filter.Template, FilterTmplData{ 74 Key: k, 75 Value: v, 76 Meta: data.VerMeta[k], 77 }) 78 if err != nil { 79 return "", fmt.Errorf("failed to process filter template \"%s\": %w", confExp.Filter.Template, err) 80 } 81 } 82 if !filterExp.MatchString(filterStr) { 83 delete(data.VerMap, k) 84 } 85 } 86 if len(data.VerMap) <= 0 { 87 return "", fmt.Errorf("no matching versions found for expression \"%s\"", confExp.Filter.Expr) 88 } 89 } 90 // sort 91 if len(data.VerMap) > 0 { 92 keys := make([]string, 0, len(data.VerMap)) 93 for k := range data.VerMap { 94 keys = append(keys, k) 95 } 96 switch confExp.Sort.Method { 97 case "semver": 98 vers := make([]*semver.Version, 0, len(keys)) 99 for _, k := range keys { 100 sv, err := semver.NewVersion(k) 101 if err != nil { 102 continue // ignore versions that do not compile 103 } 104 vers = append(vers, sv) 105 } 106 if len(vers) == 0 { 107 return "", fmt.Errorf("no valid semver versions found in %v", keys) 108 } 109 if confExp.Sort.Asc { 110 sort.Sort(semver.Collection(vers)) 111 } else { 112 sort.Sort(sort.Reverse(semver.Collection(vers))) 113 } 114 // rebuild keys from parsed semver 115 keys = make([]string, len(vers)) 116 for i, sv := range vers { 117 keys[i] = sv.Original() 118 } 119 case "numeric": 120 keyInts := make([]int, 0, len(data.VerMap)) 121 orig := map[int]string{} // map from int back to original value 122 for _, k := range keys { 123 // parse numbers from keys 124 i, err := strconv.Atoi(k) 125 if err != nil { 126 continue // ignore versions that are not numeric 127 } 128 keyInts = append(keyInts, i) 129 orig[i] = k 130 } 131 if len(keyInts) == 0 { 132 return "", fmt.Errorf("no valid numeric versions found in %v", keys) 133 } 134 if confExp.Sort.Asc { 135 sort.Sort(sort.IntSlice(keyInts)) 136 } else { 137 sort.Sort(sort.Reverse(sort.IntSlice(keyInts))) 138 } 139 // rebuild keys from parsed semver 140 keys = make([]string, len(keyInts)) 141 for i, iv := range keyInts { 142 keys[i] = orig[iv] 143 } 144 default: 145 if confExp.Sort.Asc { 146 sort.Sort(sort.StringSlice(keys)) 147 } else { 148 sort.Sort(sort.Reverse(sort.StringSlice(keys))) 149 } 150 } 151 // select the requested offset 152 if len(keys) <= int(confExp.Sort.Offset) { 153 return "", fmt.Errorf("requested offset is too large, %d matching versions found: %v", len(keys), keys) 154 } 155 data.Version = data.VerMap[keys[confExp.Sort.Offset]] 156 data.VerList = keys 157 } 158 // template 159 if confExp.Template != "" { 160 return template.String(confExp.Template, data) 161 } 162 return data.Version, nil 163 }