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:
parent
a30edfb5f9
commit
dd2d601081
154
mcfg/cli.go
154
mcfg/cli.go
@ -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")
|
||||||
|
152
mcfg/cli_test.go
152
mcfg/cli_test.go
@ -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
|
||||||
|
)
|
||||||
|
|
||||||
// Create a sub-command "a", which has a parameter "bar" specific to it.
|
// resetExample re-initializes all variables used in this example. We'll
|
||||||
var bar *int
|
// call it multiple times to show different behaviors depending on what
|
||||||
ctx, aFlag := WithCLISubCommand(ctx, "a", "Description of a.",
|
// arguments are passed in.
|
||||||
func(ctx context.Context) context.Context {
|
resetExample := func() {
|
||||||
ctx, bar = WithInt(ctx, "bar", 0, "Description of bar.")
|
// Create a new Component with a parameter "foo", which can be used across
|
||||||
return ctx
|
// all sub-commands.
|
||||||
})
|
cmp = new(mcmp.Component)
|
||||||
|
foo = Int(cmp, "foo")
|
||||||
|
|
||||||
// Create a sub-command "b", which has a parameter "baz" specific to it.
|
// Create a sub-command "a", which has a parameter "bar" specific to it.
|
||||||
var baz *int
|
aFlag = CLISubCommand(cmp, "a", "Description of a.",
|
||||||
ctx, bFlag := WithCLISubCommand(ctx, "b", "Description of b.",
|
func(cmp *mcmp.Component) {
|
||||||
func(ctx context.Context) context.Context {
|
bar = Int(cmp, "bar")
|
||||||
ctx, baz = WithInt(ctx, "baz", 0, "Description of baz.")
|
})
|
||||||
return ctx
|
|
||||||
})
|
// 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"
|
// 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)
|
||||||
|
28
mcfg/env.go
28
mcfg/env.go
@ -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
|
||||||
}
|
}
|
||||||
|
@ -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 += "="
|
||||||
|
64
mcfg/mcfg.go
64
mcfg/mcfg.go
@ -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
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
|
252
mcfg/param.go
252
mcfg/param.go
@ -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(¶m)
|
||||||
|
}
|
||||||
|
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})
|
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
@ -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")},
|
||||||
|
Loading…
Reference in New Issue
Block a user