From dd2d601081b649c942f14fdb1609f2baf66bdc73 Mon Sep 17 00:00:00 2001 From: Brian Picciano Date: Sat, 15 Jun 2019 16:45:53 -0600 Subject: [PATCH] mcfg: refactor so as to use new mcmp.Component type, and use the option pattern to reduce number of arguments to parameters --- mcfg/cli.go | 154 ++++++++++++++------------- mcfg/cli_test.go | 152 +++++++++++++------------- mcfg/env.go | 28 ++--- mcfg/env_test.go | 4 +- mcfg/mcfg.go | 64 +++++------ mcfg/mcfg_test.go | 33 +++--- mcfg/param.go | 252 ++++++++++++++++++++++---------------------- mcfg/source.go | 30 +++--- mcfg/source_test.go | 81 ++++++-------- 9 files changed, 399 insertions(+), 399 deletions(-) diff --git a/mcfg/cli.go b/mcfg/cli.go index 13e8df6..e8c7b4f 100644 --- a/mcfg/cli.go +++ b/mcfg/cli.go @@ -1,7 +1,6 @@ package mcfg import ( - "context" "fmt" "io" "os" @@ -9,6 +8,7 @@ import ( "sort" "strings" + "github.com/mediocregopher/mediocre-go-lib/mcmp" "github.com/mediocregopher/mediocre-go-lib/mctx" "github.com/mediocregopher/mediocre-go-lib/merr" ) @@ -25,64 +25,76 @@ type cliTail struct { descr string } -// WithCLITail returns a Context which modifies the behavior of SourceCLI's -// Parse. Normally when SourceCLI encounters an unexpected Arg it will -// immediately return an error. This function modifies the Context to indicate -// to Parse that the unexpected Arg, and all subsequent Args (i.e. the tail), -// should be set to the returned []string value. +// 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. -func WithCLITail(ctx context.Context, descr string) (context.Context, *[]string) { - if ctx.Value(cliKeyTail) != nil { - panic("WithCLITail already called in this Context") +// +// 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) - ctx = context.WithValue(ctx, cliKeyTail, cliTail{ + cmp.SetValue(cliKeyTail, cliTail{ dst: tailPtr, descr: descr, }) - return ctx, tailPtr + return tailPtr } -func populateCLITail(ctx context.Context, tail []string) bool { - ct, ok := ctx.Value(cliKeyTail).(cliTail) +func populateCLITail(cmp *mcmp.Component, tail []string) bool { + ct, ok := cmp.Value(cliKeyTail).(cliTail) if ok { *ct.dst = tail } return ok } -func getCLITailDescr(ctx context.Context) string { - ct, _ := ctx.Value(cliKeyTail).(cliTail) +func getCLITailDescr(cmp *mcmp.Component) string { + ct, _ := cmp.Value(cliKeyTail).(cliTail) return ct.descr } type subCmd struct { name, descr string flag *bool - callback func(context.Context) context.Context + callback func(*mcmp.Component) } -// WithCLISubCommand establishes a sub-command which can be activated on the +// 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 Context which was passed into Parse (i.e. the one passed -// into Populate) will be passed into the given callback, and the returned one -// used 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. +// 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 Context which is passed into +// 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. // -// Sub-commands must be specified before any other options on the command-line. -func WithCLISubCommand(ctx context.Context, name, descr string, callback func(context.Context) context.Context) (context.Context, *bool) { - m, _ := ctx.Value(cliKeySubCmdM).(map[string]subCmd) +// 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{} - ctx = context.WithValue(ctx, cliKeySubCmdM, m) + cmp.SetValue(cliKeySubCmdM, m) } flag := new(bool) @@ -92,7 +104,7 @@ func WithCLISubCommand(ctx context.Context, name, descr string, callback func(co flag: flag, callback: callback, } - return ctx, flag + return flag } // SourceCLI is a Source which will parse configuration from the CLI. @@ -100,27 +112,30 @@ func WithCLISubCommand(ctx context.Context, name, descr string, callback func(co // Possible CLI options are generated by joining a Param's Path and Name with // dashes. For example: // -// ctx := mctx.New() -// ctx = mctx.ChildOf(ctx, "foo") -// ctx = mctx.ChildOf(ctx, "bar") -// addr := mcfg.String(ctx, "addr", "", "Some address") +// 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. +// 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, 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. +// 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 = "--" @@ -128,51 +143,51 @@ const ( cliHelpArg = "-h" ) -// Parse implements the method for the Source interface -func (cli *SourceCLI) Parse(ctx context.Context) (context.Context, []ParamValue, error) { +// 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(ctx, nil, args) + return cli.parse(cmp, nil, args) } func (cli *SourceCLI) parse( - ctx context.Context, + cmp *mcmp.Component, subCmdPrefix, args []string, ) ( - context.Context, []ParamValue, error, ) { - pM, err := cli.cliParams(CollectParams(ctx)) + pM, err := cli.cliParams(CollectParams(cmp)) if err != nil { - return nil, nil, err + return nil, err } printHelpAndExit := func() { - cli.printHelp(ctx, os.Stderr, subCmdPrefix, pM) + // TODO check DisableHelpPage here? + cli.printHelp(cmp, os.Stderr, subCmdPrefix, pM) os.Stderr.Sync() os.Exit(1) } - // if sub-commands were defined on this Context 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, _ := ctx.Value(cliKeySubCmdM).(map[string]subCmd) + // 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() } - ctx = context.WithValue(ctx, cliKeySubCmdM, nil) + cmp.SetValue(cliKeySubCmdM, nil) if subCmd.callback != nil { - ctx = subCmd.callback(ctx) + subCmd.callback(cmp) } subCmdPrefix = append(subCmdPrefix, subCmd.name) *subCmd.flag = true - return cli.parse(ctx, subCmdPrefix, args) + return cli.parse(cmp, subCmdPrefix, args) } // if sub-commands were not set, then proceed with normal command-line arg @@ -208,11 +223,11 @@ func (cli *SourceCLI) parse( break } if !pOk { - if ok := populateCLITail(ctx, args[i:]); ok { - return ctx, pvs, nil + if ok := populateCLITail(cmp, args[i:]); ok { + return pvs, nil } - ctx := mctx.Annotate(context.Background(), "param", arg) - return nil, nil, merr.New("unexpected config parameter", ctx) + return nil, merr.New("unexpected config parameter", + mctx.Annotated("param", arg)) } } @@ -231,7 +246,7 @@ func (cli *SourceCLI) parse( pvs = append(pvs, ParamValue{ Name: p.Name, - Path: mctx.Path(p.Context), + Path: p.Component.Path(), Value: p.fuzzyParse(pvStrVal), }) @@ -242,11 +257,11 @@ func (cli *SourceCLI) parse( pvStrValOk = false } if pOk && !pvStrValOk { - ctx := mctx.Annotate(p.Context, "param", key) - return nil, nil, merr.New("param expected a value", ctx) + ctx := mctx.Annotate(p.Component.Annotated(), "param", key) + return nil, merr.New("param expected a value", ctx) } - return ctx, pvs, nil + return pvs, nil } func (cli *SourceCLI) getSubCmd(subCmdM map[string]subCmd, args []string) (subCmd, []string, bool) { @@ -265,14 +280,14 @@ func (cli *SourceCLI) getSubCmd(subCmdM map[string]subCmd, args []string) (subCm func (cli *SourceCLI) cliParams(params []Param) (map[string]Param, error) { m := map[string]Param{} for _, p := range params { - key := strings.Join(append(mctx.Path(p.Context), p.Name), cliKeyJoin) + key := strings.Join(append(p.Component.Path(), p.Name), cliKeyJoin) m[cliKeyPrefix+key] = p } return m, nil } func (cli *SourceCLI) printHelp( - ctx context.Context, + cmp *mcmp.Component, w io.Writer, subCmdPrefix []string, pM map[string]Param, @@ -313,7 +328,7 @@ func (cli *SourceCLI) printHelp( subCmd } - subCmdM, _ := ctx.Value(cliKeySubCmdM).(map[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}) @@ -333,7 +348,7 @@ func (cli *SourceCLI) printHelp( if len(pA) > 0 { fmt.Fprint(w, " [options]") } - if descr := getCLITailDescr(ctx); descr != "" { + if descr := getCLITailDescr(cmp); descr != "" { fmt.Fprintf(w, " %s", descr) } fmt.Fprint(w, "\n\n") @@ -359,11 +374,6 @@ func (cli *SourceCLI) printHelp( } fmt.Fprint(w, "\n") if usage := p.Usage; usage != "" { - // make all usages end with a period, because I say so - usage = strings.TrimSpace(usage) - if !strings.HasSuffix(usage, ".") { - usage += "." - } fmt.Fprintln(w, "\t\t"+usage) } fmt.Fprint(w, "\n") diff --git a/mcfg/cli_test.go b/mcfg/cli_test.go index 9381ac3..12818c8 100644 --- a/mcfg/cli_test.go +++ b/mcfg/cli_test.go @@ -2,13 +2,13 @@ package mcfg import ( "bytes" - "context" "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" @@ -17,37 +17,38 @@ import ( ) func TestSourceCLIHelp(t *T) { - assertHelp := func(ctx context.Context, subCmdPrefix []string, exp string) { + assertHelp := func(cmp *mcmp.Component, subCmdPrefix []string, exp string) { buf := new(bytes.Buffer) src := &SourceCLI{} - pM, err := src.cliParams(CollectParams(ctx)) + pM, err := src.cliParams(CollectParams(cmp)) require.NoError(t, err) - src.printHelp(ctx, buf, subCmdPrefix, pM) + 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) } - ctx := context.Background() - assertHelp(ctx, nil, `^Usage: \S+ + cmp := new(mcmp.Component) + assertHelp(cmp, nil, `^Usage: \S+ $`) - assertHelp(ctx, []string{"foo", "bar"}, `^Usage: \S+ foo bar + assertHelp(cmp, []string{"foo", "bar"}, `^Usage: \S+ foo bar $`) - ctx, _ = WithInt(ctx, "foo", 5, "Test int param ") // trailing space should be trimmed - ctx, _ = WithBool(ctx, "bar", "Test bool param.") - ctx, _ = WithString(ctx, "baz", "baz", "Test string param") - ctx, _ = WithRequiredString(ctx, "baz2", "") - ctx, _ = WithRequiredString(ctx, "baz3", "") + 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(ctx, nil, `^Usage: \S+ \[options\] + assertHelp(cmp, nil, `^Usage: \S+ \[options\] Options: --baz2 \(Required\) + Required string param. --baz3 \(Required\) @@ -62,11 +63,12 @@ Options: $`) - assertHelp(ctx, []string{"foo", "bar"}, `^Usage: \S+ foo bar \[options\] + assertHelp(cmp, []string{"foo", "bar"}, `^Usage: \S+ foo bar \[options\] Options: --baz2 \(Required\) + Required string param. --baz3 \(Required\) @@ -81,9 +83,9 @@ Options: $`) - ctx, _ = WithCLISubCommand(ctx, "first", "First sub-command", nil) - ctx, _ = WithCLISubCommand(ctx, "second", "Second sub-command", nil) - assertHelp(ctx, []string{"foo", "bar"}, `^Usage: \S+ foo bar \[options\] + CLISubCommand(cmp, "first", "First sub-command", nil) + CLISubCommand(cmp, "second", "Second sub-command", nil) + assertHelp(cmp, []string{"foo", "bar"}, `^Usage: \S+ foo bar \[options\] Sub-commands: @@ -93,6 +95,7 @@ Sub-commands: Options: --baz2 \(Required\) + Required string param. --baz3 \(Required\) @@ -107,8 +110,8 @@ Options: $`) - ctx, _ = WithCLITail(ctx, "[arg...]") - assertHelp(ctx, nil, `^Usage: \S+ \[options\] \[arg\.\.\.\] + CLITail(cmp, "[arg...]") + assertHelp(cmp, nil, `^Usage: \S+ \[options\] \[arg\.\.\.\] Sub-commands: @@ -118,6 +121,7 @@ Sub-commands: Options: --baz2 \(Required\) + Required string param. --baz3 \(Required\) @@ -165,11 +169,11 @@ func TestSourceCLI(t *T) { s := ss.(state) p := a.Params.(params) - s.srcCommonState = s.srcCommonState.applyCtxAndPV(p.srcCommonParams) + s.srcCommonState = s.srcCommonState.applyCmpAndPV(p.srcCommonParams) if !p.unset { arg := cliKeyPrefix - if len(p.path) > 0 { - arg += strings.Join(p.path, cliKeyJoin) + cliKeyJoin + if path := p.cmp.Path(); len(path) > 0 { + arg += strings.Join(path, cliKeyJoin) + cliKeyJoin } arg += p.name if !p.isBool { @@ -194,10 +198,10 @@ func TestSourceCLI(t *T) { } } -func TestWithCLITail(t *T) { - ctx := context.Background() - ctx, _ = WithInt(ctx, "foo", 5, "") - ctx, _ = WithBool(ctx, "bar", "") +func TestCLITail(t *T) { + cmp := new(mcmp.Component) + Int(cmp, "foo", ParamDefault(5)) + Bool(cmp, "bar") type testCase struct { args []string @@ -228,8 +232,8 @@ func TestWithCLITail(t *T) { } for _, tc := range cases { - ctx, tail := WithCLITail(ctx, "foo") - _, err := Populate(ctx, &SourceCLI{Args: tc.args}) + 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), @@ -237,13 +241,13 @@ func TestWithCLITail(t *T) { } } -func ExampleWithCLITail() { - ctx := context.Background() - ctx, foo := WithInt(ctx, "foo", 1, "Description of foo.") - ctx, tail := WithCLITail(ctx, "[arg...]") - ctx, bar := WithString(ctx, "bar", "defaultVal", "Description of bar.") +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(ctx, &SourceCLI{ + err := Populate(cmp, &SourceCLI{ Args: []string{"--foo=100", "arg1", "arg2", "arg3"}, }) @@ -251,9 +255,9 @@ func ExampleWithCLITail() { // Output: err: foo:100 bar:defaultVal tail:[]string{"arg1", "arg2", "arg3"} } -func TestWithCLISubCommand(t *T) { +func TestCLISubCommand(t *T) { var ( - ctx context.Context + cmp *mcmp.Component foo *int bar *int baz *int @@ -262,22 +266,20 @@ func TestWithCLISubCommand(t *T) { ) reset := func() { foo, bar, baz, aFlag, bFlag = nil, nil, nil, nil, nil - ctx = context.Background() - ctx, foo = WithInt(ctx, "foo", 0, "Description of foo.") - ctx, aFlag = WithCLISubCommand(ctx, "a", "Description of a.", - func(ctx context.Context) context.Context { - ctx, bar = WithInt(ctx, "bar", 0, "Description of bar.") - return ctx + cmp = new(mcmp.Component) + foo = Int(cmp, "foo") + aFlag = CLISubCommand(cmp, "a", "Description of a.", + func(cmp *mcmp.Component) { + bar = Int(cmp, "bar") }) - ctx, bFlag = WithCLISubCommand(ctx, "b", "Description of b.", - func(ctx context.Context) context.Context { - ctx, baz = WithInt(ctx, "baz", 0, "Description of baz.") - return ctx + bFlag = CLISubCommand(cmp, "b", "Description of b.", + func(cmp *mcmp.Component) { + baz = Int(cmp, "baz") }) } reset() - _, err := Populate(ctx, &SourceCLI{ + err := Populate(cmp, &SourceCLI{ Args: []string{"a", "--foo=1", "--bar=2"}, }) massert.Require(t, @@ -290,7 +292,7 @@ func TestWithCLISubCommand(t *T) { ) reset() - _, err = Populate(ctx, &SourceCLI{ + err = Populate(cmp, &SourceCLI{ Args: []string{"b", "--foo=1", "--baz=3"}, }) massert.Require(t, @@ -303,40 +305,48 @@ func TestWithCLISubCommand(t *T) { ) } -func ExampleWithCLISubCommand() { - // Create a new Context with a parameter "foo", which can be used across all - // sub-commands. - ctx := context.Background() - ctx, foo := WithInt(ctx, "foo", 0, "Description of foo.") +func ExampleCLISubCommand() { + var ( + cmp *mcmp.Component + foo, bar, baz *int + aFlag, bFlag *bool + ) - // Create a sub-command "a", which has a parameter "bar" specific to it. - var bar *int - ctx, aFlag := WithCLISubCommand(ctx, "a", "Description of a.", - func(ctx context.Context) context.Context { - ctx, bar = WithInt(ctx, "bar", 0, "Description of bar.") - return ctx - }) + // 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 "b", which has a parameter "baz" specific to it. - var baz *int - ctx, bFlag := WithCLISubCommand(ctx, "b", "Description of b.", - func(ctx context.Context) context.Context { - ctx, baz = WithInt(ctx, "baz", 0, "Description of baz.") - return ctx - }) + // 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(ctx, &SourceCLI{Args: args}); err != nil { + 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 output for another Populate, this time calling the "b" sub-command. - *aFlag = false + // reset for another Populate, this time calling the "b" sub-command. + resetExample() args = []string{"b", "--foo=1", "--baz=3"} - if _, err := Populate(ctx, &SourceCLI{Args: args}); err != nil { + 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) diff --git a/mcfg/env.go b/mcfg/env.go index b94aec6..30b6c3e 100644 --- a/mcfg/env.go +++ b/mcfg/env.go @@ -1,10 +1,10 @@ package mcfg import ( - "context" "os" "strings" + "github.com/mediocregopher/mediocre-go-lib/mcmp" "github.com/mediocregopher/mediocre-go-lib/mctx" "github.com/mediocregopher/mediocre-go-lib/merr" ) @@ -16,10 +16,10 @@ import ( // underscores and making all characters uppercase, as well as changing all // dashes to underscores. // -// ctx := mctx.New() -// ctx = mctx.ChildOf(ctx, "foo") -// ctx = mctx.ChildOf(ctx, "bar") -// addr := mcfg.String(ctx, "srv-addr", "", "Some address") +// 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 { @@ -32,6 +32,8 @@ type SourceEnv struct { Prefix string } +var _ Source = new(SourceEnv) + func (env *SourceEnv) expectedName(path []string, name string) string { out := strings.Join(append(path, name), "_") if env.Prefix != "" { @@ -42,17 +44,17 @@ func (env *SourceEnv) expectedName(path []string, name string) string { return out } -// Parse implements the method for the Source interface -func (env *SourceEnv) Parse(ctx context.Context) (context.Context, []ParamValue, error) { +// 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(ctx) + params := CollectParams(cmp) pM := map[string]Param{} for _, p := range params { - name := env.expectedName(mctx.Path(p.Context), p.Name) + name := env.expectedName(p.Component.Path(), p.Name) pM[name] = p } @@ -60,18 +62,18 @@ func (env *SourceEnv) Parse(ctx context.Context) (context.Context, []ParamValue, for _, kv := range kvs { split := strings.SplitN(kv, "=", 2) if len(split) != 2 { - ctx := mctx.Annotate(context.Background(), "kv", kv) - return nil, nil, merr.New("malformed environment key/value pair", ctx) + 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: mctx.Path(p.Context), + Path: p.Component.Path(), Value: p.fuzzyParse(v), }) } } - return ctx, pvs, nil + return pvs, nil } diff --git a/mcfg/env_test.go b/mcfg/env_test.go index 17f1d49..9373b86 100644 --- a/mcfg/env_test.go +++ b/mcfg/env_test.go @@ -36,9 +36,9 @@ func TestSourceEnv(t *T) { Apply: func(ss mchk.State, a mchk.Action) (mchk.State, error) { s := ss.(state) p := a.Params.(params) - s.srcCommonState = s.srcCommonState.applyCtxAndPV(p.srcCommonParams) + s.srcCommonState = s.srcCommonState.applyCmpAndPV(p.srcCommonParams) if !p.unset { - kv := strings.Join(append(p.path, p.name), "_") + kv := strings.Join(append(p.cmp.Path(), p.name), "_") kv = strings.Replace(kv, "-", "_", -1) kv = strings.ToUpper(kv) kv += "=" diff --git a/mcfg/mcfg.go b/mcfg/mcfg.go index ff742cb..f834cb0 100644 --- a/mcfg/mcfg.go +++ b/mcfg/mcfg.go @@ -2,21 +2,18 @@ // 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 context, and that same context is used later -// to collect and fulfill those parameters. When used with the mctx package's -// child/parent context functionality, the path of a context is incorporated -// into the parameter's full name in order to namespace parameters which exist -// in different contexts. +// 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 ( - "context" "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" ) @@ -34,7 +31,7 @@ import ( 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) + aPath, bPath := a.Component.Path(), b.Component.Path() for { switch { case len(aPath) == 0 && len(bPath) == 0: @@ -52,23 +49,23 @@ func sortParams(params []Param) { }) } -// CollectParams gathers all Params 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 { +// 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(context.Context) - visit = func(ctx context.Context) { - for _, param := range getLocalParams(ctx) { + var visit func(*mcmp.Component) + visit = func(cmp *mcmp.Component) { + for _, param := range getLocalParams(cmp) { params = append(params, param) } - for _, childCtx := range mctx.Children(ctx) { - visit(childCtx) + for _, childCmp := range cmp.Children() { + visit(childCmp) } } - visit(ctx) + visit(cmp) sortParams(params) return params @@ -86,34 +83,31 @@ func paramHash(path []string, name string) string { } // 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 +// 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. // -// Populating Params can affect the Context itself, for example in the case of -// sub-commands. For this reason Populate will return a Context instance which -// may have been affected by the Params (or, if not, will be the same one which -// was passed in). -// // 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. -func Populate(ctx context.Context, src Source) (context.Context, 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) } - ogCtx := ctx - ctx, pvs, err := src.Parse(ctx) + pvs, err := src.Parse(cmp) if err != nil { - return ogCtx, err + 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(ctx) + params := CollectParams(cmp) pM := map[string]Param{} for _, p := range params { - path := mctx.Path(p.Context) + path := p.Component.Path() hash := paramHash(path, p.Name) if _, ok := pM[hash]; ok { panic("duplicate Param: " + paramFullName(path, p.Name)) @@ -137,9 +131,9 @@ func Populate(ctx context.Context, src Source) (context.Context, error) { if !p.Required { continue } else if _, ok := pvM[hash]; !ok { - ctx := mctx.Annotate(p.Context, - "param", paramFullName(mctx.Path(p.Context), p.Name)) - return ogCtx, merr.New("required parameter is not set", ctx) + ctx := mctx.Annotate(p.Component.Annotated(), + "param", paramFullName(p.Component.Path(), p.Name)) + return merr.New("required parameter is not set", ctx) } } @@ -148,9 +142,9 @@ func Populate(ctx context.Context, src Source) (context.Context, error) { // 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 ogCtx, err + return err } } - return ctx, nil + return nil } diff --git a/mcfg/mcfg_test.go b/mcfg/mcfg_test.go index 583c756..bbc7470 100644 --- a/mcfg/mcfg_test.go +++ b/mcfg/mcfg_test.go @@ -1,45 +1,44 @@ package mcfg import ( - "context" . "testing" - "github.com/mediocregopher/mediocre-go-lib/mctx" + "github.com/mediocregopher/mediocre-go-lib/mcmp" "github.com/stretchr/testify/assert" ) func TestPopulate(t *T) { { - ctx := context.Background() - ctx, a := WithInt(ctx, "a", 0, "") - ctxChild := mctx.NewChild(ctx, "foo") - ctxChild, b := WithInt(ctxChild, "b", 0, "") - ctxChild, c := WithInt(ctxChild, "c", 0, "") - ctx = mctx.WithChild(ctx, ctxChild) + 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(ctx, &SourceCLI{ + 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 - ctx := context.Background() - ctx, a := WithInt(ctx, "a", 0, "") - ctxChild := mctx.NewChild(ctx, "foo") - ctxChild, b := WithInt(ctxChild, "b", 0, "") - ctxChild, c := WithRequiredInt(ctxChild, "c", "") - ctx = mctx.WithChild(ctx, ctxChild) + cmp := new(mcmp.Component) + a := Int(cmp, "a") + cmpFoo := cmp.Child("foo") + b := Int(cmpFoo, "b") + c := Int(cmpFoo, "c", ParamRequired()) - _, err := Populate(ctx, &SourceCLI{ + err := Populate(cmp, &SourceCLI{ Args: []string{"--a=1", "--foo-b=2"}, }) assert.Error(t, err) - _, err = Populate(ctx, &SourceCLI{ + err = Populate(cmp, &SourceCLI{ Args: []string{"--a=1", "--foo-b=2", "--foo-c=3"}, }) assert.NoError(t, err) diff --git a/mcfg/param.go b/mcfg/param.go index 84c0708..26a405b 100644 --- a/mcfg/param.go +++ b/mcfg/param.go @@ -1,26 +1,25 @@ package mcfg import ( - "context" "encoding/json" "fmt" + "reflect" "strings" - "github.com/mediocregopher/mediocre-go-lib/mctx" + "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 Context, relative to its path (see the mctx -// package for more on Context path). For example, a Param with name "addr" -// under a Context 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 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 Context. + // How the parameter will be identified within a Component. Name string // A helpful description of how a parameter is expected to be used. @@ -44,9 +43,59 @@ type Param struct { // value of the parameter. Into interface{} - // The Context this Param was added to. NOTE that this will be automatically - // filled in by WithParam when the Param is added to the Context. - Context context.Context + // 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) + } +} + +// 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 { @@ -67,31 +116,36 @@ func (p Param) fuzzyParse(v string) json.RawMessage { return json.RawMessage(v) } -type ctxKey string +type cmpParamKey string -func getParam(ctx context.Context, name string) (Param, bool) { - param, ok := mctx.LocalValue(ctx, ctxKey(name)).(Param) +// used in tests +func getParam(cmp *mcmp.Component, name string) (Param, bool) { + param, ok := cmp.Value(cmpParamKey(name)).(Param) return param, ok } -// WithParam returns a Context with the given Param added to it. It will panic -// if a Param with the same Name already exists in the Context. -func WithParam(ctx context.Context, param Param) context.Context { +// 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.Context = ctx + param.Component = cmp + key := cmpParamKey(param.Name) - if _, ok := getParam(ctx, param.Name); ok { - path := mctx.Path(ctx) - panic(fmt.Sprintf("Context Path:%#v Name:%q already exists", path, param.Name)) + if cmp.HasValue(key) { + path := cmp.Path() + panic(fmt.Sprintf("Component.Path:%#v Param.Name:%q already exists", path, param.Name)) } - return mctx.WithLocalValue(ctx, ctxKey(param.Name), param) + for _, opt := range opts { + opt(¶m) + } + cmp.SetValue(key, param) } -func getLocalParams(ctx context.Context) []Param { - localVals := mctx.LocalValues(ctx) - params := make([]Param, 0, len(localVals)) - for _, val := range localVals { +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) } @@ -99,131 +153,73 @@ func getLocalParams(ctx context.Context) []Param { return params } -// WithInt64 returns an *int64 which will be populated once Populate is run on -// the returned Context. -func WithInt64(ctx context.Context, name string, defaultVal int64, usage string) (context.Context, *int64) { - i := defaultVal - ctx = WithParam(ctx, Param{Name: name, Usage: usage, Into: &i}) - return ctx, &i -} - -// WithRequiredInt64 returns an *int64 which will be populated once Populate is -// run on the returned Context, and which must be supplied by a configuration -// Source. -func WithRequiredInt64(ctx context.Context, name string, usage string) (context.Context, *int64) { +// 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 - ctx = WithParam(ctx, Param{Name: name, Required: true, Usage: usage, Into: &i}) - return ctx, &i + AddParam(cmp, Param{Name: name, Into: &i}, opts...) + return &i } -// WithInt returns an *int which will be populated once Populate is run on the -// returned Context. -func WithInt(ctx context.Context, name string, defaultVal int, usage string) (context.Context, *int) { - i := defaultVal - ctx = WithParam(ctx, Param{Name: name, Usage: usage, Into: &i}) - return ctx, &i -} - -// WithRequiredInt returns an *int which will be populated once Populate is run -// on the returned Context, and which must be supplied by a configuration -// Source. -func WithRequiredInt(ctx context.Context, name string, usage string) (context.Context, *int) { +// 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 - ctx = WithParam(ctx, Param{Name: name, Required: true, Usage: usage, Into: &i}) - return ctx, &i + AddParam(cmp, Param{Name: name, Into: &i}, opts...) + return &i } -// WithFloat64 returns a *float64 which will be populated once Populate is run on -// the returned Context. -func WithFloat64(ctx context.Context, name string, defaultVal float64, usage string) (context.Context, *float64) { - f := defaultVal - ctx = WithParam(ctx, Param{Name: name, Usage: usage, Into: &f}) - return ctx, &f +// 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 } -// WithRequiredFloat64 returns a *float64 which will be populated once Populate -// is run on the returned Context, and which must be supplied by a configuration -// Source. -func WithRequiredFloat64(ctx context.Context, name string, defaultVal float64, usage string) (context.Context, *float64) { - f := defaultVal - ctx = WithParam(ctx, Param{Name: name, Required: true, Usage: usage, Into: &f}) - return ctx, &f -} - -// WithString returns a *string which will be populated once Populate is run on -// the returned Context. -func WithString(ctx context.Context, name, defaultVal, usage string) (context.Context, *string) { - s := defaultVal - ctx = WithParam(ctx, Param{Name: name, Usage: usage, IsString: true, Into: &s}) - return ctx, &s -} - -// WithRequiredString returns a *string which will be populated once Populate is -// run on the returned Context, and which must be supplied by a configuration -// Source. -func WithRequiredString(ctx context.Context, name, usage string) (context.Context, *string) { +// 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 - ctx = WithParam(ctx, Param{Name: name, Required: true, Usage: usage, IsString: true, Into: &s}) - return ctx, &s + AddParam(cmp, Param{Name: name, IsString: true, Into: &s}, opts...) + return &s } -// WithBool returns a *bool which will be populated once Populate is run on the -// returned Context, and which defaults to false if unconfigured. +// 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 WithBool(ctx context.Context, name, usage string) (context.Context, *bool) { +func Bool(cmp *mcmp.Component, name string, opts ...ParamOption) *bool { var b bool - ctx = WithParam(ctx, Param{Name: name, Usage: usage, IsBool: true, Into: &b}) - return ctx, &b + AddParam(cmp, Param{Name: name, IsBool: true, Into: &b}, opts...) + return &b } -// WithTS returns an *mtime.TS which will be populated once Populate is run on -// the returned Context. -func WithTS(ctx context.Context, name string, defaultVal mtime.TS, usage string) (context.Context, *mtime.TS) { - t := defaultVal - ctx = WithParam(ctx, Param{Name: name, Usage: usage, Into: &t}) - return ctx, &t -} - -// WithRequiredTS returns an *mtime.TS which will be populated once Populate is -// run on the returned Context, and which must be supplied by a configuration -// Source. -func WithRequiredTS(ctx context.Context, name, usage string) (context.Context, *mtime.TS) { +// 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 - ctx = WithParam(ctx, Param{Name: name, Required: true, Usage: usage, Into: &t}) - return ctx, &t + AddParam(cmp, Param{Name: name, Into: &t}, opts...) + return &t } -// WithDuration returns an *mtime.Duration which will be populated once Populate -// is run on the returned Context. -func WithDuration(ctx context.Context, name string, defaultVal mtime.Duration, usage string) (context.Context, *mtime.Duration) { - d := defaultVal - ctx = WithParam(ctx, Param{Name: name, Usage: usage, IsString: true, Into: &d}) - return ctx, &d -} - -// WithRequiredDuration returns an *mtime.Duration which will be populated once -// Populate is run on the returned Context, and which must be supplied by a -// configuration Source. -func WithRequiredDuration(ctx context.Context, name string, defaultVal mtime.Duration, usage string) (context.Context, *mtime.Duration) { +// 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 - ctx = WithParam(ctx, Param{Name: name, Required: true, Usage: usage, IsString: true, Into: &d}) - return ctx, &d + AddParam(cmp, Param{Name: name, IsString: true, Into: &d}, opts...) + return &d } -// WithJSON reads the parameter value as a JSON value and unmarshals it into the -// given interface{} (which should be a pointer). The receiver (into) is also -// used to determine the default value. -func WithJSON(ctx context.Context, name string, into interface{}, usage string) context.Context { - return WithParam(ctx, Param{Name: name, Usage: usage, Into: into}) -} - -// WithRequiredJSON reads the parameter value as a JSON value and unmarshals it -// into the given interface{} (which should be a pointer). The value must be -// supplied by a configuration Source. -func WithRequiredJSON(ctx context.Context, name string, into interface{}, usage string) context.Context { - return WithParam(ctx, Param{Name: name, Required: true, Usage: usage, Into: into}) +// 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...) } diff --git a/mcfg/source.go b/mcfg/source.go index 92642cf..48dca2c 100644 --- a/mcfg/source.go +++ b/mcfg/source.go @@ -1,8 +1,9 @@ package mcfg import ( - "context" "encoding/json" + + "github.com/mediocregopher/mediocre-go-lib/mcmp" ) // ParamValue describes a value for a parameter which has been parsed by a @@ -14,31 +15,32 @@ type ParamValue struct { } // Source parses ParamValues out of a particular configuration source, given the -// Context which the Params were added to (via WithInt, WithString, etc...). +// 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 Context itself, for example in the -// case of sub-commands. For this reason Parse can return a Context, which will -// get used for subsequent Parse commands inside Populate. +// 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 later value takes precedence. It may also contain +// 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(context.Context) (context.Context, []ParamValue, error) + 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(ctx context.Context) (context.Context, []ParamValue, error) { - return ctx, pvs, nil +func (pvs ParamValues) Parse(*mcmp.Component) ([]ParamValue, error) { + return pvs, nil } // Sources combines together multiple Source instances into one. It will call @@ -46,16 +48,18 @@ func (pvs ParamValues) Parse(ctx context.Context) (context.Context, []ParamValue // over previous ones. type Sources []Source +var _ Source = Sources{} + // Parse implements the method for the Source interface. -func (ss Sources) Parse(ctx context.Context) (context.Context, []ParamValue, error) { +func (ss Sources) Parse(cmp *mcmp.Component) ([]ParamValue, error) { var pvs []ParamValue for _, s := range ss { var innerPVs []ParamValue var err error - if ctx, innerPVs, err = s.Parse(ctx); err != nil { - return nil, nil, err + if innerPVs, err = s.Parse(cmp); err != nil { + return nil, err } pvs = append(pvs, innerPVs...) } - return ctx, pvs, nil + return pvs, nil } diff --git a/mcfg/source_test.go b/mcfg/source_test.go index e72483b..8937d68 100644 --- a/mcfg/source_test.go +++ b/mcfg/source_test.go @@ -1,13 +1,12 @@ package mcfg import ( - "context" "encoding/json" "fmt" . "testing" "time" - "github.com/mediocregopher/mediocre-go-lib/mctx" + "github.com/mediocregopher/mediocre-go-lib/mcmp" "github.com/mediocregopher/mediocre-go-lib/mrand" "github.com/mediocregopher/mediocre-go-lib/mtest/massert" ) @@ -17,10 +16,9 @@ import ( // all the code they share type srcCommonState struct { - // availCtxs get updated in place as the run goes on, and mkRoot is used to - // create the latest version of the root context based on them - availCtxs []*context.Context - mkRoot func() context.Context + // 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 @@ -29,31 +27,21 @@ type srcCommonState struct { func newSrcCommonState() srcCommonState { var scs srcCommonState { - root := context.Background() - a := mctx.NewChild(root, "a") - b := mctx.NewChild(root, "b") - c := mctx.NewChild(root, "c") - ab := mctx.NewChild(a, "b") - bc := mctx.NewChild(b, "c") - abc := mctx.NewChild(ab, "c") - scs.availCtxs = []*context.Context{&root, &a, &b, &c, &ab, &bc, &abc} - scs.mkRoot = func() context.Context { - ab := mctx.WithChild(ab, abc) - a := mctx.WithChild(a, ab) - b := mctx.WithChild(b, bc) - root := mctx.WithChild(root, a) - root = mctx.WithChild(root, b) - root = mctx.WithChild(root, c) - return root - } + 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 - availCtxI int // not technically needed, but makes finding the ctx easier - path []string + cmp *mcmp.Component isBool bool nonBoolType string // "int", "str", "duration", "json" unset bool @@ -68,8 +56,8 @@ func (scs srcCommonState) next() srcCommonParams { p.name = mrand.Hex(8) } - p.availCtxI = mrand.Intn(len(scs.availCtxs)) - p.path = mctx.Path(*scs.availCtxs[p.availCtxI]) + availCmpI := mrand.Intn(len(scs.availCmps)) + p.cmp = scs.availCmps[availCmpI] p.isBool = mrand.Intn(8) == 0 if !p.isBool { @@ -105,22 +93,21 @@ func (scs srcCommonState) next() srcCommonParams { return p } -// adds the new param to the ctx, and if the param is expected to be set in +// adds the new param to the cmp, and if the param is expected to be set in // the Source adds it to the expected ParamValues as well -func (scs srcCommonState) applyCtxAndPV(p srcCommonParams) srcCommonState { - thisCtx := scs.availCtxs[p.availCtxI] - ctxP := Param{ +func (scs srcCommonState) applyCmpAndPV(p srcCommonParams) srcCommonState { + param := Param{ Name: p.name, IsString: p.nonBoolType == "str" || p.nonBoolType == "duration", IsBool: p.isBool, // the Sources don't actually care about the other fields of Param, // those are only used by Populate once it has all ParamValues together } - *thisCtx = WithParam(*thisCtx, ctxP) - ctxP, _ = getParam(*thisCtx, ctxP.Name) // get it back out to get any added fields + AddParam(p.cmp, param) + param, _ = getParam(p.cmp, param.Name) // get it back out to get any added fields if !p.unset { - pv := ParamValue{Name: ctxP.Name, Path: mctx.Path(ctxP.Context)} + pv := ParamValue{Name: param.Name, Path: p.cmp.Path()} if p.isBool { pv.Value = json.RawMessage("true") } else { @@ -142,8 +129,7 @@ func (scs srcCommonState) applyCtxAndPV(p srcCommonParams) srcCommonState { // given a Source asserts that it's Parse method returns the expected // ParamValues func (scs srcCommonState) assert(s Source) error { - root := scs.mkRoot() - _, gotPVs, err := s.Parse(root) + gotPVs, err := s.Parse(scs.availCmps[0]) // Parse(root) if err != nil { return err } @@ -154,12 +140,12 @@ func (scs srcCommonState) assert(s Source) error { } func TestSources(t *T) { - ctx := context.Background() - ctx, a := WithRequiredInt(ctx, "a", "") - ctx, b := WithRequiredInt(ctx, "b", "") - ctx, c := WithRequiredInt(ctx, "c", "") + cmp := new(mcmp.Component) + a := Int(cmp, "a", ParamRequired()) + b := Int(cmp, "b", ParamRequired()) + c := Int(cmp, "c", ParamRequired()) - _, err := Populate(ctx, Sources{ + err := Populate(cmp, Sources{ &SourceCLI{Args: []string{"--a=1", "--b=666"}}, &SourceEnv{Env: []string{"B=2", "C=3"}}, }) @@ -172,14 +158,13 @@ func TestSources(t *T) { } func TestSourceParamValues(t *T) { - ctx := context.Background() - ctx, a := WithRequiredInt(ctx, "a", "") - foo := mctx.NewChild(ctx, "foo") - foo, b := WithRequiredString(foo, "b", "") - foo, c := WithBool(foo, "c", "") - ctx = mctx.WithChild(ctx, foo) + cmp := new(mcmp.Component) + a := Int(cmp, "a", ParamRequired()) + cmpFoo := cmp.Child("foo") + b := String(cmpFoo, "b", ParamRequired()) + c := Bool(cmpFoo, "c") - _, err := Populate(ctx, ParamValues{ + err := Populate(cmp, 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")},