From 3de30eb81998b08f1dbef03437b2089451839df9 Mon Sep 17 00:00:00 2001 From: Brian Picciano Date: Mon, 13 Aug 2018 20:44:03 -0400 Subject: [PATCH] mcfg: centralize logic for fuzzy parsing strings, use it to implement SourceMap --- mcfg/cli.go | 27 ++++++++++++--------------- mcfg/env.go | 14 +------------- mcfg/param.go | 5 +++++ mcfg/source.go | 33 +++++++++++++++++++++++++++++++++ mcfg/source_test.go | 20 ++++++++++++++++++++ 5 files changed, 71 insertions(+), 28 deletions(-) diff --git a/mcfg/cli.go b/mcfg/cli.go index 320fecd..dd39be8 100644 --- a/mcfg/cli.go +++ b/mcfg/cli.go @@ -1,7 +1,6 @@ package mcfg import ( - "encoding/json" "fmt" "io" "os" @@ -23,6 +22,11 @@ import ( // stdout and the process will exit. Since all normally-defined parameters must // being with double-dash ("--") they won't ever conflict with the help option. // +// SourceCLI behaves a little differently with boolean parameters. Setting the +// value of a boolean parameter directly _must_ be done with an equals, for +// example: `--boolean-flag=1` or `--boolean-flag=false`. Using the +// space-separated format will not work. If a boolean has no equal-separated +// value it is assumed to be setting the value to `true`, as would be expected. type SourceCLI struct { Args []string // if nil then os.Args[1:] is used @@ -86,27 +90,20 @@ func (cli SourceCLI) Parse(cfg *Cfg) ([]ParamValue, error) { // pvOk is always true at this point, and so pv is filled in - if pv.IsBool { - // if it's a boolean we don't expect there to be a following value, - // it's just a flag - if pvStrValOk { - return nil, fmt.Errorf("param %q is a boolean and cannot have a value", arg) - } - pv.Value = json.RawMessage("true") - + // As a special case for CLI, if a boolean has no value set it means it + // is true. + if pv.IsBool && !pvStrValOk { + pvStrVal = "true" + pvStrValOk = true } else if !pvStrValOk { // everything else should have a value. if pvStrVal isn't filled it // means the next arg should be one. Continue the loop, it'll get // filled with the next one (hopefully) continue - - } else if pv.IsString && (pvStrVal == "" || pvStrVal[0] != '"') { - pv.Value = json.RawMessage(`"` + pvStrVal + `"`) - - } else { - pv.Value = json.RawMessage(pvStrVal) } + pv.Value = fuzzyParse(pv.Param, pvStrVal) + pvs = append(pvs, pv) key = "" pv = ParamValue{} diff --git a/mcfg/env.go b/mcfg/env.go index 1267ad0..588960a 100644 --- a/mcfg/env.go +++ b/mcfg/env.go @@ -1,7 +1,6 @@ package mcfg import ( - "encoding/json" "fmt" "os" "strings" @@ -58,18 +57,7 @@ func (env SourceEnv) Parse(cfg *Cfg) ([]ParamValue, error) { } k, v := split[0], split[1] if pv, ok := pvM[k]; ok { - if pv.IsBool { - if v == "" { - pv.Value = json.RawMessage("false") - } else { - pv.Value = json.RawMessage("true") - } - } else if pv.IsString && (v == "" || v[0] != '"') { - pv.Value = json.RawMessage(`"` + v + `"`) - - } else { - pv.Value = json.RawMessage(v) - } + pv.Value = fuzzyParse(pv.Param, v) pvs = append(pvs, pv) } } diff --git a/mcfg/param.go b/mcfg/param.go index eca6ebe..4bca026 100644 --- a/mcfg/param.go +++ b/mcfg/param.go @@ -95,6 +95,11 @@ func (c *Cfg) ParamRequiredString(name, usage string) *string { // ParamBool returns a *bool which will be populated once the Cfg is run, and // which defaults to false if unconfigured +// +// The default behavior of all Sources is that a boolean parameter will be set +// to true unless the value is "", 0, or false. In the case of the CLI Source +// the value will also be true when the parameter is used with no value at all, +// as would be expected. func (c *Cfg) ParamBool(name, usage string) *bool { var b bool c.ParamAdd(Param{Name: name, Usage: usage, IsBool: true, Into: &b}) diff --git a/mcfg/source.go b/mcfg/source.go index 972e7a1..ebcb4e7 100644 --- a/mcfg/source.go +++ b/mcfg/source.go @@ -8,6 +8,22 @@ import ( "strings" ) +func fuzzyParse(p Param, v string) json.RawMessage { + if p.IsBool { + if v == "" || v == "0" || v == "false" { + return json.RawMessage("false") + } + return json.RawMessage("true") + + } else if p.IsString && (v == "" || v[0] != '"') { + return json.RawMessage(`"` + v + `"`) + } + + return json.RawMessage(v) +} + +// TODO moving Path into Param would make a lot more sense + // ParamValue describes a value for a parameter which has been parsed by a // Source type ParamValue struct { @@ -69,3 +85,20 @@ func (ss Sources) Parse(c *Cfg) ([]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(p.Path, p.Name), "-")`. +// Values will be parsed in the same way that SourceEnv parses its variables. +type SourceMap map[string]string + +func (m SourceMap) Parse(c *Cfg) ([]ParamValue, error) { + pvs := make([]ParamValue, 0, len(m)) + for _, pv := range c.allParamValues() { + if v, ok := m[pv.displayName()]; ok { + pv.Value = fuzzyParse(pv.Param, v) + pvs = append(pvs, pv) + } + } + return pvs, nil +} diff --git a/mcfg/source_test.go b/mcfg/source_test.go index c26e532..062b935 100644 --- a/mcfg/source_test.go +++ b/mcfg/source_test.go @@ -159,3 +159,23 @@ func TestSources(t *T) { massert.Equal(3, *c), )) } + +func TestSourceMap(t *T) { + cfg := New() + a := cfg.ParamRequiredInt("a", "") + foo := cfg.Child("foo") + b := foo.ParamRequiredString("b", "") + c := foo.ParamBool("c", "") + + err := cfg.populateParams(SourceMap{ + "a": "4", + "foo-b": "bbb", + "foo-c": "1", + }) + massert.Fatal(t, massert.All( + massert.Nil(err), + massert.Equal(4, *a), + massert.Equal("bbb", *b), + massert.Equal(true, *c), + )) +}