mcfg: make ParamValue not embed Param, so that a Source which is []ParamValue makes sense, and can replace SourceMap

This commit is contained in:
Brian Picciano 2019-01-25 17:33:36 -05:00
parent ddd26259b2
commit 8e2cffd65b
6 changed files with 80 additions and 69 deletions

View File

@ -58,7 +58,7 @@ func (cli SourceCLI) Parse(params []Param) ([]ParamValue, error) {
pvs := make([]ParamValue, 0, len(args))
var (
key string
pv ParamValue
p Param
pvOk bool
pvStrVal string
pvStrValOk bool
@ -72,7 +72,7 @@ func (cli SourceCLI) Parse(params []Param) ([]ParamValue, error) {
os.Stdout.Sync()
os.Exit(1)
} else {
for key, pv.Param = range pM {
for key, p = range pM {
if arg == key {
pvOk = true
break
@ -88,8 +88,7 @@ func (cli SourceCLI) Parse(params []Param) ([]ParamValue, error) {
break
}
if !pvOk {
err := merr.New("unexpected config parameter")
return nil, merr.WithValue(err, "param", arg, true)
return nil, merr.New("unexpected config parameter", "param", arg)
}
}
@ -97,7 +96,7 @@ func (cli SourceCLI) Parse(params []Param) ([]ParamValue, error) {
// As a special case for CLI, if a boolean has no value set it means it
// is true.
if pv.IsBool && !pvStrValOk {
if p.IsBool && !pvStrValOk {
pvStrVal = "true"
pvStrValOk = true
} else if !pvStrValOk {
@ -107,18 +106,20 @@ func (cli SourceCLI) Parse(params []Param) ([]ParamValue, error) {
continue
}
pv.Value = pv.Param.fuzzyParse(pvStrVal)
pvs = append(pvs, ParamValue{
Name: p.Name,
Path: p.Path,
Value: p.fuzzyParse(pvStrVal),
})
pvs = append(pvs, pv)
key = ""
pv = ParamValue{}
p = Param{}
pvOk = false
pvStrVal = ""
pvStrValOk = false
}
if pvOk && !pvStrValOk {
err := merr.New("param expected a value")
return nil, merr.WithValue(err, "param", key, true)
return nil, merr.New("param expected a value", "param", key)
}
return pvs, nil
}

View File

@ -57,13 +57,13 @@ func (env SourceEnv) Parse(params []Param) ([]ParamValue, error) {
for _, kv := range kvs {
split := strings.SplitN(kv, "=", 2)
if len(split) != 2 {
err := merr.New("malformed environment key/value pair")
return nil, merr.WithValue(err, "kv", kv, true)
return nil, merr.New("malformed environment key/value pair", "kv", kv)
}
k, v := split[0], split[1]
if p, ok := pM[k]; ok {
pvs = append(pvs, ParamValue{
Param: p,
Name: p.Name,
Path: p.Path,
Value: p.fuzzyParse(v),
})
}

View File

@ -3,7 +3,10 @@
package mcfg
import (
"crypto/md5"
"encoding/hex"
"encoding/json"
"fmt"
"sort"
"github.com/mediocregopher/mediocre-go-lib/mctx"
@ -75,9 +78,31 @@ func collectParams(ctx mctx.Context) []Param {
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 = SourceMap{}
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 {
hash := paramHash(p.Path, p.Name)
if _, ok := pM[hash]; ok {
panic("duplicate Param: " + paramFullName(p.Path, p.Name))
}
pM[hash] = p
}
pvs, err := src.Parse(params)
@ -86,24 +111,31 @@ func populate(params []Param, src Source) error {
}
// dedupe the ParamValues based on their hashes, with the last ParamValue
// taking precedence
// taking precedence. Also filter out those with no corresponding Param.
pvM := map[string]ParamValue{}
for _, pv := range pvs {
pvM[pv.hash()] = pv
hash := paramHash(pv.Path, pv.Name)
if _, ok := pM[hash]; !ok {
continue
}
pvM[hash] = pv
}
// check for required params
for _, param := range params {
if !param.Required {
for hash, p := range pM {
if !p.Required {
continue
} else if _, ok := pvM[param.hash()]; !ok {
err := merr.New("required parameter is not set")
return merr.WithValue(err, "param", param.fullName(), true)
} else if _, ok := pvM[hash]; !ok {
return merr.New("required parameter is not set",
"param", paramFullName(p.Path, p.Name))
}
}
for _, pv := range pvM {
if err := json.Unmarshal(pv.Value, pv.Into); err != nil {
// 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
}
}

View File

@ -1,8 +1,6 @@
package mcfg
import (
"crypto/md5"
"encoding/hex"
"encoding/json"
"fmt"
"strings"
@ -49,6 +47,10 @@ type Param struct {
Path []string
}
func paramFullName(path []string, name string) string {
return strings.Join(append(path, name), "-")
}
func (p Param) fuzzyParse(v string) json.RawMessage {
if p.IsBool {
if v == "" || v == "0" || v == "false" {
@ -63,21 +65,6 @@ func (p Param) fuzzyParse(v string) json.RawMessage {
return json.RawMessage(v)
}
func (p Param) fullName() string {
return strings.Join(append(p.Path, p.Name), "-")
}
func (p Param) hash() string {
h := md5.New()
for _, path := range p.Path {
fmt.Fprintf(h, "pathEl:%q\n", path)
}
fmt.Fprintf(h, "name:%q\n", p.Name)
hStr := hex.EncodeToString(h.Sum(nil))
// we add the displayName to it to make debugging easier
return p.fullName() + "/" + hStr
}
// MustAdd adds the given Param to the mctx.Context. It will panic if a Param of
// the same Name already exists in the mctx.Context.
func MustAdd(ctx mctx.Context, param Param) {

View File

@ -7,7 +7,8 @@ import (
// ParamValue describes a value for a parameter which has been parsed by a
// Source.
type ParamValue struct {
Param
Name string
Path []string
Value json.RawMessage
}
@ -18,11 +19,22 @@ type ParamValue struct {
// by the configuration source.
//
// The returned []ParamValue may contain duplicates of the same Param's value.
// in which case the later value takes precedence.
// in which case the later value takes precedence. It may also contain
// ParamValues which do not correspond to any of the passed in Params. These
// will be ignored in Populate.
type Source interface {
Parse([]Param) ([]ParamValue, error)
}
// ParamValues is simply a slice of ParamValue elements, which implements Parse
// by always returning itself as-is.
type ParamValues []ParamValue
// Parse implements the method for the Source interface.
func (pvs ParamValues) Parse([]Param) ([]ParamValue, error) {
return pvs, nil
}
// Sources combines together multiple Source instances into one. It will call
// Parse on each element individually. Values from later Sources take precedence
// over previous ones.
@ -40,24 +52,3 @@ func (ss Sources) Parse(params []Param) ([]ParamValue, error) {
}
return pvs, nil
}
// SourceMap implements the Source interface by mapping parameter names to
// values for them. The names are comprised of the path and name of a Param
// joined by "-" characters, i.e. `strings.Join(append(param.Path, param.Name),
// "-")`. Values will be parsed in the same way that SourceEnv parses its
// variables.
type SourceMap map[string]string
// Parse implements the method for the Source interface.
func (m SourceMap) Parse(params []Param) ([]ParamValue, error) {
pvs := make([]ParamValue, 0, len(m))
for _, p := range params {
if v, ok := m[p.fullName()]; ok {
pvs = append(pvs, ParamValue{
Param: p,
Value: p.fuzzyParse(v),
})
}
}
return pvs, nil
}

View File

@ -109,7 +109,7 @@ func (scs srcCommonState) applyCtxAndPV(p srcCommonParams) srcCommonState {
ctxP = get(thisCtx).params[p.name] // get it back out to get any added fields
if !p.unset {
pv := ParamValue{Param: ctxP}
pv := ParamValue{Name: ctxP.Name, Path: ctxP.Path}
if p.isBool {
pv.Value = json.RawMessage("true")
} else {
@ -159,17 +159,17 @@ func TestSources(t *T) {
))
}
func TestSourceMap(t *T) {
func TestSourceParamValues(t *T) {
ctx := mctx.New()
a := RequiredInt(ctx, "a", "")
foo := mctx.ChildOf(ctx, "foo")
b := RequiredString(foo, "b", "")
c := Bool(foo, "c", "")
err := Populate(ctx, SourceMap{
"a": "4",
"foo-b": "bbb",
"foo-c": "1",
err := Populate(ctx, ParamValues{
{Name: "a", Value: json.RawMessage(`4`)},
{Path: []string{"foo"}, Name: "b", Value: json.RawMessage(`"bbb"`)},
{Path: []string{"foo"}, Name: "c", Value: json.RawMessage("true")},
})
massert.Fatal(t, massert.All(
massert.Nil(err),