Getting rid of mcmp pretty much breaks this whole package, but this commit makes a good start on fixing all the things worth keeping.main
parent
c20f884d68
commit
3e2713a850
@ -1,382 +0,0 @@ |
||||
package mcfg |
||||
|
||||
import ( |
||||
"fmt" |
||||
"io" |
||||
"os" |
||||
"reflect" |
||||
"sort" |
||||
"strings" |
||||
|
||||
"github.com/mediocregopher/mediocre-go-lib/mcmp" |
||||
"github.com/mediocregopher/mediocre-go-lib/mctx" |
||||
"github.com/mediocregopher/mediocre-go-lib/merr" |
||||
) |
||||
|
||||
type cliKey int |
||||
|
||||
const ( |
||||
cliKeyTail cliKey = iota |
||||
cliKeySubCmdM |
||||
) |
||||
|
||||
type cliTail struct { |
||||
dst *[]string |
||||
descr string |
||||
} |
||||
|
||||
// CLITail modifies the behavior of SourceCLI's Parse. Normally when SourceCLI
|
||||
// encounters an unexpected Arg it will immediately return an error. This
|
||||
// function modifies the Component to indicate to Parse that the unexpected Arg,
|
||||
// and all subsequent Args (i.e. the tail), should be set to the returned
|
||||
// []string value.
|
||||
//
|
||||
// The descr (optional) will be appended to the "Usage" line which is printed
|
||||
// with the help document when "-h" is passed in.
|
||||
//
|
||||
// This function panics if not called on a root Component (i.e. a Component
|
||||
// which has no parents).
|
||||
func CLITail(cmp *mcmp.Component, descr string) *[]string { |
||||
if len(cmp.Path()) != 0 { |
||||
panic("CLITail can only be used on a root Component") |
||||
} |
||||
|
||||
tailPtr := new([]string) |
||||
cmp.SetValue(cliKeyTail, cliTail{ |
||||
dst: tailPtr, |
||||
descr: descr, |
||||
}) |
||||
return tailPtr |
||||
} |
||||
|
||||
func populateCLITail(cmp *mcmp.Component, tail []string) bool { |
||||
ct, ok := cmp.Value(cliKeyTail).(cliTail) |
||||
if ok { |
||||
*ct.dst = tail |
||||
} |
||||
return ok |
||||
} |
||||
|
||||
func getCLITailDescr(cmp *mcmp.Component) string { |
||||
ct, _ := cmp.Value(cliKeyTail).(cliTail) |
||||
return ct.descr |
||||
} |
||||
|
||||
type subCmd struct { |
||||
name, descr string |
||||
flag *bool |
||||
callback func(*mcmp.Component) |
||||
} |
||||
|
||||
// CLISubCommand establishes a sub-command which can be activated on the
|
||||
// command-line. When a sub-command is given on the command-line, the bool
|
||||
// returned for that sub-command will be set to true.
|
||||
//
|
||||
// Additionally, the Component which was passed into Parse (i.e. the one passed
|
||||
// into Populate) will be passed into the given callback, and can be modified
|
||||
// for subsequent parsing. This allows for setting sub-command specific Params,
|
||||
// sub-command specific runtime behavior (via mrun.WithStartHook), support for
|
||||
// sub-sub-commands, and more. The callback may be nil.
|
||||
//
|
||||
// If any sub-commands have been defined on a Component which is passed into
|
||||
// Parse, it is assumed that a sub-command is required on the command-line.
|
||||
//
|
||||
// When parsing the command-line options, it is assumed that sub-commands will
|
||||
// be found before any other options.
|
||||
//
|
||||
// This function panics if not called on a root Component (i.e. a Component
|
||||
// which has no parents).
|
||||
func CLISubCommand(cmp *mcmp.Component, name, descr string, callback func(*mcmp.Component)) *bool { |
||||
if len(cmp.Path()) != 0 { |
||||
panic("CLISubCommand can only be used on a root Component") |
||||
} |
||||
|
||||
m, _ := cmp.Value(cliKeySubCmdM).(map[string]subCmd) |
||||
if m == nil { |
||||
m = map[string]subCmd{} |
||||
cmp.SetValue(cliKeySubCmdM, m) |
||||
} |
||||
|
||||
flag := new(bool) |
||||
m[name] = subCmd{ |
||||
name: name, |
||||
descr: descr, |
||||
flag: flag, |
||||
callback: callback, |
||||
} |
||||
return flag |
||||
} |
||||
|
||||
// SourceCLI is a Source which will parse configuration from the CLI.
|
||||
//
|
||||
// Possible CLI options are generated by joining a Param's Path and Name with
|
||||
// dashes. For example:
|
||||
//
|
||||
// cmp := new(mcmp.Component)
|
||||
// cmpFoo = cmp.Child("foo")
|
||||
// cmpFooBar = foo.Child("bar")
|
||||
// addr := mcfg.String(cmpFooBar, "addr", "", "Some address")
|
||||
// // the CLI option to fill addr will be "--foo-bar-addr"
|
||||
//
|
||||
// If the "-h" option is seen then a help page will be printed to stdout and the
|
||||
// process will exit. Since all normally-defined parameters must being with
|
||||
// double-dash ("--") they won't ever conflict with the help option.
|
||||
//
|
||||
// SourceCLI behaves a little differently with boolean parameters. Setting the
|
||||
// value of a boolean parameter directly _must_ be done with an equals, or with
|
||||
// no value at all. For example: `--boolean-flag`, `--boolean-flag=1` or
|
||||
// `--boolean-flag=false`. Using the space-separated format will not work. If a
|
||||
// boolean has no equal-separated value it is assumed to be setting the value to
|
||||
// `true`.
|
||||
type SourceCLI struct { |
||||
Args []string // if nil then os.Args[1:] is used
|
||||
|
||||
DisableHelpPage bool |
||||
} |
||||
|
||||
var _ Source = new(SourceCLI) |
||||
|
||||
const ( |
||||
cliKeyJoin = "-" |
||||
cliKeyPrefix = "--" |
||||
cliValSep = "=" |
||||
cliHelpArg = "-h" |
||||
) |
||||
|
||||
// Parse implements the method for the Source interface.
|
||||
func (cli *SourceCLI) Parse(cmp *mcmp.Component) ([]ParamValue, error) { |
||||
args := cli.Args |
||||
if cli.Args == nil { |
||||
args = os.Args[1:] |
||||
} |
||||
return cli.parse(cmp, nil, args) |
||||
} |
||||
|
||||
func (cli *SourceCLI) parse( |
||||
cmp *mcmp.Component, |
||||
subCmdPrefix, args []string, |
||||
) ( |
||||
[]ParamValue, |
||||
error, |
||||
) { |
||||
pM, err := cli.cliParams(CollectParams(cmp)) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
printHelpAndExit := func() { |
||||
// TODO check DisableHelpPage here?
|
||||
cli.printHelp(cmp, os.Stderr, subCmdPrefix, pM) |
||||
os.Stderr.Sync() |
||||
os.Exit(1) |
||||
} |
||||
|
||||
// if sub-commands were defined on this Component then handle that first.
|
||||
// One of them should have been given, in which case send the Context
|
||||
// through the callback to obtain a new one (which presumably has further
|
||||
// config options the previous didn't) and call parse again.
|
||||
subCmdM, _ := cmp.Value(cliKeySubCmdM).(map[string]subCmd) |
||||
if len(subCmdM) > 0 { |
||||
subCmd, args, ok := cli.getSubCmd(subCmdM, args) |
||||
if !ok { |
||||
printHelpAndExit() |
||||
} |
||||
cmp.SetValue(cliKeySubCmdM, nil) |
||||
if subCmd.callback != nil { |
||||
subCmd.callback(cmp) |
||||
} |
||||
subCmdPrefix = append(subCmdPrefix, subCmd.name) |
||||
*subCmd.flag = true |
||||
return cli.parse(cmp, subCmdPrefix, args) |
||||
} |
||||
|
||||
// if sub-commands were not set, then proceed with normal command-line arg
|
||||
// processing.
|
||||
pvs := make([]ParamValue, 0, len(args)) |
||||
var ( |
||||
key string |
||||
p Param |
||||
pOk bool |
||||
pvStrVal string |
||||
pvStrValOk bool |
||||
) |
||||
for i, arg := range args { |
||||
if pOk { |
||||
pvStrVal = arg |
||||
pvStrValOk = true |
||||
} else if !cli.DisableHelpPage && arg == cliHelpArg { |
||||
printHelpAndExit() |
||||
} else { |
||||
for key, p = range pM { |
||||
if arg == key { |
||||
pOk = true |
||||
break |
||||
} |
||||
|
||||
prefix := key + cliValSep |
||||
if !strings.HasPrefix(arg, prefix) { |
||||
continue |
||||
} |
||||
pOk = true |
||||
pvStrVal = strings.TrimPrefix(arg, prefix) |
||||
pvStrValOk = true |
||||
break |
||||
} |
||||
if !pOk { |
||||
if ok := populateCLITail(cmp, args[i:]); ok { |
||||
return pvs, nil |
||||
} |
||||
return nil, merr.New("unexpected config parameter", |
||||
mctx.Annotated("param", arg)) |
||||
} |
||||
} |
||||
|
||||
// pOk is always true at this point, and so p is filled in
|
||||
|
||||
// As a special case for CLI, if a boolean has no value set it means it
|
||||
// is true.
|
||||
if p.IsBool && !pvStrValOk { |
||||
pvStrVal = "true" |
||||
} else if !pvStrValOk { |
||||
// everything else should have a value. if pvStrVal isn't filled it
|
||||
// means the next arg should be one. Continue the loop, it'll get
|
||||
// filled with the next one (hopefully)
|
||||
continue |
||||
} |
||||
|
||||
pvs = append(pvs, ParamValue{ |
||||
Name: p.Name, |
||||
Path: p.Component.Path(), |
||||
Value: p.fuzzyParse(pvStrVal), |
||||
}) |
||||
|
||||
key = "" |
||||
p = Param{} |
||||
pOk = false |
||||
pvStrVal = "" |
||||
pvStrValOk = false |
||||
} |
||||
if pOk && !pvStrValOk { |
||||
ctx := mctx.Annotate(p.Component.Context(), "param", key) |
||||
return nil, merr.New("param expected a value", ctx) |
||||
} |
||||
|
||||
return pvs, nil |
||||
} |
||||
|
||||
func (cli *SourceCLI) getSubCmd(subCmdM map[string]subCmd, args []string) (subCmd, []string, bool) { |
||||
if len(args) == 0 { |
||||
return subCmd{}, args, false |
||||
} |
||||
|
||||
s, ok := subCmdM[args[0]] |
||||
if !ok { |
||||
return subCmd{}, args, false |
||||
} |
||||
|
||||
return s, args[1:], true |
||||
} |
||||
|
||||
func (cli *SourceCLI) cliParams(params []Param) (map[string]Param, error) { |
||||
m := map[string]Param{} |
||||
for _, p := range params { |
||||
key := strings.Join(append(p.Component.Path(), p.Name), cliKeyJoin) |
||||
m[cliKeyPrefix+key] = p |
||||
} |
||||
return m, nil |
||||
} |
||||
|
||||
func (cli *SourceCLI) printHelp( |
||||
cmp *mcmp.Component, |
||||
w io.Writer, |
||||
subCmdPrefix []string, |
||||
pM map[string]Param, |
||||
) { |
||||
type pEntry struct { |
||||
arg string |
||||
Param |
||||
} |
||||
|
||||
pA := make([]pEntry, 0, len(pM)) |
||||
for arg, p := range pM { |
||||
pA = append(pA, pEntry{arg: arg, Param: p}) |
||||
} |
||||
|
||||
sort.Slice(pA, func(i, j int) bool { |
||||
if pA[i].Required != pA[j].Required { |
||||
return pA[i].Required |
||||
} |
||||
return pA[i].arg < pA[j].arg |
||||
}) |
||||
|
||||
fmtDefaultVal := func(ptr interface{}) string { |
||||
if ptr == nil { |
||||
return "" |
||||
} |
||||
val := reflect.Indirect(reflect.ValueOf(ptr)) |
||||
zero := reflect.Zero(val.Type()) |
||||
if reflect.DeepEqual(val.Interface(), zero.Interface()) { |
||||
return "" |
||||
} else if val.Type().Kind() == reflect.String { |
||||
return fmt.Sprintf("%q", val.Interface()) |
||||
} |
||||
return fmt.Sprint(val.Interface()) |
||||
} |
||||
|
||||
type subCmdEntry struct { |
||||
name string |
||||
subCmd |
||||
} |
||||
|
||||
subCmdM, _ := cmp.Value(cliKeySubCmdM).(map[string]subCmd) |
||||
subCmdA := make([]subCmdEntry, 0, len(subCmdM)) |
||||
for name, subCmd := range subCmdM { |
||||
subCmdA = append(subCmdA, subCmdEntry{name: name, subCmd: subCmd}) |
||||
} |
||||
|
||||
sort.Slice(subCmdA, func(i, j int) bool { |
||||
return subCmdA[i].name < subCmdA[j].name |
||||
}) |
||||
|
||||
fmt.Fprintf(w, "Usage: %s", os.Args[0]) |
||||
if len(subCmdPrefix) > 0 { |
||||
fmt.Fprintf(w, " %s", strings.Join(subCmdPrefix, " ")) |
||||
} |
||||
if len(subCmdA) > 0 { |
||||
fmt.Fprint(w, " <sub-command>") |
||||
} |
||||
if len(pA) > 0 { |
||||
fmt.Fprint(w, " [options]") |
||||
} |
||||
if descr := getCLITailDescr(cmp); descr != "" { |
||||
fmt.Fprintf(w, " %s", descr) |
||||
} |
||||
fmt.Fprint(w, "\n\n") |
||||
|
||||
if len(subCmdA) > 0 { |
||||
fmt.Fprint(w, "Sub-commands:\n\n") |
||||
for _, subCmd := range subCmdA { |
||||
fmt.Fprintf(w, "\t%s\t%s\n", subCmd.name, subCmd.descr) |
||||
} |
||||
fmt.Fprint(w, "\n") |
||||
} |
||||
|
||||
if len(pA) > 0 { |
||||
fmt.Fprint(w, "Options:\n\n") |
||||
for _, p := range pA { |
||||
fmt.Fprintf(w, "\t%s", p.arg) |
||||
if p.IsBool { |
||||
fmt.Fprintf(w, " (Flag)") |
||||
} else if p.Required { |
||||
fmt.Fprintf(w, " (Required)") |
||||
} else if defVal := fmtDefaultVal(p.Into); defVal != "" { |
||||
fmt.Fprintf(w, " (Default: %s)", defVal) |
||||
} |
||||
fmt.Fprint(w, "\n") |
||||
if usage := p.Usage; usage != "" { |
||||
fmt.Fprintln(w, "\t\t"+usage) |
||||
} |
||||
fmt.Fprint(w, "\n") |
||||
} |
||||
} |
||||
} |
@ -1,356 +0,0 @@ |
||||
package mcfg |
||||
|
||||
import ( |
||||
"bytes" |
||||
"fmt" |
||||
"regexp" |
||||
"strings" |
||||
. "testing" |
||||
"time" |
||||
|
||||
"github.com/mediocregopher/mediocre-go-lib/mcmp" |
||||
"github.com/mediocregopher/mediocre-go-lib/mrand" |
||||
"github.com/mediocregopher/mediocre-go-lib/mtest/massert" |
||||
"github.com/mediocregopher/mediocre-go-lib/mtest/mchk" |
||||
"github.com/stretchr/testify/assert" |
||||
"github.com/stretchr/testify/require" |
||||
) |
||||
|
||||
func TestSourceCLIHelp(t *T) { |
||||
assertHelp := func(cmp *mcmp.Component, subCmdPrefix []string, exp string) { |
||||
buf := new(bytes.Buffer) |
||||
src := &SourceCLI{} |
||||
pM, err := src.cliParams(CollectParams(cmp)) |
||||
require.NoError(t, err) |
||||
src.printHelp(cmp, buf, subCmdPrefix, pM) |
||||
|
||||
out := buf.String() |
||||
ok := regexp.MustCompile(exp).MatchString(out) |
||||
assert.True(t, ok, "exp:%s (%q)\ngot:%s (%q)", exp, exp, out, out) |
||||
} |
||||
|
||||
cmp := new(mcmp.Component) |
||||
assertHelp(cmp, nil, `^Usage: \S+ |
||||
|
||||
$`) |
||||
assertHelp(cmp, []string{"foo", "bar"}, `^Usage: \S+ foo bar |
||||
|
||||
$`) |
||||
|
||||
Int(cmp, "foo", ParamDefault(5), ParamUsage("Test int param ")) // trailing space should be trimmed
|
||||
Bool(cmp, "bar", ParamUsage("Test bool param.")) |
||||
String(cmp, "baz", ParamDefault("baz"), ParamUsage("Test string param")) |
||||
String(cmp, "baz2", ParamUsage("Required string param"), ParamRequired()) |
||||
String(cmp, "baz3", ParamRequired()) |
||||
|
||||
assertHelp(cmp, nil, `^Usage: \S+ \[options\] |
||||
|
||||
Options: |
||||
|
||||
--baz2 \(Required\) |
||||
Required string param. |
||||
|
||||
--baz3 \(Required\) |
||||
|
||||
--bar \(Flag\) |
||||
Test bool param. |
||||
|
||||
--baz \(Default: "baz"\) |
||||
Test string param. |
||||
|
||||
--foo \(Default: 5\) |
||||
Test int param. |
||||
|
||||
$`) |
||||
|
||||
assertHelp(cmp, []string{"foo", "bar"}, `^Usage: \S+ foo bar \[options\] |
||||
|
||||
Options: |
||||
|
||||
--baz2 \(Required\) |
||||
Required string param. |
||||
|
||||
--baz3 \(Required\) |
||||
|
||||
--bar \(Flag\) |
||||
Test bool param. |
||||
|
||||
--baz \(Default: "baz"\) |
||||
Test string param. |
||||
|
||||
--foo \(Default: 5\) |
||||
Test int param. |
||||
|
||||
$`) |
||||
|
||||
CLISubCommand(cmp, "first", "First sub-command", nil) |
||||
CLISubCommand(cmp, "second", "Second sub-command", nil) |
||||
assertHelp(cmp, []string{"foo", "bar"}, `^Usage: \S+ foo bar <sub-command> \[options\] |
||||
|
||||
Sub-commands: |
||||
|
||||
first First sub-command |
||||
second Second sub-command |
||||
|
||||
Options: |
||||
|
||||
--baz2 \(Required\) |
||||
Required string param. |
||||
|
||||
--baz3 \(Required\) |
||||
|
||||
--bar \(Flag\) |
||||
Test bool param. |
||||
|
||||
--baz \(Default: "baz"\) |
||||
Test string param. |
||||
|
||||
--foo \(Default: 5\) |
||||
Test int param. |
||||
|
||||
$`) |
||||
|
||||
CLITail(cmp, "[arg...]") |
||||
assertHelp(cmp, nil, `^Usage: \S+ <sub-command> \[options\] \[arg\.\.\.\] |
||||
|
||||
Sub-commands: |
||||
|
||||
first First sub-command |
||||
second Second sub-command |
||||
|
||||
Options: |
||||
|
||||
--baz2 \(Required\) |
||||
Required string param. |
||||
|
||||
--baz3 \(Required\) |
||||
|
||||
--bar \(Flag\) |
||||
Test bool param. |
||||
|
||||
--baz \(Default: "baz"\) |
||||
Test string param. |
||||
|
||||
--foo \(Default: 5\) |
||||
Test int param. |
||||
|
||||
$`) |
||||
} |
||||
|
||||
func TestSourceCLI(t *T) { |
||||
type state struct { |
||||
srcCommonState |
||||
*SourceCLI |
||||
} |
||||
|
||||
type params struct { |
||||
srcCommonParams |
||||
nonBoolWEq bool // use equal sign when setting value
|
||||
} |
||||
|
||||
chk := mchk.Checker{ |
||||
Init: func() mchk.State { |
||||
var s state |
||||
s.srcCommonState = newSrcCommonState() |
||||
s.SourceCLI = &SourceCLI{ |
||||
Args: make([]string, 0, 16), |
||||
} |
||||
return s |
||||
}, |
||||
Next: func(ss mchk.State) mchk.Action { |
||||
s := ss.(state) |
||||
var p params |
||||
p.srcCommonParams = s.srcCommonState.next() |
||||
// if the param is a bool or unset this won't get used, but w/e
|
||||
p.nonBoolWEq = mrand.Intn(2) == 0 |
||||
return mchk.Action{Params: p} |
||||
}, |
||||
Apply: func(ss mchk.State, a mchk.Action) (mchk.State, error) { |
||||
s := ss.(state) |
||||
p := a.Params.(params) |
||||
|
||||
s.srcCommonState = s.srcCommonState.applyCmpAndPV(p.srcCommonParams) |
||||
if !p.unset { |
||||
arg := cliKeyPrefix |
||||
if path := p.cmp.Path(); len(path) > 0 { |
||||
arg += strings.Join(path, cliKeyJoin) + cliKeyJoin |
||||
} |
||||
arg += p.name |
||||
if !p.isBool { |
||||
if p.nonBoolWEq { |
||||
arg += "=" |
||||
} else { |
||||
s.SourceCLI.Args = append(s.SourceCLI.Args, arg) |
||||
arg = "" |
||||
} |
||||
arg += p.nonBoolVal |
||||
} |
||||
s.SourceCLI.Args = append(s.SourceCLI.Args, arg) |
||||
} |
||||
|
||||
err := s.srcCommonState.assert(s.SourceCLI) |
||||
return s, err |
||||
}, |
||||
} |
||||
|
||||
if err := chk.RunFor(2 * time.Second); err != nil { |
||||
t.Fatal(err) |
||||
} |
||||
} |
||||
|
||||
func TestCLITail(t *T) { |
||||
cmp := new(mcmp.Component) |
||||
Int(cmp, "foo", ParamDefault(5)) |
||||
Bool(cmp, "bar") |
||||
|
||||
type testCase struct { |
||||
args []string |
||||
expTail []string |
||||
} |
||||
|
||||
cases := []testCase{ |
||||
{ |
||||
args: []string{"--foo", "5"}, |
||||
expTail: nil, |
||||
}, |
||||
{ |
||||
args: []string{"--foo", "5", "a", "b", "c"}, |
||||
expTail: []string{"a", "b", "c"}, |
||||
}, |
||||
{ |
||||
args: []string{"--foo=5", "a", "b", "c"}, |
||||
expTail: []string{"a", "b", "c"}, |
||||
}, |
||||
{ |
||||
args: []string{"--foo", "5", "--bar"}, |
||||
expTail: nil, |
||||
}, |
||||
{ |
||||
args: []string{"--foo", "5", "--bar", "a", "b", "c"}, |
||||
expTail: []string{"a", "b", "c"}, |
||||
}, |
||||
} |
||||
|
||||
for _, tc := range cases { |
||||
tail := CLITail(cmp, "foo") |
||||
err := Populate(cmp, &SourceCLI{Args: tc.args}) |
||||
massert.Require(t, massert.Comment(massert.All( |
||||
massert.Nil(err), |
||||
massert.Equal(tc.expTail, *tail), |
||||
), "tc: %#v", tc)) |
||||
} |
||||
} |
||||
|
||||
func ExampleCLITail() { |
||||
cmp := new(mcmp.Component) |
||||
foo := Int(cmp, "foo", ParamDefault(1), ParamUsage("Description of foo.")) |
||||
tail := CLITail(cmp, "[arg...]") |
||||
bar := String(cmp, "bar", ParamDefault("defaultVal"), ParamUsage("Description of bar.")) |
||||
|
||||
err := Populate(cmp, &SourceCLI{ |
||||
Args: []string{"--foo=100", "arg1", "arg2", "arg3"}, |
||||
}) |
||||
|
||||
fmt.Printf("err:%v foo:%v bar:%v tail:%#v\n", err, *foo, *bar, *tail) |
||||
// Output: err:<nil> foo:100 bar:defaultVal tail:[]string{"arg1", "arg2", "arg3"}
|
||||
} |
||||
|
||||
func TestCLISubCommand(t *T) { |
||||
var ( |
||||
cmp *mcmp.Component |
||||
foo *int |
||||
bar *int |
||||
baz *int |
||||
aFlag *bool |
||||
bFlag *bool |
||||
) |
||||
reset := func() { |
||||
foo, bar, baz, aFlag, bFlag = nil, nil, nil, nil, nil |
||||
cmp = new(mcmp.Component) |
||||
foo = Int(cmp, "foo") |
||||
aFlag = CLISubCommand(cmp, "a", "Description of a.", |
||||
func(cmp *mcmp.Component) { |
||||
bar = Int(cmp, "bar") |
||||
}) |
||||
bFlag = CLISubCommand(cmp, "b", "Description of b.", |
||||
func(cmp *mcmp.Component) { |
||||
baz = Int(cmp, "baz") |
||||
}) |
||||
} |
||||
|
||||
reset() |
||||
err := Populate(cmp, &SourceCLI{ |
||||
Args: []string{"a", "--foo=1", "--bar=2"}, |
||||
}) |
||||
massert.Require(t, |
||||
massert.Comment(massert.Nil(err), "%v", err), |
||||
massert.Equal(1, *foo), |
||||
massert.Equal(2, *bar), |
||||
massert.Nil(baz), |
||||
massert.Equal(true, *aFlag), |
||||
massert.Equal(false, *bFlag), |
||||
) |
||||
|
||||
reset() |
||||
err = Populate(cmp, &SourceCLI{ |
||||
Args: []string{"b", "--foo=1", "--baz=3"}, |
||||
}) |
||||
massert.Require(t, |
||||
massert.Comment(massert.Nil(err), "%v", err), |
||||
massert.Equal(1, *foo), |
||||
massert.Nil(bar), |
||||
massert.Equal(3, *baz), |
||||
massert.Equal(false, *aFlag), |
||||
massert.Equal(true, *bFlag), |
||||
) |
||||
} |
||||
|
||||
func ExampleCLISubCommand() { |
||||
var ( |
||||
cmp *mcmp.Component |
||||
foo, bar, baz *int |
||||
aFlag, bFlag *bool |
||||
) |
||||
|
||||
// resetExample re-initializes all variables used in this example. We'll
|
||||
// call it multiple times to show different behaviors depending on what
|
||||
// arguments are passed in.
|
||||
resetExample := func() { |
||||
// Create a new Component with a parameter "foo", which can be used across
|
||||
// all sub-commands.
|
||||
cmp = new(mcmp.Component) |
||||
foo = Int(cmp, "foo") |
||||
|
||||
// Create a sub-command "a", which has a parameter "bar" specific to it.
|
||||
aFlag = CLISubCommand(cmp, "a", "Description of a.", |
||||
func(cmp *mcmp.Component) { |
||||
bar = Int(cmp, "bar") |
||||
}) |
||||
|
||||
// Create a sub-command "b", which has a parameter "baz" specific to it.
|
||||
bFlag = CLISubCommand(cmp, "b", "Description of b.", |
||||
func(cmp *mcmp.Component) { |
||||
baz = Int(cmp, "baz") |
||||
}) |
||||
} |
||||
|
||||
// Use Populate with manually generated CLI arguments, calling the "a"
|
||||
// sub-command.
|
||||
resetExample() |
||||
args := []string{"a", "--foo=1", "--bar=2"} |
||||
if err := Populate(cmp, &SourceCLI{Args: args}); err != nil { |
||||
panic(err) |
||||
} |
||||
fmt.Printf("foo:%d bar:%d aFlag:%v bFlag:%v\n", *foo, *bar, *aFlag, *bFlag) |
||||
|
||||
// reset for another Populate, this time calling the "b" sub-command.
|
||||
resetExample() |
||||
args = []string{"b", "--foo=1", "--baz=3"} |
||||
if err := Populate(cmp, &SourceCLI{Args: args}); err != nil { |
||||
panic(err) |
||||
} |
||||
fmt.Printf("foo:%d baz:%d aFlag:%v bFlag:%v\n", *foo, *baz, *aFlag, *bFlag) |
||||
|
||||
// Output: foo:1 bar:2 aFlag:true bFlag:false
|
||||
// foo:1 baz:3 aFlag:false bFlag:true
|
||||
} |
@ -1,79 +0,0 @@ |
||||
package mcfg |
||||
|
||||
import ( |
||||
"os" |
||||
"strings" |
||||
|
||||
"github.com/mediocregopher/mediocre-go-lib/mcmp" |
||||
"github.com/mediocregopher/mediocre-go-lib/mctx" |
||||
"github.com/mediocregopher/mediocre-go-lib/merr" |
||||
) |
||||
|
||||
// SourceEnv is a Source which will parse configuration from the process
|
||||
// environment.
|
||||
//
|
||||
// Possible Env options are generated by joining a Param's Path and Name with
|
||||
// underscores and making all characters uppercase, as well as changing all
|
||||
// dashes to underscores.
|
||||
//
|
||||
// cmp := new(mcmp.Component)
|
||||
// cmpFoo := cmp.Child("foo")
|
||||
// cmpFooBar := cmp.Child("bar")
|
||||
// addr := mcfg.String(cmpFooBar, "srv-addr", "", "Some address")
|
||||
// // the Env option to fill addr will be "FOO_BAR_SRV_ADDR"
|
||||
//
|
||||
type SourceEnv struct { |
||||
// In the format key=value. Defaults to os.Environ() if nil.
|
||||
Env []string |
||||
|
||||
// If set then all expected Env options must be prefixed with this string,
|
||||
// which will be uppercased and have dashes replaced with underscores like
|
||||
// all the other parts of the option names.
|
||||
Prefix string |
||||
} |
||||
|
||||
var _ Source = new(SourceEnv) |
||||
|
||||
func (env *SourceEnv) expectedName(path []string, name string) string { |
||||
out := strings.Join(append(path, name), "_") |
||||
if env.Prefix != "" { |
||||
out = env.Prefix + "_" + out |
||||
} |
||||
out = strings.Replace(out, "-", "_", -1) |
||||
out = strings.ToUpper(out) |
||||
return out |
||||
} |
||||
|
||||
// Parse implements the method for the Source interface.
|
||||
func (env *SourceEnv) Parse(cmp *mcmp.Component) ([]ParamValue, error) { |
||||
kvs := env.Env |
||||
if kvs == nil { |
||||
kvs = os.Environ() |
||||
} |
||||
|
||||
params := CollectParams(cmp) |
||||
pM := map[string]Param{} |
||||
for _, p := range params { |
||||
name := env.expectedName(p.Component.Path(), p.Name) |
||||
pM[name] = p |
||||
} |
||||
|
||||
pvs := make([]ParamValue, 0, len(kvs)) |
||||
for _, kv := range kvs { |
||||
split := strings.SplitN(kv, "=", 2) |
||||
if len(split) != 2 { |
||||
return nil, merr.New("malformed environment key/value pair", |
||||
mctx.Annotated("kv", kv)) |
||||
} |
||||
k, v := split[0], split[1] |
||||
if p, ok := pM[k]; ok { |
||||
pvs = append(pvs, ParamValue{ |
||||
Name: p.Name, |
||||
Path: p.Component.Path(), |
||||
Value: p.fuzzyParse(v), |
||||
}) |
||||
} |
||||
} |
||||
|
||||
return pvs, nil |
||||
} |
@ -1,60 +0,0 @@ |
||||
package mcfg |
||||
|
||||
import ( |
||||
"strings" |
||||
. "testing" |
||||
"time" |
||||
|
||||
"github.com/mediocregopher/mediocre-go-lib/mtest/mchk" |
||||
) |
||||
|
||||
func TestSourceEnv(t *T) { |
||||
type state struct { |
||||
srcCommonState |
||||
*SourceEnv |
||||
} |
||||
|
||||
type params struct { |
||||
srcCommonParams |
||||
} |
||||
|
||||
chk := mchk.Checker{ |
||||
Init: func() mchk.State { |
||||
var s state |
||||
s.srcCommonState = newSrcCommonState() |
||||
s.SourceEnv = &SourceEnv{ |
||||
Env: make([]string, 0, 16), |
||||
} |
||||
return s |
||||
}, |
||||
Next: func(ss mchk.State) mchk.Action { |
||||
s := ss.(state) |
||||
var p params |
||||
p.srcCommonParams = s.srcCommonState.next() |
||||
return mchk.Action{Params: p} |
||||
}, |
||||
Apply: func(ss mchk.State, a mchk.Action) (mchk.State, error) { |
||||
s := ss.(state) |
||||
p := a.Params.(params) |
||||
s.srcCommonState = s.srcCommonState.applyCmpAndPV(p.srcCommonParams) |
||||
if !p.unset { |
||||
kv := strings.Join(append(p.cmp.Path(), p.name), "_") |
||||
kv = strings.Replace(kv, "-", "_", -1) |
||||
kv = strings.ToUpper(kv) |
||||
kv += "=" |
||||
if p.isBool { |
||||
kv += "1" |
||||
} else { |
||||
kv += p.nonBoolVal |
||||
} |
||||
s.SourceEnv.Env = append(s.SourceEnv.Env, kv) |
||||
} |
||||
err := s.srcCommonState.assert(s.SourceEnv) |
||||
return s, err |
||||
}, |
||||
} |
||||
|
||||
if err := chk.RunFor(2 * time.Second); err != nil { |
||||
t.Fatal(err) |
||||
} |
||||
} |
@ -1,150 +0,0 @@ |
||||
// Package mcfg implements the creation of different types of configuration
|
||||
// parameters and various methods of filling those parameters from external
|
||||
// configuration sources (e.g. the command line and environment variables).
|
||||
//
|
||||
// Parameters are registered onto a Component, and that same Component (or one
|
||||
// of its ancestors) is used later to collect and fill those parameters.
|
||||
package mcfg |
||||
|
||||
import ( |
||||
"crypto/md5" |
||||
"encoding/hex" |
||||
"encoding/json" |
||||
"fmt" |
||||
"sort" |
||||
|
||||
"github.com/mediocregopher/mediocre-go-lib/mcmp" |
||||
"github.com/mediocregopher/mediocre-go-lib/mctx" |
||||
"github.com/mediocregopher/mediocre-go-lib/merr" |
||||
) |
||||
|
||||
// TODO Sources:
|
||||
// - JSON file
|
||||
// - YAML file
|
||||
|
||||
// TODO WithCLISubCommand does not play nice with the expected use-case of
|
||||
// having CLI params overwrite Env ones. If Env is specified first in the
|
||||
// Sources slice then it won't know about any extra Params which might get added
|
||||
// due to a sub-command, but if it's specified second then Env values will
|
||||
// overwrite CLI ones.
|
||||
|
||||
func sortParams(params []Param) { |
||||
sort.Slice(params, func(i, j int) bool { |
||||
a, b := params[i], params[j] |
||||
aPath, bPath := a.Component.Path(), b.Component.Path() |
||||
for { |
||||
switch { |
||||
case len(aPath) == 0 && len(bPath) == 0: |
||||
return a.Name < b.Name |
||||
case len(aPath) == 0 && len(bPath) > 0: |
||||
return false |
||||
case len(aPath) > 0 && len(bPath) == 0: |
||||
return true |
||||
case aPath[0] != bPath[0]: |
||||
return aPath[0] < bPath[0] |
||||
default: |
||||
aPath, bPath = aPath[1:], bPath[1:] |
||||
} |
||||
} |
||||
}) |
||||
} |
||||
|
||||
// CollectParams gathers all Params by recursively retrieving them from the
|
||||
// given Component and its children. Returned Params are sorted according to
|
||||
// their Path and Name.
|
||||
func CollectParams(cmp *mcmp.Component) []Param { |
||||
var params []Param |
||||
|
||||
var visit func(*mcmp.Component) |
||||
visit = func(cmp *mcmp.Component) { |
||||
for _, param := range getLocalParams(cmp) { |
||||
params = append(params, param) |
||||
} |
||||
|
||||
for _, childCmp := range cmp.Children() { |
||||
visit(childCmp) |
||||
} |
||||
} |
||||
visit(cmp) |
||||
|
||||
sortParams(params) |
||||
return params |
||||
} |
||||
|
||||
func paramHash(path []string, name string) string { |
||||
h := md5.New() |
||||
for _, pathEl := range path { |
||||
fmt.Fprintf(h, "pathEl:%q\n", pathEl) |
||||
} |
||||
fmt.Fprintf(h, "name:%q\n", name) |
||||
hStr := hex.EncodeToString(h.Sum(nil)) |
||||
// we add the displayName to it to make debugging easier
|
||||
return paramFullName(path, name) + "/" + hStr |
||||
} |
||||
|
||||
// Populate uses the Source to populate the values of all Params which were
|
||||
// added to the given Component, and all of its children. Populate may be called
|
||||
// multiple times with the same Component, each time will only affect the values
|
||||
// of the Params which were provided by the respective Source.
|
||||
//
|
||||
// Source may be nil to indicate that no configuration is provided. Only default
|
||||
// values will be used, and if any parameters are required this will error.
|
||||
//
|
||||
// Populating Params can affect the Component itself, for example in the case of
|
||||
// sub-commands.
|
||||
func Populate(cmp *mcmp.Component, src Source) error { |
||||
if src == nil { |
||||
src = ParamValues(nil) |
||||
} |
||||
|
||||
pvs, err := src.Parse(cmp) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
// map Params to their hash, so we can match them to their ParamValues.
|
||||
// later. There should not be any duplicates here.
|
||||
params := CollectParams(cmp) |
||||
pM := map[string]Param{} |
||||
for _, p := range params { |
||||
path := p.Component.Path() |
||||
hash := paramHash(path, p.Name) |
||||
if _, ok := pM[hash]; ok { |
||||
panic("duplicate Param: " + paramFullName(path, p.Name)) |
||||
} |
||||
pM[hash] = p |
||||
} |
||||
|
||||
// dedupe the ParamValues based on their hashes, with the last ParamValue
|
||||
// taking precedence. Also filter out those with no corresponding Param.
|
||||
pvM := map[string]ParamValue{} |
||||
for _, pv := range pvs { |
||||
hash := paramHash(pv.Path, pv.Name) |
||||
if _, ok := pM[hash]; !ok { |
||||
continue |
||||
} |
||||
pvM[hash] = pv |
||||
} |
||||
|
||||
// check for required params
|
||||
for hash, p := range pM { |
||||
if !p.Required { |
||||
continue |
||||
} else if _, ok := pvM[hash]; !ok { |
||||
ctx := mctx.Annotate(p.Component.Context(), |
||||
"param", paramFullName(p.Component.Path(), p.Name)) |
||||
return merr.New("required parameter is not set", ctx) |
||||
} |
||||
} |
||||
|
||||
// do the actual populating
|
||||
for hash, pv := range pvM { |
||||
// at this point, all ParamValues in pvM have a corresponding pM Param
|
||||
p := pM[hash] |
||||
if err := json.Unmarshal(pv.Value, p.Into); err != nil { |
||||
return err |
||||
} |
||||
} |
||||
|
||||
return nil |
||||
} |
@ -1,69 +0,0 @@ |
||||
package mcfg |
||||
|
||||
import ( |
||||
. "testing" |
||||
|
||||
"github.com/mediocregopher/mediocre-go-lib/mcmp" |
||||
"github.com/stretchr/testify/assert" |
||||
) |
||||
|
||||
func TestPopulate(t *T) { |
||||
{ |
||||
cmp := new(mcmp.Component) |
||||
a := Int(cmp, "a") |
||||
cmpFoo := cmp.Child("foo") |
||||
b := Int(cmpFoo, "b") |
||||
c := Int(cmpFoo, "c") |
||||
d := Int(cmp, "d", ParamDefault(4)) |
||||
|
||||
err := Populate(cmp, &SourceCLI{ |
||||
Args: []string{"--a=1", "--foo-b=2"}, |
||||
}) |
||||
assert.NoError(t, err) |
||||
assert.Equal(t, 1, *a) |
||||
assert.Equal(t, 2, *b) |
||||
assert.Equal(t, 0, *c) |
||||
assert.Equal(t, 4, *d) |
||||
} |
||||
|
||||
{ // test that required params are enforced
|
||||
cmp := new(mcmp.Component) |
||||
a := Int(cmp, "a") |
||||
cmpFoo := cmp.Child("foo") |
||||
b := Int(cmpFoo, "b") |
||||
c := Int(cmpFoo, "c", ParamRequired()) |
||||
|
||||
err := Populate(cmp, &SourceCLI{ |
||||
Args: []string{"--a=1", "--foo-b=2"}, |
||||
}) |
||||
assert.Error(t, err) |
||||
|
||||
err = Populate(cmp, &SourceCLI{ |
||||
Args: []string{"--a=1", "--foo-b=2", "--foo-c=3"}, |
||||
}) |
||||
assert.NoError(t, err) |
||||
assert.Equal(t, 1, *a) |
||||
assert.Equal(t, 2, *b) |
||||
assert.Equal(t, 3, *c) |
||||
} |
||||
} |
||||
|
||||
func TestParamDefaultOrRequired(t *T) { |
||||
{ |
||||
cmp := new(mcmp.Component) |
||||
Int(cmp, "a", ParamDefaultOrRequired(0)) |
||||
params := CollectParams(cmp) |
||||
assert.Equal(t, "a", params[0].Name) |
||||
assert.Equal(t, true, params[0].Required) |
||||
assert.Equal(t, new(int), params[0].Into) |
||||
} |
||||
{ |
||||
cmp := new(mcmp.Component) |
||||
Int(cmp, "a", ParamDefaultOrRequired(1)) |
||||
i := 1 |
||||
params := CollectParams(cmp) |
||||
assert.Equal(t, "a", params[0].Name) |
||||
assert.Equal(t, false, params[0].Required) |
||||
assert.Equal(t, &i, params[0].Into) |
||||
} |
||||
} |
@ -1,237 +0,0 @@ |
||||
package mcfg |
||||
|
||||
import ( |
||||
"encoding/json" |
||||
"fmt" |
||||
"reflect" |
||||
"strings" |
||||
|
||||
"github.com/mediocregopher/mediocre-go-lib/mcmp" |
||||
"github.com/mediocregopher/mediocre-go-lib/mtime" |
||||
) |
||||
|
||||
// Param is a configuration parameter which can be populated by Populate. The
|
||||
// Param will exist as part of a Component. For example, a Param with name
|
||||
// "addr" under a Component with path of []string{"foo","bar"} will be setable
|
||||
// on the CLI via "--foo-bar-addr". Other configuration Sources may treat the
|
||||
// path/name differently, however.
|
||||
//
|
||||
// Param values are always unmarshaled as JSON values into the Into field of the
|
||||
// Param, regardless of the actual Source.
|
||||
type Param struct { |
||||
// How the parameter will be identified within a Component.
|
||||
Name string |
||||
|
||||
// A helpful description of how a parameter is expected to be used.
|
||||
Usage string |
||||
|
||||
// If the parameter's value is expected to be read as a go string. This is
|
||||
// used for configuration sources like CLI which will automatically add
|
||||
// double-quotes around the value if they aren't already there.
|
||||
IsString bool |
||||
|
||||
// If the parameter's value is expected to be a boolean. This is used for
|
||||
// configuration sources like CLI which treat boolean parameters (aka flags)
|
||||
// differently.
|
||||
IsBool bool |
||||
|
||||
// If true then the parameter _must_ be set by at least one Source.
|
||||
Required bool |
||||
|
||||
// The pointer/interface into which the configuration value will be
|
||||
// json.Unmarshal'd. The value being pointed to also determines the default
|
||||
// value of the parameter.
|
||||
Into interface{} |
||||
|
||||
// The Component this Param was added to. NOTE that this will be
|
||||
// automatically filled in by AddParam when the Param is added to the
|
||||
// Component.
|
||||
Component *mcmp.Component |
||||
} |
||||
|
||||
// ParamOption is a modifier which can be passed into most Param-generating
|
||||
// functions (e.g. String, Int, etc...)
|
||||
type ParamOption func(*Param) |
||||
|
||||
// ParamRequired returns a ParamOption which ensures the parameter is required
|
||||
// to be set by some configuration source. The default value of the parameter
|
||||
// will be ignored.
|
||||
func ParamRequired() ParamOption { |
||||
return func(param *Param) { |
||||
param.Required = true |
||||
} |
||||
} |
||||
|
||||
// ParamDefault returns a ParamOption which ensures the parameter uses the given
|
||||
// default value when no Sources set a value for it. If not given then mcfg will
|
||||
// use the zero value of the Param's type as the default value.
|
||||
//
|
||||
// If ParamRequired is given then this does nothing.
|
||||
func ParamDefault(value interface{}) ParamOption { |
||||
return func(param *Param) { |
||||
intoV := reflect.ValueOf(param.Into).Elem() |
||||
valueV := reflect.ValueOf(value) |
||||
|
||||
intoType, valueType := intoV.Type(), valueV.Type() |
||||
if intoType != valueType { |
||||
panic(fmt.Sprintf("ParamDefault value is type %s, but should be %s", valueType, intoType)) |
||||
} else if !intoV.CanSet() { |
||||
panic(fmt.Sprintf("Param.Into value %#v can't be set using reflection", param.Into)) |
||||
} |
||||
|
||||
intoV.Set(valueV) |
||||
} |
||||
} |
||||
|
||||
// ParamDefaultOrRequired returns a ParamOption whose behavior depends on the
|
||||
// given value. If the given value is the zero value for its type, then this returns
|
||||
// ParamRequired(), otherwise this returns ParamDefault(value).
|
||||
func ParamDefaultOrRequired(value interface{}) ParamOption { |
||||
v := reflect.ValueOf(value) |
||||
zero := reflect.Zero(v.Type()) |
||||
if v.Interface() == zero.Interface() { |
||||
return ParamRequired() |
||||
} |
||||
return ParamDefault(value) |
||||
} |
||||
|
||||
// ParamUsage returns a ParamOption which sets the usage string on the Param.
|
||||
// This is used in some Sources, like SourceCLI, when displaying information
|
||||
// about available parameters.
|
||||
func ParamUsage(usage string) ParamOption { |
||||
// make all usages end with a period, because I say so
|
||||
usage = strings.TrimSpace(usage) |
||||
if !strings.HasSuffix(usage, ".") { |
||||
usage += "." |
||||
} |
||||
|
||||
return func(param *Param) { |
||||
param.Usage = usage |
||||
} |
||||
} |
||||
|
||||
func paramFullName(path []string, name string) string { |
||||
return strings.Join(append(path, name), "-") |
||||
} |
||||
|
||||
func (p Param) fuzzyParse(v string) json.RawMessage { |
||||
if p.IsBool { |
||||
if v == "" || v == "0" || v == "false" { |
||||
return json.RawMessage("false") |
||||
} |
||||
return json.RawMessage("true") |
||||
|
||||
} else if p.IsString && (v == "" || v[0] != '"') { |
||||
return json.RawMessage(`"` + v + `"`) |
||||
} |
||||
|
||||
return json.RawMessage(v) |
||||
} |
||||
|
||||
type cmpParamKey string |
||||
|
||||
// used in tests
|
||||
func getParam(cmp *mcmp.Component, name string) (Param, bool) { |
||||
param, ok := cmp.Value(cmpParamKey(name)).(Param) |
||||
return param, ok |
||||
} |
||||
|
||||
// AddParam adds the given Param to the given Component. It will panic if a
|
||||
// Param with the same Name already exists in the Component.
|
||||
func AddParam(cmp *mcmp.Component, param Param, opts ...ParamOption) { |
||||
param.Name = strings.ToLower(param.Name) |
||||
param.Component = cmp |
||||
key := cmpParamKey(param.Name) |
||||
|
||||
if cmp.HasValue(key) { |
||||
path := cmp.Path() |
||||
panic(fmt.Sprintf("Component.Path:%#v Param.Name:%q already exists", path, param.Name)) |
||||
} |
||||
|
||||
for _, opt := range opts { |
||||
opt(¶m) |
||||
} |
||||
cmp.SetValue(key, param) |
||||
} |
||||
|
||||
func getLocalParams(cmp *mcmp.Component) []Param { |
||||
values := cmp.Values() |
||||
params := make([]Param, 0, len(values)) |
||||
for _, val := range values { |
||||
if param, ok := val.(Param); ok { |
||||
params = append(params, param) |
||||
} |
||||
} |
||||
return params |
||||
} |
||||
|
||||
// Int64 returns an *int64 which will be populated once Populate is run on the
|
||||
// Component.
|
||||
func Int64(cmp *mcmp.Component, name string, opts ...ParamOption) *int64 { |
||||
var i int64 |
||||
AddParam(cmp, Param{Name: name, Into: &i}, opts...) |
||||
return &i |
||||
} |
||||
|
||||
// Int returns an *int which will be populated once Populate is run on the
|
||||
// Component.
|
||||
func Int(cmp *mcmp.Component, name string, opts ...ParamOption) *int { |
||||
var i int |
||||
AddParam(cmp, Param{Name: name, Into: &i}, opts...) |
||||
return &i |
||||
} |
||||
|
||||
// Float64 returns a *float64 which will be populated once Populate is run on
|
||||
// the Component
|
||||
func Float64(cmp *mcmp.Component, name string, opts ...ParamOption) *float64 { |
||||
var f float64 |
||||
AddParam(cmp, Param{Name: name, Into: &f}, opts...) |
||||
return &f |
||||
} |
||||
|
||||
// String returns a *string which will be populated once Populate is run on
|
||||
// the Component.
|
||||
func String(cmp *mcmp.Component, name string, opts ...ParamOption) *string { |
||||
var s string |
||||
AddParam(cmp, Param{Name: name, IsString: true, Into: &s}, opts...) |
||||
return &s |
||||
} |
||||
|
||||
// Bool returns a *bool which will be populated once Populate is run on the
|
||||
// Component, and which defaults to false if unconfigured.
|
||||
//
|
||||
// The default behavior of all Sources is that a boolean parameter will be set
|
||||
// to true unless the value is "", 0, or false. In the case of the CLI Source
|
||||
// the value will also be true when the parameter is used with no value at all,
|
||||
// as would be expected.
|
||||
func Bool(cmp *mcmp.Component, name string, opts ...ParamOption) *bool { |
||||
var b bool |
||||
AddParam(cmp, Param{Name: name, IsBool: true, Into: &b}, opts...) |
||||
return &b |
||||
} |
||||
|
||||
// TS returns an *mtime.TS which will be populated once Populate is run on
|
||||
// the Component.
|
||||
func TS(cmp *mcmp.Component, name string, opts ...ParamOption) *mtime.TS { |
||||
var t mtime.TS |
||||
AddParam(cmp, Param{Name: name, Into: &t}, opts...) |
||||
return &t |
||||
} |
||||
|
||||
// Duration returns an *mtime.Duration which will be populated once Populate
|
||||
// is run on the Component.
|
||||
func Duration(cmp *mcmp.Component, name string, opts ...ParamOption) *mtime.Duration { |
||||
var d mtime.Duration |
||||
AddParam(cmp, Param{Name: name, IsString: true, Into: &d}, opts...) |
||||
return &d |
||||
} |
||||
|
||||
// JSON reads the parameter value as a JSON value and unmarshals it into the
|
||||
// given interface{} (which should be a pointer) once Populate is run on the
|
||||
// Component.
|
||||
//
|
||||
// The receiver (into) is also used to determine the default value. ParamDefault
|
||||
// should not be used as one of the opts.
|
||||
func JSON(cmp *mcmp.Component, name string, into interface{}, opts ...ParamOption) { |
||||
AddParam(cmp, Param{Name: name, Into: into}, opts...) |
||||
} |
@ -1,65 +0,0 @@ |
||||
package mcfg |
||||
|
||||
import ( |
||||
"encoding/json" |
||||
|
||||
"github.com/mediocregopher/mediocre-go-lib/mcmp" |
||||
) |
||||
|
||||
// ParamValue describes a value for a parameter which has been parsed by a
|
||||
// Source.
|
||||
type ParamValue struct { |
||||
Name string |
||||
Path []string |
||||
Value json.RawMessage |
||||
} |
||||
|
||||
// Source parses ParamValues out of a particular configuration source, given the
|
||||
// Component which the Params were added to (via WithInt, WithString, etc...).
|
||||
// CollectParams can be used to retrieve these Params.
|
||||
//
|
||||
// It's possible for Parsing to affect the Component itself, for example in the
|
||||
// case of sub-commands.
|
||||
//
|
||||
// Source should not return ParamValues which were not explicitly set to a value
|
||||
// by the configuration source.
|
||||
//
|
||||
// The returned []ParamValue may contain duplicates of the same Param's value.
|
||||
// in which case the latter value takes precedence. It may also contain
|
||||
// ParamValues which do not correspond to any of the passed in Params. These
|
||||
// will be ignored in Populate.
|
||||
type Source interface { |
||||
Parse(*mcmp.Component) ([]ParamValue, error) |
||||
} |
||||
|
||||
// ParamValues is simply a slice of ParamValue elements, which implements Parse
|
||||
// by always returning itself as-is.
|
||||
type ParamValues []ParamValue |
||||
|
||||
var _ Source = ParamValues{} |
||||
|
||||
// Parse implements the method for the Source interface.
|
||||
func (pvs ParamValues) Parse(*mcmp.Component) ([]ParamValue, error) { |
||||
return pvs, nil |
||||
} |
||||
|
||||
// Sources combines together multiple Source instances into one. It will call
|
||||
// Parse on each element individually. Values from later Sources take precedence
|
||||
// over previous ones.
|
||||
type Sources []Source |
||||
|
||||
var _ Source = Sources{} |
||||
|
||||
// Parse implements the method for the Source interface.
|
||||
func (ss Sources) Parse(cmp *mcmp.Component) ([]ParamValue, error) { |
||||
var pvs []ParamValue |
||||
for _, s := range ss { |
||||
var innerPVs []ParamValue |
||||
var err error |
||||
if innerPVs, err = s.Parse(cmp); err != nil { |
||||
return nil, err |
||||
} |
||||
pvs = append(pvs, innerPVs...) |
||||
} |
||||
return pvs, nil |
||||
} |
@ -1,178 +0,0 @@ |
||||
package mcfg |
||||
|
||||
import ( |
||||
"encoding/json" |
||||
"fmt" |
||||
. "testing" |
||||
"time" |
||||
|
||||
"github.com/mediocregopher/mediocre-go-lib/mcmp" |
||||
"github.com/mediocregopher/mediocre-go-lib/mrand" |
||||
"github.com/mediocregopher/mediocre-go-lib/mtest/massert" |
||||
) |
||||
|
||||
// The tests for the different Sources use mchk as their primary method of
|
||||
// checking. They end up sharing a lot of the same functionality, so in here is
|
||||
// all the code they share
|
||||
|
||||
type srcCommonState struct { |
||||
// availCmps get updated in place as the run goes on, it's easier to keep
|
||||
// track of them this way than by traversing the hierarchy.
|
||||
availCmps []*mcmp.Component |
||||
|
||||
expPVs []ParamValue |
||||
// each specific test should wrap this to add the Source itself
|
||||
} |
||||
|
||||
func newSrcCommonState() srcCommonState { |
||||
var scs srcCommonState |
||||
{ |
||||
root := new(mcmp.Component) |
||||
a := root.Child("a") |
||||
b := root.Child("b") |
||||
c := root.Child("c") |
||||
ab := a.Child("b") |
||||
bc := b.Child("c") |
||||
abc := ab.Child("c") |
||||
scs.availCmps = []*mcmp.Component{root, a, b, c, ab, bc, abc} |
||||
} |
||||
return scs |
||||
} |
||||
|
||||
type srcCommonParams struct { |
||||
name string |
||||
cmp *mcmp.Component |
||||
isBool bool |
||||
nonBoolType string // "int", "str", "duration", "json"
|
||||
unset bool |
||||
nonBoolVal string |
||||
} |
||||
|
||||
func (scs srcCommonState) next() srcCommonParams { |
||||
var p srcCommonParams |
||||
if i := mrand.Intn(8); i == 0 { |
||||
p.name = mrand.Hex(1) + "-" + mrand.Hex(8) |
||||
} else { |
||||
p.name = mrand.Hex(8) |
||||
} |
||||
|
||||
availCmpI := mrand.Intn(len(scs.availCmps)) |
||||
p.cmp = scs.availCmps[availCmpI] |
||||
|
||||
p.isBool = mrand.Intn(8) == 0 |
||||
if !p.isBool { |
||||
p.nonBoolType = mrand.Element([]string{ |
||||
"int", |
||||
"str", |
||||
"duration", |
||||
"json", |
||||
}, nil).(string) |
||||
} |
||||
p.unset = mrand.Intn(10) == 0 |
||||
|
||||
if p.isBool || p.unset { |
||||
return p |
||||
} |
||||
|
||||
switch p.nonBoolType { |
||||
case "int": |
||||
p.nonBoolVal = fmt.Sprint(mrand.Int()) |
||||
case "str": |
||||
p.nonBoolVal = mrand.Hex(16) |
||||
case "duration": |
||||
dur := time.Duration(mrand.Intn(86400)) * time.Second |
||||
p.nonBoolVal = dur.String() |
||||
case "json": |
||||
b, _ := json.Marshal(map[string]int{ |
||||
mrand.Hex(4): mrand.Int(), |
||||
mrand.Hex(4): mrand.Int(), |
||||
mrand.Hex(4): mrand.Int(), |
||||
}) |
||||
p.nonBoolVal = string(b) |
||||
} |
||||
return p |
||||
} |
||||
|
||||
// adds the new param to the cmp, and if the param is expected to be set in
|
||||
// the Source adds it to the expected ParamValues as well
|
||||
func (scs srcCommonState) applyCmpAndPV(p srcCommonParams) srcCommonState { |
||||
param := Param{ |
||||
Name: p.name, |
||||
IsString: p.nonBoolType == "str" || p.nonBoolType == "duration", |
||||
IsBool: p.isBool, |
||||
// the Sources don't actually care about the other fields of Param,
|
||||
// those are only used by Populate once it has all ParamValues together
|
||||
} |
||||
AddParam(p.cmp, param) |
||||
param, _ = getParam(p.cmp, param.Name) // get it back out to get any added fields
|
||||
|
||||
if !p.unset { |
||||
pv := ParamValue{Name: param.Name, Path: p.cmp.Path()} |
||||
if p.isBool { |
||||
pv.Value = json.RawMessage("true") |
||||
} else { |
||||
switch p.nonBoolType { |
||||
case "str", "duration": |
||||
pv.Value = json.RawMessage(fmt.Sprintf("%q", p.nonBoolVal)) |
||||
case "int", "json": |
||||
pv.Value = json.RawMessage(p.nonBoolVal) |
||||
default: |
||||
panic("shouldn't get here") |
||||
} |
||||
} |
||||
scs.expPVs = append(scs.expPVs, pv) |
||||
} |
||||
|
||||
return scs |
||||
} |
||||
|
||||
// given a Source asserts that it's Parse method returns the expected
|
||||
// ParamValues
|
||||
func (scs srcCommonState) assert(s Source) error { |
||||
gotPVs, err := s.Parse(scs.availCmps[0]) // Parse(root)
|
||||
if err != nil { |
||||
return err |
||||
} |
||||
return massert.All( |
||||
massert.Length(gotPVs, len(scs.expPVs)), |
||||
massert.Subset(scs.expPVs, gotPVs), |
||||
).Assert() |
||||
} |
||||
|
||||
func TestSources(t *T) { |
||||
cmp := new(mcmp.Component) |
||||
a := Int(cmp, "a", ParamRequired()) |
||||
b := Int(cmp, "b", ParamRequired()) |
||||
c := Int(cmp, "c", ParamRequired()) |
||||
|
||||
err := Populate(cmp, Sources{ |
||||
&SourceCLI{Args: []string{"--a=1", "--b=666"}}, |
||||
&SourceEnv{Env: []string{"B=2", "C=3"}}, |
||||
}) |
||||
massert.Require(t, |
||||
massert.Nil(err), |
||||
massert.Equal(1, *a), |
||||
massert.Equal(2, *b), |
||||
massert.Equal(3, *c), |
||||
) |
||||
} |
||||
|
||||
func TestSourceParamValues(t *T) { |
||||
cmp := new(mcmp.Component) |
||||
a := Int(cmp, "a", ParamRequired()) |
||||
cmpFoo := cmp.Child("foo") |
||||
b := String(cmpFoo, "b", ParamRequired()) |
||||
c := Bool(cmpFoo, "c") |
||||
|
||||
err := Populate(cmp, ParamValues{ |
||||
{Name: "a", Value: json.RawMessage(`4`)}, |
||||
{Path: []string{"foo"}, Name: "b", Value: json.RawMessage(`"bbb"`)}, |
||||
{Path: []string{"foo"}, Name: "c", Value: json.RawMessage("true")}, |
||||
}) |
||||
massert.Require(t, |
||||
massert.Nil(err), |
||||
massert.Equal(4, *a), |
||||
massert.Equal("bbb", *b), |
||||
massert.Equal(true, *c), |
||||
) |
||||
} |
@ -1,225 +0,0 @@ |
||||
package mcmp |
||||
|
||||
import ( |
||||
"context" |
||||
"fmt" |
||||
"strings" |
||||
"sync" |
||||
|
||||
"github.com/mediocregopher/mediocre-go-lib/mctx" |
||||
) |
||||
|
||||
type child struct { |
||||
*Component |
||||
name string |
||||
} |
||||
|
||||
// Component describes a single component of a program, and holds onto
|
||||
// key/values for that component for use in generic libraries which instantiate
|
||||
// those components.
|
||||
//
|
||||
// When instantiating a component it's generally necessary to know where in the
|
||||
// component hierarchy it lies, for purposes of creating configuration
|
||||
// parameters and so-forth. To support this, Components are able to spawn of
|
||||
// child Components, each with a blank key/value namespace. Each child is
|
||||
// differentiated from the other by a name, and a Component is able to use its
|
||||
// Path (the sequence of names of its ancestors) to differentiate itself from
|
||||
// any other component in the hierarchy.
|
||||
//
|
||||
// A new Component, i.e. the root Component in the hierarchy, can be initialized
|
||||
// by doing:
|
||||
// new(Component).
|
||||
//
|
||||
// Method's on Component are thread-safe.
|
||||
type Component struct { |
||||
l sync.RWMutex |
||||
|
||||
path []string |
||||
parent *Component |
||||
children []child |
||||
|
||||
kv map[interface{}]interface{} |
||||
ctx context.Context |
||||
} |
||||
|
||||
// SetValue sets the given key to the given value on the Component, overwriting
|
||||
// any previous value for that key.
|
||||
func (c *Component) SetValue(key, value interface{}) { |
||||
c.l.Lock() |
||||
defer c.l.Unlock() |
||||
if c.kv == nil { |
||||
c.kv = make(map[interface{}]interface{}, 1) |
||||
} |
||||
c.kv[key] = value |
||||
} |
||||
|
||||
func (c *Component) value(key interface{}) (interface{}, bool) { |
||||
c.l.RLock() |
||||
defer c.l.RUnlock() |
||||
if c.kv == nil { |
||||
return nil, false |
||||
} |
||||
value, ok := c.kv[key] |
||||
return value, ok |
||||
} |
||||
|
||||
// Value returns the value which has been set for the given key.
|
||||
func (c *Component) Value(key interface{}) interface{} { |
||||
value, _ := c.value(key) |
||||
return value |
||||
} |
||||
|
||||
// Values returns all key/value pairs which have been set via SetValue.
|
||||
func (c *Component) Values() map[interface{}]interface{} { |
||||
c.l.RLock() |
||||
defer c.l.RUnlock() |
||||
out := make(map[interface{}]interface{}, len(c.kv)) |
||||
for k, v := range c.kv { |
||||
out[k] = v |
||||
} |
||||
return out |
||||
} |
||||
|
||||
// HasValue returns true if the given key has had a value set on it with
|
||||
// SetValue.
|
||||
func (c *Component) HasValue(key interface{}) bool { |
||||
c.l.RLock() |
||||
defer c.l.RUnlock() |
||||
_, ok := c.kv[key] |
||||
return ok |
||||
} |
||||
|
||||
// Child returns a new child component of the method receiver. The child will
|
||||
// have the given name, and its Path will be the receiver's path with the name
|
||||
// appended. The child will not inherit any of the receiver's key/value pairs.
|
||||
//
|
||||
// If a child of the given name has already been created this method will panic.
|
||||
func (c *Component) Child(name string) *Component { |
||||
c.l.Lock() |
||||
defer c.l.Unlock() |
||||
for _, child := range c.children { |
||||
if child.name == name { |
||||
panic(fmt.Sprintf("child with name %q already exists", name)) |
||||
} |
||||
} |
||||
|
||||
childComp := &Component{ |
||||
path: append(c.path, name), |
||||
parent: c, |
||||
} |
||||
c.children = append(c.children, child{name: name, Component: childComp}) |
||||
return childComp |
||||
} |
||||
|
||||
// Children returns all Components created via the Child method on this
|
||||
// Component, in the order they were created.
|
||||
func (c *Component) Children() []*Component { |
||||
c.l.RLock() |
||||
defer c.l.RUnlock() |
||||
children := make([]*Component, len(c.children)) |
||||
for i := range c.children { |
||||
children[i] = c.children[i].Component |
||||
} |
||||
return children |
||||
} |
||||
|
||||
// Parent returns the Component from which this one was created via the Child
|
||||
// method. This returns nil if this Component was not created via Child (and is
|
||||
// therefore the root Component).
|
||||
func (c *Component) Parent() *Component { |
||||
return c.parent |
||||
} |
||||
|
||||
// Name returns the name this Component was created with (via the Child method),
|
||||
// or false if this Component was not created via Child (and is therefore the
|
||||
// root Component).
|
||||
func (c *Component) Name() (string, bool) { |
||||
c.l.RLock() |
||||
defer c.l.RUnlock() |
||||
if len(c.path) == 0 { |
||||
return "", false |
||||
} |
||||
return c.path[len(c.path)-1], true |
||||
} |
||||
|
||||
// Path returns the sequence of names which were passed into Child calls in
|
||||
// order to create this Component. If the Component was not created via Child
|
||||
// (and is therefore the root Component) this will return an empty slice.
|
||||
//
|
||||
// root := new(Component)
|
||||
// child := root.Child("child")
|
||||
// grandChild := child.Child("grandchild")
|
||||
// fmt.Printf("%#v\n", root.Path()) // "[]string(nil)"
|
||||
// fmt.Printf("%#v\n", child.Path()) // []string{"child"}
|
||||
// fmt.Printf("%#v\n", grandChild.Path()) // []string{"child", "grandchild"}
|
||||
//
|
||||
func (c *Component) Path() []string { |
||||
c.l.RLock() |
||||
defer c.l.RUnlock() |
||||
return c.path |
||||
} |
||||
|
||||
func (c *Component) pathStr() string { |
||||
path := make([]string, len(c.path)) |
||||
copy(path, c.path) |
||||
for i := range path { |
||||
path[i] = strings.ReplaceAll(path[i], "/", `\/`) |
||||
} |
||||
return "/" + strings.Join(path, "/") |
||||
} |
||||
|
||||
type annotateKey string |
||||
|
||||
func (c *Component) getCtx() context.Context { |
||||
if c.ctx == nil { |
||||
c.ctx = mctx.Annotated(annotateKey("componentPath"), c.pathStr()) |
||||
} |
||||
return c.ctx |
||||
} |
||||
|
||||
// Annotate annotates the Component's internal Context in-place, such that they
|
||||
// will be included in any future calls to the Context method.
|
||||
func (c *Component) Annotate(kv ...interface{}) { |
||||
c.l.Lock() |
||||
defer c.l.Unlock() |
||||
c.ctx = mctx.Annotate(c.getCtx(), kv...) |
||||
} |
||||
|
||||
// Context returns a Context which has been annotated with any annotations from
|
||||
// Annotate calls to this Component, as well as some default annotations which
|
||||
// are always included.
|
||||
func (c *Component) Context() context.Context { |
||||
c.l.Lock() |
||||
defer c.l.Unlock() |
||||
return c.getCtx() |
||||
} |
||||
|
||||
// BreadthFirstVisit visits this Component and all of its children, and their
|
||||
// children, etc... in a breadth-first order. If the callback returns false then
|
||||
// the function returns without visiting any more Components.
|
||||
func BreadthFirstVisit(c *Component, callback func(*Component) bool) { |
||||
queue := []*Component{c} |
||||
for len(queue) > 0 { |
||||
if !callback(queue[0]) { |
||||
return |
||||
} |
||||
for _, child := range queue[0].Children() { |
||||
queue = append(queue, child) |
||||
} |
||||
queue = queue[1:] |
||||
} |
||||
} |
||||
|
||||
// InheritedValue returns the value which has been set for the given key. It
|
||||
// first looks for the key on the receiver Component. If not found, it will look
|
||||
// on its parent Component, and so on, until the key is found. If the key is not
|
||||
// found on any Components, up to the root Component, then false is returned.
|
||||
func InheritedValue(c *Component, key interface{}) (interface{}, bool) { |
||||
if c.HasValue(key) { |
||||
return c.kv[key], true |
||||
} else if parent := c.Parent(); parent == nil { |
||||
return nil, false |
||||
} else { |
||||
return InheritedValue(parent, key) |
||||
} |
||||
} |
@ -1,114 +0,0 @@ |
||||
package mcmp |
||||
|
||||
import ( |
||||
. "testing" |
||||
|
||||
"github.com/mediocregopher/mediocre-go-lib/mtest/massert" |
||||
) |
||||
|
||||
func TestComponent(t *T) { |
||||
assertValue := func(c *Component, key, expectedValue interface{}) massert.Assertion { |
||||
val := c.Value(key) |
||||
ok := c.HasValue(key) |
||||
return massert.All( |
||||
massert.Equal(expectedValue, val), |
||||
massert.Equal(expectedValue != nil, ok), |
||||
) |
||||
} |
||||
|
||||
assertName := func(c *Component, expectedName string) massert.Assertion { |
||||
name, ok := c.Name() |
||||
return massert.All( |
||||
massert.Equal(expectedName, name), |
||||
massert.Equal(expectedName != "", ok), |
||||
) |
||||
} |
||||
|
||||
// test that a Component is initialized correctly
|
||||
c := new(Component) |
||||
massert.Require(t, |
||||
assertName(c, ""), |
||||
massert.Length(c.Path(), 0), |
||||
massert.Length(c.Children(), 0), |
||||
assertValue(c, "foo", nil), |
||||
assertValue(c, "bar", nil), |
||||
) |
||||
|
||||
// test that setting values work, and that values aren't inherited
|
||||
c.SetValue("foo", 1) |
||||
child := c.Child("child") |
||||
massert.Require(t, |
||||
assertName(child, "child"), |
||||
massert.Equal([]string{"child"}, child.Path()), |
||||
massert.Length(child.Children(), 0), |
||||
massert.Equal([]*Component{child}, c.Children()), |
||||
assertValue(c, "foo", 1), |
||||
assertValue(child, "foo", nil), |
||||
) |
||||
|
||||
// test that a child setting a value does not affect the parent
|
||||
child.SetValue("bar", 2) |
||||
massert.Require(t, |
||||
assertValue(c, "bar", nil), |
||||
assertValue(child, "bar", 2), |
||||
) |
||||
|
||||
} |
||||
func TestBreadFirstVisit(t *T) { |
||||
cmp := new(Component) |
||||
cmp1 := cmp.Child("1") |
||||
cmp1a := cmp1.Child("a") |
||||
cmp1b := cmp1.Child("b") |
||||
cmp2 := cmp.Child("2") |
||||
|
||||
{ |
||||
got := make([]*Component, 0, 5) |
||||
BreadthFirstVisit(cmp, func(cmp *Component) bool { |
||||
got = append(got, cmp) |
||||
return true |
||||
}) |
||||
massert.Require(t, |
||||
massert.Equal([]*Component{cmp, cmp1, cmp2, cmp1a, cmp1b}, got), |
||||
) |
||||
} |
||||
|
||||
{ |
||||
got := make([]*Component, 0, 3) |
||||
BreadthFirstVisit(cmp, func(cmp *Component) bool { |
||||
if len(cmp.Path()) > 1 { |
||||
return false |
||||
} |
||||
got = append(got, cmp) |
||||
return true |
||||
}) |
||||
massert.Require(t, |
||||
massert.Equal([]*Component{cmp, cmp1, cmp2}, got), |
||||
) |
||||
} |
||||
} |
||||
|
||||
func TestInheritedValue(t *T) { |
||||
|
||||
assertInheritedValue := func(c *Component, key, expectedValue interface{}) massert.Assertion { |
||||
val, ok := InheritedValue(c, key) |
||||
return massert.All( |
||||
massert.Equal(expectedValue, val), |
||||
massert.Equal(expectedValue != nil, ok), |
||||
) |
||||
} |
||||
|
||||
c := new(Component) |
||||
c.SetValue("foo", 1) |
||||
child := c.Child("child") |
||||
child.SetValue("bar", 2) |
||||
|
||||
// test that InheritedValue does what it's supposed to
|
||||
massert.Require(t, |
||||
assertInheritedValue(c, "foo", 1), |
||||
assertInheritedValue(child, "foo", 1), |
||||
assertInheritedValue(c, "bar", nil), |
||||
assertInheritedValue(child, "bar", 2), |
||||
assertInheritedValue(c, "xxx", nil), |
||||
assertInheritedValue(child, "xxx", nil), |
||||
) |
||||
} |
@ -1,93 +0,0 @@ |
||||
package mcmp |
||||
|
||||
const ( |
||||
seriesEls int = iota |
||||
seriesNumValueEls |
||||
) |
||||
|
||||
type seriesKey struct { |
||||
userKey interface{} |
||||
mod int |
||||
} |
||||
|
||||
// SeriesElement is used to describe a single element in a series, as
|
||||
// implemented by AddSeriesValue. A SeriesElement can either be a Child which
|
||||
// was spawned from the Component, or a Value which was added via
|
||||
// AddSeriesValue.
|
||||
type SeriesElement struct { |
||||
Child *Component |
||||
Value interface{} |
||||
} |
||||
|
||||
func seriesKeys(key interface{}) (seriesKey, seriesKey) { |
||||
return seriesKey{userKey: key, mod: seriesEls}, |
||||
seriesKey{userKey: key, mod: seriesNumValueEls} |
||||
} |
||||
|
||||
func getSeriesElements(c *Component, key interface{}) ([]SeriesElement, int) { |
||||
elsKey, numValueElsKey := seriesKeys(key) |
||||
lastEls, _ := c.Value(elsKey).([]SeriesElement) |
||||
lastNumValueEls, _ := c.Value(numValueElsKey).(int) |
||||
|
||||
children := c.Children() |
||||
lastNumChildrenEls := len(lastEls) - lastNumValueEls |
||||
|
||||
els := lastEls |
||||
for _, child := range children[lastNumChildrenEls:] { |
||||
els = append(els, SeriesElement{Child: child}) |
||||
} |
||||
return els, lastNumValueEls |
||||
} |
||||
|
||||
// AddSeriesValue is a helper which adds a value to a series which is being
|
||||
// stored under the given key on the given Component. The series of values added
|
||||
// under any key can be retrieved with GetSeriesValues.
|
||||
//
|
||||
// Additionally, AddSeriesValue keeps track of the order of calls to itself and
|
||||
// children spawned from the Component. By using GetSeriesElements you can
|
||||
// retrieve the sequence of values and children in the order they were added to
|
||||
// the Component.
|
||||
func AddSeriesValue(c *Component, key, value interface{}) { |
||||
lastEls, lastNumValueEls := getSeriesElements(c, key) |
||||
els := append(lastEls, SeriesElement{Value: value}) |
||||
|
||||
elsKey, numValueElsKey := seriesKeys(key) |
||||
c.SetValue(elsKey, els) |
||||
c.SetValue(numValueElsKey, lastNumValueEls+1) |
||||
} |
||||
|
||||
// SeriesElements returns the sequence of values that have been added to the
|
||||
// Component under the given key via AddSeriesValue, interlaced with children
|
||||
// which have been spawned from the Component, in the same respective order the
|
||||
// events originally happened.
|
||||
func SeriesElements(c *Component, key interface{}) []SeriesElement { |
||||
els, _ := getSeriesElements(c, key) |
||||
return els |
||||
} |
||||
|
||||
// SeriesGetElement returns the ith element in the series at the given key.
|
||||
func SeriesGetElement(c *Component, key interface{}, i int) (SeriesElement, bool) { |
||||
els, _ := getSeriesElements(c, key) |
||||
if i >= len(els) { |
||||
return SeriesElement{}, false |
||||
} |
||||
return els[i], true |
||||
} |
||||
|
||||
// SeriesValues returns the sequence of values that have been added to the
|
||||
// Component under the given key via AddSeriesValue, in the same order the
|
||||
// values were added.
|
||||
func SeriesValues(c *Component, key interface{}) []interface{} { |
||||
elsKey, numValueElsKey := seriesKeys(key) |
||||
els, _ := c.Value(elsKey).([]SeriesElement) |
||||
numValueEls, _ := c.Value(numValueElsKey).(int) |
||||
|
||||
values := make([]interface{}, 0, numValueEls) |
||||
for _, el := range els { |
||||
if el.Child != nil { |
||||
continue |
||||
} |
||||
values = append(values, el.Value) |
||||
} |
||||
return values |
||||
} |
@ -1,66 +0,0 @@ |
||||
package mcmp |
||||
|
||||
import ( |
||||
. "testing" |
||||
|
||||
"github.com/mediocregopher/mediocre-go-lib/mtest/massert" |
||||
) |
||||
|
||||
func TestSeries(t *T) { |
||||
key := "foo" |
||||
c := new(Component) |
||||
|
||||
assertGetElement := func(i int, expEl SeriesElement) massert.Assertion { |
||||
el, ok := SeriesGetElement(c, key, i) |
||||
if expEl == (SeriesElement{}) { |
||||
return massert.Equal(false, ok) |
||||
} |
||||
return massert.All( |
||||
massert.Equal(expEl, el), |
||||
massert.Equal(true, ok), |
||||
) |
||||
} |
||||
|
||||
// test empty state
|
||||
massert.Require(t, |
||||
massert.Length(SeriesElements(c, key), 0), |
||||
massert.Length(SeriesValues(c, key), 0), |
||||
assertGetElement(0, SeriesElement{}), |
||||
) |
||||
|
||||
// test after a single value has been added
|
||||
AddSeriesValue(c, key, 1) |
||||
massert.Require(t, |
||||
massert.Equal([]SeriesElement{{Value: 1}}, SeriesElements(c, key)), |
||||
massert.Equal([]interface{}{1}, SeriesValues(c, key)), |
||||
assertGetElement(0, SeriesElement{Value: 1}), |
||||
assertGetElement(1, SeriesElement{}), |
||||
) |
||||
|
||||
// test after a child has been added
|
||||
childA := c.Child("a") |
||||
massert.Require(t, |
||||
massert.Equal( |
||||
[]SeriesElement{{Value: 1}, {Child: childA}}, |
||||
SeriesElements(c, key), |
||||
), |
||||
massert.Equal([]interface{}{1}, SeriesValues(c, key)), |
||||
assertGetElement(0, SeriesElement{Value: 1}), |
||||
assertGetElement(1, SeriesElement{Child: childA}), |
||||
assertGetElement(2, SeriesElement{}), |
||||
) |
||||
|
||||
// test after another value has been added
|
||||
AddSeriesValue(c, key, 2) |
||||
massert.Require(t, |
||||
massert.Equal( |
||||
[]SeriesElement{{Value: 1}, {Child: childA}, {Value: 2}}, |
||||
SeriesElements(c, key), |
||||
), |
||||
massert.Equal([]interface{}{1, 2}, SeriesValues(c, key)), |
||||
assertGetElement(0, SeriesElement{Value: 1}), |
||||
assertGetElement(1, SeriesElement{Child: childA}), |
||||
assertGetElement(2, SeriesElement{Value: 2}), |
||||
assertGetElement(3, SeriesElement{}), |
||||
) |
||||
} |
@ -1,72 +0,0 @@ |
||||
package mlog |
||||
|
||||
import ( |
||||
"github.com/mediocregopher/mediocre-go-lib/mcmp" |
||||
"github.com/mediocregopher/mediocre-go-lib/mctx" |
||||
) |
||||
|
||||
type cmpKey int |
||||
|
||||
const ( |
||||
cmpKeyLogger cmpKey = iota |
||||
cmpKeyCachedLogger |
||||
) |
||||
|
||||
// SetLogger sets the given logger onto the Component. The logger can later be
|
||||
// retrieved from the Component, or any of its children, using From.
|
||||
//
|
||||
// NOTE that if a Logger is set onto a Component and then changed, even though
|
||||
// the Logger is a pointer and so is changed within the Component, SetLogger
|
||||
// should still be called. This is due to some caching that From does for
|
||||
// performance.
|
||||
func SetLogger(cmp *mcmp.Component, l *Logger) { |
||||
cmp.SetValue(cmpKeyLogger, l) |
||||
|
||||
// If the base Logger on this Component gets changed, then the cached Logger
|
||||
// from From on this Component, and all of its Children, ought to be reset,
|
||||
// so that any changes can be reflected in their loggers.
|
||||
var resetFromLogger func(*mcmp.Component) |
||||
resetFromLogger = func(cmp *mcmp.Component) { |
||||
cmp.SetValue(cmpKeyCachedLogger, nil) |
||||
for _, childCmp := range cmp.Children() { |
||||
resetFromLogger(childCmp) |
||||
} |
||||
} |
||||
resetFromLogger(cmp) |
||||
} |
||||
|
||||
// DefaultLogger is an instance of Logger which is returned by From when a
|
||||
// Logger hasn't been previously set with SetLogger on the passed in Component.
|
||||
var DefaultLogger = NewLogger() |
||||
|
||||
// GetLogger returns the Logger which was set on the Component, or on of its
|
||||
// ancestors, using SetLogger. If no Logger was ever set then DefaultLogger is
|
||||
// returned.
|
||||
func GetLogger(cmp *mcmp.Component) *Logger { |
||||
if l, ok := mcmp.InheritedValue(cmp, cmpKeyLogger); ok { |
||||
return l.(*Logger) |
||||
} |
||||
return DefaultLogger |
||||
} |
||||
|
||||
// From returns the result from GetLogger, modified so as to automatically add
|
||||
// some annotations related to the Component itself to all Messages being
|
||||
// logged.
|
||||
func From(cmp *mcmp.Component) *Logger { |
||||
if l, _ := cmp.Value(cmpKeyCachedLogger).(*Logger); l != nil { |
||||
return l |
||||
} |
||||
|
||||
// if we're here it means a modified Logger wasn't set on this particular
|
||||
// Component, and therefore the current one must be modified.
|
||||
l := GetLogger(cmp).Clone() |
||||
oldHandler := l.Handler() |
||||
l.SetHandler(func(msg Message) error { |
||||
ctx := mctx.MergeAnnotationsInto(cmp.Context(), msg.Contexts...) |
||||
msg.Contexts = append(msg.Contexts[:0], ctx) |
||||
return oldHandler(msg) |
||||
}) |
||||
cmp.SetValue(cmpKeyCachedLogger, l) |
||||
|
||||
return l |
||||
} |
@ -1,82 +0,0 @@ |
||||
package mlog |
||||
|
||||
import ( |
||||
"fmt" |
||||
. "testing" |
||||
|
||||
"github.com/mediocregopher/mediocre-go-lib/mcmp" |
||||
"github.com/mediocregopher/mediocre-go-lib/mctx" |
||||
"github.com/mediocregopher/mediocre-go-lib/mtest/massert" |
||||
) |
||||
|
||||
func TestGetSetLogger(t *T) { |
||||
cmp := new(mcmp.Component) |
||||
cmpChild := cmp.Child("child") |
||||
ctx := mctx.Annotated("foo", "bar") |
||||
|
||||
var msgs []string |
||||
l := NewLogger() |
||||
l.SetHandler(func(msg Message) error { |
||||
msgStr := fmt.Sprintf("%s %q", msg.Level, msg.Description) |
||||
for _, ctx := range msg.Contexts { |
||||
for _, kv := range mctx.Annotations(ctx).StringSlice(true) { |
||||
msgStr += fmt.Sprintf(" %s=%s", kv[0], kv[1]) |
||||
} |
||||
} |
||||
msgs = append(msgs, msgStr) |
||||
return nil |
||||
}) |
||||
SetLogger(cmp, l) |
||||
|
||||
msgs = msgs[:0] |
||||
GetLogger(cmp).Info("get-cmp", ctx) |
||||
GetLogger(cmpChild).Info("get-cmpChild", ctx) |
||||
From(cmp).Info("from-cmp", ctx) |
||||
From(cmpChild).Info("from-cmpChild", ctx) |
||||
massert.Require(t, |
||||
massert.Equal(`INFO "get-cmp" foo=bar`, msgs[0]), |
||||
massert.Equal(`INFO "get-cmpChild" foo=bar`, msgs[1]), |
||||
massert.Equal(`INFO "from-cmp" componentPath=/ foo=bar`, msgs[2]), |
||||
massert.Equal(`INFO "from-cmpChild" componentPath=/child foo=bar`, msgs[3]), |
||||
) |
||||
|
||||
l2 := l.Clone() |
||||
l2.SetHandler(func(msg Message) error { |
||||
msg.Description += " (2)" |
||||
return l.Handler()(msg) |
||||
}) |
||||
SetLogger(cmp, l2) |
||||
|
||||
msgs = msgs[:0] |
||||
GetLogger(cmp).Info("get-cmp", ctx) |
||||
GetLogger(cmpChild).Info("get-cmpChild", ctx) |
||||
From(cmp).Info("from-cmp", ctx) |
||||
From(cmpChild).Info("from-cmpChild", ctx) |
||||
massert.Require(t, |
||||
massert.Equal(`INFO "get-cmp (2)" foo=bar`, msgs[0]), |
||||
massert.Equal(`INFO "get-cmpChild (2)" foo=bar`, msgs[1]), |
||||
massert.Equal(`INFO "from-cmp (2)" componentPath=/ foo=bar`, msgs[2]), |
||||
massert.Equal(`INFO "from-cmpChild (2)" componentPath=/child foo=bar`, msgs[3]), |
||||
) |
||||
|
||||
// If a Logger is set on the child, that shouldn't affect the parent
|
||||
l3 := l.Clone() |
||||
l3.SetHandler(func(msg Message) error { |
||||
msg.Description += " (3)" |
||||
return l.Handler()(msg) |
||||
}) |
||||
SetLogger(cmpChild, l3) |
||||
|
||||
msgs = msgs[:0] |
||||
GetLogger(cmp).Info("get-cmp", ctx) |
||||
GetLogger(cmpChild).Info("get-cmpChild", ctx) |
||||
From(cmp).Info("from-cmp", ctx) |
||||
From(cmpChild).Info("from-cmpChild", ctx) |
||||
massert.Require(t, |
||||
massert.Equal(`INFO "get-cmp (2)" foo=bar`, msgs[0]), |
||||
massert.Equal(`INFO "get-cmpChild (3)" foo=bar`, msgs[1]), |
||||
massert.Equal(`INFO "from-cmp (2)" componentPath=/ foo=bar`, msgs[2]), |
||||
massert.Equal(`INFO "from-cmpChild (3)" componentPath=/child foo=bar`, msgs[3]), |
||||
) |
||||
|
||||
} |
@ -1,127 +0,0 @@ |
||||
package mrun |
||||
|
||||
import ( |
||||
"context" |
||||
|
||||
"github.com/mediocregopher/mediocre-go-lib/mcmp" |
||||
) |
||||
|
||||
// Hook describes a function which can be registered to trigger on an event via
|
||||
// the WithHook function.
|
||||
type Hook func(context.Context) error |
||||
|
||||
type hookKey struct { |
||||
key interface{} |
||||
} |
||||
|
||||
// AddHook registers a Hook under a typed key. The Hook will be called when
|
||||
// TriggerHooks is called with that same key. Multiple Hooks can be registered
|
||||
// for the same key, and will be called sequentially when triggered.
|
||||
//
|
||||
// Hooks will be called with whatever Context is passed into TriggerHooks.
|
||||
func AddHook(cmp *mcmp.Component, key interface{}, hook Hook) { |
||||
mcmp.AddSeriesValue(cmp, hookKey{key}, hook) |
||||
} |
||||
|
||||
func triggerHooks( |
||||
ctx context.Context, |
||||
cmp *mcmp.Component, |
||||
key interface{}, |
||||
start func(*mcmp.Component) int, |
||||
next func(int) int, |
||||
) error { |
||||
i := start(cmp) |
||||
for { |
||||
if i < 0 { |
||||
return nil |
||||
} |
||||
|
||||
el, ok := mcmp.SeriesGetElement(cmp, hookKey{key}, i) |
||||
if !ok { |
||||
return nil |
||||
} else if el.Child != nil { |
||||
if err := triggerHooks(ctx, el.Child, key, start, next); err != nil { |
||||
return err |
||||
} |
||||
} else { |
||||
hook := el.Value.(Hook) |
||||
if err := hook(ctx); err != nil { |
||||
return err |
||||
} |
||||
} |
||||
|
||||
i = next(i) |
||||
} |
||||
} |
||||
|
||||
// TriggerHooks causes all Hooks registered with AddHook on the Component under
|
||||
// the given key to be called in the order they were registered. The given
|
||||
// Context is passed into all Hooks being called.
|
||||
//
|
||||
// If any Hook returns an error no further Hooks will be called and that error
|
||||
// will be returned.
|
||||
//
|
||||
// If the Component has children (see the mcmp package), and those children have
|
||||
// Hooks registered under this key, then their Hooks will be called in the
|
||||
// expected order. See package docs for an example.
|
||||
func TriggerHooks( |
||||
ctx context.Context, |
||||
cmp *mcmp.Component, |
||||
key interface{}, |
||||
) error { |
||||
start := func(*mcmp.Component) int { return 0 } |
||||
next := func(i int) int { return i + 1 } |
||||
return triggerHooks(ctx, cmp, key, start, next) |
||||
} |
||||
|
||||
// TriggerHooksReverse is the same as TriggerHooks except that registered Hooks
|
||||
// are called in the reverse order in which they were registered.
|
||||
func TriggerHooksReverse(ctx context.Context, cmp *mcmp.Component, key interface{}) error { |
||||
start := func(cmp *mcmp.Component) int { |
||||
els := mcmp.SeriesElements(cmp, hookKey{key}) |
||||
return len(els) - 1 |
||||
} |
||||
next := func(i int) int { return i - 1 } |
||||
return triggerHooks(ctx, cmp, key, start, next) |
||||
} |
||||
|
||||
type builtinEvent int |
||||
|
||||
const ( |
||||
initEvent builtinEvent = iota |
||||
shutdownEvent |
||||
) |
||||
|
||||
// InitHook registers the given Hook to run when Init is called. This is a
|
||||
// special case of AddHook.
|
||||
//
|
||||
// As a convention Hooks running on the init event should block only as long as
|
||||
// it takes to ensure that whatever is running can do so successfully. For
|
||||
// short-lived tasks this isn't a problem, but long-lived tasks (e.g. a web
|
||||
// server) will want to use the Hook only to initialize, and spawn off a
|
||||
// go-routine to do their actual work. Long-lived tasks should set themselves up
|
||||
// to shutdown on the shutdown event (see ShutdownHook).
|
||||
func InitHook(cmp *mcmp.Component, hook Hook) { |
||||
AddHook(cmp, initEvent, hook) |
||||
} |
||||
|
||||
// Init runs all Hooks registered using InitHook. This is a special case of
|
||||
// TriggerHooks.
|
||||
func Init(ctx context.Context, cmp *mcmp.Component) error { |
||||
return TriggerHooks(ctx, cmp, initEvent) |
||||
} |
||||
|
||||
// ShutdownHook registers the given Hook to run when Shutdown is called. This is
|
||||
// a special case of AddHook.
|
||||
//
|
||||
// See InitHook for more on the relationship between Init(Hook) and
|
||||
// Shutdown(Hook).
|
||||
func ShutdownHook(cmp *mcmp.Component, hook Hook) { |
||||
AddHook(cmp, shutdownEvent, hook) |
||||
} |
||||
|
||||
// Shutdown runs all Hooks registered using ShutdownHook in the reverse order in
|
||||
// which they were registered. This is a special case of TriggerHooks.
|
||||
func Shutdown(ctx context.Context, cmp *mcmp.Component) error { |
||||
return TriggerHooksReverse(ctx, cmp, shutdownEvent) |
||||
} |
@ -1,47 +0,0 @@ |
||||
package mrun |
||||
|
||||
import ( |
||||
"context" |
||||
. "testing" |
||||
|
||||
"github.com/mediocregopher/mediocre-go-lib/mcmp" |
||||
"github.com/mediocregopher/mediocre-go-lib/mtest/massert" |
||||
) |
||||
|
||||
func TestHooks(t *T) { |
||||
var out []int |
||||
mkHook := func(i int) Hook { |
||||
return func(context.Context) error { |
||||
out = append(out, i) |
||||
return nil |
||||
} |
||||
} |
||||
|
||||
cmp := new(mcmp.Component) |
||||
AddHook(cmp, 0, mkHook(1)) |
||||
AddHook(cmp, 0, mkHook(2)) |
||||
|
||||
cmpA := cmp.Child("a") |
||||
AddHook(cmpA, 0, mkHook(3)) |
||||
AddHook(cmpA, 999, mkHook(999)) // different key
|
||||
|
||||
AddHook(cmp, 0, mkHook(4)) |
||||
|
||||
cmpB := cmp.Child("b") |
||||
AddHook(cmpB, 0, mkHook(5)) |
||||
cmpB1 := cmpB.Child("1") |
||||
AddHook(cmpB1, 0, mkHook(6)) |
||||
|
||||
AddHook(cmp, 0, mkHook(7)) |
||||
|
||||
massert.Require(t, |
||||
massert.Nil(TriggerHooks(context.Background(), cmp, 0)), |
||||
massert.Equal([]int{1, 2, 3, 4, 5, 6, 7}, out), |
||||
) |
||||
|
||||
out = nil |
||||
massert.Require(t, |
||||
massert.Nil(TriggerHooksReverse(context.Background(), cmp, 0)), |
||||
massert.Equal([]int{7, 6, 5, 4, 3, 2, 1}, out), |
||||
) |
||||
} |
@ -1,117 +0,0 @@ |
||||
// Package mrun provides the ability to register callback hooks on Components,
|
||||
// as well as some convenience functions which allow using a context as a
|
||||
// wait-group.
|
||||
//
|
||||
// Hooks
|
||||
//
|
||||
// Hooks are registered onto Components and later called in bulk. mrun will take
|
||||
// into account the order Hooks are registered, including Hooks within a
|
||||
// Component's children (see mcmp package), and execute them in the same order
|
||||
// they were registered. For example:
|
||||
//
|
||||
// newHook := func(i int) mrun.Hook {
|
||||
// return func(context.Context) error {
|
||||
// fmt.Println(i)
|
||||
// return nil
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// cmp := new(mcmp.Component)
|
||||
// mrun.InitHook(cmp, newHook(0))
|
||||
//
|
||||
// cmpChild := cmp.Child("child")
|
||||
// mrun.InitHook(cmpChild, newHook(1))
|
||||
//
|
||||
// mrun.InitHook(cmp, newHook(2))
|
||||
// mrun.Init(context.Background(), cmp) // prints "0", "1", then "2"
|
||||
//
|
||||
package mrun |
||||
|
||||
import ( |
||||
"context" |
||||
"errors" |
||||
) |
||||
|
||||
type futureErr struct { |
||||
doneCh chan struct{} |
||||
err error |
||||
} |
||||
|
||||
func newFutureErr() *futureErr { |
||||
return &futureErr{ |
||||
doneCh: make(chan struct{}), |
||||
} |
||||
} |
||||
|
||||
func (fe *futureErr) get(cancelCh <-chan struct{}) (error, bool) { |
||||
select { |
||||
case <-fe.doneCh: |
||||
return fe.err, true |
||||
case <-cancelCh: |
||||
return nil, false |
||||
} |
||||
} |
||||
|
||||
func (fe *futureErr) set(err error) { |
||||
fe.err = err |
||||
close(fe.doneCh) |
||||
} |
||||
|
||||
type threadCtxKey int |
||||
|
||||
// WithThreads spawns n go-routines, each of which executes the given function.
|
||||
// The returned Context tracks these go-routines, and can then be passed into
|
||||
// the Wait function to block until the spawned go-routines all return.
|
||||
func WithThreads(ctx context.Context, n uint, fn func() error) context.Context { |
||||
// I dunno why this would happen, but it wouldn't actually hurt anything
|
||||
if n == 0 { |
||||
return ctx |
||||
} |
||||
|
||||
oldFutErrs, _ := ctx.Value(threadCtxKey(0)).([]*futureErr) |
||||
futErrs := make([]*futureErr, len(oldFutErrs), len(oldFutErrs)+int(n)) |
||||
copy(futErrs, oldFutErrs) |
||||
|
||||
for i := uint(0); i < n; i++ { |
||||
futErr := newFutureErr() |
||||
futErrs = append(futErrs, futErr) |
||||
|
||||
go func() { |
||||
futErr.set(fn()) |
||||
}() |
||||
} |
||||
|
||||
return context.WithValue(ctx, threadCtxKey(0), futErrs) |
||||
} |
||||
|
||||
// ErrDone is returned from Wait if cancelCh is closed before all threads have
|
||||
// returned.
|
||||
var ErrDone = errors.New("Wait is done waiting") |
||||
|
||||
// Wait blocks until all go-routines spawned using WithThreads on the passed in
|
||||
// Context (and its predecessors) have returned. Any number of the go-routines
|
||||
// may have returned already when Wait is called, and not all go-routines need
|
||||
// to be from the same WithThreads call.
|
||||
//
|
||||
// If any of the thread functions returned an error during its runtime Wait will
|
||||
// return that error. If multiple returned an error only one of those will be
|
||||
// returned. TODO: Handle multi-errors better.
|
||||
//
|
||||
// If cancelCh is not nil and is closed before all threads have returned then
|
||||
// this function stops waiting and returns ErrDone.
|
||||
//
|
||||
// Wait is safe to call in parallel, and will return the same result if called
|
||||
// multiple times.
|
||||
func Wait(ctx context.Context, cancelCh <-chan struct{}) error { |
||||
futErrs, _ := ctx.Value(threadCtxKey(0)).([]*futureErr) |
||||
for _, futErr := range futErrs { |
||||
err, ok := futErr.get(cancelCh) |
||||
if !ok { |
||||
return ErrDone |
||||
} else if err != nil { |
||||
return err |
||||
} |
||||
} |
||||
|
||||
return nil |
||||
} |
@ -1,79 +0,0 @@ |
||||
package mrun |
||||
|
||||
import ( |
||||
"context" |
||||
"errors" |
||||
. "testing" |
||||
"time" |
||||
) |
||||
|
||||
func TestThreadWait(t *T) { |
||||
testErr := errors.New("test error") |
||||
|
||||
cancelCh := func(t time.Duration) <-chan struct{} { |
||||
tCtx, _ := context.WithTimeout(context.Background(), t*2) |
||||
return tCtx.Done() |
||||
} |
||||
|
||||
wait := func(ctx context.Context, shouldTake time.Duration) error { |
||||
start := time.Now() |
||||
err := Wait(ctx, cancelCh(shouldTake*2)) |
||||
if took := time.Since(start); took < shouldTake || took > shouldTake*4/3 { |
||||
t.Fatalf("wait took %v, should have taken %v", took, shouldTake) |
||||
} |
||||
return err |
||||
} |
||||
|
||||
t.Run("noBlock", func(t *T) { |
||||
t.Run("noErr", func(t *T) { |
||||
ctx := context.Background() |
||||
ctx = WithThreads(ctx, 1, func() error { return nil }) |
||||
if err := Wait(ctx, nil); err != nil { |
||||
t.Fatal(err) |
||||
} |
||||
}) |
||||
|
||||
t.Run("err", func(t *T) { |
||||
ctx := context.Background() |
||||
ctx = WithThreads(ctx, 1, func() error { return testErr }) |
||||
if err := Wait(ctx, nil); err != testErr { |
||||
t.Fatalf("should have got test error, got: %v", err) |
||||
} |
||||
}) |
||||
}) |
||||
|
||||
t.Run("block", func(t *T) { |
||||
t.Run("noErr", func(t *T) { |
||||
ctx := context.Background() |
||||
ctx = WithThreads(ctx, 1, func() error { |
||||
time.Sleep(1 * time.Second) |
||||
return nil |
||||
}) |
||||
if err := wait(ctx, 1*time.Second); err != nil { |
||||
t.Fatal(err) |
||||
} |
||||
}) |
||||
|
||||
t.Run("err", func(t *T) { |
||||
ctx := context.Background() |
||||
ctx = WithThreads(ctx, 1, func() error { |
||||
time.Sleep(1 * time.Second) |
||||
return testErr |
||||
}) |
||||
if err := wait(ctx, 1*time.Second); err != testErr { |
||||
t.Fatalf("should have got test error, got: %v", err) |
||||
} |
||||
}) |
||||
|
||||
t.Run("canceled", func(t *T) { |
||||
ctx := context.Background() |
||||
ctx = WithThreads(ctx, 1, func() error { |
||||
time.Sleep(5 * time.Second) |
||||
return testErr |
||||
}) |
||||
if err := Wait(ctx, cancelCh(500*time.Millisecond)); err != ErrDone { |
||||
t.Fatalf("should have got ErrDone, got: %v", err) |
||||
} |
||||
}) |
||||
}) |
||||
} |
Loading…
Reference in new issue