mcfg: centralize logic for fuzzy parsing strings, use it to implement SourceMap

This commit is contained in:
Brian Picciano 2018-08-13 20:44:03 -04:00
parent 8ff2abf02c
commit 3de30eb819
5 changed files with 71 additions and 28 deletions

View File

@ -1,7 +1,6 @@
package mcfg package mcfg
import ( import (
"encoding/json"
"fmt" "fmt"
"io" "io"
"os" "os"
@ -23,6 +22,11 @@ import (
// stdout and the process will exit. Since all normally-defined parameters must // 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. // 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 { type SourceCLI struct {
Args []string // if nil then os.Args[1:] is used 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 // pvOk is always true at this point, and so pv is filled in
if pv.IsBool { // As a special case for CLI, if a boolean has no value set it means it
// if it's a boolean we don't expect there to be a following value, // is true.
// it's just a flag if pv.IsBool && !pvStrValOk {
if pvStrValOk { pvStrVal = "true"
return nil, fmt.Errorf("param %q is a boolean and cannot have a value", arg) pvStrValOk = true
}
pv.Value = json.RawMessage("true")
} else if !pvStrValOk { } else if !pvStrValOk {
// everything else should have a value. if pvStrVal isn't filled it // 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 // means the next arg should be one. Continue the loop, it'll get
// filled with the next one (hopefully) // filled with the next one (hopefully)
continue 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) pvs = append(pvs, pv)
key = "" key = ""
pv = ParamValue{} pv = ParamValue{}

View File

@ -1,7 +1,6 @@
package mcfg package mcfg
import ( import (
"encoding/json"
"fmt" "fmt"
"os" "os"
"strings" "strings"
@ -58,18 +57,7 @@ func (env SourceEnv) Parse(cfg *Cfg) ([]ParamValue, error) {
} }
k, v := split[0], split[1] k, v := split[0], split[1]
if pv, ok := pvM[k]; ok { if pv, ok := pvM[k]; ok {
if pv.IsBool { pv.Value = fuzzyParse(pv.Param, v)
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)
}
pvs = append(pvs, pv) pvs = append(pvs, pv)
} }
} }

View File

@ -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 // ParamBool returns a *bool which will be populated once the Cfg is run, and
// which defaults to false if unconfigured // 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 { func (c *Cfg) ParamBool(name, usage string) *bool {
var b bool var b bool
c.ParamAdd(Param{Name: name, Usage: usage, IsBool: true, Into: &b}) c.ParamAdd(Param{Name: name, Usage: usage, IsBool: true, Into: &b})

View File

@ -8,6 +8,22 @@ import (
"strings" "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 // ParamValue describes a value for a parameter which has been parsed by a
// Source // Source
type ParamValue struct { type ParamValue struct {
@ -69,3 +85,20 @@ func (ss Sources) Parse(c *Cfg) ([]ParamValue, error) {
} }
return pvs, nil 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
}

View File

@ -159,3 +159,23 @@ func TestSources(t *T) {
massert.Equal(3, *c), 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),
))
}