mcfg: refactor so as to use new mcmp.Component type, and use the option pattern to reduce number of arguments to parameters

This commit is contained in:
Brian Picciano 2019-06-15 16:45:53 -06:00
parent a30edfb5f9
commit dd2d601081
9 changed files with 399 additions and 399 deletions

View File

@ -1,7 +1,6 @@
package mcfg package mcfg
import ( import (
"context"
"fmt" "fmt"
"io" "io"
"os" "os"
@ -9,6 +8,7 @@ import (
"sort" "sort"
"strings" "strings"
"github.com/mediocregopher/mediocre-go-lib/mcmp"
"github.com/mediocregopher/mediocre-go-lib/mctx" "github.com/mediocregopher/mediocre-go-lib/mctx"
"github.com/mediocregopher/mediocre-go-lib/merr" "github.com/mediocregopher/mediocre-go-lib/merr"
) )
@ -25,64 +25,76 @@ type cliTail struct {
descr string descr string
} }
// WithCLITail returns a Context which modifies the behavior of SourceCLI's // CLITail modifies the behavior of SourceCLI's Parse. Normally when SourceCLI
// Parse. Normally when SourceCLI encounters an unexpected Arg it will // encounters an unexpected Arg it will immediately return an error. This
// immediately return an error. This function modifies the Context to indicate // function modifies the Component to indicate to Parse that the unexpected Arg,
// to Parse that the unexpected Arg, and all subsequent Args (i.e. the tail), // and all subsequent Args (i.e. the tail), should be set to the returned
// should be set to the returned []string value. // []string value.
// //
// The descr (optional) will be appended to the "Usage" line which is printed // The descr (optional) will be appended to the "Usage" line which is printed
// with the help document when "-h" is passed in. // with the help document when "-h" is passed in.
func WithCLITail(ctx context.Context, descr string) (context.Context, *[]string) { //
if ctx.Value(cliKeyTail) != nil { // This function panics if not called on a root Component (i.e. a Component
panic("WithCLITail already called in this Context") // 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) tailPtr := new([]string)
ctx = context.WithValue(ctx, cliKeyTail, cliTail{ cmp.SetValue(cliKeyTail, cliTail{
dst: tailPtr, dst: tailPtr,
descr: descr, descr: descr,
}) })
return ctx, tailPtr return tailPtr
} }
func populateCLITail(ctx context.Context, tail []string) bool { func populateCLITail(cmp *mcmp.Component, tail []string) bool {
ct, ok := ctx.Value(cliKeyTail).(cliTail) ct, ok := cmp.Value(cliKeyTail).(cliTail)
if ok { if ok {
*ct.dst = tail *ct.dst = tail
} }
return ok return ok
} }
func getCLITailDescr(ctx context.Context) string { func getCLITailDescr(cmp *mcmp.Component) string {
ct, _ := ctx.Value(cliKeyTail).(cliTail) ct, _ := cmp.Value(cliKeyTail).(cliTail)
return ct.descr return ct.descr
} }
type subCmd struct { type subCmd struct {
name, descr string name, descr string
flag *bool 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 // command-line. When a sub-command is given on the command-line, the bool
// returned for that sub-command will be set to true. // returned for that sub-command will be set to true.
// //
// Additionally, the Context which was passed into Parse (i.e. the one passed // Additionally, the Component which was passed into Parse (i.e. the one passed
// into Populate) will be passed into the given callback, and the returned one // into Populate) will be passed into the given callback, and can be modified
// used for subsequent parsing. This allows for setting sub-command specific // for subsequent parsing. This allows for setting sub-command specific Params,
// Params, sub-command specific runtime behavior (via mrun.WithStartHook), // sub-command specific runtime behavior (via mrun.WithStartHook), support for
// support for sub-sub-commands, and more. The callback may be nil. // 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. // 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. // When parsing the command-line options, it is assumed that sub-commands will
func WithCLISubCommand(ctx context.Context, name, descr string, callback func(context.Context) context.Context) (context.Context, *bool) { // be found before any other options.
m, _ := ctx.Value(cliKeySubCmdM).(map[string]subCmd) //
// 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 { if m == nil {
m = map[string]subCmd{} m = map[string]subCmd{}
ctx = context.WithValue(ctx, cliKeySubCmdM, m) cmp.SetValue(cliKeySubCmdM, m)
} }
flag := new(bool) flag := new(bool)
@ -92,7 +104,7 @@ func WithCLISubCommand(ctx context.Context, name, descr string, callback func(co
flag: flag, flag: flag,
callback: callback, callback: callback,
} }
return ctx, flag return flag
} }
// SourceCLI is a Source which will parse configuration from the CLI. // 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 // Possible CLI options are generated by joining a Param's Path and Name with
// dashes. For example: // dashes. For example:
// //
// ctx := mctx.New() // cmp := new(mcmp.Component)
// ctx = mctx.ChildOf(ctx, "foo") // cmpFoo = cmp.Child("foo")
// ctx = mctx.ChildOf(ctx, "bar") // cmpFooBar = foo.Child("bar")
// addr := mcfg.String(ctx, "addr", "", "Some address") // addr := mcfg.String(cmpFooBar, "addr", "", "Some address")
// // the CLI option to fill addr will be "--foo-bar-addr" // // 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 // If the "-h" option is seen then a help page will be printed to stdout and the
// stdout and the process will exit. Since all normally-defined parameters must // process will exit. Since all normally-defined parameters must being with
// being with double-dash ("--") they won't ever conflict with the help option. // double-dash ("--") they won't ever conflict with the help option.
// //
// SourceCLI behaves a little differently with boolean parameters. Setting the // SourceCLI behaves a little differently with boolean parameters. Setting the
// value of a boolean parameter directly _must_ be done with an equals, for // value of a boolean parameter directly _must_ be done with an equals, or with
// example: `--boolean-flag=1` or `--boolean-flag=false`. Using the // no value at all. For example: `--boolean-flag`, `--boolean-flag=1` or
// space-separated format will not work. If a boolean has no equal-separated // `--boolean-flag=false`. Using the space-separated format will not work. If a
// value it is assumed to be setting the value to `true`, as would be expected. // boolean has no equal-separated value it is assumed to be setting the value to
// `true`.
type SourceCLI struct { type SourceCLI struct {
Args []string // if nil then os.Args[1:] is used Args []string // if nil then os.Args[1:] is used
DisableHelpPage bool DisableHelpPage bool
} }
var _ Source = new(SourceCLI)
const ( const (
cliKeyJoin = "-" cliKeyJoin = "-"
cliKeyPrefix = "--" cliKeyPrefix = "--"
@ -128,51 +143,51 @@ const (
cliHelpArg = "-h" cliHelpArg = "-h"
) )
// Parse implements the method for the Source interface // Parse implements the method for the Source interface.
func (cli *SourceCLI) Parse(ctx context.Context) (context.Context, []ParamValue, error) { func (cli *SourceCLI) Parse(cmp *mcmp.Component) ([]ParamValue, error) {
args := cli.Args args := cli.Args
if cli.Args == nil { if cli.Args == nil {
args = os.Args[1:] args = os.Args[1:]
} }
return cli.parse(ctx, nil, args) return cli.parse(cmp, nil, args)
} }
func (cli *SourceCLI) parse( func (cli *SourceCLI) parse(
ctx context.Context, cmp *mcmp.Component,
subCmdPrefix, args []string, subCmdPrefix, args []string,
) ( ) (
context.Context,
[]ParamValue, []ParamValue,
error, error,
) { ) {
pM, err := cli.cliParams(CollectParams(ctx)) pM, err := cli.cliParams(CollectParams(cmp))
if err != nil { if err != nil {
return nil, nil, err return nil, err
} }
printHelpAndExit := func() { printHelpAndExit := func() {
cli.printHelp(ctx, os.Stderr, subCmdPrefix, pM) // TODO check DisableHelpPage here?
cli.printHelp(cmp, os.Stderr, subCmdPrefix, pM)
os.Stderr.Sync() os.Stderr.Sync()
os.Exit(1) os.Exit(1)
} }
// if sub-commands were defined on this Context then handle that first. One // if sub-commands were defined on this Component then handle that first.
// of them should have been given, in which case send the Context through // One of them should have been given, in which case send the Context
// the callback to obtain a new one (which presumably has further config // through the callback to obtain a new one (which presumably has further
// options the previous didn't) and call parse again. // config options the previous didn't) and call parse again.
subCmdM, _ := ctx.Value(cliKeySubCmdM).(map[string]subCmd) subCmdM, _ := cmp.Value(cliKeySubCmdM).(map[string]subCmd)
if len(subCmdM) > 0 { if len(subCmdM) > 0 {
subCmd, args, ok := cli.getSubCmd(subCmdM, args) subCmd, args, ok := cli.getSubCmd(subCmdM, args)
if !ok { if !ok {
printHelpAndExit() printHelpAndExit()
} }
ctx = context.WithValue(ctx, cliKeySubCmdM, nil) cmp.SetValue(cliKeySubCmdM, nil)
if subCmd.callback != nil { if subCmd.callback != nil {
ctx = subCmd.callback(ctx) subCmd.callback(cmp)
} }
subCmdPrefix = append(subCmdPrefix, subCmd.name) subCmdPrefix = append(subCmdPrefix, subCmd.name)
*subCmd.flag = true *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 // if sub-commands were not set, then proceed with normal command-line arg
@ -208,11 +223,11 @@ func (cli *SourceCLI) parse(
break break
} }
if !pOk { if !pOk {
if ok := populateCLITail(ctx, args[i:]); ok { if ok := populateCLITail(cmp, args[i:]); ok {
return ctx, pvs, nil return pvs, nil
} }
ctx := mctx.Annotate(context.Background(), "param", arg) return nil, merr.New("unexpected config parameter",
return nil, nil, merr.New("unexpected config parameter", ctx) mctx.Annotated("param", arg))
} }
} }
@ -231,7 +246,7 @@ func (cli *SourceCLI) parse(
pvs = append(pvs, ParamValue{ pvs = append(pvs, ParamValue{
Name: p.Name, Name: p.Name,
Path: mctx.Path(p.Context), Path: p.Component.Path(),
Value: p.fuzzyParse(pvStrVal), Value: p.fuzzyParse(pvStrVal),
}) })
@ -242,11 +257,11 @@ func (cli *SourceCLI) parse(
pvStrValOk = false pvStrValOk = false
} }
if pOk && !pvStrValOk { if pOk && !pvStrValOk {
ctx := mctx.Annotate(p.Context, "param", key) ctx := mctx.Annotate(p.Component.Annotated(), "param", key)
return nil, nil, merr.New("param expected a value", ctx) 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) { 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) { func (cli *SourceCLI) cliParams(params []Param) (map[string]Param, error) {
m := map[string]Param{} m := map[string]Param{}
for _, p := range params { 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 m[cliKeyPrefix+key] = p
} }
return m, nil return m, nil
} }
func (cli *SourceCLI) printHelp( func (cli *SourceCLI) printHelp(
ctx context.Context, cmp *mcmp.Component,
w io.Writer, w io.Writer,
subCmdPrefix []string, subCmdPrefix []string,
pM map[string]Param, pM map[string]Param,
@ -313,7 +328,7 @@ func (cli *SourceCLI) printHelp(
subCmd subCmd
} }
subCmdM, _ := ctx.Value(cliKeySubCmdM).(map[string]subCmd) subCmdM, _ := cmp.Value(cliKeySubCmdM).(map[string]subCmd)
subCmdA := make([]subCmdEntry, 0, len(subCmdM)) subCmdA := make([]subCmdEntry, 0, len(subCmdM))
for name, subCmd := range subCmdM { for name, subCmd := range subCmdM {
subCmdA = append(subCmdA, subCmdEntry{name: name, subCmd: subCmd}) subCmdA = append(subCmdA, subCmdEntry{name: name, subCmd: subCmd})
@ -333,7 +348,7 @@ func (cli *SourceCLI) printHelp(
if len(pA) > 0 { if len(pA) > 0 {
fmt.Fprint(w, " [options]") fmt.Fprint(w, " [options]")
} }
if descr := getCLITailDescr(ctx); descr != "" { if descr := getCLITailDescr(cmp); descr != "" {
fmt.Fprintf(w, " %s", descr) fmt.Fprintf(w, " %s", descr)
} }
fmt.Fprint(w, "\n\n") fmt.Fprint(w, "\n\n")
@ -359,11 +374,6 @@ func (cli *SourceCLI) printHelp(
} }
fmt.Fprint(w, "\n") fmt.Fprint(w, "\n")
if usage := p.Usage; usage != "" { 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.Fprintln(w, "\t\t"+usage)
} }
fmt.Fprint(w, "\n") fmt.Fprint(w, "\n")

View File

@ -2,13 +2,13 @@ package mcfg
import ( import (
"bytes" "bytes"
"context"
"fmt" "fmt"
"regexp" "regexp"
"strings" "strings"
. "testing" . "testing"
"time" "time"
"github.com/mediocregopher/mediocre-go-lib/mcmp"
"github.com/mediocregopher/mediocre-go-lib/mrand" "github.com/mediocregopher/mediocre-go-lib/mrand"
"github.com/mediocregopher/mediocre-go-lib/mtest/massert" "github.com/mediocregopher/mediocre-go-lib/mtest/massert"
"github.com/mediocregopher/mediocre-go-lib/mtest/mchk" "github.com/mediocregopher/mediocre-go-lib/mtest/mchk"
@ -17,37 +17,38 @@ import (
) )
func TestSourceCLIHelp(t *T) { 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) buf := new(bytes.Buffer)
src := &SourceCLI{} src := &SourceCLI{}
pM, err := src.cliParams(CollectParams(ctx)) pM, err := src.cliParams(CollectParams(cmp))
require.NoError(t, err) require.NoError(t, err)
src.printHelp(ctx, buf, subCmdPrefix, pM) src.printHelp(cmp, buf, subCmdPrefix, pM)
out := buf.String() out := buf.String()
ok := regexp.MustCompile(exp).MatchString(out) ok := regexp.MustCompile(exp).MatchString(out)
assert.True(t, ok, "exp:%s (%q)\ngot:%s (%q)", exp, exp, out, out) assert.True(t, ok, "exp:%s (%q)\ngot:%s (%q)", exp, exp, out, out)
} }
ctx := context.Background() cmp := new(mcmp.Component)
assertHelp(ctx, nil, `^Usage: \S+ 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 Int(cmp, "foo", ParamDefault(5), ParamUsage("Test int param ")) // trailing space should be trimmed
ctx, _ = WithBool(ctx, "bar", "Test bool param.") Bool(cmp, "bar", ParamUsage("Test bool param."))
ctx, _ = WithString(ctx, "baz", "baz", "Test string param") String(cmp, "baz", ParamDefault("baz"), ParamUsage("Test string param"))
ctx, _ = WithRequiredString(ctx, "baz2", "") String(cmp, "baz2", ParamUsage("Required string param"), ParamRequired())
ctx, _ = WithRequiredString(ctx, "baz3", "") String(cmp, "baz3", ParamRequired())
assertHelp(ctx, nil, `^Usage: \S+ \[options\] assertHelp(cmp, nil, `^Usage: \S+ \[options\]
Options: Options:
--baz2 \(Required\) --baz2 \(Required\)
Required string param.
--baz3 \(Required\) --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: Options:
--baz2 \(Required\) --baz2 \(Required\)
Required string param.
--baz3 \(Required\) --baz3 \(Required\)
@ -81,9 +83,9 @@ Options:
$`) $`)
ctx, _ = WithCLISubCommand(ctx, "first", "First sub-command", nil) CLISubCommand(cmp, "first", "First sub-command", nil)
ctx, _ = WithCLISubCommand(ctx, "second", "Second sub-command", nil) CLISubCommand(cmp, "second", "Second sub-command", nil)
assertHelp(ctx, []string{"foo", "bar"}, `^Usage: \S+ foo bar <sub-command> \[options\] assertHelp(cmp, []string{"foo", "bar"}, `^Usage: \S+ foo bar <sub-command> \[options\]
Sub-commands: Sub-commands:
@ -93,6 +95,7 @@ Sub-commands:
Options: Options:
--baz2 \(Required\) --baz2 \(Required\)
Required string param.
--baz3 \(Required\) --baz3 \(Required\)
@ -107,8 +110,8 @@ Options:
$`) $`)
ctx, _ = WithCLITail(ctx, "[arg...]") CLITail(cmp, "[arg...]")
assertHelp(ctx, nil, `^Usage: \S+ <sub-command> \[options\] \[arg\.\.\.\] assertHelp(cmp, nil, `^Usage: \S+ <sub-command> \[options\] \[arg\.\.\.\]
Sub-commands: Sub-commands:
@ -118,6 +121,7 @@ Sub-commands:
Options: Options:
--baz2 \(Required\) --baz2 \(Required\)
Required string param.
--baz3 \(Required\) --baz3 \(Required\)
@ -165,11 +169,11 @@ func TestSourceCLI(t *T) {
s := ss.(state) s := ss.(state)
p := a.Params.(params) p := a.Params.(params)
s.srcCommonState = s.srcCommonState.applyCtxAndPV(p.srcCommonParams) s.srcCommonState = s.srcCommonState.applyCmpAndPV(p.srcCommonParams)
if !p.unset { if !p.unset {
arg := cliKeyPrefix arg := cliKeyPrefix
if len(p.path) > 0 { if path := p.cmp.Path(); len(path) > 0 {
arg += strings.Join(p.path, cliKeyJoin) + cliKeyJoin arg += strings.Join(path, cliKeyJoin) + cliKeyJoin
} }
arg += p.name arg += p.name
if !p.isBool { if !p.isBool {
@ -194,10 +198,10 @@ func TestSourceCLI(t *T) {
} }
} }
func TestWithCLITail(t *T) { func TestCLITail(t *T) {
ctx := context.Background() cmp := new(mcmp.Component)
ctx, _ = WithInt(ctx, "foo", 5, "") Int(cmp, "foo", ParamDefault(5))
ctx, _ = WithBool(ctx, "bar", "") Bool(cmp, "bar")
type testCase struct { type testCase struct {
args []string args []string
@ -228,8 +232,8 @@ func TestWithCLITail(t *T) {
} }
for _, tc := range cases { for _, tc := range cases {
ctx, tail := WithCLITail(ctx, "foo") tail := CLITail(cmp, "foo")
_, err := Populate(ctx, &SourceCLI{Args: tc.args}) err := Populate(cmp, &SourceCLI{Args: tc.args})
massert.Require(t, massert.Comment(massert.All( massert.Require(t, massert.Comment(massert.All(
massert.Nil(err), massert.Nil(err),
massert.Equal(tc.expTail, *tail), massert.Equal(tc.expTail, *tail),
@ -237,13 +241,13 @@ func TestWithCLITail(t *T) {
} }
} }
func ExampleWithCLITail() { func ExampleCLITail() {
ctx := context.Background() cmp := new(mcmp.Component)
ctx, foo := WithInt(ctx, "foo", 1, "Description of foo.") foo := Int(cmp, "foo", ParamDefault(1), ParamUsage("Description of foo."))
ctx, tail := WithCLITail(ctx, "[arg...]") tail := CLITail(cmp, "[arg...]")
ctx, bar := WithString(ctx, "bar", "defaultVal", "Description of bar.") 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"}, Args: []string{"--foo=100", "arg1", "arg2", "arg3"},
}) })
@ -251,9 +255,9 @@ func ExampleWithCLITail() {
// Output: err:<nil> foo:100 bar:defaultVal tail:[]string{"arg1", "arg2", "arg3"} // Output: err:<nil> foo:100 bar:defaultVal tail:[]string{"arg1", "arg2", "arg3"}
} }
func TestWithCLISubCommand(t *T) { func TestCLISubCommand(t *T) {
var ( var (
ctx context.Context cmp *mcmp.Component
foo *int foo *int
bar *int bar *int
baz *int baz *int
@ -262,22 +266,20 @@ func TestWithCLISubCommand(t *T) {
) )
reset := func() { reset := func() {
foo, bar, baz, aFlag, bFlag = nil, nil, nil, nil, nil foo, bar, baz, aFlag, bFlag = nil, nil, nil, nil, nil
ctx = context.Background() cmp = new(mcmp.Component)
ctx, foo = WithInt(ctx, "foo", 0, "Description of foo.") foo = Int(cmp, "foo")
ctx, aFlag = WithCLISubCommand(ctx, "a", "Description of a.", aFlag = CLISubCommand(cmp, "a", "Description of a.",
func(ctx context.Context) context.Context { func(cmp *mcmp.Component) {
ctx, bar = WithInt(ctx, "bar", 0, "Description of bar.") bar = Int(cmp, "bar")
return ctx
}) })
ctx, bFlag = WithCLISubCommand(ctx, "b", "Description of b.", bFlag = CLISubCommand(cmp, "b", "Description of b.",
func(ctx context.Context) context.Context { func(cmp *mcmp.Component) {
ctx, baz = WithInt(ctx, "baz", 0, "Description of baz.") baz = Int(cmp, "baz")
return ctx
}) })
} }
reset() reset()
_, err := Populate(ctx, &SourceCLI{ err := Populate(cmp, &SourceCLI{
Args: []string{"a", "--foo=1", "--bar=2"}, Args: []string{"a", "--foo=1", "--bar=2"},
}) })
massert.Require(t, massert.Require(t,
@ -290,7 +292,7 @@ func TestWithCLISubCommand(t *T) {
) )
reset() reset()
_, err = Populate(ctx, &SourceCLI{ err = Populate(cmp, &SourceCLI{
Args: []string{"b", "--foo=1", "--baz=3"}, Args: []string{"b", "--foo=1", "--baz=3"},
}) })
massert.Require(t, massert.Require(t,
@ -303,40 +305,48 @@ func TestWithCLISubCommand(t *T) {
) )
} }
func ExampleWithCLISubCommand() { func ExampleCLISubCommand() {
// Create a new Context with a parameter "foo", which can be used across all var (
// sub-commands. cmp *mcmp.Component
ctx := context.Background() foo, bar, baz *int
ctx, foo := WithInt(ctx, "foo", 0, "Description of foo.") 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. // Create a sub-command "a", which has a parameter "bar" specific to it.
var bar *int aFlag = CLISubCommand(cmp, "a", "Description of a.",
ctx, aFlag := WithCLISubCommand(ctx, "a", "Description of a.", func(cmp *mcmp.Component) {
func(ctx context.Context) context.Context { bar = Int(cmp, "bar")
ctx, bar = WithInt(ctx, "bar", 0, "Description of bar.")
return ctx
}) })
// Create a sub-command "b", which has a parameter "baz" specific to it. // Create a sub-command "b", which has a parameter "baz" specific to it.
var baz *int bFlag = CLISubCommand(cmp, "b", "Description of b.",
ctx, bFlag := WithCLISubCommand(ctx, "b", "Description of b.", func(cmp *mcmp.Component) {
func(ctx context.Context) context.Context { baz = Int(cmp, "baz")
ctx, baz = WithInt(ctx, "baz", 0, "Description of baz.")
return ctx
}) })
}
// Use Populate with manually generated CLI arguments, calling the "a" // Use Populate with manually generated CLI arguments, calling the "a"
// sub-command. // sub-command.
resetExample()
args := []string{"a", "--foo=1", "--bar=2"} 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) panic(err)
} }
fmt.Printf("foo:%d bar:%d aFlag:%v bFlag:%v\n", *foo, *bar, *aFlag, *bFlag) 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. // reset for another Populate, this time calling the "b" sub-command.
*aFlag = false resetExample()
args = []string{"b", "--foo=1", "--baz=3"} 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) panic(err)
} }
fmt.Printf("foo:%d baz:%d aFlag:%v bFlag:%v\n", *foo, *baz, *aFlag, *bFlag) fmt.Printf("foo:%d baz:%d aFlag:%v bFlag:%v\n", *foo, *baz, *aFlag, *bFlag)

View File

@ -1,10 +1,10 @@
package mcfg package mcfg
import ( import (
"context"
"os" "os"
"strings" "strings"
"github.com/mediocregopher/mediocre-go-lib/mcmp"
"github.com/mediocregopher/mediocre-go-lib/mctx" "github.com/mediocregopher/mediocre-go-lib/mctx"
"github.com/mediocregopher/mediocre-go-lib/merr" "github.com/mediocregopher/mediocre-go-lib/merr"
) )
@ -16,10 +16,10 @@ import (
// underscores and making all characters uppercase, as well as changing all // underscores and making all characters uppercase, as well as changing all
// dashes to underscores. // dashes to underscores.
// //
// ctx := mctx.New() // cmp := new(mcmp.Component)
// ctx = mctx.ChildOf(ctx, "foo") // cmpFoo := cmp.Child("foo")
// ctx = mctx.ChildOf(ctx, "bar") // cmpFooBar := cmp.Child("bar")
// addr := mcfg.String(ctx, "srv-addr", "", "Some address") // addr := mcfg.String(cmpFooBar, "srv-addr", "", "Some address")
// // the Env option to fill addr will be "FOO_BAR_SRV_ADDR" // // the Env option to fill addr will be "FOO_BAR_SRV_ADDR"
// //
type SourceEnv struct { type SourceEnv struct {
@ -32,6 +32,8 @@ type SourceEnv struct {
Prefix string Prefix string
} }
var _ Source = new(SourceEnv)
func (env *SourceEnv) expectedName(path []string, name string) string { func (env *SourceEnv) expectedName(path []string, name string) string {
out := strings.Join(append(path, name), "_") out := strings.Join(append(path, name), "_")
if env.Prefix != "" { if env.Prefix != "" {
@ -42,17 +44,17 @@ func (env *SourceEnv) expectedName(path []string, name string) string {
return out return out
} }
// Parse implements the method for the Source interface // Parse implements the method for the Source interface.
func (env *SourceEnv) Parse(ctx context.Context) (context.Context, []ParamValue, error) { func (env *SourceEnv) Parse(cmp *mcmp.Component) ([]ParamValue, error) {
kvs := env.Env kvs := env.Env
if kvs == nil { if kvs == nil {
kvs = os.Environ() kvs = os.Environ()
} }
params := CollectParams(ctx) params := CollectParams(cmp)
pM := map[string]Param{} pM := map[string]Param{}
for _, p := range params { for _, p := range params {
name := env.expectedName(mctx.Path(p.Context), p.Name) name := env.expectedName(p.Component.Path(), p.Name)
pM[name] = p pM[name] = p
} }
@ -60,18 +62,18 @@ func (env *SourceEnv) Parse(ctx context.Context) (context.Context, []ParamValue,
for _, kv := range kvs { for _, kv := range kvs {
split := strings.SplitN(kv, "=", 2) split := strings.SplitN(kv, "=", 2)
if len(split) != 2 { if len(split) != 2 {
ctx := mctx.Annotate(context.Background(), "kv", kv) return nil, merr.New("malformed environment key/value pair",
return nil, nil, merr.New("malformed environment key/value pair", ctx) mctx.Annotated("kv", kv))
} }
k, v := split[0], split[1] k, v := split[0], split[1]
if p, ok := pM[k]; ok { if p, ok := pM[k]; ok {
pvs = append(pvs, ParamValue{ pvs = append(pvs, ParamValue{
Name: p.Name, Name: p.Name,
Path: mctx.Path(p.Context), Path: p.Component.Path(),
Value: p.fuzzyParse(v), Value: p.fuzzyParse(v),
}) })
} }
} }
return ctx, pvs, nil return pvs, nil
} }

View File

@ -36,9 +36,9 @@ func TestSourceEnv(t *T) {
Apply: func(ss mchk.State, a mchk.Action) (mchk.State, error) { Apply: func(ss mchk.State, a mchk.Action) (mchk.State, error) {
s := ss.(state) s := ss.(state)
p := a.Params.(params) p := a.Params.(params)
s.srcCommonState = s.srcCommonState.applyCtxAndPV(p.srcCommonParams) s.srcCommonState = s.srcCommonState.applyCmpAndPV(p.srcCommonParams)
if !p.unset { 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.Replace(kv, "-", "_", -1)
kv = strings.ToUpper(kv) kv = strings.ToUpper(kv)
kv += "=" kv += "="

View File

@ -2,21 +2,18 @@
// parameters and various methods of filling those parameters from external // parameters and various methods of filling those parameters from external
// configuration sources (e.g. the command line and environment variables). // configuration sources (e.g. the command line and environment variables).
// //
// Parameters are registered onto a context, and that same context is used later // Parameters are registered onto a Component, and that same Component (or one
// to collect and fulfill those parameters. When used with the mctx package's // of its ancestors) is used later to collect and fill those parameters.
// 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.
package mcfg package mcfg
import ( import (
"context"
"crypto/md5" "crypto/md5"
"encoding/hex" "encoding/hex"
"encoding/json" "encoding/json"
"fmt" "fmt"
"sort" "sort"
"github.com/mediocregopher/mediocre-go-lib/mcmp"
"github.com/mediocregopher/mediocre-go-lib/mctx" "github.com/mediocregopher/mediocre-go-lib/mctx"
"github.com/mediocregopher/mediocre-go-lib/merr" "github.com/mediocregopher/mediocre-go-lib/merr"
) )
@ -34,7 +31,7 @@ import (
func sortParams(params []Param) { func sortParams(params []Param) {
sort.Slice(params, func(i, j int) bool { sort.Slice(params, func(i, j int) bool {
a, b := params[i], params[j] 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 { for {
switch { switch {
case len(aPath) == 0 && len(bPath) == 0: 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 // CollectParams gathers all Params by recursively retrieving them from the
// Context and its children. Returned Params are sorted according to their Path // given Component and its children. Returned Params are sorted according to
// and Name. // their Path and Name.
func CollectParams(ctx context.Context) []Param { func CollectParams(cmp *mcmp.Component) []Param {
var params []Param var params []Param
var visit func(context.Context) var visit func(*mcmp.Component)
visit = func(ctx context.Context) { visit = func(cmp *mcmp.Component) {
for _, param := range getLocalParams(ctx) { for _, param := range getLocalParams(cmp) {
params = append(params, param) params = append(params, param)
} }
for _, childCtx := range mctx.Children(ctx) { for _, childCmp := range cmp.Children() {
visit(childCtx) visit(childCmp)
} }
} }
visit(ctx) visit(cmp)
sortParams(params) sortParams(params)
return 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 // 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 // added to the given Component, and all of its children. Populate may be called
// multiple times with the same Context, each time will only affect the values // multiple times with the same Component, each time will only affect the values
// of the Params which were provided by the respective Source. // 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 // 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. // 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 { if src == nil {
src = ParamValues(nil) src = ParamValues(nil)
} }
ogCtx := ctx
ctx, pvs, err := src.Parse(ctx) pvs, err := src.Parse(cmp)
if err != nil { if err != nil {
return ogCtx, err return err
} }
// map Params to their hash, so we can match them to their ParamValues. // map Params to their hash, so we can match them to their ParamValues.
// later. There should not be any duplicates here. // later. There should not be any duplicates here.
params := CollectParams(ctx) params := CollectParams(cmp)
pM := map[string]Param{} pM := map[string]Param{}
for _, p := range params { for _, p := range params {
path := mctx.Path(p.Context) path := p.Component.Path()
hash := paramHash(path, p.Name) hash := paramHash(path, p.Name)
if _, ok := pM[hash]; ok { if _, ok := pM[hash]; ok {
panic("duplicate Param: " + paramFullName(path, p.Name)) panic("duplicate Param: " + paramFullName(path, p.Name))
@ -137,9 +131,9 @@ func Populate(ctx context.Context, src Source) (context.Context, error) {
if !p.Required { if !p.Required {
continue continue
} else if _, ok := pvM[hash]; !ok { } else if _, ok := pvM[hash]; !ok {
ctx := mctx.Annotate(p.Context, ctx := mctx.Annotate(p.Component.Annotated(),
"param", paramFullName(mctx.Path(p.Context), p.Name)) "param", paramFullName(p.Component.Path(), p.Name))
return ogCtx, merr.New("required parameter is not set", ctx) 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 // at this point, all ParamValues in pvM have a corresponding pM Param
p := pM[hash] p := pM[hash]
if err := json.Unmarshal(pv.Value, p.Into); err != nil { if err := json.Unmarshal(pv.Value, p.Into); err != nil {
return ogCtx, err return err
} }
} }
return ctx, nil return nil
} }

View File

@ -1,45 +1,44 @@
package mcfg package mcfg
import ( import (
"context"
. "testing" . "testing"
"github.com/mediocregopher/mediocre-go-lib/mctx" "github.com/mediocregopher/mediocre-go-lib/mcmp"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
func TestPopulate(t *T) { func TestPopulate(t *T) {
{ {
ctx := context.Background() cmp := new(mcmp.Component)
ctx, a := WithInt(ctx, "a", 0, "") a := Int(cmp, "a")
ctxChild := mctx.NewChild(ctx, "foo") cmpFoo := cmp.Child("foo")
ctxChild, b := WithInt(ctxChild, "b", 0, "") b := Int(cmpFoo, "b")
ctxChild, c := WithInt(ctxChild, "c", 0, "") c := Int(cmpFoo, "c")
ctx = mctx.WithChild(ctx, ctxChild) d := Int(cmp, "d", ParamDefault(4))
_, err := Populate(ctx, &SourceCLI{ err := Populate(cmp, &SourceCLI{
Args: []string{"--a=1", "--foo-b=2"}, Args: []string{"--a=1", "--foo-b=2"},
}) })
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, 1, *a) assert.Equal(t, 1, *a)
assert.Equal(t, 2, *b) assert.Equal(t, 2, *b)
assert.Equal(t, 0, *c) assert.Equal(t, 0, *c)
assert.Equal(t, 4, *d)
} }
{ // test that required params are enforced { // test that required params are enforced
ctx := context.Background() cmp := new(mcmp.Component)
ctx, a := WithInt(ctx, "a", 0, "") a := Int(cmp, "a")
ctxChild := mctx.NewChild(ctx, "foo") cmpFoo := cmp.Child("foo")
ctxChild, b := WithInt(ctxChild, "b", 0, "") b := Int(cmpFoo, "b")
ctxChild, c := WithRequiredInt(ctxChild, "c", "") c := Int(cmpFoo, "c", ParamRequired())
ctx = mctx.WithChild(ctx, ctxChild)
_, err := Populate(ctx, &SourceCLI{ err := Populate(cmp, &SourceCLI{
Args: []string{"--a=1", "--foo-b=2"}, Args: []string{"--a=1", "--foo-b=2"},
}) })
assert.Error(t, err) assert.Error(t, err)
_, err = Populate(ctx, &SourceCLI{ err = Populate(cmp, &SourceCLI{
Args: []string{"--a=1", "--foo-b=2", "--foo-c=3"}, Args: []string{"--a=1", "--foo-b=2", "--foo-c=3"},
}) })
assert.NoError(t, err) assert.NoError(t, err)

View File

@ -1,26 +1,25 @@
package mcfg package mcfg
import ( import (
"context"
"encoding/json" "encoding/json"
"fmt" "fmt"
"reflect"
"strings" "strings"
"github.com/mediocregopher/mediocre-go-lib/mctx" "github.com/mediocregopher/mediocre-go-lib/mcmp"
"github.com/mediocregopher/mediocre-go-lib/mtime" "github.com/mediocregopher/mediocre-go-lib/mtime"
) )
// Param is a configuration parameter which can be populated by Populate. The // 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 // Param will exist as part of a Component. For example, a Param with name
// package for more on Context path). For example, a Param with name "addr" // "addr" under a Component with path of []string{"foo","bar"} will be setable
// under a Context with path of []string{"foo","bar"} will be setable on the CLI // on the CLI via "--foo-bar-addr". Other configuration Sources may treat the
// via "--foo-bar-addr". Other configuration Sources may treat the path/name // path/name differently, however.
// differently, however.
// //
// Param values are always unmarshaled as JSON values into the Into field of the // Param values are always unmarshaled as JSON values into the Into field of the
// Param, regardless of the actual Source. // Param, regardless of the actual Source.
type Param struct { type Param struct {
// How the parameter will be identified within a Context. // How the parameter will be identified within a Component.
Name string Name string
// A helpful description of how a parameter is expected to be used. // A helpful description of how a parameter is expected to be used.
@ -44,9 +43,59 @@ type Param struct {
// value of the parameter. // value of the parameter.
Into interface{} Into interface{}
// The Context this Param was added to. NOTE that this will be automatically // The Component this Param was added to. NOTE that this will be
// filled in by WithParam when the Param is added to the Context. // automatically filled in by AddParam when the Param is added to the
Context context.Context // 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 { func paramFullName(path []string, name string) string {
@ -67,31 +116,36 @@ func (p Param) fuzzyParse(v string) json.RawMessage {
return json.RawMessage(v) return json.RawMessage(v)
} }
type ctxKey string type cmpParamKey string
func getParam(ctx context.Context, name string) (Param, bool) { // used in tests
param, ok := mctx.LocalValue(ctx, ctxKey(name)).(Param) func getParam(cmp *mcmp.Component, name string) (Param, bool) {
param, ok := cmp.Value(cmpParamKey(name)).(Param)
return param, ok return param, ok
} }
// WithParam returns a Context with the given Param added to it. It will panic // AddParam adds the given Param to the given Component. It will panic if a
// if a Param with the same Name already exists in the Context. // Param with the same Name already exists in the Component.
func WithParam(ctx context.Context, param Param) context.Context { func AddParam(cmp *mcmp.Component, param Param, opts ...ParamOption) {
param.Name = strings.ToLower(param.Name) param.Name = strings.ToLower(param.Name)
param.Context = ctx param.Component = cmp
key := cmpParamKey(param.Name)
if _, ok := getParam(ctx, param.Name); ok { if cmp.HasValue(key) {
path := mctx.Path(ctx) path := cmp.Path()
panic(fmt.Sprintf("Context Path:%#v Name:%q already exists", path, param.Name)) 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(&param)
}
cmp.SetValue(key, param)
} }
func getLocalParams(ctx context.Context) []Param { func getLocalParams(cmp *mcmp.Component) []Param {
localVals := mctx.LocalValues(ctx) values := cmp.Values()
params := make([]Param, 0, len(localVals)) params := make([]Param, 0, len(values))
for _, val := range localVals { for _, val := range values {
if param, ok := val.(Param); ok { if param, ok := val.(Param); ok {
params = append(params, param) params = append(params, param)
} }
@ -99,131 +153,73 @@ func getLocalParams(ctx context.Context) []Param {
return params return params
} }
// WithInt64 returns an *int64 which will be populated once Populate is run on // Int64 returns an *int64 which will be populated once Populate is run on the
// the returned Context. // Component.
func WithInt64(ctx context.Context, name string, defaultVal int64, usage string) (context.Context, *int64) { func Int64(cmp *mcmp.Component, name string, opts ...ParamOption) *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) {
var i int64 var i int64
ctx = WithParam(ctx, Param{Name: name, Required: true, Usage: usage, Into: &i}) AddParam(cmp, Param{Name: name, Into: &i}, opts...)
return ctx, &i return &i
} }
// WithInt returns an *int which will be populated once Populate is run on the // Int returns an *int which will be populated once Populate is run on the
// returned Context. // Component.
func WithInt(ctx context.Context, name string, defaultVal int, usage string) (context.Context, *int) { func Int(cmp *mcmp.Component, name string, opts ...ParamOption) *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) {
var i int var i int
ctx = WithParam(ctx, Param{Name: name, Required: true, Usage: usage, Into: &i}) AddParam(cmp, Param{Name: name, Into: &i}, opts...)
return ctx, &i return &i
} }
// WithFloat64 returns a *float64 which will be populated once Populate is run on // Float64 returns a *float64 which will be populated once Populate is run on
// the returned Context. // the Component
func WithFloat64(ctx context.Context, name string, defaultVal float64, usage string) (context.Context, *float64) { func Float64(cmp *mcmp.Component, name string, opts ...ParamOption) *float64 {
f := defaultVal var f float64
ctx = WithParam(ctx, Param{Name: name, Usage: usage, Into: &f}) AddParam(cmp, Param{Name: name, Into: &f}, opts...)
return ctx, &f return &f
} }
// WithRequiredFloat64 returns a *float64 which will be populated once Populate // String returns a *string which will be populated once Populate is run on
// is run on the returned Context, and which must be supplied by a configuration // the Component.
// Source. func String(cmp *mcmp.Component, name string, opts ...ParamOption) *string {
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) {
var s string var s string
ctx = WithParam(ctx, Param{Name: name, Required: true, Usage: usage, IsString: true, Into: &s}) AddParam(cmp, Param{Name: name, IsString: true, Into: &s}, opts...)
return ctx, &s return &s
} }
// WithBool returns a *bool which will be populated once Populate is run on the // Bool returns a *bool which will be populated once Populate is run on the
// returned Context, and which defaults to false if unconfigured. // Component, and which defaults to false if unconfigured.
// //
// The default behavior of all Sources is that a boolean parameter will be set // 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 // 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, // the value will also be true when the parameter is used with no value at all,
// as would be expected. // 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 var b bool
ctx = WithParam(ctx, Param{Name: name, Usage: usage, IsBool: true, Into: &b}) AddParam(cmp, Param{Name: name, IsBool: true, Into: &b}, opts...)
return ctx, &b return &b
} }
// WithTS returns an *mtime.TS which will be populated once Populate is run on // TS returns an *mtime.TS which will be populated once Populate is run on
// the returned Context. // the Component.
func WithTS(ctx context.Context, name string, defaultVal mtime.TS, usage string) (context.Context, *mtime.TS) { func TS(cmp *mcmp.Component, name string, opts ...ParamOption) *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) {
var t mtime.TS var t mtime.TS
ctx = WithParam(ctx, Param{Name: name, Required: true, Usage: usage, Into: &t}) AddParam(cmp, Param{Name: name, Into: &t}, opts...)
return ctx, &t return &t
} }
// WithDuration returns an *mtime.Duration which will be populated once Populate // Duration returns an *mtime.Duration which will be populated once Populate
// is run on the returned Context. // is run on the Component.
func WithDuration(ctx context.Context, name string, defaultVal mtime.Duration, usage string) (context.Context, *mtime.Duration) { func Duration(cmp *mcmp.Component, name string, opts ...ParamOption) *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) {
var d mtime.Duration var d mtime.Duration
ctx = WithParam(ctx, Param{Name: name, Required: true, Usage: usage, IsString: true, Into: &d}) AddParam(cmp, Param{Name: name, IsString: true, Into: &d}, opts...)
return ctx, &d return &d
} }
// WithJSON reads the parameter value as a JSON value and unmarshals it into the // JSON 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 // given interface{} (which should be a pointer) once Populate is run on the
// used to determine the default value. // Component.
func WithJSON(ctx context.Context, name string, into interface{}, usage string) context.Context { //
return WithParam(ctx, Param{Name: name, Usage: usage, Into: into}) // 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) {
// WithRequiredJSON reads the parameter value as a JSON value and unmarshals it AddParam(cmp, Param{Name: name, Into: into}, opts...)
// 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})
} }

View File

@ -1,8 +1,9 @@
package mcfg package mcfg
import ( import (
"context"
"encoding/json" "encoding/json"
"github.com/mediocregopher/mediocre-go-lib/mcmp"
) )
// ParamValue describes a value for a parameter which has been parsed by a // ParamValue describes a value for a parameter which has been parsed by a
@ -14,31 +15,32 @@ type ParamValue struct {
} }
// Source parses ParamValues out of a particular configuration source, given the // 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. // CollectParams can be used to retrieve these Params.
// //
// It's possible for Parsing to affect the Context itself, for example in the // It's possible for Parsing to affect the Component itself, for example in the
// case of sub-commands. For this reason Parse can return a Context, which will // case of sub-commands.
// get used for subsequent Parse commands inside Populate.
// //
// Source should not return ParamValues which were not explicitly set to a value // Source should not return ParamValues which were not explicitly set to a value
// by the configuration source. // by the configuration source.
// //
// The returned []ParamValue may contain duplicates of the same Param's value. // 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 // ParamValues which do not correspond to any of the passed in Params. These
// will be ignored in Populate. // will be ignored in Populate.
type Source interface { 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 // ParamValues is simply a slice of ParamValue elements, which implements Parse
// by always returning itself as-is. // by always returning itself as-is.
type ParamValues []ParamValue type ParamValues []ParamValue
var _ Source = ParamValues{}
// Parse implements the method for the Source interface. // Parse implements the method for the Source interface.
func (pvs ParamValues) Parse(ctx context.Context) (context.Context, []ParamValue, error) { func (pvs ParamValues) Parse(*mcmp.Component) ([]ParamValue, error) {
return ctx, pvs, nil return pvs, nil
} }
// Sources combines together multiple Source instances into one. It will call // 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. // over previous ones.
type Sources []Source type Sources []Source
var _ Source = Sources{}
// Parse implements the method for the Source interface. // 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 var pvs []ParamValue
for _, s := range ss { for _, s := range ss {
var innerPVs []ParamValue var innerPVs []ParamValue
var err error var err error
if ctx, innerPVs, err = s.Parse(ctx); err != nil { if innerPVs, err = s.Parse(cmp); err != nil {
return nil, nil, err return nil, err
} }
pvs = append(pvs, innerPVs...) pvs = append(pvs, innerPVs...)
} }
return ctx, pvs, nil return pvs, nil
} }

View File

@ -1,13 +1,12 @@
package mcfg package mcfg
import ( import (
"context"
"encoding/json" "encoding/json"
"fmt" "fmt"
. "testing" . "testing"
"time" "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/mrand"
"github.com/mediocregopher/mediocre-go-lib/mtest/massert" "github.com/mediocregopher/mediocre-go-lib/mtest/massert"
) )
@ -17,10 +16,9 @@ import (
// all the code they share // all the code they share
type srcCommonState struct { type srcCommonState struct {
// availCtxs get updated in place as the run goes on, and mkRoot is used to // availCmps get updated in place as the run goes on, it's easier to keep
// create the latest version of the root context based on them // track of them this way than by traversing the hierarchy.
availCtxs []*context.Context availCmps []*mcmp.Component
mkRoot func() context.Context
expPVs []ParamValue expPVs []ParamValue
// each specific test should wrap this to add the Source itself // each specific test should wrap this to add the Source itself
@ -29,31 +27,21 @@ type srcCommonState struct {
func newSrcCommonState() srcCommonState { func newSrcCommonState() srcCommonState {
var scs srcCommonState var scs srcCommonState
{ {
root := context.Background() root := new(mcmp.Component)
a := mctx.NewChild(root, "a") a := root.Child("a")
b := mctx.NewChild(root, "b") b := root.Child("b")
c := mctx.NewChild(root, "c") c := root.Child("c")
ab := mctx.NewChild(a, "b") ab := a.Child("b")
bc := mctx.NewChild(b, "c") bc := b.Child("c")
abc := mctx.NewChild(ab, "c") abc := ab.Child("c")
scs.availCtxs = []*context.Context{&root, &a, &b, &c, &ab, &bc, &abc} scs.availCmps = []*mcmp.Component{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
}
} }
return scs return scs
} }
type srcCommonParams struct { type srcCommonParams struct {
name string name string
availCtxI int // not technically needed, but makes finding the ctx easier cmp *mcmp.Component
path []string
isBool bool isBool bool
nonBoolType string // "int", "str", "duration", "json" nonBoolType string // "int", "str", "duration", "json"
unset bool unset bool
@ -68,8 +56,8 @@ func (scs srcCommonState) next() srcCommonParams {
p.name = mrand.Hex(8) p.name = mrand.Hex(8)
} }
p.availCtxI = mrand.Intn(len(scs.availCtxs)) availCmpI := mrand.Intn(len(scs.availCmps))
p.path = mctx.Path(*scs.availCtxs[p.availCtxI]) p.cmp = scs.availCmps[availCmpI]
p.isBool = mrand.Intn(8) == 0 p.isBool = mrand.Intn(8) == 0
if !p.isBool { if !p.isBool {
@ -105,22 +93,21 @@ func (scs srcCommonState) next() srcCommonParams {
return p 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 // the Source adds it to the expected ParamValues as well
func (scs srcCommonState) applyCtxAndPV(p srcCommonParams) srcCommonState { func (scs srcCommonState) applyCmpAndPV(p srcCommonParams) srcCommonState {
thisCtx := scs.availCtxs[p.availCtxI] param := Param{
ctxP := Param{
Name: p.name, Name: p.name,
IsString: p.nonBoolType == "str" || p.nonBoolType == "duration", IsString: p.nonBoolType == "str" || p.nonBoolType == "duration",
IsBool: p.isBool, IsBool: p.isBool,
// the Sources don't actually care about the other fields of Param, // the Sources don't actually care about the other fields of Param,
// those are only used by Populate once it has all ParamValues together // those are only used by Populate once it has all ParamValues together
} }
*thisCtx = WithParam(*thisCtx, ctxP) AddParam(p.cmp, param)
ctxP, _ = getParam(*thisCtx, ctxP.Name) // get it back out to get any added fields param, _ = getParam(p.cmp, param.Name) // get it back out to get any added fields
if !p.unset { 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 { if p.isBool {
pv.Value = json.RawMessage("true") pv.Value = json.RawMessage("true")
} else { } else {
@ -142,8 +129,7 @@ func (scs srcCommonState) applyCtxAndPV(p srcCommonParams) srcCommonState {
// given a Source asserts that it's Parse method returns the expected // given a Source asserts that it's Parse method returns the expected
// ParamValues // ParamValues
func (scs srcCommonState) assert(s Source) error { func (scs srcCommonState) assert(s Source) error {
root := scs.mkRoot() gotPVs, err := s.Parse(scs.availCmps[0]) // Parse(root)
_, gotPVs, err := s.Parse(root)
if err != nil { if err != nil {
return err return err
} }
@ -154,12 +140,12 @@ func (scs srcCommonState) assert(s Source) error {
} }
func TestSources(t *T) { func TestSources(t *T) {
ctx := context.Background() cmp := new(mcmp.Component)
ctx, a := WithRequiredInt(ctx, "a", "") a := Int(cmp, "a", ParamRequired())
ctx, b := WithRequiredInt(ctx, "b", "") b := Int(cmp, "b", ParamRequired())
ctx, c := WithRequiredInt(ctx, "c", "") c := Int(cmp, "c", ParamRequired())
_, err := Populate(ctx, Sources{ err := Populate(cmp, Sources{
&SourceCLI{Args: []string{"--a=1", "--b=666"}}, &SourceCLI{Args: []string{"--a=1", "--b=666"}},
&SourceEnv{Env: []string{"B=2", "C=3"}}, &SourceEnv{Env: []string{"B=2", "C=3"}},
}) })
@ -172,14 +158,13 @@ func TestSources(t *T) {
} }
func TestSourceParamValues(t *T) { func TestSourceParamValues(t *T) {
ctx := context.Background() cmp := new(mcmp.Component)
ctx, a := WithRequiredInt(ctx, "a", "") a := Int(cmp, "a", ParamRequired())
foo := mctx.NewChild(ctx, "foo") cmpFoo := cmp.Child("foo")
foo, b := WithRequiredString(foo, "b", "") b := String(cmpFoo, "b", ParamRequired())
foo, c := WithBool(foo, "c", "") c := Bool(cmpFoo, "c")
ctx = mctx.WithChild(ctx, foo)
_, err := Populate(ctx, ParamValues{ err := Populate(cmp, ParamValues{
{Name: "a", Value: json.RawMessage(`4`)}, {Name: "a", Value: json.RawMessage(`4`)},
{Path: []string{"foo"}, Name: "b", Value: json.RawMessage(`"bbb"`)}, {Path: []string{"foo"}, Name: "b", Value: json.RawMessage(`"bbb"`)},
{Path: []string{"foo"}, Name: "c", Value: json.RawMessage("true")}, {Path: []string{"foo"}, Name: "c", Value: json.RawMessage("true")},