cfger/cfger.go
2023-08-21 16:47:32 +01:00

113 lines
2.9 KiB
Go

package cfger
import (
"errors"
"fmt"
"os"
"regexp"
"strconv"
"strings"
"gopkg.in/yaml.v3"
)
// FatalErrorHandler is called with a fatal error before cfger calls os.Exit.
// This is to allow fatal errors to be processed correctly depending on the
// application itself.
var FatalErrorHandler = func(err error) {
_, _ = fmt.Fprintf(os.Stderr, "%v\n", err)
}
type ConfigLoader struct {
rawConfigFileContents map[string]any
lastKey string
}
func New() *ConfigLoader {
return &ConfigLoader{}
}
// Load reads a YAML config file from fname and holds it in memory.
//
// If fname cannot be found, a blank config file will be loaded
// instead.
func (cl *ConfigLoader) Load(fname string) error {
cl.rawConfigFileContents = make(map[string]any)
fcont, err := os.ReadFile(fname)
if err != nil {
if errors.Is(err, os.ErrNotExist) {
return nil
} else {
return err
}
}
if err := yaml.Unmarshal(fcont, &cl.rawConfigFileContents); err != nil {
return fmt.Errorf("cfger: unmarshal %s: %w", fname, err)
}
return nil
}
var indexedPartRegexp = regexp.MustCompile(`(?m)([a-zA-Z]+)(?:\[(\d+)\])?`)
// Get gets a given key from the currently loaded configuration.
//
// Within a key representation, a dot represents a recursion into a named
// object and square brackets represent an index in a named array. You could access the value "hello" using the key `alpha[0].beta` in the below example.
//
// alpha:
// - beta: "hello"
func (cl *ConfigLoader) Get(key string) OptionalItem {
// httpcore[2].bananas
cl.lastKey = key
parts := strings.Split(key, ".")
var cursor any = cl.rawConfigFileContents
for _, part := range parts {
components := indexedPartRegexp.FindStringSubmatch(part)
key := components[1]
index, _ := strconv.ParseInt(components[2], 10, 32)
isIndexed := components[2] != ""
item, found := cursor.(map[string]any)[key]
if !found {
return OptionalItem{nil, false}
}
if isIndexed {
arr, conversionOk := item.([]any)
if !conversionOk {
panic(fmt.Errorf("cfger: attempted to index non-indexable config item %s", key))
}
cursor = arr[index]
} else {
cursor = item
}
}
return OptionalItem{cursor, true}
}
// Required gets a given key from the currently loaded configuration and raises
// a fatal error if it cannot be found.
//
// See documentation for Get for key format.
func (cl *ConfigLoader) Required(key string) OptionalItem {
opt := cl.Get(key)
if !opt.found {
FatalErrorHandler(fmt.Errorf("Required key %s not found in config file", key))
os.Exit(1)
}
return opt
}
// Required gets a given key from the currently loaded configuration and
// returns a default value if it cannot be found.
//
// See documentation for Get for key format.
func (cl *ConfigLoader) WithDefault(key string, defaultValue any) OptionalItem {
opt := cl.Get(key)
if !opt.found {
return OptionalItem{item: defaultValue, found: true}
}
return opt
}