Get rid of mcmp, and lots of subsequent refactors
Getting rid of mcmp pretty much breaks this whole package, but this commit makes a good start on fixing all the things worth keeping.
This commit is contained in:
parent
c20f884d68
commit
3e2713a850
4
go.mod
4
go.mod
@ -1,4 +1,6 @@
|
||||
module github.com/mediocregopher/mediocre-go-lib
|
||||
module github.com/mediocregopher/mediocre-go-lib/v2
|
||||
|
||||
go 1.15
|
||||
|
||||
require (
|
||||
cloud.google.com/go v0.36.0
|
||||
|
382
mcfg/cli.go
382
mcfg/cli.go
@ -1,382 +0,0 @@
|
||||
package mcfg
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"reflect"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/mediocregopher/mediocre-go-lib/mcmp"
|
||||
"github.com/mediocregopher/mediocre-go-lib/mctx"
|
||||
"github.com/mediocregopher/mediocre-go-lib/merr"
|
||||
)
|
||||
|
||||
type cliKey int
|
||||
|
||||
const (
|
||||
cliKeyTail cliKey = iota
|
||||
cliKeySubCmdM
|
||||
)
|
||||
|
||||
type cliTail struct {
|
||||
dst *[]string
|
||||
descr string
|
||||
}
|
||||
|
||||
// CLITail modifies the behavior of SourceCLI's Parse. Normally when SourceCLI
|
||||
// encounters an unexpected Arg it will immediately return an error. This
|
||||
// function modifies the Component to indicate to Parse that the unexpected Arg,
|
||||
// and all subsequent Args (i.e. the tail), should be set to the returned
|
||||
// []string value.
|
||||
//
|
||||
// The descr (optional) will be appended to the "Usage" line which is printed
|
||||
// with the help document when "-h" is passed in.
|
||||
//
|
||||
// This function panics if not called on a root Component (i.e. a Component
|
||||
// which has no parents).
|
||||
func CLITail(cmp *mcmp.Component, descr string) *[]string {
|
||||
if len(cmp.Path()) != 0 {
|
||||
panic("CLITail can only be used on a root Component")
|
||||
}
|
||||
|
||||
tailPtr := new([]string)
|
||||
cmp.SetValue(cliKeyTail, cliTail{
|
||||
dst: tailPtr,
|
||||
descr: descr,
|
||||
})
|
||||
return tailPtr
|
||||
}
|
||||
|
||||
func populateCLITail(cmp *mcmp.Component, tail []string) bool {
|
||||
ct, ok := cmp.Value(cliKeyTail).(cliTail)
|
||||
if ok {
|
||||
*ct.dst = tail
|
||||
}
|
||||
return ok
|
||||
}
|
||||
|
||||
func getCLITailDescr(cmp *mcmp.Component) string {
|
||||
ct, _ := cmp.Value(cliKeyTail).(cliTail)
|
||||
return ct.descr
|
||||
}
|
||||
|
||||
type subCmd struct {
|
||||
name, descr string
|
||||
flag *bool
|
||||
callback func(*mcmp.Component)
|
||||
}
|
||||
|
||||
// CLISubCommand establishes a sub-command which can be activated on the
|
||||
// command-line. When a sub-command is given on the command-line, the bool
|
||||
// returned for that sub-command will be set to true.
|
||||
//
|
||||
// Additionally, the Component which was passed into Parse (i.e. the one passed
|
||||
// into Populate) will be passed into the given callback, and can be modified
|
||||
// for subsequent parsing. This allows for setting sub-command specific Params,
|
||||
// sub-command specific runtime behavior (via mrun.WithStartHook), support for
|
||||
// sub-sub-commands, and more. The callback may be nil.
|
||||
//
|
||||
// If any sub-commands have been defined on a Component which is passed into
|
||||
// Parse, it is assumed that a sub-command is required on the command-line.
|
||||
//
|
||||
// When parsing the command-line options, it is assumed that sub-commands will
|
||||
// be found before any other options.
|
||||
//
|
||||
// This function panics if not called on a root Component (i.e. a Component
|
||||
// which has no parents).
|
||||
func CLISubCommand(cmp *mcmp.Component, name, descr string, callback func(*mcmp.Component)) *bool {
|
||||
if len(cmp.Path()) != 0 {
|
||||
panic("CLISubCommand can only be used on a root Component")
|
||||
}
|
||||
|
||||
m, _ := cmp.Value(cliKeySubCmdM).(map[string]subCmd)
|
||||
if m == nil {
|
||||
m = map[string]subCmd{}
|
||||
cmp.SetValue(cliKeySubCmdM, m)
|
||||
}
|
||||
|
||||
flag := new(bool)
|
||||
m[name] = subCmd{
|
||||
name: name,
|
||||
descr: descr,
|
||||
flag: flag,
|
||||
callback: callback,
|
||||
}
|
||||
return flag
|
||||
}
|
||||
|
||||
// SourceCLI is a Source which will parse configuration from the CLI.
|
||||
//
|
||||
// Possible CLI options are generated by joining a Param's Path and Name with
|
||||
// dashes. For example:
|
||||
//
|
||||
// cmp := new(mcmp.Component)
|
||||
// cmpFoo = cmp.Child("foo")
|
||||
// cmpFooBar = foo.Child("bar")
|
||||
// addr := mcfg.String(cmpFooBar, "addr", "", "Some address")
|
||||
// // the CLI option to fill addr will be "--foo-bar-addr"
|
||||
//
|
||||
// If the "-h" option is seen then a help page will be printed to 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, or with
|
||||
// no value at all. For example: `--boolean-flag`, `--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`.
|
||||
type SourceCLI struct {
|
||||
Args []string // if nil then os.Args[1:] is used
|
||||
|
||||
DisableHelpPage bool
|
||||
}
|
||||
|
||||
var _ Source = new(SourceCLI)
|
||||
|
||||
const (
|
||||
cliKeyJoin = "-"
|
||||
cliKeyPrefix = "--"
|
||||
cliValSep = "="
|
||||
cliHelpArg = "-h"
|
||||
)
|
||||
|
||||
// Parse implements the method for the Source interface.
|
||||
func (cli *SourceCLI) Parse(cmp *mcmp.Component) ([]ParamValue, error) {
|
||||
args := cli.Args
|
||||
if cli.Args == nil {
|
||||
args = os.Args[1:]
|
||||
}
|
||||
return cli.parse(cmp, nil, args)
|
||||
}
|
||||
|
||||
func (cli *SourceCLI) parse(
|
||||
cmp *mcmp.Component,
|
||||
subCmdPrefix, args []string,
|
||||
) (
|
||||
[]ParamValue,
|
||||
error,
|
||||
) {
|
||||
pM, err := cli.cliParams(CollectParams(cmp))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
printHelpAndExit := func() {
|
||||
// TODO check DisableHelpPage here?
|
||||
cli.printHelp(cmp, os.Stderr, subCmdPrefix, pM)
|
||||
os.Stderr.Sync()
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// if sub-commands were defined on this Component then handle that first.
|
||||
// One of them should have been given, in which case send the Context
|
||||
// through the callback to obtain a new one (which presumably has further
|
||||
// config options the previous didn't) and call parse again.
|
||||
subCmdM, _ := cmp.Value(cliKeySubCmdM).(map[string]subCmd)
|
||||
if len(subCmdM) > 0 {
|
||||
subCmd, args, ok := cli.getSubCmd(subCmdM, args)
|
||||
if !ok {
|
||||
printHelpAndExit()
|
||||
}
|
||||
cmp.SetValue(cliKeySubCmdM, nil)
|
||||
if subCmd.callback != nil {
|
||||
subCmd.callback(cmp)
|
||||
}
|
||||
subCmdPrefix = append(subCmdPrefix, subCmd.name)
|
||||
*subCmd.flag = true
|
||||
return cli.parse(cmp, subCmdPrefix, args)
|
||||
}
|
||||
|
||||
// if sub-commands were not set, then proceed with normal command-line arg
|
||||
// processing.
|
||||
pvs := make([]ParamValue, 0, len(args))
|
||||
var (
|
||||
key string
|
||||
p Param
|
||||
pOk bool
|
||||
pvStrVal string
|
||||
pvStrValOk bool
|
||||
)
|
||||
for i, arg := range args {
|
||||
if pOk {
|
||||
pvStrVal = arg
|
||||
pvStrValOk = true
|
||||
} else if !cli.DisableHelpPage && arg == cliHelpArg {
|
||||
printHelpAndExit()
|
||||
} else {
|
||||
for key, p = range pM {
|
||||
if arg == key {
|
||||
pOk = true
|
||||
break
|
||||
}
|
||||
|
||||
prefix := key + cliValSep
|
||||
if !strings.HasPrefix(arg, prefix) {
|
||||
continue
|
||||
}
|
||||
pOk = true
|
||||
pvStrVal = strings.TrimPrefix(arg, prefix)
|
||||
pvStrValOk = true
|
||||
break
|
||||
}
|
||||
if !pOk {
|
||||
if ok := populateCLITail(cmp, args[i:]); ok {
|
||||
return pvs, nil
|
||||
}
|
||||
return nil, merr.New("unexpected config parameter",
|
||||
mctx.Annotated("param", arg))
|
||||
}
|
||||
}
|
||||
|
||||
// pOk is always true at this point, and so p is filled in
|
||||
|
||||
// As a special case for CLI, if a boolean has no value set it means it
|
||||
// is true.
|
||||
if p.IsBool && !pvStrValOk {
|
||||
pvStrVal = "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
|
||||
}
|
||||
|
||||
pvs = append(pvs, ParamValue{
|
||||
Name: p.Name,
|
||||
Path: p.Component.Path(),
|
||||
Value: p.fuzzyParse(pvStrVal),
|
||||
})
|
||||
|
||||
key = ""
|
||||
p = Param{}
|
||||
pOk = false
|
||||
pvStrVal = ""
|
||||
pvStrValOk = false
|
||||
}
|
||||
if pOk && !pvStrValOk {
|
||||
ctx := mctx.Annotate(p.Component.Context(), "param", key)
|
||||
return nil, merr.New("param expected a value", ctx)
|
||||
}
|
||||
|
||||
return pvs, nil
|
||||
}
|
||||
|
||||
func (cli *SourceCLI) getSubCmd(subCmdM map[string]subCmd, args []string) (subCmd, []string, bool) {
|
||||
if len(args) == 0 {
|
||||
return subCmd{}, args, false
|
||||
}
|
||||
|
||||
s, ok := subCmdM[args[0]]
|
||||
if !ok {
|
||||
return subCmd{}, args, false
|
||||
}
|
||||
|
||||
return s, args[1:], true
|
||||
}
|
||||
|
||||
func (cli *SourceCLI) cliParams(params []Param) (map[string]Param, error) {
|
||||
m := map[string]Param{}
|
||||
for _, p := range params {
|
||||
key := strings.Join(append(p.Component.Path(), p.Name), cliKeyJoin)
|
||||
m[cliKeyPrefix+key] = p
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func (cli *SourceCLI) printHelp(
|
||||
cmp *mcmp.Component,
|
||||
w io.Writer,
|
||||
subCmdPrefix []string,
|
||||
pM map[string]Param,
|
||||
) {
|
||||
type pEntry struct {
|
||||
arg string
|
||||
Param
|
||||
}
|
||||
|
||||
pA := make([]pEntry, 0, len(pM))
|
||||
for arg, p := range pM {
|
||||
pA = append(pA, pEntry{arg: arg, Param: p})
|
||||
}
|
||||
|
||||
sort.Slice(pA, func(i, j int) bool {
|
||||
if pA[i].Required != pA[j].Required {
|
||||
return pA[i].Required
|
||||
}
|
||||
return pA[i].arg < pA[j].arg
|
||||
})
|
||||
|
||||
fmtDefaultVal := func(ptr interface{}) string {
|
||||
if ptr == nil {
|
||||
return ""
|
||||
}
|
||||
val := reflect.Indirect(reflect.ValueOf(ptr))
|
||||
zero := reflect.Zero(val.Type())
|
||||
if reflect.DeepEqual(val.Interface(), zero.Interface()) {
|
||||
return ""
|
||||
} else if val.Type().Kind() == reflect.String {
|
||||
return fmt.Sprintf("%q", val.Interface())
|
||||
}
|
||||
return fmt.Sprint(val.Interface())
|
||||
}
|
||||
|
||||
type subCmdEntry struct {
|
||||
name string
|
||||
subCmd
|
||||
}
|
||||
|
||||
subCmdM, _ := cmp.Value(cliKeySubCmdM).(map[string]subCmd)
|
||||
subCmdA := make([]subCmdEntry, 0, len(subCmdM))
|
||||
for name, subCmd := range subCmdM {
|
||||
subCmdA = append(subCmdA, subCmdEntry{name: name, subCmd: subCmd})
|
||||
}
|
||||
|
||||
sort.Slice(subCmdA, func(i, j int) bool {
|
||||
return subCmdA[i].name < subCmdA[j].name
|
||||
})
|
||||
|
||||
fmt.Fprintf(w, "Usage: %s", os.Args[0])
|
||||
if len(subCmdPrefix) > 0 {
|
||||
fmt.Fprintf(w, " %s", strings.Join(subCmdPrefix, " "))
|
||||
}
|
||||
if len(subCmdA) > 0 {
|
||||
fmt.Fprint(w, " <sub-command>")
|
||||
}
|
||||
if len(pA) > 0 {
|
||||
fmt.Fprint(w, " [options]")
|
||||
}
|
||||
if descr := getCLITailDescr(cmp); descr != "" {
|
||||
fmt.Fprintf(w, " %s", descr)
|
||||
}
|
||||
fmt.Fprint(w, "\n\n")
|
||||
|
||||
if len(subCmdA) > 0 {
|
||||
fmt.Fprint(w, "Sub-commands:\n\n")
|
||||
for _, subCmd := range subCmdA {
|
||||
fmt.Fprintf(w, "\t%s\t%s\n", subCmd.name, subCmd.descr)
|
||||
}
|
||||
fmt.Fprint(w, "\n")
|
||||
}
|
||||
|
||||
if len(pA) > 0 {
|
||||
fmt.Fprint(w, "Options:\n\n")
|
||||
for _, p := range pA {
|
||||
fmt.Fprintf(w, "\t%s", p.arg)
|
||||
if p.IsBool {
|
||||
fmt.Fprintf(w, " (Flag)")
|
||||
} else if p.Required {
|
||||
fmt.Fprintf(w, " (Required)")
|
||||
} else if defVal := fmtDefaultVal(p.Into); defVal != "" {
|
||||
fmt.Fprintf(w, " (Default: %s)", defVal)
|
||||
}
|
||||
fmt.Fprint(w, "\n")
|
||||
if usage := p.Usage; usage != "" {
|
||||
fmt.Fprintln(w, "\t\t"+usage)
|
||||
}
|
||||
fmt.Fprint(w, "\n")
|
||||
}
|
||||
}
|
||||
}
|
356
mcfg/cli_test.go
356
mcfg/cli_test.go
@ -1,356 +0,0 @@
|
||||
package mcfg
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
. "testing"
|
||||
"time"
|
||||
|
||||
"github.com/mediocregopher/mediocre-go-lib/mcmp"
|
||||
"github.com/mediocregopher/mediocre-go-lib/mrand"
|
||||
"github.com/mediocregopher/mediocre-go-lib/mtest/massert"
|
||||
"github.com/mediocregopher/mediocre-go-lib/mtest/mchk"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestSourceCLIHelp(t *T) {
|
||||
assertHelp := func(cmp *mcmp.Component, subCmdPrefix []string, exp string) {
|
||||
buf := new(bytes.Buffer)
|
||||
src := &SourceCLI{}
|
||||
pM, err := src.cliParams(CollectParams(cmp))
|
||||
require.NoError(t, err)
|
||||
src.printHelp(cmp, buf, subCmdPrefix, pM)
|
||||
|
||||
out := buf.String()
|
||||
ok := regexp.MustCompile(exp).MatchString(out)
|
||||
assert.True(t, ok, "exp:%s (%q)\ngot:%s (%q)", exp, exp, out, out)
|
||||
}
|
||||
|
||||
cmp := new(mcmp.Component)
|
||||
assertHelp(cmp, nil, `^Usage: \S+
|
||||
|
||||
$`)
|
||||
assertHelp(cmp, []string{"foo", "bar"}, `^Usage: \S+ foo bar
|
||||
|
||||
$`)
|
||||
|
||||
Int(cmp, "foo", ParamDefault(5), ParamUsage("Test int param ")) // trailing space should be trimmed
|
||||
Bool(cmp, "bar", ParamUsage("Test bool param."))
|
||||
String(cmp, "baz", ParamDefault("baz"), ParamUsage("Test string param"))
|
||||
String(cmp, "baz2", ParamUsage("Required string param"), ParamRequired())
|
||||
String(cmp, "baz3", ParamRequired())
|
||||
|
||||
assertHelp(cmp, nil, `^Usage: \S+ \[options\]
|
||||
|
||||
Options:
|
||||
|
||||
--baz2 \(Required\)
|
||||
Required string param.
|
||||
|
||||
--baz3 \(Required\)
|
||||
|
||||
--bar \(Flag\)
|
||||
Test bool param.
|
||||
|
||||
--baz \(Default: "baz"\)
|
||||
Test string param.
|
||||
|
||||
--foo \(Default: 5\)
|
||||
Test int param.
|
||||
|
||||
$`)
|
||||
|
||||
assertHelp(cmp, []string{"foo", "bar"}, `^Usage: \S+ foo bar \[options\]
|
||||
|
||||
Options:
|
||||
|
||||
--baz2 \(Required\)
|
||||
Required string param.
|
||||
|
||||
--baz3 \(Required\)
|
||||
|
||||
--bar \(Flag\)
|
||||
Test bool param.
|
||||
|
||||
--baz \(Default: "baz"\)
|
||||
Test string param.
|
||||
|
||||
--foo \(Default: 5\)
|
||||
Test int param.
|
||||
|
||||
$`)
|
||||
|
||||
CLISubCommand(cmp, "first", "First sub-command", nil)
|
||||
CLISubCommand(cmp, "second", "Second sub-command", nil)
|
||||
assertHelp(cmp, []string{"foo", "bar"}, `^Usage: \S+ foo bar <sub-command> \[options\]
|
||||
|
||||
Sub-commands:
|
||||
|
||||
first First sub-command
|
||||
second Second sub-command
|
||||
|
||||
Options:
|
||||
|
||||
--baz2 \(Required\)
|
||||
Required string param.
|
||||
|
||||
--baz3 \(Required\)
|
||||
|
||||
--bar \(Flag\)
|
||||
Test bool param.
|
||||
|
||||
--baz \(Default: "baz"\)
|
||||
Test string param.
|
||||
|
||||
--foo \(Default: 5\)
|
||||
Test int param.
|
||||
|
||||
$`)
|
||||
|
||||
CLITail(cmp, "[arg...]")
|
||||
assertHelp(cmp, nil, `^Usage: \S+ <sub-command> \[options\] \[arg\.\.\.\]
|
||||
|
||||
Sub-commands:
|
||||
|
||||
first First sub-command
|
||||
second Second sub-command
|
||||
|
||||
Options:
|
||||
|
||||
--baz2 \(Required\)
|
||||
Required string param.
|
||||
|
||||
--baz3 \(Required\)
|
||||
|
||||
--bar \(Flag\)
|
||||
Test bool param.
|
||||
|
||||
--baz \(Default: "baz"\)
|
||||
Test string param.
|
||||
|
||||
--foo \(Default: 5\)
|
||||
Test int param.
|
||||
|
||||
$`)
|
||||
}
|
||||
|
||||
func TestSourceCLI(t *T) {
|
||||
type state struct {
|
||||
srcCommonState
|
||||
*SourceCLI
|
||||
}
|
||||
|
||||
type params struct {
|
||||
srcCommonParams
|
||||
nonBoolWEq bool // use equal sign when setting value
|
||||
}
|
||||
|
||||
chk := mchk.Checker{
|
||||
Init: func() mchk.State {
|
||||
var s state
|
||||
s.srcCommonState = newSrcCommonState()
|
||||
s.SourceCLI = &SourceCLI{
|
||||
Args: make([]string, 0, 16),
|
||||
}
|
||||
return s
|
||||
},
|
||||
Next: func(ss mchk.State) mchk.Action {
|
||||
s := ss.(state)
|
||||
var p params
|
||||
p.srcCommonParams = s.srcCommonState.next()
|
||||
// if the param is a bool or unset this won't get used, but w/e
|
||||
p.nonBoolWEq = mrand.Intn(2) == 0
|
||||
return mchk.Action{Params: p}
|
||||
},
|
||||
Apply: func(ss mchk.State, a mchk.Action) (mchk.State, error) {
|
||||
s := ss.(state)
|
||||
p := a.Params.(params)
|
||||
|
||||
s.srcCommonState = s.srcCommonState.applyCmpAndPV(p.srcCommonParams)
|
||||
if !p.unset {
|
||||
arg := cliKeyPrefix
|
||||
if path := p.cmp.Path(); len(path) > 0 {
|
||||
arg += strings.Join(path, cliKeyJoin) + cliKeyJoin
|
||||
}
|
||||
arg += p.name
|
||||
if !p.isBool {
|
||||
if p.nonBoolWEq {
|
||||
arg += "="
|
||||
} else {
|
||||
s.SourceCLI.Args = append(s.SourceCLI.Args, arg)
|
||||
arg = ""
|
||||
}
|
||||
arg += p.nonBoolVal
|
||||
}
|
||||
s.SourceCLI.Args = append(s.SourceCLI.Args, arg)
|
||||
}
|
||||
|
||||
err := s.srcCommonState.assert(s.SourceCLI)
|
||||
return s, err
|
||||
},
|
||||
}
|
||||
|
||||
if err := chk.RunFor(2 * time.Second); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCLITail(t *T) {
|
||||
cmp := new(mcmp.Component)
|
||||
Int(cmp, "foo", ParamDefault(5))
|
||||
Bool(cmp, "bar")
|
||||
|
||||
type testCase struct {
|
||||
args []string
|
||||
expTail []string
|
||||
}
|
||||
|
||||
cases := []testCase{
|
||||
{
|
||||
args: []string{"--foo", "5"},
|
||||
expTail: nil,
|
||||
},
|
||||
{
|
||||
args: []string{"--foo", "5", "a", "b", "c"},
|
||||
expTail: []string{"a", "b", "c"},
|
||||
},
|
||||
{
|
||||
args: []string{"--foo=5", "a", "b", "c"},
|
||||
expTail: []string{"a", "b", "c"},
|
||||
},
|
||||
{
|
||||
args: []string{"--foo", "5", "--bar"},
|
||||
expTail: nil,
|
||||
},
|
||||
{
|
||||
args: []string{"--foo", "5", "--bar", "a", "b", "c"},
|
||||
expTail: []string{"a", "b", "c"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
tail := CLITail(cmp, "foo")
|
||||
err := Populate(cmp, &SourceCLI{Args: tc.args})
|
||||
massert.Require(t, massert.Comment(massert.All(
|
||||
massert.Nil(err),
|
||||
massert.Equal(tc.expTail, *tail),
|
||||
), "tc: %#v", tc))
|
||||
}
|
||||
}
|
||||
|
||||
func ExampleCLITail() {
|
||||
cmp := new(mcmp.Component)
|
||||
foo := Int(cmp, "foo", ParamDefault(1), ParamUsage("Description of foo."))
|
||||
tail := CLITail(cmp, "[arg...]")
|
||||
bar := String(cmp, "bar", ParamDefault("defaultVal"), ParamUsage("Description of bar."))
|
||||
|
||||
err := Populate(cmp, &SourceCLI{
|
||||
Args: []string{"--foo=100", "arg1", "arg2", "arg3"},
|
||||
})
|
||||
|
||||
fmt.Printf("err:%v foo:%v bar:%v tail:%#v\n", err, *foo, *bar, *tail)
|
||||
// Output: err:<nil> foo:100 bar:defaultVal tail:[]string{"arg1", "arg2", "arg3"}
|
||||
}
|
||||
|
||||
func TestCLISubCommand(t *T) {
|
||||
var (
|
||||
cmp *mcmp.Component
|
||||
foo *int
|
||||
bar *int
|
||||
baz *int
|
||||
aFlag *bool
|
||||
bFlag *bool
|
||||
)
|
||||
reset := func() {
|
||||
foo, bar, baz, aFlag, bFlag = nil, nil, nil, nil, nil
|
||||
cmp = new(mcmp.Component)
|
||||
foo = Int(cmp, "foo")
|
||||
aFlag = CLISubCommand(cmp, "a", "Description of a.",
|
||||
func(cmp *mcmp.Component) {
|
||||
bar = Int(cmp, "bar")
|
||||
})
|
||||
bFlag = CLISubCommand(cmp, "b", "Description of b.",
|
||||
func(cmp *mcmp.Component) {
|
||||
baz = Int(cmp, "baz")
|
||||
})
|
||||
}
|
||||
|
||||
reset()
|
||||
err := Populate(cmp, &SourceCLI{
|
||||
Args: []string{"a", "--foo=1", "--bar=2"},
|
||||
})
|
||||
massert.Require(t,
|
||||
massert.Comment(massert.Nil(err), "%v", err),
|
||||
massert.Equal(1, *foo),
|
||||
massert.Equal(2, *bar),
|
||||
massert.Nil(baz),
|
||||
massert.Equal(true, *aFlag),
|
||||
massert.Equal(false, *bFlag),
|
||||
)
|
||||
|
||||
reset()
|
||||
err = Populate(cmp, &SourceCLI{
|
||||
Args: []string{"b", "--foo=1", "--baz=3"},
|
||||
})
|
||||
massert.Require(t,
|
||||
massert.Comment(massert.Nil(err), "%v", err),
|
||||
massert.Equal(1, *foo),
|
||||
massert.Nil(bar),
|
||||
massert.Equal(3, *baz),
|
||||
massert.Equal(false, *aFlag),
|
||||
massert.Equal(true, *bFlag),
|
||||
)
|
||||
}
|
||||
|
||||
func ExampleCLISubCommand() {
|
||||
var (
|
||||
cmp *mcmp.Component
|
||||
foo, bar, baz *int
|
||||
aFlag, bFlag *bool
|
||||
)
|
||||
|
||||
// resetExample re-initializes all variables used in this example. We'll
|
||||
// call it multiple times to show different behaviors depending on what
|
||||
// arguments are passed in.
|
||||
resetExample := func() {
|
||||
// Create a new Component with a parameter "foo", which can be used across
|
||||
// all sub-commands.
|
||||
cmp = new(mcmp.Component)
|
||||
foo = Int(cmp, "foo")
|
||||
|
||||
// Create a sub-command "a", which has a parameter "bar" specific to it.
|
||||
aFlag = CLISubCommand(cmp, "a", "Description of a.",
|
||||
func(cmp *mcmp.Component) {
|
||||
bar = Int(cmp, "bar")
|
||||
})
|
||||
|
||||
// Create a sub-command "b", which has a parameter "baz" specific to it.
|
||||
bFlag = CLISubCommand(cmp, "b", "Description of b.",
|
||||
func(cmp *mcmp.Component) {
|
||||
baz = Int(cmp, "baz")
|
||||
})
|
||||
}
|
||||
|
||||
// Use Populate with manually generated CLI arguments, calling the "a"
|
||||
// sub-command.
|
||||
resetExample()
|
||||
args := []string{"a", "--foo=1", "--bar=2"}
|
||||
if err := Populate(cmp, &SourceCLI{Args: args}); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
fmt.Printf("foo:%d bar:%d aFlag:%v bFlag:%v\n", *foo, *bar, *aFlag, *bFlag)
|
||||
|
||||
// reset for another Populate, this time calling the "b" sub-command.
|
||||
resetExample()
|
||||
args = []string{"b", "--foo=1", "--baz=3"}
|
||||
if err := Populate(cmp, &SourceCLI{Args: args}); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
fmt.Printf("foo:%d baz:%d aFlag:%v bFlag:%v\n", *foo, *baz, *aFlag, *bFlag)
|
||||
|
||||
// Output: foo:1 bar:2 aFlag:true bFlag:false
|
||||
// foo:1 baz:3 aFlag:false bFlag:true
|
||||
}
|
79
mcfg/env.go
79
mcfg/env.go
@ -1,79 +0,0 @@
|
||||
package mcfg
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/mediocregopher/mediocre-go-lib/mcmp"
|
||||
"github.com/mediocregopher/mediocre-go-lib/mctx"
|
||||
"github.com/mediocregopher/mediocre-go-lib/merr"
|
||||
)
|
||||
|
||||
// SourceEnv is a Source which will parse configuration from the process
|
||||
// environment.
|
||||
//
|
||||
// Possible Env options are generated by joining a Param's Path and Name with
|
||||
// underscores and making all characters uppercase, as well as changing all
|
||||
// dashes to underscores.
|
||||
//
|
||||
// cmp := new(mcmp.Component)
|
||||
// cmpFoo := cmp.Child("foo")
|
||||
// cmpFooBar := cmp.Child("bar")
|
||||
// addr := mcfg.String(cmpFooBar, "srv-addr", "", "Some address")
|
||||
// // the Env option to fill addr will be "FOO_BAR_SRV_ADDR"
|
||||
//
|
||||
type SourceEnv struct {
|
||||
// In the format key=value. Defaults to os.Environ() if nil.
|
||||
Env []string
|
||||
|
||||
// If set then all expected Env options must be prefixed with this string,
|
||||
// which will be uppercased and have dashes replaced with underscores like
|
||||
// all the other parts of the option names.
|
||||
Prefix string
|
||||
}
|
||||
|
||||
var _ Source = new(SourceEnv)
|
||||
|
||||
func (env *SourceEnv) expectedName(path []string, name string) string {
|
||||
out := strings.Join(append(path, name), "_")
|
||||
if env.Prefix != "" {
|
||||
out = env.Prefix + "_" + out
|
||||
}
|
||||
out = strings.Replace(out, "-", "_", -1)
|
||||
out = strings.ToUpper(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// Parse implements the method for the Source interface.
|
||||
func (env *SourceEnv) Parse(cmp *mcmp.Component) ([]ParamValue, error) {
|
||||
kvs := env.Env
|
||||
if kvs == nil {
|
||||
kvs = os.Environ()
|
||||
}
|
||||
|
||||
params := CollectParams(cmp)
|
||||
pM := map[string]Param{}
|
||||
for _, p := range params {
|
||||
name := env.expectedName(p.Component.Path(), p.Name)
|
||||
pM[name] = p
|
||||
}
|
||||
|
||||
pvs := make([]ParamValue, 0, len(kvs))
|
||||
for _, kv := range kvs {
|
||||
split := strings.SplitN(kv, "=", 2)
|
||||
if len(split) != 2 {
|
||||
return nil, merr.New("malformed environment key/value pair",
|
||||
mctx.Annotated("kv", kv))
|
||||
}
|
||||
k, v := split[0], split[1]
|
||||
if p, ok := pM[k]; ok {
|
||||
pvs = append(pvs, ParamValue{
|
||||
Name: p.Name,
|
||||
Path: p.Component.Path(),
|
||||
Value: p.fuzzyParse(v),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return pvs, nil
|
||||
}
|
@ -1,60 +0,0 @@
|
||||
package mcfg
|
||||
|
||||
import (
|
||||
"strings"
|
||||
. "testing"
|
||||
"time"
|
||||
|
||||
"github.com/mediocregopher/mediocre-go-lib/mtest/mchk"
|
||||
)
|
||||
|
||||
func TestSourceEnv(t *T) {
|
||||
type state struct {
|
||||
srcCommonState
|
||||
*SourceEnv
|
||||
}
|
||||
|
||||
type params struct {
|
||||
srcCommonParams
|
||||
}
|
||||
|
||||
chk := mchk.Checker{
|
||||
Init: func() mchk.State {
|
||||
var s state
|
||||
s.srcCommonState = newSrcCommonState()
|
||||
s.SourceEnv = &SourceEnv{
|
||||
Env: make([]string, 0, 16),
|
||||
}
|
||||
return s
|
||||
},
|
||||
Next: func(ss mchk.State) mchk.Action {
|
||||
s := ss.(state)
|
||||
var p params
|
||||
p.srcCommonParams = s.srcCommonState.next()
|
||||
return mchk.Action{Params: p}
|
||||
},
|
||||
Apply: func(ss mchk.State, a mchk.Action) (mchk.State, error) {
|
||||
s := ss.(state)
|
||||
p := a.Params.(params)
|
||||
s.srcCommonState = s.srcCommonState.applyCmpAndPV(p.srcCommonParams)
|
||||
if !p.unset {
|
||||
kv := strings.Join(append(p.cmp.Path(), p.name), "_")
|
||||
kv = strings.Replace(kv, "-", "_", -1)
|
||||
kv = strings.ToUpper(kv)
|
||||
kv += "="
|
||||
if p.isBool {
|
||||
kv += "1"
|
||||
} else {
|
||||
kv += p.nonBoolVal
|
||||
}
|
||||
s.SourceEnv.Env = append(s.SourceEnv.Env, kv)
|
||||
}
|
||||
err := s.srcCommonState.assert(s.SourceEnv)
|
||||
return s, err
|
||||
},
|
||||
}
|
||||
|
||||
if err := chk.RunFor(2 * time.Second); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
150
mcfg/mcfg.go
150
mcfg/mcfg.go
@ -1,150 +0,0 @@
|
||||
// Package mcfg implements the creation of different types of configuration
|
||||
// parameters and various methods of filling those parameters from external
|
||||
// configuration sources (e.g. the command line and environment variables).
|
||||
//
|
||||
// Parameters are registered onto a Component, and that same Component (or one
|
||||
// of its ancestors) is used later to collect and fill those parameters.
|
||||
package mcfg
|
||||
|
||||
import (
|
||||
"crypto/md5"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"sort"
|
||||
|
||||
"github.com/mediocregopher/mediocre-go-lib/mcmp"
|
||||
"github.com/mediocregopher/mediocre-go-lib/mctx"
|
||||
"github.com/mediocregopher/mediocre-go-lib/merr"
|
||||
)
|
||||
|
||||
// TODO Sources:
|
||||
// - JSON file
|
||||
// - YAML file
|
||||
|
||||
// TODO WithCLISubCommand does not play nice with the expected use-case of
|
||||
// having CLI params overwrite Env ones. If Env is specified first in the
|
||||
// Sources slice then it won't know about any extra Params which might get added
|
||||
// due to a sub-command, but if it's specified second then Env values will
|
||||
// overwrite CLI ones.
|
||||
|
||||
func sortParams(params []Param) {
|
||||
sort.Slice(params, func(i, j int) bool {
|
||||
a, b := params[i], params[j]
|
||||
aPath, bPath := a.Component.Path(), b.Component.Path()
|
||||
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:]
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// CollectParams gathers all Params by recursively retrieving them from the
|
||||
// given Component and its children. Returned Params are sorted according to
|
||||
// their Path and Name.
|
||||
func CollectParams(cmp *mcmp.Component) []Param {
|
||||
var params []Param
|
||||
|
||||
var visit func(*mcmp.Component)
|
||||
visit = func(cmp *mcmp.Component) {
|
||||
for _, param := range getLocalParams(cmp) {
|
||||
params = append(params, param)
|
||||
}
|
||||
|
||||
for _, childCmp := range cmp.Children() {
|
||||
visit(childCmp)
|
||||
}
|
||||
}
|
||||
visit(cmp)
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
// Populate uses the Source to populate the values of all Params which were
|
||||
// added to the given Component, and all of its children. Populate may be called
|
||||
// multiple times with the same Component, 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 parameters are required this will error.
|
||||
//
|
||||
// Populating Params can affect the Component itself, for example in the case of
|
||||
// sub-commands.
|
||||
func Populate(cmp *mcmp.Component, src Source) error {
|
||||
if src == nil {
|
||||
src = ParamValues(nil)
|
||||
}
|
||||
|
||||
pvs, err := src.Parse(cmp)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// map Params to their hash, so we can match them to their ParamValues.
|
||||
// later. There should not be any duplicates here.
|
||||
params := CollectParams(cmp)
|
||||
pM := map[string]Param{}
|
||||
for _, p := range params {
|
||||
path := p.Component.Path()
|
||||
hash := paramHash(path, p.Name)
|
||||
if _, ok := pM[hash]; ok {
|
||||
panic("duplicate Param: " + paramFullName(path, p.Name))
|
||||
}
|
||||
pM[hash] = p
|
||||
}
|
||||
|
||||
// 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 {
|
||||
ctx := mctx.Annotate(p.Component.Context(),
|
||||
"param", paramFullName(p.Component.Path(), p.Name))
|
||||
return merr.New("required parameter is not set", ctx)
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
@ -1,69 +0,0 @@
|
||||
package mcfg
|
||||
|
||||
import (
|
||||
. "testing"
|
||||
|
||||
"github.com/mediocregopher/mediocre-go-lib/mcmp"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestPopulate(t *T) {
|
||||
{
|
||||
cmp := new(mcmp.Component)
|
||||
a := Int(cmp, "a")
|
||||
cmpFoo := cmp.Child("foo")
|
||||
b := Int(cmpFoo, "b")
|
||||
c := Int(cmpFoo, "c")
|
||||
d := Int(cmp, "d", ParamDefault(4))
|
||||
|
||||
err := Populate(cmp, &SourceCLI{
|
||||
Args: []string{"--a=1", "--foo-b=2"},
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 1, *a)
|
||||
assert.Equal(t, 2, *b)
|
||||
assert.Equal(t, 0, *c)
|
||||
assert.Equal(t, 4, *d)
|
||||
}
|
||||
|
||||
{ // test that required params are enforced
|
||||
cmp := new(mcmp.Component)
|
||||
a := Int(cmp, "a")
|
||||
cmpFoo := cmp.Child("foo")
|
||||
b := Int(cmpFoo, "b")
|
||||
c := Int(cmpFoo, "c", ParamRequired())
|
||||
|
||||
err := Populate(cmp, &SourceCLI{
|
||||
Args: []string{"--a=1", "--foo-b=2"},
|
||||
})
|
||||
assert.Error(t, err)
|
||||
|
||||
err = Populate(cmp, &SourceCLI{
|
||||
Args: []string{"--a=1", "--foo-b=2", "--foo-c=3"},
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 1, *a)
|
||||
assert.Equal(t, 2, *b)
|
||||
assert.Equal(t, 3, *c)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParamDefaultOrRequired(t *T) {
|
||||
{
|
||||
cmp := new(mcmp.Component)
|
||||
Int(cmp, "a", ParamDefaultOrRequired(0))
|
||||
params := CollectParams(cmp)
|
||||
assert.Equal(t, "a", params[0].Name)
|
||||
assert.Equal(t, true, params[0].Required)
|
||||
assert.Equal(t, new(int), params[0].Into)
|
||||
}
|
||||
{
|
||||
cmp := new(mcmp.Component)
|
||||
Int(cmp, "a", ParamDefaultOrRequired(1))
|
||||
i := 1
|
||||
params := CollectParams(cmp)
|
||||
assert.Equal(t, "a", params[0].Name)
|
||||
assert.Equal(t, false, params[0].Required)
|
||||
assert.Equal(t, &i, params[0].Into)
|
||||
}
|
||||
}
|
237
mcfg/param.go
237
mcfg/param.go
@ -1,237 +0,0 @@
|
||||
package mcfg
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strings"
|
||||
|
||||
"github.com/mediocregopher/mediocre-go-lib/mcmp"
|
||||
"github.com/mediocregopher/mediocre-go-lib/mtime"
|
||||
)
|
||||
|
||||
// Param is a configuration parameter which can be populated by Populate. The
|
||||
// Param will exist as part of a Component. For example, a Param with name
|
||||
// "addr" under a Component with path of []string{"foo","bar"} will be setable
|
||||
// on the CLI via "--foo-bar-addr". Other configuration Sources may treat the
|
||||
// path/name differently, however.
|
||||
//
|
||||
// Param values are always unmarshaled as JSON values into the Into field of the
|
||||
// Param, regardless of the actual Source.
|
||||
type Param struct {
|
||||
// How the parameter will be identified within a Component.
|
||||
Name string
|
||||
|
||||
// A helpful description of how a parameter is expected to be used.
|
||||
Usage string
|
||||
|
||||
// If the parameter's value is expected to be read as a go string. This is
|
||||
// used for configuration sources like CLI which will automatically add
|
||||
// double-quotes around the value if they aren't already there.
|
||||
IsString bool
|
||||
|
||||
// If the parameter's value is expected to be a boolean. This is used for
|
||||
// configuration sources like CLI which treat boolean parameters (aka flags)
|
||||
// differently.
|
||||
IsBool bool
|
||||
|
||||
// If true then the parameter _must_ be set by at least one Source.
|
||||
Required bool
|
||||
|
||||
// The pointer/interface into which the configuration value will be
|
||||
// json.Unmarshal'd. The value being pointed to also determines the default
|
||||
// value of the parameter.
|
||||
Into interface{}
|
||||
|
||||
// The Component this Param was added to. NOTE that this will be
|
||||
// automatically filled in by AddParam when the Param is added to the
|
||||
// Component.
|
||||
Component *mcmp.Component
|
||||
}
|
||||
|
||||
// ParamOption is a modifier which can be passed into most Param-generating
|
||||
// functions (e.g. String, Int, etc...)
|
||||
type ParamOption func(*Param)
|
||||
|
||||
// ParamRequired returns a ParamOption which ensures the parameter is required
|
||||
// to be set by some configuration source. The default value of the parameter
|
||||
// will be ignored.
|
||||
func ParamRequired() ParamOption {
|
||||
return func(param *Param) {
|
||||
param.Required = true
|
||||
}
|
||||
}
|
||||
|
||||
// ParamDefault returns a ParamOption which ensures the parameter uses the given
|
||||
// default value when no Sources set a value for it. If not given then mcfg will
|
||||
// use the zero value of the Param's type as the default value.
|
||||
//
|
||||
// If ParamRequired is given then this does nothing.
|
||||
func ParamDefault(value interface{}) ParamOption {
|
||||
return func(param *Param) {
|
||||
intoV := reflect.ValueOf(param.Into).Elem()
|
||||
valueV := reflect.ValueOf(value)
|
||||
|
||||
intoType, valueType := intoV.Type(), valueV.Type()
|
||||
if intoType != valueType {
|
||||
panic(fmt.Sprintf("ParamDefault value is type %s, but should be %s", valueType, intoType))
|
||||
} else if !intoV.CanSet() {
|
||||
panic(fmt.Sprintf("Param.Into value %#v can't be set using reflection", param.Into))
|
||||
}
|
||||
|
||||
intoV.Set(valueV)
|
||||
}
|
||||
}
|
||||
|
||||
// ParamDefaultOrRequired returns a ParamOption whose behavior depends on the
|
||||
// given value. If the given value is the zero value for its type, then this returns
|
||||
// ParamRequired(), otherwise this returns ParamDefault(value).
|
||||
func ParamDefaultOrRequired(value interface{}) ParamOption {
|
||||
v := reflect.ValueOf(value)
|
||||
zero := reflect.Zero(v.Type())
|
||||
if v.Interface() == zero.Interface() {
|
||||
return ParamRequired()
|
||||
}
|
||||
return ParamDefault(value)
|
||||
}
|
||||
|
||||
// ParamUsage returns a ParamOption which sets the usage string on the Param.
|
||||
// This is used in some Sources, like SourceCLI, when displaying information
|
||||
// about available parameters.
|
||||
func ParamUsage(usage string) ParamOption {
|
||||
// make all usages end with a period, because I say so
|
||||
usage = strings.TrimSpace(usage)
|
||||
if !strings.HasSuffix(usage, ".") {
|
||||
usage += "."
|
||||
}
|
||||
|
||||
return func(param *Param) {
|
||||
param.Usage = usage
|
||||
}
|
||||
}
|
||||
|
||||
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" {
|
||||
return json.RawMessage("false")
|
||||
}
|
||||
return json.RawMessage("true")
|
||||
|
||||
} else if p.IsString && (v == "" || v[0] != '"') {
|
||||
return json.RawMessage(`"` + v + `"`)
|
||||
}
|
||||
|
||||
return json.RawMessage(v)
|
||||
}
|
||||
|
||||
type cmpParamKey string
|
||||
|
||||
// used in tests
|
||||
func getParam(cmp *mcmp.Component, name string) (Param, bool) {
|
||||
param, ok := cmp.Value(cmpParamKey(name)).(Param)
|
||||
return param, ok
|
||||
}
|
||||
|
||||
// AddParam adds the given Param to the given Component. It will panic if a
|
||||
// Param with the same Name already exists in the Component.
|
||||
func AddParam(cmp *mcmp.Component, param Param, opts ...ParamOption) {
|
||||
param.Name = strings.ToLower(param.Name)
|
||||
param.Component = cmp
|
||||
key := cmpParamKey(param.Name)
|
||||
|
||||
if cmp.HasValue(key) {
|
||||
path := cmp.Path()
|
||||
panic(fmt.Sprintf("Component.Path:%#v Param.Name:%q already exists", path, param.Name))
|
||||
}
|
||||
|
||||
for _, opt := range opts {
|
||||
opt(¶m)
|
||||
}
|
||||
cmp.SetValue(key, param)
|
||||
}
|
||||
|
||||
func getLocalParams(cmp *mcmp.Component) []Param {
|
||||
values := cmp.Values()
|
||||
params := make([]Param, 0, len(values))
|
||||
for _, val := range values {
|
||||
if param, ok := val.(Param); ok {
|
||||
params = append(params, param)
|
||||
}
|
||||
}
|
||||
return params
|
||||
}
|
||||
|
||||
// Int64 returns an *int64 which will be populated once Populate is run on the
|
||||
// Component.
|
||||
func Int64(cmp *mcmp.Component, name string, opts ...ParamOption) *int64 {
|
||||
var i int64
|
||||
AddParam(cmp, Param{Name: name, Into: &i}, opts...)
|
||||
return &i
|
||||
}
|
||||
|
||||
// Int returns an *int which will be populated once Populate is run on the
|
||||
// Component.
|
||||
func Int(cmp *mcmp.Component, name string, opts ...ParamOption) *int {
|
||||
var i int
|
||||
AddParam(cmp, Param{Name: name, Into: &i}, opts...)
|
||||
return &i
|
||||
}
|
||||
|
||||
// Float64 returns a *float64 which will be populated once Populate is run on
|
||||
// the Component
|
||||
func Float64(cmp *mcmp.Component, name string, opts ...ParamOption) *float64 {
|
||||
var f float64
|
||||
AddParam(cmp, Param{Name: name, Into: &f}, opts...)
|
||||
return &f
|
||||
}
|
||||
|
||||
// String returns a *string which will be populated once Populate is run on
|
||||
// the Component.
|
||||
func String(cmp *mcmp.Component, name string, opts ...ParamOption) *string {
|
||||
var s string
|
||||
AddParam(cmp, Param{Name: name, IsString: true, Into: &s}, opts...)
|
||||
return &s
|
||||
}
|
||||
|
||||
// Bool returns a *bool which will be populated once Populate is run on the
|
||||
// Component, 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 Bool(cmp *mcmp.Component, name string, opts ...ParamOption) *bool {
|
||||
var b bool
|
||||
AddParam(cmp, Param{Name: name, IsBool: true, Into: &b}, opts...)
|
||||
return &b
|
||||
}
|
||||
|
||||
// TS returns an *mtime.TS which will be populated once Populate is run on
|
||||
// the Component.
|
||||
func TS(cmp *mcmp.Component, name string, opts ...ParamOption) *mtime.TS {
|
||||
var t mtime.TS
|
||||
AddParam(cmp, Param{Name: name, Into: &t}, opts...)
|
||||
return &t
|
||||
}
|
||||
|
||||
// Duration returns an *mtime.Duration which will be populated once Populate
|
||||
// is run on the Component.
|
||||
func Duration(cmp *mcmp.Component, name string, opts ...ParamOption) *mtime.Duration {
|
||||
var d mtime.Duration
|
||||
AddParam(cmp, Param{Name: name, IsString: true, Into: &d}, opts...)
|
||||
return &d
|
||||
}
|
||||
|
||||
// JSON reads the parameter value as a JSON value and unmarshals it into the
|
||||
// given interface{} (which should be a pointer) once Populate is run on the
|
||||
// Component.
|
||||
//
|
||||
// The receiver (into) is also used to determine the default value. ParamDefault
|
||||
// should not be used as one of the opts.
|
||||
func JSON(cmp *mcmp.Component, name string, into interface{}, opts ...ParamOption) {
|
||||
AddParam(cmp, Param{Name: name, Into: into}, opts...)
|
||||
}
|
@ -1,65 +0,0 @@
|
||||
package mcfg
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"github.com/mediocregopher/mediocre-go-lib/mcmp"
|
||||
)
|
||||
|
||||
// ParamValue describes a value for a parameter which has been parsed by a
|
||||
// Source.
|
||||
type ParamValue struct {
|
||||
Name string
|
||||
Path []string
|
||||
Value json.RawMessage
|
||||
}
|
||||
|
||||
// Source parses ParamValues out of a particular configuration source, given the
|
||||
// Component which the Params were added to (via WithInt, WithString, etc...).
|
||||
// CollectParams can be used to retrieve these Params.
|
||||
//
|
||||
// It's possible for Parsing to affect the Component itself, for example in the
|
||||
// case of sub-commands.
|
||||
//
|
||||
// Source should not return ParamValues which were not explicitly set to a value
|
||||
// by the configuration source.
|
||||
//
|
||||
// The returned []ParamValue may contain duplicates of the same Param's value.
|
||||
// in which case the latter 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(*mcmp.Component) ([]ParamValue, error)
|
||||
}
|
||||
|
||||
// ParamValues is simply a slice of ParamValue elements, which implements Parse
|
||||
// by always returning itself as-is.
|
||||
type ParamValues []ParamValue
|
||||
|
||||
var _ Source = ParamValues{}
|
||||
|
||||
// Parse implements the method for the Source interface.
|
||||
func (pvs ParamValues) Parse(*mcmp.Component) ([]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.
|
||||
type Sources []Source
|
||||
|
||||
var _ Source = Sources{}
|
||||
|
||||
// Parse implements the method for the Source interface.
|
||||
func (ss Sources) Parse(cmp *mcmp.Component) ([]ParamValue, error) {
|
||||
var pvs []ParamValue
|
||||
for _, s := range ss {
|
||||
var innerPVs []ParamValue
|
||||
var err error
|
||||
if innerPVs, err = s.Parse(cmp); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
pvs = append(pvs, innerPVs...)
|
||||
}
|
||||
return pvs, nil
|
||||
}
|
@ -1,178 +0,0 @@
|
||||
package mcfg
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
. "testing"
|
||||
"time"
|
||||
|
||||
"github.com/mediocregopher/mediocre-go-lib/mcmp"
|
||||
"github.com/mediocregopher/mediocre-go-lib/mrand"
|
||||
"github.com/mediocregopher/mediocre-go-lib/mtest/massert"
|
||||
)
|
||||
|
||||
// The tests for the different Sources use mchk as their primary method of
|
||||
// checking. They end up sharing a lot of the same functionality, so in here is
|
||||
// all the code they share
|
||||
|
||||
type srcCommonState struct {
|
||||
// availCmps get updated in place as the run goes on, it's easier to keep
|
||||
// track of them this way than by traversing the hierarchy.
|
||||
availCmps []*mcmp.Component
|
||||
|
||||
expPVs []ParamValue
|
||||
// each specific test should wrap this to add the Source itself
|
||||
}
|
||||
|
||||
func newSrcCommonState() srcCommonState {
|
||||
var scs srcCommonState
|
||||
{
|
||||
root := new(mcmp.Component)
|
||||
a := root.Child("a")
|
||||
b := root.Child("b")
|
||||
c := root.Child("c")
|
||||
ab := a.Child("b")
|
||||
bc := b.Child("c")
|
||||
abc := ab.Child("c")
|
||||
scs.availCmps = []*mcmp.Component{root, a, b, c, ab, bc, abc}
|
||||
}
|
||||
return scs
|
||||
}
|
||||
|
||||
type srcCommonParams struct {
|
||||
name string
|
||||
cmp *mcmp.Component
|
||||
isBool bool
|
||||
nonBoolType string // "int", "str", "duration", "json"
|
||||
unset bool
|
||||
nonBoolVal string
|
||||
}
|
||||
|
||||
func (scs srcCommonState) next() srcCommonParams {
|
||||
var p srcCommonParams
|
||||
if i := mrand.Intn(8); i == 0 {
|
||||
p.name = mrand.Hex(1) + "-" + mrand.Hex(8)
|
||||
} else {
|
||||
p.name = mrand.Hex(8)
|
||||
}
|
||||
|
||||
availCmpI := mrand.Intn(len(scs.availCmps))
|
||||
p.cmp = scs.availCmps[availCmpI]
|
||||
|
||||
p.isBool = mrand.Intn(8) == 0
|
||||
if !p.isBool {
|
||||
p.nonBoolType = mrand.Element([]string{
|
||||
"int",
|
||||
"str",
|
||||
"duration",
|
||||
"json",
|
||||
}, nil).(string)
|
||||
}
|
||||
p.unset = mrand.Intn(10) == 0
|
||||
|
||||
if p.isBool || p.unset {
|
||||
return p
|
||||
}
|
||||
|
||||
switch p.nonBoolType {
|
||||
case "int":
|
||||
p.nonBoolVal = fmt.Sprint(mrand.Int())
|
||||
case "str":
|
||||
p.nonBoolVal = mrand.Hex(16)
|
||||
case "duration":
|
||||
dur := time.Duration(mrand.Intn(86400)) * time.Second
|
||||
p.nonBoolVal = dur.String()
|
||||
case "json":
|
||||
b, _ := json.Marshal(map[string]int{
|
||||
mrand.Hex(4): mrand.Int(),
|
||||
mrand.Hex(4): mrand.Int(),
|
||||
mrand.Hex(4): mrand.Int(),
|
||||
})
|
||||
p.nonBoolVal = string(b)
|
||||
}
|
||||