github.com/sudo-bmitch/version-bump@v0.0.0-20240503123857-70b0e3f646dd/internal/config/config.go (about)

     1  // Package config defines the config file and load methods
     2  package config
     3  
     4  import (
     5  	"errors"
     6  	"fmt"
     7  	"io"
     8  	"os"
     9  
    10  	"github.com/sudo-bmitch/version-bump/internal/template"
    11  	"gopkg.in/yaml.v3"
    12  )
    13  
    14  // File defines a file to process for version bumps
    15  type File struct {
    16  	Name  string   `yaml:"-" json:"-"`         // Name is a filename or glob to match against
    17  	Scans []string `yaml:"scans" json:"scans"` // Scans are Scan names to apply to the file
    18  }
    19  
    20  // Scan defines how to search a file for versions to bump
    21  type Scan struct {
    22  	Name   string            `yaml:"-" json:"-"`           // Name is the name of the scan entry
    23  	Type   string            `yaml:"type" json:"type"`     // Type is the method for scanning the file
    24  	Source string            `yaml:"source" json:"source"` // Source is the name of the source for updating the version
    25  	Args   map[string]string `yaml:"args" json:"args"`     // Args provide additional options used by scanners, sources, and templating
    26  }
    27  
    28  // Source defines how to get the latest version
    29  type Source struct {
    30  	Name     string            `yaml:"-" json:"-"`               // Name is the name of the source entry
    31  	Type     string            `yaml:"type" json:"type"`         // Type is the method used to query the source
    32  	Key      string            `yaml:"key" json:"key"`           // Key is a unique value to store with the source and version in a lock file
    33  	Filter   SourceFilter      `yaml:"filter" json:"filter"`     // Filter specifies which items to include from the source
    34  	Sort     SourceSort        `yaml:"sort" json:"sort"`         // Sort is used to pick from multiple results
    35  	Template string            `yaml:"template" json:"template"` // Template is used to output the version
    36  	Args     map[string]string `yaml:"args" json:"args"`         // Args provide additional options used by sources
    37  	// TODO: delete exec?
    38  	Exec []string `yaml:"exec" json:"exec"` // Exec defines a command to run for custom sources
    39  }
    40  
    41  // SourceFilter defines how items are filtered in from the source.
    42  // By default, all items are included.
    43  type SourceFilter struct {
    44  	Expr     string `yaml:"expr" json:"expr"`
    45  	Template string `yaml:"template" json:"template"`
    46  }
    47  
    48  // SourceSort defines how multiple results should be filtered and sorted.
    49  // By default, sort returns the 0 offset of a descending sort.
    50  type SourceSort struct {
    51  	Method string `yaml:"method" json:"method"`
    52  	Asc    bool   `yaml:"asc" json:"asc"`
    53  	Offset uint   `yaml:"offset" json:"offset"`
    54  }
    55  
    56  // Script defines an addition command to run
    57  type Script struct {
    58  	Name string   `yaml:"-" json:"-"`       // Name is the name of the script
    59  	Step string   `yaml:"step" json:"step"` // Step is when to execute the script, pre-check, post-check, pre-update, post-update
    60  	Exec []string `yaml:"exec" json:"exec"` // Exec defines the command to run for this script
    61  }
    62  
    63  // Config contains the configuration options for the project
    64  type Config struct {
    65  	Version int                `yaml:"version" json:"version"`
    66  	Files   map[string]*File   `yaml:"files" json:"files"`
    67  	Scans   map[string]*Scan   `yaml:"scans" json:"scans"`
    68  	Sources map[string]*Source `yaml:"sources" json:"sources"`
    69  	Scripts map[string]*Script `yaml:"scripts" json:"scripts"`
    70  }
    71  
    72  // SourceTmplData is passed from the scan to the source for performing a version lookup
    73  type SourceTmplData struct {
    74  	Filename   string            // name of the file being updated
    75  	ScanArgs   map[string]string // args to the scan
    76  	ScanMatch  map[string]string // result of a match
    77  	SourceArgs map[string]string // args to the source
    78  }
    79  
    80  // New creates an empty config
    81  func New() *Config {
    82  	return &Config{
    83  		Files:   map[string]*File{},
    84  		Scans:   map[string]*Scan{},
    85  		Sources: map[string]*Source{},
    86  		Scripts: map[string]*Script{},
    87  	}
    88  }
    89  
    90  // LoadReader imports a config from a reader
    91  func LoadReader(r io.Reader) (*Config, error) {
    92  	c := New()
    93  	err := yaml.NewDecoder(r).Decode(c)
    94  	if err != nil && !errors.Is(err, io.EOF) {
    95  		return nil, fmt.Errorf("failed to parse config: %w", err)
    96  	}
    97  	if c.Version > 1 {
    98  		return nil, fmt.Errorf("unsupported config version: %d", c.Version)
    99  	}
   100  	for k := range c.Files {
   101  		c.Files[k].Name = k
   102  	}
   103  	for k := range c.Scans {
   104  		c.Scans[k].Name = k
   105  	}
   106  	for k := range c.Sources {
   107  		c.Sources[k].Name = k
   108  	}
   109  	for k := range c.Scripts {
   110  		c.Scripts[k].Name = k
   111  	}
   112  	return c, nil
   113  }
   114  
   115  // LoadFile imports a config from a filename
   116  func LoadFile(filename string) (*Config, error) {
   117  	file, err := os.Open(filename)
   118  	if err != nil {
   119  		return nil, err
   120  	}
   121  	defer file.Close()
   122  	return LoadReader(file)
   123  }
   124  
   125  func (s Source) ExpandTemplate(data interface{}) (Source, error) {
   126  	sExp := Source{
   127  		Name:     s.Name,
   128  		Type:     s.Type,
   129  		Filter:   s.Filter,
   130  		Sort:     s.Sort,
   131  		Template: s.Template,
   132  		Args:     map[string]string{},
   133  	}
   134  	var err error
   135  	sExp.Key, err = template.String(s.Key, data)
   136  	if err != nil {
   137  		return sExp, fmt.Errorf("failed to parse template for source \"%s\": \"%s\": %w", s.Name, s.Key, err)
   138  	}
   139  	for k := range s.Args {
   140  		sExp.Args[k], err = template.String(s.Args[k], data)
   141  		if err != nil {
   142  			return sExp, fmt.Errorf("failed to parse template for source \"%s\": \"%s\": %w", s.Name, s.Args[k], err)
   143  		}
   144  	}
   145  	sExp.Filter.Expr, err = template.String(s.Filter.Expr, data)
   146  	if err != nil {
   147  		return sExp, fmt.Errorf("failed to parse template for source \"%s\": \"%s\": %w", s.Name, s.Filter.Expr, err)
   148  	}
   149  	// TODO: support exec field too?
   150  	return sExp, nil
   151  }