mediocre-go-lib/mcfg/mcfg.go
Brian Picciano 4b446a0efc mctx: refactor so that contexts no longer carry mutable data
This change required refactoring nearly every package in this project,
but it does a lot to simplify mctx and make other code using it easier
to think about.

Other code, such as mlog and mcfg, had to be slightly modified for this
change to work as well.
2019-02-07 19:42:12 -05:00

140 lines
3.5 KiB
Go

// Package mcfg provides a simple foundation for complex service/binary
// configuration, initialization, and destruction
package mcfg
import (
"context"
"crypto/md5"
"encoding/hex"
"encoding/json"
"fmt"
"sort"
"github.com/mediocregopher/mediocre-go-lib/mctx"
"github.com/mediocregopher/mediocre-go-lib/merr"
)
// TODO Sources:
// - JSON file
// - YAML file
func sortParams(params []Param) {
sort.Slice(params, func(i, j int) bool {
a, b := params[i], params[j]
aPath, bPath := mctx.Path(a.Context), mctx.Path(b.Context)
for {
switch {
case len(aPath) == 0 && len(bPath) == 0:
return a.Name < b.Name
case len(aPath) == 0 && len(bPath) > 0:
return false
case len(aPath) > 0 && len(bPath) == 0:
return true
case aPath[0] != bPath[0]:
return aPath[0] < bPath[0]
default:
aPath, bPath = aPath[1:], bPath[1:]
}
}
})
}
// returns all Params gathered by recursively retrieving them from this Context
// and its children. Returned Params are sorted according to their Path and
// Name.
func collectParams(ctx context.Context) []Param {
var params []Param
var visit func(context.Context)
visit = func(ctx context.Context) {
for _, param := range getLocalParams(ctx) {
params = append(params, param)
}
for _, childCtx := range mctx.Children(ctx) {
visit(childCtx)
}
}
visit(ctx)
sortParams(params)
return params
}
func paramHash(path []string, name string) string {
h := md5.New()
for _, pathEl := range path {
fmt.Fprintf(h, "pathEl:%q\n", pathEl)
}
fmt.Fprintf(h, "name:%q\n", name)
hStr := hex.EncodeToString(h.Sum(nil))
// we add the displayName to it to make debugging easier
return paramFullName(path, name) + "/" + hStr
}
func populate(params []Param, src Source) error {
if src == nil {
src = ParamValues(nil)
}
// map Params to their hash, so we can match them to their ParamValues
// later. There should not be any duplicates here.
pM := map[string]Param{}
for _, p := range params {
path := mctx.Path(p.Context)
hash := paramHash(path, p.Name)
if _, ok := pM[hash]; ok {
panic("duplicate Param: " + paramFullName(path, p.Name))
}
pM[hash] = p
}
pvs, err := src.Parse(params)
if err != nil {
return err
}
// dedupe the ParamValues based on their hashes, with the last ParamValue
// taking precedence. Also filter out those with no corresponding Param.
pvM := map[string]ParamValue{}
for _, pv := range pvs {
hash := paramHash(pv.Path, pv.Name)
if _, ok := pM[hash]; !ok {
continue
}
pvM[hash] = pv
}
// check for required params
for hash, p := range pM {
if !p.Required {
continue
} else if _, ok := pvM[hash]; !ok {
return merr.New("required parameter is not set",
"param", paramFullName(mctx.Path(p.Context), p.Name))
}
}
// do the actual populating
for hash, pv := range pvM {
// at this point, all ParamValues in pvM have a corresponding pM Param
p := pM[hash]
if err := json.Unmarshal(pv.Value, p.Into); err != nil {
return err
}
}
return nil
}
// Populate uses the Source to populate the values of all Params which were
// added to the given Context, and all of its children. Populate may be called
// multiple times with the same Context, each time will only affect the values
// of the Params which were provided by the respective Source.
//
// Source may be nil to indicate that no configuration is provided. Only default
// values will be used, and if any paramaters are required this will error.
func Populate(ctx context.Context, src Source) error {
return populate(collectParams(ctx), src)
}