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  }