Get rid of mcmp, and lots of subsequent refactors
Getting rid of mcmp pretty much breaks this whole package, but this commit makes a good start on fixing all the things worth keeping.
This commit is contained in:
parent
c20f884d68
commit
3e2713a850
4
go.mod
4
go.mod
@ -1,4 +1,6 @@
|
|||||||
module github.com/mediocregopher/mediocre-go-lib
|
module github.com/mediocregopher/mediocre-go-lib/v2
|
||||||
|
|
||||||
|
go 1.15
|
||||||
|
|
||||||
require (
|
require (
|
||||||
cloud.google.com/go v0.36.0
|
cloud.google.com/go v0.36.0
|
||||||
|
382
mcfg/cli.go
382
mcfg/cli.go
@ -1,382 +0,0 @@
|
|||||||
package mcfg
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"os"
|
|
||||||
"reflect"
|
|
||||||
"sort"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/mediocregopher/mediocre-go-lib/mcmp"
|
|
||||||
"github.com/mediocregopher/mediocre-go-lib/mctx"
|
|
||||||
"github.com/mediocregopher/mediocre-go-lib/merr"
|
|
||||||
)
|
|
||||||
|
|
||||||
type cliKey int
|
|
||||||
|
|
||||||
const (
|
|
||||||
cliKeyTail cliKey = iota
|
|
||||||
cliKeySubCmdM
|
|
||||||
)
|
|
||||||
|
|
||||||
type cliTail struct {
|
|
||||||
dst *[]string
|
|
||||||
descr string
|
|
||||||
}
|
|
||||||
|
|
||||||
// CLITail modifies the behavior of SourceCLI's Parse. Normally when SourceCLI
|
|
||||||
// encounters an unexpected Arg it will immediately return an error. This
|
|
||||||
// function modifies the Component to indicate to Parse that the unexpected Arg,
|
|
||||||
// and all subsequent Args (i.e. the tail), should be set to the returned
|
|
||||||
// []string value.
|
|
||||||
//
|
|
||||||
// The descr (optional) will be appended to the "Usage" line which is printed
|
|
||||||
// with the help document when "-h" is passed in.
|
|
||||||
//
|
|
||||||
// This function panics if not called on a root Component (i.e. a Component
|
|
||||||
// which has no parents).
|
|
||||||
func CLITail(cmp *mcmp.Component, descr string) *[]string {
|
|
||||||
if len(cmp.Path()) != 0 {
|
|
||||||
panic("CLITail can only be used on a root Component")
|
|
||||||
}
|
|
||||||
|
|
||||||
tailPtr := new([]string)
|
|
||||||
cmp.SetValue(cliKeyTail, cliTail{
|
|
||||||
dst: tailPtr,
|
|
||||||
descr: descr,
|
|
||||||
})
|
|
||||||
return tailPtr
|
|
||||||
}
|
|
||||||
|
|
||||||
func populateCLITail(cmp *mcmp.Component, tail []string) bool {
|
|
||||||
ct, ok := cmp.Value(cliKeyTail).(cliTail)
|
|
||||||
if ok {
|
|
||||||
*ct.dst = tail
|
|
||||||
}
|
|
||||||
return ok
|
|
||||||
}
|
|
||||||
|
|
||||||
func getCLITailDescr(cmp *mcmp.Component) string {
|
|
||||||
ct, _ := cmp.Value(cliKeyTail).(cliTail)
|
|
||||||
return ct.descr
|
|
||||||
}
|
|
||||||
|
|
||||||
type subCmd struct {
|
|
||||||
name, descr string
|
|
||||||
flag *bool
|
|
||||||
callback func(*mcmp.Component)
|
|
||||||
}
|
|
||||||
|
|
||||||
// CLISubCommand establishes a sub-command which can be activated on the
|
|
||||||
// command-line. When a sub-command is given on the command-line, the bool
|
|
||||||
// returned for that sub-command will be set to true.
|
|
||||||
//
|
|
||||||
// Additionally, the Component which was passed into Parse (i.e. the one passed
|
|
||||||
// into Populate) will be passed into the given callback, and can be modified
|
|
||||||
// for subsequent parsing. This allows for setting sub-command specific Params,
|
|
||||||
// sub-command specific runtime behavior (via mrun.WithStartHook), support for
|
|
||||||
// sub-sub-commands, and more. The callback may be nil.
|
|
||||||
//
|
|
||||||
// If any sub-commands have been defined on a Component which is passed into
|
|
||||||
// Parse, it is assumed that a sub-command is required on the command-line.
|
|
||||||
//
|
|
||||||
// When parsing the command-line options, it is assumed that sub-commands will
|
|
||||||
// be found before any other options.
|
|
||||||
//
|
|
||||||
// This function panics if not called on a root Component (i.e. a Component
|
|
||||||
// which has no parents).
|
|
||||||
func CLISubCommand(cmp *mcmp.Component, name, descr string, callback func(*mcmp.Component)) *bool {
|
|
||||||
if len(cmp.Path()) != 0 {
|
|
||||||
panic("CLISubCommand can only be used on a root Component")
|
|
||||||
}
|
|
||||||
|
|
||||||
m, _ := cmp.Value(cliKeySubCmdM).(map[string]subCmd)
|
|
||||||
if m == nil {
|
|
||||||
m = map[string]subCmd{}
|
|
||||||
cmp.SetValue(cliKeySubCmdM, m)
|
|
||||||
}
|
|
||||||
|
|
||||||
flag := new(bool)
|
|
||||||
m[name] = subCmd{
|
|
||||||
name: name,
|
|
||||||
descr: descr,
|
|
||||||
flag: flag,
|
|
||||||
callback: callback,
|
|
||||||
}
|
|
||||||
return flag
|
|
||||||
}
|
|
||||||
|
|
||||||
// SourceCLI is a Source which will parse configuration from the CLI.
|
|
||||||
//
|
|
||||||
// Possible CLI options are generated by joining a Param's Path and Name with
|
|
||||||
// dashes. For example:
|
|
||||||
//
|
|
||||||
// cmp := new(mcmp.Component)
|
|
||||||
// cmpFoo = cmp.Child("foo")
|
|
||||||
// cmpFooBar = foo.Child("bar")
|
|
||||||
// addr := mcfg.String(cmpFooBar, "addr", "", "Some address")
|
|
||||||
// // the CLI option to fill addr will be "--foo-bar-addr"
|
|
||||||
//
|
|
||||||
// If the "-h" option is seen then a help page will be printed to stdout and the
|
|
||||||
// process will exit. Since all normally-defined parameters must being with
|
|
||||||
// double-dash ("--") they won't ever conflict with the help option.
|
|
||||||
//
|
|
||||||
// SourceCLI behaves a little differently with boolean parameters. Setting the
|
|
||||||
// value of a boolean parameter directly _must_ be done with an equals, or with
|
|
||||||
// no value at all. For example: `--boolean-flag`, `--boolean-flag=1` or
|
|
||||||
// `--boolean-flag=false`. Using the space-separated format will not work. If a
|
|
||||||
// boolean has no equal-separated value it is assumed to be setting the value to
|
|
||||||
// `true`.
|
|
||||||
type SourceCLI struct {
|
|
||||||
Args []string // if nil then os.Args[1:] is used
|
|
||||||
|
|
||||||
DisableHelpPage bool
|
|
||||||
}
|
|
||||||
|
|
||||||
var _ Source = new(SourceCLI)
|
|
||||||
|
|
||||||
const (
|
|
||||||
cliKeyJoin = "-"
|
|
||||||
cliKeyPrefix = "--"
|
|
||||||
cliValSep = "="
|
|
||||||
cliHelpArg = "-h"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Parse implements the method for the Source interface.
|
|
||||||
func (cli *SourceCLI) Parse(cmp *mcmp.Component) ([]ParamValue, error) {
|
|
||||||
args := cli.Args
|
|
||||||
if cli.Args == nil {
|
|
||||||
args = os.Args[1:]
|
|
||||||
}
|
|
||||||
return cli.parse(cmp, nil, args)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (cli *SourceCLI) parse(
|
|
||||||
cmp *mcmp.Component,
|
|
||||||
subCmdPrefix, args []string,
|
|
||||||
) (
|
|
||||||
[]ParamValue,
|
|
||||||
error,
|
|
||||||
) {
|
|
||||||
pM, err := cli.cliParams(CollectParams(cmp))
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
printHelpAndExit := func() {
|
|
||||||
// TODO check DisableHelpPage here?
|
|
||||||
cli.printHelp(cmp, os.Stderr, subCmdPrefix, pM)
|
|
||||||
os.Stderr.Sync()
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
// if sub-commands were defined on this Component then handle that first.
|
|
||||||
// One of them should have been given, in which case send the Context
|
|
||||||
// through the callback to obtain a new one (which presumably has further
|
|
||||||
// config options the previous didn't) and call parse again.
|
|
||||||
subCmdM, _ := cmp.Value(cliKeySubCmdM).(map[string]subCmd)
|
|
||||||
if len(subCmdM) > 0 {
|
|
||||||
subCmd, args, ok := cli.getSubCmd(subCmdM, args)
|
|
||||||
if !ok {
|
|
||||||
printHelpAndExit()
|
|
||||||
}
|
|
||||||
cmp.SetValue(cliKeySubCmdM, nil)
|
|
||||||
if subCmd.callback != nil {
|
|
||||||
subCmd.callback(cmp)
|
|
||||||
}
|
|
||||||
subCmdPrefix = append(subCmdPrefix, subCmd.name)
|
|
||||||
*subCmd.flag = true
|
|
||||||
return cli.parse(cmp, subCmdPrefix, args)
|
|
||||||
}
|
|
||||||
|
|
||||||
// if sub-commands were not set, then proceed with normal command-line arg
|
|
||||||
// processing.
|
|
||||||
pvs := make([]ParamValue, 0, len(args))
|
|
||||||
var (
|
|
||||||
key string
|
|
||||||
p Param
|
|
||||||
pOk bool
|
|
||||||
pvStrVal string
|
|
||||||
pvStrValOk bool
|
|
||||||
)
|
|
||||||
for i, arg := range args {
|
|
||||||
if pOk {
|
|
||||||
pvStrVal = arg
|
|
||||||
pvStrValOk = true
|
|
||||||
} else if !cli.DisableHelpPage && arg == cliHelpArg {
|
|
||||||
printHelpAndExit()
|
|
||||||
} else {
|
|
||||||
for key, p = range pM {
|
|
||||||
if arg == key {
|
|
||||||
pOk = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
prefix := key + cliValSep
|
|
||||||
if !strings.HasPrefix(arg, prefix) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
pOk = true
|
|
||||||
pvStrVal = strings.TrimPrefix(arg, prefix)
|
|
||||||
pvStrValOk = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
if !pOk {
|
|
||||||
if ok := populateCLITail(cmp, args[i:]); ok {
|
|
||||||
return pvs, nil
|
|
||||||
}
|
|
||||||
return nil, merr.New("unexpected config parameter",
|
|
||||||
mctx.Annotated("param", arg))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// pOk is always true at this point, and so p is filled in
|
|
||||||
|
|
||||||
// As a special case for CLI, if a boolean has no value set it means it
|
|
||||||
// is true.
|
|
||||||
if p.IsBool && !pvStrValOk {
|
|
||||||
pvStrVal = "true"
|
|
||||||
} else if !pvStrValOk {
|
|
||||||
// everything else should have a value. if pvStrVal isn't filled it
|
|
||||||
// means the next arg should be one. Continue the loop, it'll get
|
|
||||||
// filled with the next one (hopefully)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
pvs = append(pvs, ParamValue{
|
|
||||||
Name: p.Name,
|
|
||||||
Path: p.Component.Path(),
|
|
||||||
Value: p.fuzzyParse(pvStrVal),
|
|
||||||
})
|
|
||||||
|
|
||||||
key = ""
|
|
||||||
p = Param{}
|
|
||||||
pOk = false
|
|
||||||
pvStrVal = ""
|
|
||||||
pvStrValOk = false
|
|
||||||
}
|
|
||||||
if pOk && !pvStrValOk {
|
|
||||||
ctx := mctx.Annotate(p.Component.Context(), "param", key)
|
|
||||||
return nil, merr.New("param expected a value", ctx)
|
|
||||||
}
|
|
||||||
|
|
||||||
return pvs, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (cli *SourceCLI) getSubCmd(subCmdM map[string]subCmd, args []string) (subCmd, []string, bool) {
|
|
||||||
if len(args) == 0 {
|
|
||||||
return subCmd{}, args, false
|
|
||||||
}
|
|
||||||
|
|
||||||
s, ok := subCmdM[args[0]]
|
|
||||||
if !ok {
|
|
||||||
return subCmd{}, args, false
|
|
||||||
}
|
|
||||||
|
|
||||||
return s, args[1:], true
|
|
||||||
}
|
|
||||||
|
|
||||||
func (cli *SourceCLI) cliParams(params []Param) (map[string]Param, error) {
|
|
||||||
m := map[string]Param{}
|
|
||||||
for _, p := range params {
|
|
||||||
key := strings.Join(append(p.Component.Path(), p.Name), cliKeyJoin)
|
|
||||||
m[cliKeyPrefix+key] = p
|
|
||||||
}
|
|
||||||
return m, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (cli *SourceCLI) printHelp(
|
|
||||||
cmp *mcmp.Component,
|
|
||||||
w io.Writer,
|
|
||||||
subCmdPrefix []string,
|
|
||||||
pM map[string]Param,
|
|
||||||
) {
|
|
||||||
type pEntry struct {
|
|
||||||
arg string
|
|
||||||
Param
|
|
||||||
}
|
|
||||||
|
|
||||||
pA := make([]pEntry, 0, len(pM))
|
|
||||||
for arg, p := range pM {
|
|
||||||
pA = append(pA, pEntry{arg: arg, Param: p})
|
|
||||||
}
|
|
||||||
|
|
||||||
sort.Slice(pA, func(i, j int) bool {
|
|
||||||
if pA[i].Required != pA[j].Required {
|
|
||||||
return pA[i].Required
|
|
||||||
}
|
|
||||||
return pA[i].arg < pA[j].arg
|
|
||||||
})
|
|
||||||
|
|
||||||
fmtDefaultVal := func(ptr interface{}) string {
|
|
||||||
if ptr == nil {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
val := reflect.Indirect(reflect.ValueOf(ptr))
|
|
||||||
zero := reflect.Zero(val.Type())
|
|
||||||
if reflect.DeepEqual(val.Interface(), zero.Interface()) {
|
|
||||||
return ""
|
|
||||||
} else if val.Type().Kind() == reflect.String {
|
|
||||||
return fmt.Sprintf("%q", val.Interface())
|
|
||||||
}
|
|
||||||
return fmt.Sprint(val.Interface())
|
|
||||||
}
|
|
||||||
|
|
||||||
type subCmdEntry struct {
|
|
||||||
name string
|
|
||||||
subCmd
|
|
||||||
}
|
|
||||||
|
|
||||||
subCmdM, _ := cmp.Value(cliKeySubCmdM).(map[string]subCmd)
|
|
||||||
subCmdA := make([]subCmdEntry, 0, len(subCmdM))
|
|
||||||
for name, subCmd := range subCmdM {
|
|
||||||
subCmdA = append(subCmdA, subCmdEntry{name: name, subCmd: subCmd})
|
|
||||||
}
|
|
||||||
|
|
||||||
sort.Slice(subCmdA, func(i, j int) bool {
|
|
||||||
return subCmdA[i].name < subCmdA[j].name
|
|
||||||
})
|
|
||||||
|
|
||||||
fmt.Fprintf(w, "Usage: %s", os.Args[0])
|
|
||||||
if len(subCmdPrefix) > 0 {
|
|
||||||
fmt.Fprintf(w, " %s", strings.Join(subCmdPrefix, " "))
|
|
||||||
}
|
|
||||||
if len(subCmdA) > 0 {
|
|
||||||
fmt.Fprint(w, " <sub-command>")
|
|
||||||
}
|
|
||||||
if len(pA) > 0 {
|
|
||||||
fmt.Fprint(w, " [options]")
|
|
||||||
}
|
|
||||||
if descr := getCLITailDescr(cmp); descr != "" {
|
|
||||||
fmt.Fprintf(w, " %s", descr)
|
|
||||||
}
|
|
||||||
fmt.Fprint(w, "\n\n")
|
|
||||||
|
|
||||||
if len(subCmdA) > 0 {
|
|
||||||
fmt.Fprint(w, "Sub-commands:\n\n")
|
|
||||||
for _, subCmd := range subCmdA {
|
|
||||||
fmt.Fprintf(w, "\t%s\t%s\n", subCmd.name, subCmd.descr)
|
|
||||||
}
|
|
||||||
fmt.Fprint(w, "\n")
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(pA) > 0 {
|
|
||||||
fmt.Fprint(w, "Options:\n\n")
|
|
||||||
for _, p := range pA {
|
|
||||||
fmt.Fprintf(w, "\t%s", p.arg)
|
|
||||||
if p.IsBool {
|
|
||||||
fmt.Fprintf(w, " (Flag)")
|
|
||||||
} else if p.Required {
|
|
||||||
fmt.Fprintf(w, " (Required)")
|
|
||||||
} else if defVal := fmtDefaultVal(p.Into); defVal != "" {
|
|
||||||
fmt.Fprintf(w, " (Default: %s)", defVal)
|
|
||||||
}
|
|
||||||
fmt.Fprint(w, "\n")
|
|
||||||
if usage := p.Usage; usage != "" {
|
|
||||||
fmt.Fprintln(w, "\t\t"+usage)
|
|
||||||
}
|
|
||||||
fmt.Fprint(w, "\n")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
356
mcfg/cli_test.go
356
mcfg/cli_test.go
@ -1,356 +0,0 @@
|
|||||||
package mcfg
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"fmt"
|
|
||||||
"regexp"
|
|
||||||
"strings"
|
|
||||||
. "testing"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/mediocregopher/mediocre-go-lib/mcmp"
|
|
||||||
"github.com/mediocregopher/mediocre-go-lib/mrand"
|
|
||||||
"github.com/mediocregopher/mediocre-go-lib/mtest/massert"
|
|
||||||
"github.com/mediocregopher/mediocre-go-lib/mtest/mchk"
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestSourceCLIHelp(t *T) {
|
|
||||||
assertHelp := func(cmp *mcmp.Component, subCmdPrefix []string, exp string) {
|
|
||||||
buf := new(bytes.Buffer)
|
|
||||||
src := &SourceCLI{}
|
|
||||||
pM, err := src.cliParams(CollectParams(cmp))
|
|
||||||
require.NoError(t, err)
|
|
||||||
src.printHelp(cmp, buf, subCmdPrefix, pM)
|
|
||||||
|
|
||||||
out := buf.String()
|
|
||||||
ok := regexp.MustCompile(exp).MatchString(out)
|
|
||||||
assert.True(t, ok, "exp:%s (%q)\ngot:%s (%q)", exp, exp, out, out)
|
|
||||||
}
|
|
||||||
|
|
||||||
cmp := new(mcmp.Component)
|
|
||||||
assertHelp(cmp, nil, `^Usage: \S+
|
|
||||||
|
|
||||||
$`)
|
|
||||||
assertHelp(cmp, []string{"foo", "bar"}, `^Usage: \S+ foo bar
|
|
||||||
|
|
||||||
$`)
|
|
||||||
|
|
||||||
Int(cmp, "foo", ParamDefault(5), ParamUsage("Test int param ")) // trailing space should be trimmed
|
|
||||||
Bool(cmp, "bar", ParamUsage("Test bool param."))
|
|
||||||
String(cmp, "baz", ParamDefault("baz"), ParamUsage("Test string param"))
|
|
||||||
String(cmp, "baz2", ParamUsage("Required string param"), ParamRequired())
|
|
||||||
String(cmp, "baz3", ParamRequired())
|
|
||||||
|
|
||||||
assertHelp(cmp, nil, `^Usage: \S+ \[options\]
|
|
||||||
|
|
||||||
Options:
|
|
||||||
|
|
||||||
--baz2 \(Required\)
|
|
||||||
Required string param.
|
|
||||||
|
|
||||||
--baz3 \(Required\)
|
|
||||||
|
|
||||||
--bar \(Flag\)
|
|
||||||
Test bool param.
|
|
||||||
|
|
||||||
--baz \(Default: "baz"\)
|
|
||||||
Test string param.
|
|
||||||
|
|
||||||
--foo \(Default: 5\)
|
|
||||||
Test int param.
|
|
||||||
|
|
||||||
$`)
|
|
||||||
|
|
||||||
assertHelp(cmp, []string{"foo", "bar"}, `^Usage: \S+ foo bar \[options\]
|
|
||||||
|
|
||||||
Options:
|
|
||||||
|
|
||||||
--baz2 \(Required\)
|
|
||||||
Required string param.
|
|
||||||
|
|
||||||
--baz3 \(Required\)
|
|
||||||
|
|
||||||
--bar \(Flag\)
|
|
||||||
Test bool param.
|
|
||||||
|
|
||||||
--baz \(Default: "baz"\)
|
|
||||||
Test string param.
|
|
||||||
|
|
||||||
--foo \(Default: 5\)
|
|
||||||
Test int param.
|
|
||||||
|
|
||||||
$`)
|
|
||||||
|
|
||||||
CLISubCommand(cmp, "first", "First sub-command", nil)
|
|
||||||
CLISubCommand(cmp, "second", "Second sub-command", nil)
|
|
||||||
assertHelp(cmp, []string{"foo", "bar"}, `^Usage: \S+ foo bar <sub-command> \[options\]
|
|
||||||
|
|
||||||
Sub-commands:
|
|
||||||
|
|
||||||
first First sub-command
|
|
||||||
second Second sub-command
|
|
||||||
|
|
||||||
Options:
|
|
||||||
|
|
||||||
--baz2 \(Required\)
|
|
||||||
Required string param.
|
|
||||||
|
|
||||||
--baz3 \(Required\)
|
|
||||||
|
|
||||||
--bar \(Flag\)
|
|
||||||
Test bool param.
|
|
||||||
|
|
||||||
--baz \(Default: "baz"\)
|
|
||||||
Test string param.
|
|
||||||
|
|
||||||
--foo \(Default: 5\)
|
|
||||||
Test int param.
|
|
||||||
|
|
||||||
$`)
|
|
||||||
|
|
||||||
CLITail(cmp, "[arg...]")
|
|
||||||
assertHelp(cmp, nil, `^Usage: \S+ <sub-command> \[options\] \[arg\.\.\.\]
|
|
||||||
|
|
||||||
Sub-commands:
|
|
||||||
|
|
||||||
first First sub-command
|
|
||||||
second Second sub-command
|
|
||||||
|
|
||||||
Options:
|
|
||||||
|
|
||||||
--baz2 \(Required\)
|
|
||||||
Required string param.
|
|
||||||
|
|
||||||
--baz3 \(Required\)
|
|
||||||
|
|
||||||
--bar \(Flag\)
|
|
||||||
Test bool param.
|
|
||||||
|
|
||||||
--baz \(Default: "baz"\)
|
|
||||||
Test string param.
|
|
||||||
|
|
||||||
--foo \(Default: 5\)
|
|
||||||
Test int param.
|
|
||||||
|
|
||||||
$`)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestSourceCLI(t *T) {
|
|
||||||
type state struct {
|
|
||||||
srcCommonState
|
|
||||||
*SourceCLI
|
|
||||||
}
|
|
||||||
|
|
||||||
type params struct {
|
|
||||||
srcCommonParams
|
|
||||||
nonBoolWEq bool // use equal sign when setting value
|
|
||||||
}
|
|
||||||
|
|
||||||
chk := mchk.Checker{
|
|
||||||
Init: func() mchk.State {
|
|
||||||
var s state
|
|
||||||
s.srcCommonState = newSrcCommonState()
|
|
||||||
s.SourceCLI = &SourceCLI{
|
|
||||||
Args: make([]string, 0, 16),
|
|
||||||
}
|
|
||||||
return s
|
|
||||||
},
|
|
||||||
Next: func(ss mchk.State) mchk.Action {
|
|
||||||
s := ss.(state)
|
|
||||||
var p params
|
|
||||||
p.srcCommonParams = s.srcCommonState.next()
|
|
||||||
// if the param is a bool or unset this won't get used, but w/e
|
|
||||||
p.nonBoolWEq = mrand.Intn(2) == 0
|
|
||||||
return mchk.Action{Params: p}
|
|
||||||
},
|
|
||||||
Apply: func(ss mchk.State, a mchk.Action) (mchk.State, error) {
|
|
||||||
s := ss.(state)
|
|
||||||
p := a.Params.(params)
|
|
||||||
|
|
||||||
s.srcCommonState = s.srcCommonState.applyCmpAndPV(p.srcCommonParams)
|
|
||||||
if !p.unset {
|
|
||||||
arg := cliKeyPrefix
|
|
||||||
if path := p.cmp.Path(); len(path) > 0 {
|
|
||||||
arg += strings.Join(path, cliKeyJoin) + cliKeyJoin
|
|
||||||
}
|
|
||||||
arg += p.name
|
|
||||||
if !p.isBool {
|
|
||||||
if p.nonBoolWEq {
|
|
||||||
arg += "="
|
|
||||||
} else {
|
|
||||||
s.SourceCLI.Args = append(s.SourceCLI.Args, arg)
|
|
||||||
arg = ""
|
|
||||||
}
|
|
||||||
arg += p.nonBoolVal
|
|
||||||
}
|
|
||||||
s.SourceCLI.Args = append(s.SourceCLI.Args, arg)
|
|
||||||
}
|
|
||||||
|
|
||||||
err := s.srcCommonState.assert(s.SourceCLI)
|
|
||||||
return s, err
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := chk.RunFor(2 * time.Second); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestCLITail(t *T) {
|
|
||||||
cmp := new(mcmp.Component)
|
|
||||||
Int(cmp, "foo", ParamDefault(5))
|
|
||||||
Bool(cmp, "bar")
|
|
||||||
|
|
||||||
type testCase struct {
|
|
||||||
args []string
|
|
||||||
expTail []string
|
|
||||||
}
|
|
||||||
|
|
||||||
cases := []testCase{
|
|
||||||
{
|
|
||||||
args: []string{"--foo", "5"},
|
|
||||||
expTail: nil,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
args: []string{"--foo", "5", "a", "b", "c"},
|
|
||||||
expTail: []string{"a", "b", "c"},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
args: []string{"--foo=5", "a", "b", "c"},
|
|
||||||
expTail: []string{"a", "b", "c"},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
args: []string{"--foo", "5", "--bar"},
|
|
||||||
expTail: nil,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
args: []string{"--foo", "5", "--bar", "a", "b", "c"},
|
|
||||||
expTail: []string{"a", "b", "c"},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tc := range cases {
|
|
||||||
tail := CLITail(cmp, "foo")
|
|
||||||
err := Populate(cmp, &SourceCLI{Args: tc.args})
|
|
||||||
massert.Require(t, massert.Comment(massert.All(
|
|
||||||
massert.Nil(err),
|
|
||||||
massert.Equal(tc.expTail, *tail),
|
|
||||||
), "tc: %#v", tc))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func ExampleCLITail() {
|
|
||||||
cmp := new(mcmp.Component)
|
|
||||||
foo := Int(cmp, "foo", ParamDefault(1), ParamUsage("Description of foo."))
|
|
||||||
tail := CLITail(cmp, "[arg...]")
|
|
||||||
bar := String(cmp, "bar", ParamDefault("defaultVal"), ParamUsage("Description of bar."))
|
|
||||||
|
|
||||||
err := Populate(cmp, &SourceCLI{
|
|
||||||
Args: []string{"--foo=100", "arg1", "arg2", "arg3"},
|
|
||||||
})
|
|
||||||
|
|
||||||
fmt.Printf("err:%v foo:%v bar:%v tail:%#v\n", err, *foo, *bar, *tail)
|
|
||||||
// Output: err:<nil> foo:100 bar:defaultVal tail:[]string{"arg1", "arg2", "arg3"}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestCLISubCommand(t *T) {
|
|
||||||
var (
|
|
||||||
cmp *mcmp.Component
|
|
||||||
foo *int
|
|
||||||
bar *int
|
|
||||||
baz *int
|
|
||||||
aFlag *bool
|
|
||||||
bFlag *bool
|
|
||||||
)
|
|
||||||
reset := func() {
|
|
||||||
foo, bar, baz, aFlag, bFlag = nil, nil, nil, nil, nil
|
|
||||||
cmp = new(mcmp.Component)
|
|
||||||
foo = Int(cmp, "foo")
|
|
||||||
aFlag = CLISubCommand(cmp, "a", "Description of a.",
|
|
||||||
func(cmp *mcmp.Component) {
|
|
||||||
bar = Int(cmp, "bar")
|
|
||||||
})
|
|
||||||
bFlag = CLISubCommand(cmp, "b", "Description of b.",
|
|
||||||
func(cmp *mcmp.Component) {
|
|
||||||
baz = Int(cmp, "baz")
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
reset()
|
|
||||||
err := Populate(cmp, &SourceCLI{
|
|
||||||
Args: []string{"a", "--foo=1", "--bar=2"},
|
|
||||||
})
|
|
||||||
massert.Require(t,
|
|
||||||
massert.Comment(massert.Nil(err), "%v", err),
|
|
||||||
massert.Equal(1, *foo),
|
|
||||||
massert.Equal(2, *bar),
|
|
||||||
massert.Nil(baz),
|
|
||||||
massert.Equal(true, *aFlag),
|
|
||||||
massert.Equal(false, *bFlag),
|
|
||||||
)
|
|
||||||
|
|
||||||
reset()
|
|
||||||
err = Populate(cmp, &SourceCLI{
|
|
||||||
Args: []string{"b", "--foo=1", "--baz=3"},
|
|
||||||
})
|
|
||||||
massert.Require(t,
|
|
||||||
massert.Comment(massert.Nil(err), "%v", err),
|
|
||||||
massert.Equal(1, *foo),
|
|
||||||
massert.Nil(bar),
|
|
||||||
massert.Equal(3, *baz),
|
|
||||||
massert.Equal(false, *aFlag),
|
|
||||||
massert.Equal(true, *bFlag),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
func ExampleCLISubCommand() {
|
|
||||||
var (
|
|
||||||
cmp *mcmp.Component
|
|
||||||
foo, bar, baz *int
|
|
||||||
aFlag, bFlag *bool
|
|
||||||
)
|
|
||||||
|
|
||||||
// resetExample re-initializes all variables used in this example. We'll
|
|
||||||
// call it multiple times to show different behaviors depending on what
|
|
||||||
// arguments are passed in.
|
|
||||||
resetExample := func() {
|
|
||||||
// Create a new Component with a parameter "foo", which can be used across
|
|
||||||
// all sub-commands.
|
|
||||||
cmp = new(mcmp.Component)
|
|
||||||
foo = Int(cmp, "foo")
|
|
||||||
|
|
||||||
// Create a sub-command "a", which has a parameter "bar" specific to it.
|
|
||||||
aFlag = CLISubCommand(cmp, "a", "Description of a.",
|
|
||||||
func(cmp *mcmp.Component) {
|
|
||||||
bar = Int(cmp, "bar")
|
|
||||||
})
|
|
||||||
|
|
||||||
// Create a sub-command "b", which has a parameter "baz" specific to it.
|
|
||||||
bFlag = CLISubCommand(cmp, "b", "Description of b.",
|
|
||||||
func(cmp *mcmp.Component) {
|
|
||||||
baz = Int(cmp, "baz")
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Use Populate with manually generated CLI arguments, calling the "a"
|
|
||||||
// sub-command.
|
|
||||||
resetExample()
|
|
||||||
args := []string{"a", "--foo=1", "--bar=2"}
|
|
||||||
if err := Populate(cmp, &SourceCLI{Args: args}); err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
fmt.Printf("foo:%d bar:%d aFlag:%v bFlag:%v\n", *foo, *bar, *aFlag, *bFlag)
|
|
||||||
|
|
||||||
// reset for another Populate, this time calling the "b" sub-command.
|
|
||||||
resetExample()
|
|
||||||
args = []string{"b", "--foo=1", "--baz=3"}
|
|
||||||
if err := Populate(cmp, &SourceCLI{Args: args}); err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
fmt.Printf("foo:%d baz:%d aFlag:%v bFlag:%v\n", *foo, *baz, *aFlag, *bFlag)
|
|
||||||
|
|
||||||
// Output: foo:1 bar:2 aFlag:true bFlag:false
|
|
||||||
// foo:1 baz:3 aFlag:false bFlag:true
|
|
||||||
}
|
|
79
mcfg/env.go
79
mcfg/env.go
@ -1,79 +0,0 @@
|
|||||||
package mcfg
|
|
||||||
|
|
||||||
import (
|
|
||||||
"os"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/mediocregopher/mediocre-go-lib/mcmp"
|
|
||||||
"github.com/mediocregopher/mediocre-go-lib/mctx"
|
|
||||||
"github.com/mediocregopher/mediocre-go-lib/merr"
|
|
||||||
)
|
|
||||||
|
|
||||||
// SourceEnv is a Source which will parse configuration from the process
|
|
||||||
// environment.
|
|
||||||
//
|
|
||||||
// Possible Env options are generated by joining a Param's Path and Name with
|
|
||||||
// underscores and making all characters uppercase, as well as changing all
|
|
||||||
// dashes to underscores.
|
|
||||||
//
|
|
||||||
// cmp := new(mcmp.Component)
|
|
||||||
// cmpFoo := cmp.Child("foo")
|
|
||||||
// cmpFooBar := cmp.Child("bar")
|
|
||||||
// addr := mcfg.String(cmpFooBar, "srv-addr", "", "Some address")
|
|
||||||
// // the Env option to fill addr will be "FOO_BAR_SRV_ADDR"
|
|
||||||
//
|
|
||||||
type SourceEnv struct {
|
|
||||||
// In the format key=value. Defaults to os.Environ() if nil.
|
|
||||||
Env []string
|
|
||||||
|
|
||||||
// If set then all expected Env options must be prefixed with this string,
|
|
||||||
// which will be uppercased and have dashes replaced with underscores like
|
|
||||||
// all the other parts of the option names.
|
|
||||||
Prefix string
|
|
||||||
}
|
|
||||||
|
|
||||||
var _ Source = new(SourceEnv)
|
|
||||||
|
|
||||||
func (env *SourceEnv) expectedName(path []string, name string) string {
|
|
||||||
out := strings.Join(append(path, name), "_")
|
|
||||||
if env.Prefix != "" {
|
|
||||||
out = env.Prefix + "_" + out
|
|
||||||
}
|
|
||||||
out = strings.Replace(out, "-", "_", -1)
|
|
||||||
out = strings.ToUpper(out)
|
|
||||||
return out
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parse implements the method for the Source interface.
|
|
||||||
func (env *SourceEnv) Parse(cmp *mcmp.Component) ([]ParamValue, error) {
|
|
||||||
kvs := env.Env
|
|
||||||
if kvs == nil {
|
|
||||||
kvs = os.Environ()
|
|
||||||
}
|
|
||||||
|
|
||||||
params := CollectParams(cmp)
|
|
||||||
pM := map[string]Param{}
|
|
||||||
for _, p := range params {
|
|
||||||
name := env.expectedName(p.Component.Path(), p.Name)
|
|
||||||
pM[name] = p
|
|
||||||
}
|
|
||||||
|
|
||||||
pvs := make([]ParamValue, 0, len(kvs))
|
|
||||||
for _, kv := range kvs {
|
|
||||||
split := strings.SplitN(kv, "=", 2)
|
|
||||||
if len(split) != 2 {
|
|
||||||
return nil, merr.New("malformed environment key/value pair",
|
|
||||||
mctx.Annotated("kv", kv))
|
|
||||||
}
|
|
||||||
k, v := split[0], split[1]
|
|
||||||
if p, ok := pM[k]; ok {
|
|
||||||
pvs = append(pvs, ParamValue{
|
|
||||||
Name: p.Name,
|
|
||||||
Path: p.Component.Path(),
|
|
||||||
Value: p.fuzzyParse(v),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return pvs, nil
|
|
||||||
}
|
|
@ -1,60 +0,0 @@
|
|||||||
package mcfg
|
|
||||||
|
|
||||||
import (
|
|
||||||
"strings"
|
|
||||||
. "testing"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/mediocregopher/mediocre-go-lib/mtest/mchk"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestSourceEnv(t *T) {
|
|
||||||
type state struct {
|
|
||||||
srcCommonState
|
|
||||||
*SourceEnv
|
|
||||||
}
|
|
||||||
|
|
||||||
type params struct {
|
|
||||||
srcCommonParams
|
|
||||||
}
|
|
||||||
|
|
||||||
chk := mchk.Checker{
|
|
||||||
Init: func() mchk.State {
|
|
||||||
var s state
|
|
||||||
s.srcCommonState = newSrcCommonState()
|
|
||||||
s.SourceEnv = &SourceEnv{
|
|
||||||
Env: make([]string, 0, 16),
|
|
||||||
}
|
|
||||||
return s
|
|
||||||
},
|
|
||||||
Next: func(ss mchk.State) mchk.Action {
|
|
||||||
s := ss.(state)
|
|
||||||
var p params
|
|
||||||
p.srcCommonParams = s.srcCommonState.next()
|
|
||||||
return mchk.Action{Params: p}
|
|
||||||
},
|
|
||||||
Apply: func(ss mchk.State, a mchk.Action) (mchk.State, error) {
|
|
||||||
s := ss.(state)
|
|
||||||
p := a.Params.(params)
|
|
||||||
s.srcCommonState = s.srcCommonState.applyCmpAndPV(p.srcCommonParams)
|
|
||||||
if !p.unset {
|
|
||||||
kv := strings.Join(append(p.cmp.Path(), p.name), "_")
|
|
||||||
kv = strings.Replace(kv, "-", "_", -1)
|
|
||||||
kv = strings.ToUpper(kv)
|
|
||||||
kv += "="
|
|
||||||
if p.isBool {
|
|
||||||
kv += "1"
|
|
||||||
} else {
|
|
||||||
kv += p.nonBoolVal
|
|
||||||
}
|
|
||||||
s.SourceEnv.Env = append(s.SourceEnv.Env, kv)
|
|
||||||
}
|
|
||||||
err := s.srcCommonState.assert(s.SourceEnv)
|
|
||||||
return s, err
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := chk.RunFor(2 * time.Second); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
}
|
|
150
mcfg/mcfg.go
150
mcfg/mcfg.go
@ -1,150 +0,0 @@
|
|||||||
// Package mcfg implements the creation of different types of configuration
|
|
||||||
// parameters and various methods of filling those parameters from external
|
|
||||||
// configuration sources (e.g. the command line and environment variables).
|
|
||||||
//
|
|
||||||
// Parameters are registered onto a Component, and that same Component (or one
|
|
||||||
// of its ancestors) is used later to collect and fill those parameters.
|
|
||||||
package mcfg
|
|
||||||
|
|
||||||
import (
|
|
||||||
"crypto/md5"
|
|
||||||
"encoding/hex"
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"sort"
|
|
||||||
|
|
||||||
"github.com/mediocregopher/mediocre-go-lib/mcmp"
|
|
||||||
"github.com/mediocregopher/mediocre-go-lib/mctx"
|
|
||||||
"github.com/mediocregopher/mediocre-go-lib/merr"
|
|
||||||
)
|
|
||||||
|
|
||||||
// TODO Sources:
|
|
||||||
// - JSON file
|
|
||||||
// - YAML file
|
|
||||||
|
|
||||||
// TODO WithCLISubCommand does not play nice with the expected use-case of
|
|
||||||
// having CLI params overwrite Env ones. If Env is specified first in the
|
|
||||||
// Sources slice then it won't know about any extra Params which might get added
|
|
||||||
// due to a sub-command, but if it's specified second then Env values will
|
|
||||||
// overwrite CLI ones.
|
|
||||||
|
|
||||||
func sortParams(params []Param) {
|
|
||||||
sort.Slice(params, func(i, j int) bool {
|
|
||||||
a, b := params[i], params[j]
|
|
||||||
aPath, bPath := a.Component.Path(), b.Component.Path()
|
|
||||||
for {
|
|
||||||
switch {
|
|
||||||
case len(aPath) == 0 && len(bPath) == 0:
|
|
||||||
return a.Name < b.Name
|
|
||||||
case len(aPath) == 0 && len(bPath) > 0:
|
|
||||||
return false
|
|
||||||
case len(aPath) > 0 && len(bPath) == 0:
|
|
||||||
return true
|
|
||||||
case aPath[0] != bPath[0]:
|
|
||||||
return aPath[0] < bPath[0]
|
|
||||||
default:
|
|
||||||
aPath, bPath = aPath[1:], bPath[1:]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// CollectParams gathers all Params by recursively retrieving them from the
|
|
||||||
// given Component and its children. Returned Params are sorted according to
|
|
||||||
// their Path and Name.
|
|
||||||
func CollectParams(cmp *mcmp.Component) []Param {
|
|
||||||
var params []Param
|
|
||||||
|
|
||||||
var visit func(*mcmp.Component)
|
|
||||||
visit = func(cmp *mcmp.Component) {
|
|
||||||
for _, param := range getLocalParams(cmp) {
|
|
||||||
params = append(params, param)
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, childCmp := range cmp.Children() {
|
|
||||||
visit(childCmp)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
visit(cmp)
|
|
||||||
|
|
||||||
sortParams(params)
|
|
||||||
return params
|
|
||||||
}
|
|
||||||
|
|
||||||
func paramHash(path []string, name string) string {
|
|
||||||
h := md5.New()
|
|
||||||
for _, pathEl := range path {
|
|
||||||
fmt.Fprintf(h, "pathEl:%q\n", pathEl)
|
|
||||||
}
|
|
||||||
fmt.Fprintf(h, "name:%q\n", name)
|
|
||||||
hStr := hex.EncodeToString(h.Sum(nil))
|
|
||||||
// we add the displayName to it to make debugging easier
|
|
||||||
return paramFullName(path, name) + "/" + hStr
|
|
||||||
}
|
|
||||||
|
|
||||||
// Populate uses the Source to populate the values of all Params which were
|
|
||||||
// added to the given Component, and all of its children. Populate may be called
|
|
||||||
// multiple times with the same Component, each time will only affect the values
|
|
||||||
// of the Params which were provided by the respective Source.
|
|
||||||
//
|
|
||||||
// Source may be nil to indicate that no configuration is provided. Only default
|
|
||||||
// values will be used, and if any parameters are required this will error.
|
|
||||||
//
|
|
||||||
// Populating Params can affect the Component itself, for example in the case of
|
|
||||||
// sub-commands.
|
|
||||||
func Populate(cmp *mcmp.Component, src Source) error {
|
|
||||||
if src == nil {
|
|
||||||
src = ParamValues(nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
pvs, err := src.Parse(cmp)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// map Params to their hash, so we can match them to their ParamValues.
|
|
||||||
// later. There should not be any duplicates here.
|
|
||||||
params := CollectParams(cmp)
|
|
||||||
pM := map[string]Param{}
|
|
||||||
for _, p := range params {
|
|
||||||
path := p.Component.Path()
|
|
||||||
hash := paramHash(path, p.Name)
|
|
||||||
if _, ok := pM[hash]; ok {
|
|
||||||
panic("duplicate Param: " + paramFullName(path, p.Name))
|
|
||||||
}
|
|
||||||
pM[hash] = p
|
|
||||||
}
|
|
||||||
|
|
||||||
// dedupe the ParamValues based on their hashes, with the last ParamValue
|
|
||||||
// taking precedence. Also filter out those with no corresponding Param.
|
|
||||||
pvM := map[string]ParamValue{}
|
|
||||||
for _, pv := range pvs {
|
|
||||||
hash := paramHash(pv.Path, pv.Name)
|
|
||||||
if _, ok := pM[hash]; !ok {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
pvM[hash] = pv
|
|
||||||
}
|
|
||||||
|
|
||||||
// check for required params
|
|
||||||
for hash, p := range pM {
|
|
||||||
if !p.Required {
|
|
||||||
continue
|
|
||||||
} else if _, ok := pvM[hash]; !ok {
|
|
||||||
ctx := mctx.Annotate(p.Component.Context(),
|
|
||||||
"param", paramFullName(p.Component.Path(), p.Name))
|
|
||||||
return merr.New("required parameter is not set", ctx)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// do the actual populating
|
|
||||||
for hash, pv := range pvM {
|
|
||||||
// at this point, all ParamValues in pvM have a corresponding pM Param
|
|
||||||
p := pM[hash]
|
|
||||||
if err := json.Unmarshal(pv.Value, p.Into); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
@ -1,69 +0,0 @@
|
|||||||
package mcfg
|
|
||||||
|
|
||||||
import (
|
|
||||||
. "testing"
|
|
||||||
|
|
||||||
"github.com/mediocregopher/mediocre-go-lib/mcmp"
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestPopulate(t *T) {
|
|
||||||
{
|
|
||||||
cmp := new(mcmp.Component)
|
|
||||||
a := Int(cmp, "a")
|
|
||||||
cmpFoo := cmp.Child("foo")
|
|
||||||
b := Int(cmpFoo, "b")
|
|
||||||
c := Int(cmpFoo, "c")
|
|
||||||
d := Int(cmp, "d", ParamDefault(4))
|
|
||||||
|
|
||||||
err := Populate(cmp, &SourceCLI{
|
|
||||||
Args: []string{"--a=1", "--foo-b=2"},
|
|
||||||
})
|
|
||||||
assert.NoError(t, err)
|
|
||||||
assert.Equal(t, 1, *a)
|
|
||||||
assert.Equal(t, 2, *b)
|
|
||||||
assert.Equal(t, 0, *c)
|
|
||||||
assert.Equal(t, 4, *d)
|
|
||||||
}
|
|
||||||
|
|
||||||
{ // test that required params are enforced
|
|
||||||
cmp := new(mcmp.Component)
|
|
||||||
a := Int(cmp, "a")
|
|
||||||
cmpFoo := cmp.Child("foo")
|
|
||||||
b := Int(cmpFoo, "b")
|
|
||||||
c := Int(cmpFoo, "c", ParamRequired())
|
|
||||||
|
|
||||||
err := Populate(cmp, &SourceCLI{
|
|
||||||
Args: []string{"--a=1", "--foo-b=2"},
|
|
||||||
})
|
|
||||||
assert.Error(t, err)
|
|
||||||
|
|
||||||
err = Populate(cmp, &SourceCLI{
|
|
||||||
Args: []string{"--a=1", "--foo-b=2", "--foo-c=3"},
|
|
||||||
})
|
|
||||||
assert.NoError(t, err)
|
|
||||||
assert.Equal(t, 1, *a)
|
|
||||||
assert.Equal(t, 2, *b)
|
|
||||||
assert.Equal(t, 3, *c)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestParamDefaultOrRequired(t *T) {
|
|
||||||
{
|
|
||||||
cmp := new(mcmp.Component)
|
|
||||||
Int(cmp, "a", ParamDefaultOrRequired(0))
|
|
||||||
params := CollectParams(cmp)
|
|
||||||
assert.Equal(t, "a", params[0].Name)
|
|
||||||
assert.Equal(t, true, params[0].Required)
|
|
||||||
assert.Equal(t, new(int), params[0].Into)
|
|
||||||
}
|
|
||||||
{
|
|
||||||
cmp := new(mcmp.Component)
|
|
||||||
Int(cmp, "a", ParamDefaultOrRequired(1))
|
|
||||||
i := 1
|
|
||||||
params := CollectParams(cmp)
|
|
||||||
assert.Equal(t, "a", params[0].Name)
|
|
||||||
assert.Equal(t, false, params[0].Required)
|
|
||||||
assert.Equal(t, &i, params[0].Into)
|
|
||||||
}
|
|
||||||
}
|
|
237
mcfg/param.go
237
mcfg/param.go
@ -1,237 +0,0 @@
|
|||||||
package mcfg
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"reflect"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/mediocregopher/mediocre-go-lib/mcmp"
|
|
||||||
"github.com/mediocregopher/mediocre-go-lib/mtime"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Param is a configuration parameter which can be populated by Populate. The
|
|
||||||
// Param will exist as part of a Component. For example, a Param with name
|
|
||||||
// "addr" under a Component with path of []string{"foo","bar"} will be setable
|
|
||||||
// on the CLI via "--foo-bar-addr". Other configuration Sources may treat the
|
|
||||||
// path/name differently, however.
|
|
||||||
//
|
|
||||||
// Param values are always unmarshaled as JSON values into the Into field of the
|
|
||||||
// Param, regardless of the actual Source.
|
|
||||||
type Param struct {
|
|
||||||
// How the parameter will be identified within a Component.
|
|
||||||
Name string
|
|
||||||
|
|
||||||
// A helpful description of how a parameter is expected to be used.
|
|
||||||
Usage string
|
|
||||||
|
|
||||||
// If the parameter's value is expected to be read as a go string. This is
|
|
||||||
// used for configuration sources like CLI which will automatically add
|
|
||||||
// double-quotes around the value if they aren't already there.
|
|
||||||
IsString bool
|
|
||||||
|
|
||||||
// If the parameter's value is expected to be a boolean. This is used for
|
|
||||||
// configuration sources like CLI which treat boolean parameters (aka flags)
|
|
||||||
// differently.
|
|
||||||
IsBool bool
|
|
||||||
|
|
||||||
// If true then the parameter _must_ be set by at least one Source.
|
|
||||||
Required bool
|
|
||||||
|
|
||||||
// The pointer/interface into which the configuration value will be
|
|
||||||
// json.Unmarshal'd. The value being pointed to also determines the default
|
|
||||||
// value of the parameter.
|
|
||||||
Into interface{}
|
|
||||||
|
|
||||||
// The Component this Param was added to. NOTE that this will be
|
|
||||||
// automatically filled in by AddParam when the Param is added to the
|
|
||||||
// Component.
|
|
||||||
Component *mcmp.Component
|
|
||||||
}
|
|
||||||
|
|
||||||
// ParamOption is a modifier which can be passed into most Param-generating
|
|
||||||
// functions (e.g. String, Int, etc...)
|
|
||||||
type ParamOption func(*Param)
|
|
||||||
|
|
||||||
// ParamRequired returns a ParamOption which ensures the parameter is required
|
|
||||||
// to be set by some configuration source. The default value of the parameter
|
|
||||||
// will be ignored.
|
|
||||||
func ParamRequired() ParamOption {
|
|
||||||
return func(param *Param) {
|
|
||||||
param.Required = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ParamDefault returns a ParamOption which ensures the parameter uses the given
|
|
||||||
// default value when no Sources set a value for it. If not given then mcfg will
|
|
||||||
// use the zero value of the Param's type as the default value.
|
|
||||||
//
|
|
||||||
// If ParamRequired is given then this does nothing.
|
|
||||||
func ParamDefault(value interface{}) ParamOption {
|
|
||||||
return func(param *Param) {
|
|
||||||
intoV := reflect.ValueOf(param.Into).Elem()
|
|
||||||
valueV := reflect.ValueOf(value)
|
|
||||||
|
|
||||||
intoType, valueType := intoV.Type(), valueV.Type()
|
|
||||||
if intoType != valueType {
|
|
||||||
panic(fmt.Sprintf("ParamDefault value is type %s, but should be %s", valueType, intoType))
|
|
||||||
} else if !intoV.CanSet() {
|
|
||||||
panic(fmt.Sprintf("Param.Into value %#v can't be set using reflection", param.Into))
|
|
||||||
}
|
|
||||||
|
|
||||||
intoV.Set(valueV)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ParamDefaultOrRequired returns a ParamOption whose behavior depends on the
|
|
||||||
// given value. If the given value is the zero value for its type, then this returns
|
|
||||||
// ParamRequired(), otherwise this returns ParamDefault(value).
|
|
||||||
func ParamDefaultOrRequired(value interface{}) ParamOption {
|
|
||||||
v := reflect.ValueOf(value)
|
|
||||||
zero := reflect.Zero(v.Type())
|
|
||||||
if v.Interface() == zero.Interface() {
|
|
||||||
return ParamRequired()
|
|
||||||
}
|
|
||||||
return ParamDefault(value)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ParamUsage returns a ParamOption which sets the usage string on the Param.
|
|
||||||
// This is used in some Sources, like SourceCLI, when displaying information
|
|
||||||
// about available parameters.
|
|
||||||
func ParamUsage(usage string) ParamOption {
|
|
||||||
// make all usages end with a period, because I say so
|
|
||||||
usage = strings.TrimSpace(usage)
|
|
||||||
if !strings.HasSuffix(usage, ".") {
|
|
||||||
usage += "."
|
|
||||||
}
|
|
||||||
|
|
||||||
return func(param *Param) {
|
|
||||||
param.Usage = usage
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func paramFullName(path []string, name string) string {
|
|
||||||
return strings.Join(append(path, name), "-")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p Param) fuzzyParse(v string) json.RawMessage {
|
|
||||||
if p.IsBool {
|
|
||||||
if v == "" || v == "0" || v == "false" {
|
|
||||||
return json.RawMessage("false")
|
|
||||||
}
|
|
||||||
return json.RawMessage("true")
|
|
||||||
|
|
||||||
} else if p.IsString && (v == "" || v[0] != '"') {
|
|
||||||
return json.RawMessage(`"` + v + `"`)
|
|
||||||
}
|
|
||||||
|
|
||||||
return json.RawMessage(v)
|
|
||||||
}
|
|
||||||
|
|
||||||
type cmpParamKey string
|
|
||||||
|
|
||||||
// used in tests
|
|
||||||
func getParam(cmp *mcmp.Component, name string) (Param, bool) {
|
|
||||||
param, ok := cmp.Value(cmpParamKey(name)).(Param)
|
|
||||||
return param, ok
|
|
||||||
}
|
|
||||||
|
|
||||||
// AddParam adds the given Param to the given Component. It will panic if a
|
|
||||||
// Param with the same Name already exists in the Component.
|
|
||||||
func AddParam(cmp *mcmp.Component, param Param, opts ...ParamOption) {
|
|
||||||
param.Name = strings.ToLower(param.Name)
|
|
||||||
param.Component = cmp
|
|
||||||
key := cmpParamKey(param.Name)
|
|
||||||
|
|
||||||
if cmp.HasValue(key) {
|
|
||||||
path := cmp.Path()
|
|
||||||
panic(fmt.Sprintf("Component.Path:%#v Param.Name:%q already exists", path, param.Name))
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, opt := range opts {
|
|
||||||
opt(¶m)
|
|
||||||
}
|
|
||||||
cmp.SetValue(key, param)
|
|
||||||
}
|
|
||||||
|
|
||||||
func getLocalParams(cmp *mcmp.Component) []Param {
|
|
||||||
values := cmp.Values()
|
|
||||||
params := make([]Param, 0, len(values))
|
|
||||||
for _, val := range values {
|
|
||||||
if param, ok := val.(Param); ok {
|
|
||||||
params = append(params, param)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return params
|
|
||||||
}
|
|
||||||
|
|
||||||
// Int64 returns an *int64 which will be populated once Populate is run on the
|
|
||||||
// Component.
|
|
||||||
func Int64(cmp *mcmp.Component, name string, opts ...ParamOption) *int64 {
|
|
||||||
var i int64
|
|
||||||
AddParam(cmp, Param{Name: name, Into: &i}, opts...)
|
|
||||||
return &i
|
|
||||||
}
|
|
||||||
|
|
||||||
// Int returns an *int which will be populated once Populate is run on the
|
|
||||||
// Component.
|
|
||||||
func Int(cmp *mcmp.Component, name string, opts ...ParamOption) *int {
|
|
||||||
var i int
|
|
||||||
AddParam(cmp, Param{Name: name, Into: &i}, opts...)
|
|
||||||
return &i
|
|
||||||
}
|
|
||||||
|
|
||||||
// Float64 returns a *float64 which will be populated once Populate is run on
|
|
||||||
// the Component
|
|
||||||
func Float64(cmp *mcmp.Component, name string, opts ...ParamOption) *float64 {
|
|
||||||
var f float64
|
|
||||||
AddParam(cmp, Param{Name: name, Into: &f}, opts...)
|
|
||||||
return &f
|
|
||||||
}
|
|
||||||
|
|
||||||
// String returns a *string which will be populated once Populate is run on
|
|
||||||
// the Component.
|
|
||||||
func String(cmp *mcmp.Component, name string, opts ...ParamOption) *string {
|
|
||||||
var s string
|
|
||||||
AddParam(cmp, Param{Name: name, IsString: true, Into: &s}, opts...)
|
|
||||||
return &s
|
|
||||||
}
|
|
||||||
|
|
||||||
// Bool returns a *bool which will be populated once Populate is run on the
|
|
||||||
// Component, and which defaults to false if unconfigured.
|
|
||||||
//
|
|
||||||
// The default behavior of all Sources is that a boolean parameter will be set
|
|
||||||
// to true unless the value is "", 0, or false. In the case of the CLI Source
|
|
||||||
// the value will also be true when the parameter is used with no value at all,
|
|
||||||
// as would be expected.
|
|
||||||
func Bool(cmp *mcmp.Component, name string, opts ...ParamOption) *bool {
|
|
||||||
var b bool
|
|
||||||
AddParam(cmp, Param{Name: name, IsBool: true, Into: &b}, opts...)
|
|
||||||
return &b
|
|
||||||
}
|
|
||||||
|
|
||||||
// TS returns an *mtime.TS which will be populated once Populate is run on
|
|
||||||
// the Component.
|
|
||||||
func TS(cmp *mcmp.Component, name string, opts ...ParamOption) *mtime.TS {
|
|
||||||
var t mtime.TS
|
|
||||||
AddParam(cmp, Param{Name: name, Into: &t}, opts...)
|
|
||||||
return &t
|
|
||||||
}
|
|
||||||
|
|
||||||
// Duration returns an *mtime.Duration which will be populated once Populate
|
|
||||||
// is run on the Component.
|
|
||||||
func Duration(cmp *mcmp.Component, name string, opts ...ParamOption) *mtime.Duration {
|
|
||||||
var d mtime.Duration
|
|
||||||
AddParam(cmp, Param{Name: name, IsString: true, Into: &d}, opts...)
|
|
||||||
return &d
|
|
||||||
}
|
|
||||||
|
|
||||||
// JSON reads the parameter value as a JSON value and unmarshals it into the
|
|
||||||
// given interface{} (which should be a pointer) once Populate is run on the
|
|
||||||
// Component.
|
|
||||||
//
|
|
||||||
// The receiver (into) is also used to determine the default value. ParamDefault
|
|
||||||
// should not be used as one of the opts.
|
|
||||||
func JSON(cmp *mcmp.Component, name string, into interface{}, opts ...ParamOption) {
|
|
||||||
AddParam(cmp, Param{Name: name, Into: into}, opts...)
|
|
||||||
}
|
|
@ -1,65 +0,0 @@
|
|||||||
package mcfg
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
|
|
||||||
"github.com/mediocregopher/mediocre-go-lib/mcmp"
|
|
||||||
)
|
|
||||||
|
|
||||||
// ParamValue describes a value for a parameter which has been parsed by a
|
|
||||||
// Source.
|
|
||||||
type ParamValue struct {
|
|
||||||
Name string
|
|
||||||
Path []string
|
|
||||||
Value json.RawMessage
|
|
||||||
}
|
|
||||||
|
|
||||||
// Source parses ParamValues out of a particular configuration source, given the
|
|
||||||
// Component which the Params were added to (via WithInt, WithString, etc...).
|
|
||||||
// CollectParams can be used to retrieve these Params.
|
|
||||||
//
|
|
||||||
// It's possible for Parsing to affect the Component itself, for example in the
|
|
||||||
// case of sub-commands.
|
|
||||||
//
|
|
||||||
// Source should not return ParamValues which were not explicitly set to a value
|
|
||||||
// by the configuration source.
|
|
||||||
//
|
|
||||||
// The returned []ParamValue may contain duplicates of the same Param's value.
|
|
||||||
// in which case the latter value takes precedence. It may also contain
|
|
||||||
// ParamValues which do not correspond to any of the passed in Params. These
|
|
||||||
// will be ignored in Populate.
|
|
||||||
type Source interface {
|
|
||||||
Parse(*mcmp.Component) ([]ParamValue, error)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ParamValues is simply a slice of ParamValue elements, which implements Parse
|
|
||||||
// by always returning itself as-is.
|
|
||||||
type ParamValues []ParamValue
|
|
||||||
|
|
||||||
var _ Source = ParamValues{}
|
|
||||||
|
|
||||||
// Parse implements the method for the Source interface.
|
|
||||||
func (pvs ParamValues) Parse(*mcmp.Component) ([]ParamValue, error) {
|
|
||||||
return pvs, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sources combines together multiple Source instances into one. It will call
|
|
||||||
// Parse on each element individually. Values from later Sources take precedence
|
|
||||||
// over previous ones.
|
|
||||||
type Sources []Source
|
|
||||||
|
|
||||||
var _ Source = Sources{}
|
|
||||||
|
|
||||||
// Parse implements the method for the Source interface.
|
|
||||||
func (ss Sources) Parse(cmp *mcmp.Component) ([]ParamValue, error) {
|
|
||||||
var pvs []ParamValue
|
|
||||||
for _, s := range ss {
|
|
||||||
var innerPVs []ParamValue
|
|
||||||
var err error
|
|
||||||
if innerPVs, err = s.Parse(cmp); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
pvs = append(pvs, innerPVs...)
|
|
||||||
}
|
|
||||||
return pvs, nil
|
|
||||||
}
|
|
@ -1,178 +0,0 @@
|
|||||||
package mcfg
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
. "testing"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/mediocregopher/mediocre-go-lib/mcmp"
|
|
||||||
"github.com/mediocregopher/mediocre-go-lib/mrand"
|
|
||||||
"github.com/mediocregopher/mediocre-go-lib/mtest/massert"
|
|
||||||
)
|
|
||||||
|
|
||||||
// The tests for the different Sources use mchk as their primary method of
|
|
||||||
// checking. They end up sharing a lot of the same functionality, so in here is
|
|
||||||
// all the code they share
|
|
||||||
|
|
||||||
type srcCommonState struct {
|
|
||||||
// availCmps get updated in place as the run goes on, it's easier to keep
|
|
||||||
// track of them this way than by traversing the hierarchy.
|
|
||||||
availCmps []*mcmp.Component
|
|
||||||
|
|
||||||
expPVs []ParamValue
|
|
||||||
// each specific test should wrap this to add the Source itself
|
|
||||||
}
|
|
||||||
|
|
||||||
func newSrcCommonState() srcCommonState {
|
|
||||||
var scs srcCommonState
|
|
||||||
{
|
|
||||||
root := new(mcmp.Component)
|
|
||||||
a := root.Child("a")
|
|
||||||
b := root.Child("b")
|
|
||||||
c := root.Child("c")
|
|
||||||
ab := a.Child("b")
|
|
||||||
bc := b.Child("c")
|
|
||||||
abc := ab.Child("c")
|
|
||||||
scs.availCmps = []*mcmp.Component{root, a, b, c, ab, bc, abc}
|
|
||||||
}
|
|
||||||
return scs
|
|
||||||
}
|
|
||||||
|
|
||||||
type srcCommonParams struct {
|
|
||||||
name string
|
|
||||||
cmp *mcmp.Component
|
|
||||||
isBool bool
|
|
||||||
nonBoolType string // "int", "str", "duration", "json"
|
|
||||||
unset bool
|
|
||||||
nonBoolVal string
|
|
||||||
}
|
|
||||||
|
|
||||||
func (scs srcCommonState) next() srcCommonParams {
|
|
||||||
var p srcCommonParams
|
|
||||||
if i := mrand.Intn(8); i == 0 {
|
|
||||||
p.name = mrand.Hex(1) + "-" + mrand.Hex(8)
|
|
||||||
} else {
|
|
||||||
p.name = mrand.Hex(8)
|
|
||||||
}
|
|
||||||
|
|
||||||
availCmpI := mrand.Intn(len(scs.availCmps))
|
|
||||||
p.cmp = scs.availCmps[availCmpI]
|
|
||||||
|
|
||||||
p.isBool = mrand.Intn(8) == 0
|
|
||||||
if !p.isBool {
|
|
||||||
p.nonBoolType = mrand.Element([]string{
|
|
||||||
"int",
|
|
||||||
"str",
|
|
||||||
"duration",
|
|
||||||
"json",
|
|
||||||
}, nil).(string)
|
|
||||||
}
|
|
||||||
p.unset = mrand.Intn(10) == 0
|
|
||||||
|
|
||||||
if p.isBool || p.unset {
|
|
||||||
return p
|
|
||||||
}
|
|
||||||
|
|
||||||
switch p.nonBoolType {
|
|
||||||
case "int":
|
|
||||||
p.nonBoolVal = fmt.Sprint(mrand.Int())
|
|
||||||
case "str":
|
|
||||||
p.nonBoolVal = mrand.Hex(16)
|
|
||||||
case "duration":
|
|
||||||
dur := time.Duration(mrand.Intn(86400)) * time.Second
|
|
||||||
p.nonBoolVal = dur.String()
|
|
||||||
case "json":
|
|
||||||
b, _ := json.Marshal(map[string]int{
|
|
||||||
mrand.Hex(4): mrand.Int(),
|
|
||||||
mrand.Hex(4): mrand.Int(),
|
|
||||||
mrand.Hex(4): mrand.Int(),
|
|
||||||
})
|
|
||||||
p.nonBoolVal = string(b)
|
|
||||||
}
|
|
||||||
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{}),
|
|
||||||
)
|
|
||||||
}
|
|
205
mctx/annotate.go
205
mctx/annotate.go
@ -6,107 +6,79 @@ import (
|
|||||||
"sort"
|
"sort"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Annotation describes the annotation of a key/value pair made on a Context via
|
type ctxKeyAnnotation int
|
||||||
// the Annotate call.
|
|
||||||
type Annotation struct {
|
// Annotator is a type which can add annotation data to an existing set of
|
||||||
Key, Value interface{}
|
// Annotations. The Annotate method should be expected to be called in a
|
||||||
|
// non-thread-safe manner.
|
||||||
|
type Annotator interface {
|
||||||
|
Annotate(Annotations)
|
||||||
}
|
}
|
||||||
|
|
||||||
type annotation struct {
|
type el struct {
|
||||||
Annotation
|
annotator Annotator
|
||||||
root, prev *annotation
|
prev *el
|
||||||
}
|
}
|
||||||
|
|
||||||
type annotationKey int
|
// WithAnnotator takes in an Annotator and returns a Context which will produce
|
||||||
|
// that Annotator's annotations when the Annotate function is called. The
|
||||||
|
// Annotator will be not be evaluated until the first call to Annotate.
|
||||||
|
func WithAnnotator(ctx context.Context, annotator Annotator) context.Context {
|
||||||
|
curr := &el{annotator: annotator}
|
||||||
|
curr.prev, _ = ctx.Value(ctxKeyAnnotation(0)).(*el)
|
||||||
|
return context.WithValue(ctx, ctxKeyAnnotation(0), curr)
|
||||||
|
}
|
||||||
|
|
||||||
// Annotate takes in one or more key/value pairs (kvs' length must be even) and
|
type annotationSeq []interface{}
|
||||||
// returns a Context carrying them.
|
|
||||||
|
func (s annotationSeq) Annotate(aa Annotations) {
|
||||||
|
for i := 0; i < len(s); i += 2 {
|
||||||
|
aa[s[i]] = s[i+1]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Annotate is a shortcut for calling WithAnnotator using an Annotations
|
||||||
|
// containing the given key/value pairs.
|
||||||
|
//
|
||||||
|
// NOTE If the length of kvs is not divisible by two this will panic.
|
||||||
func Annotate(ctx context.Context, kvs ...interface{}) context.Context {
|
func Annotate(ctx context.Context, kvs ...interface{}) context.Context {
|
||||||
if len(kvs)%2 > 0 {
|
if len(kvs)%2 > 0 {
|
||||||
panic("kvs being passed to mctx.Annotate must have an even number of elements")
|
panic("kvs being passed to mctx.Annotate must have an even number of elements")
|
||||||
} else if len(kvs) == 0 {
|
} else if len(kvs) == 0 {
|
||||||
return ctx
|
return ctx
|
||||||
}
|
}
|
||||||
|
return WithAnnotator(ctx, annotationSeq(kvs))
|
||||||
// if multiple annotations are passed in here it's not actually necessary to
|
|
||||||
// create an intermediate Context for each one, so keep curr outside and
|
|
||||||
// only use it later
|
|
||||||
var curr, root *annotation
|
|
||||||
prev, _ := ctx.Value(annotationKey(0)).(*annotation)
|
|
||||||
if prev != nil {
|
|
||||||
root = prev.root
|
|
||||||
}
|
|
||||||
for i := 0; i < len(kvs); i += 2 {
|
|
||||||
curr = &annotation{
|
|
||||||
Annotation: Annotation{Key: kvs[i], Value: kvs[i+1]},
|
|
||||||
prev: prev,
|
|
||||||
}
|
|
||||||
if root == nil {
|
|
||||||
root = curr
|
|
||||||
}
|
|
||||||
curr.root = curr
|
|
||||||
prev = curr
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx = context.WithValue(ctx, annotationKey(0), curr)
|
// Annotations is a set of key/value pairs representing a set of annotations. It
|
||||||
return ctx
|
// implements the Annotator interface along with other useful post-processing
|
||||||
|
// methods.
|
||||||
|
type Annotations map[interface{}]interface{}
|
||||||
|
|
||||||
|
// Annotate implements the method for the Annotator interface.
|
||||||
|
func (aa Annotations) Annotate(aa2 Annotations) {
|
||||||
|
for k, v := range aa {
|
||||||
|
aa2[k] = v
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Annotated is a shortcut for calling Annotate with a context.Background().
|
// StringMap formats each of the key/value pairs into strings using fmt.Sprint.
|
||||||
func Annotated(kvs ...interface{}) context.Context {
|
// If any two keys format to the same string, then type information will be
|
||||||
return Annotate(context.Background(), kvs...)
|
|
||||||
}
|
|
||||||
|
|
||||||
// AnnotationSet describes a set of unique Annotation values which were
|
|
||||||
// retrieved off a Context via the Annotations function. An AnnotationSet has a
|
|
||||||
// couple methods on it to aid in post-processing.
|
|
||||||
type AnnotationSet []Annotation
|
|
||||||
|
|
||||||
// Annotations returns all Annotation values which have been set via Annotate on
|
|
||||||
// this Context and its ancestors. If a key was set twice then only the most
|
|
||||||
// recent value is included.
|
|
||||||
func Annotations(ctx context.Context) AnnotationSet {
|
|
||||||
a, _ := ctx.Value(annotationKey(0)).(*annotation)
|
|
||||||
if a == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
m := map[interface{}]bool{}
|
|
||||||
|
|
||||||
var aa AnnotationSet
|
|
||||||
for {
|
|
||||||
if a == nil {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
if m[a.Key] {
|
|
||||||
a = a.prev
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
aa = append(aa, a.Annotation)
|
|
||||||
m[a.Key] = true
|
|
||||||
a = a.prev
|
|
||||||
}
|
|
||||||
return aa
|
|
||||||
}
|
|
||||||
|
|
||||||
// StringMap formats each of the Annotations into strings using fmt.Sprint. If
|
|
||||||
// any two keys format to the same string, then type information will be
|
|
||||||
// prefaced to each one.
|
// prefaced to each one.
|
||||||
func (aa AnnotationSet) StringMap() map[string]string {
|
func (aa Annotations) StringMap() map[string]string {
|
||||||
type mKey struct {
|
type mKey struct {
|
||||||
str string
|
str string
|
||||||
typ string
|
typ string
|
||||||
}
|
}
|
||||||
m := map[mKey][]Annotation{}
|
m := map[mKey][][2]interface{}{}
|
||||||
for _, a := range aa {
|
for k, v := range aa {
|
||||||
k := mKey{str: fmt.Sprint(a.Key)}
|
mk := mKey{str: fmt.Sprint(k)}
|
||||||
m[k] = append(m[k], a)
|
m[mk] = append(m[mk], [2]interface{}{k, v})
|
||||||
}
|
}
|
||||||
|
|
||||||
nextK := func(k mKey, a Annotation) mKey {
|
nextK := func(k mKey, kv [2]interface{}) mKey {
|
||||||
if k.typ == "" {
|
if k.typ == "" {
|
||||||
k.typ = fmt.Sprintf("%T", a.Key)
|
k.typ = fmt.Sprintf("%T", kv[0])
|
||||||
} else {
|
} else {
|
||||||
panic(fmt.Sprintf("mKey %#v is somehow conflicting with another", k))
|
panic(fmt.Sprintf("mKey %#v is somehow conflicting with another", k))
|
||||||
}
|
}
|
||||||
@ -120,9 +92,9 @@ func (aa AnnotationSet) StringMap() map[string]string {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
any = true
|
any = true
|
||||||
for _, a := range annotations {
|
for _, kv := range annotations {
|
||||||
k2 := nextK(k, a)
|
k2 := nextK(k, kv)
|
||||||
m[k2] = append(m[k2], a)
|
m[k2] = append(m[k2], kv)
|
||||||
}
|
}
|
||||||
delete(m, k)
|
delete(m, k)
|
||||||
}
|
}
|
||||||
@ -133,12 +105,12 @@ func (aa AnnotationSet) StringMap() map[string]string {
|
|||||||
|
|
||||||
outM := map[string]string{}
|
outM := map[string]string{}
|
||||||
for k, annotations := range m {
|
for k, annotations := range m {
|
||||||
a := annotations[0]
|
kv := annotations[0]
|
||||||
kStr := k.str
|
kStr := k.str
|
||||||
if k.typ != "" {
|
if k.typ != "" {
|
||||||
kStr = k.typ + "(" + kStr + ")"
|
kStr = k.typ + "(" + kStr + ")"
|
||||||
}
|
}
|
||||||
outM[kStr] = fmt.Sprint(a.Value)
|
outM[kStr] = fmt.Sprint(kv[1])
|
||||||
}
|
}
|
||||||
return outM
|
return outM
|
||||||
}
|
}
|
||||||
@ -146,7 +118,7 @@ func (aa AnnotationSet) StringMap() map[string]string {
|
|||||||
// StringSlice is like StringMap but it returns a slice of key/value tuples
|
// StringSlice is like StringMap but it returns a slice of key/value tuples
|
||||||
// rather than a map. If sorted is true then the slice will be sorted by key in
|
// rather than a map. If sorted is true then the slice will be sorted by key in
|
||||||
// ascending order.
|
// ascending order.
|
||||||
func (aa AnnotationSet) StringSlice(sorted bool) [][2]string {
|
func (aa Annotations) StringSlice(sorted bool) [][2]string {
|
||||||
m := aa.StringMap()
|
m := aa.StringMap()
|
||||||
slice := make([][2]string, 0, len(m))
|
slice := make([][2]string, 0, len(m))
|
||||||
for k, v := range m {
|
for k, v := range m {
|
||||||
@ -160,55 +132,40 @@ func (aa AnnotationSet) StringSlice(sorted bool) [][2]string {
|
|||||||
return slice
|
return slice
|
||||||
}
|
}
|
||||||
|
|
||||||
func mergeAnnotations(ctxA, ctxB context.Context) context.Context {
|
// EvaluateAnnotations collects all annotation key/values which have been set
|
||||||
annotationA, _ := ctxA.Value(annotationKey(0)).(*annotation)
|
// via Annotate(With) on this Context and its ancestors, and sets those
|
||||||
annotationB, _ := ctxB.Value(annotationKey(0)).(*annotation)
|
// key/values on the given Annotations. If a key was set twice then only the
|
||||||
if annotationB == nil {
|
// most recent value is included.
|
||||||
return ctxA
|
func EvaluateAnnotations(ctx context.Context, aa Annotations) {
|
||||||
} else if annotationA == nil {
|
tmp := Annotations{}
|
||||||
return context.WithValue(ctxA, annotationKey(0), annotationB)
|
for el, _ := ctx.Value(ctxKeyAnnotation(0)).(*el); el != nil; el = el.prev {
|
||||||
|
el.annotator.Annotate(tmp)
|
||||||
|
for k, v := range tmp {
|
||||||
|
if _, ok := aa[k]; ok {
|
||||||
|
continue
|
||||||
}
|
}
|
||||||
|
aa[k] = v
|
||||||
var headA, currA *annotation
|
delete(tmp, k)
|
||||||
currB := annotationB
|
|
||||||
for {
|
|
||||||
if currB == nil {
|
|
||||||
break
|
|
||||||
}
|
}
|
||||||
|
|
||||||
prevA := &annotation{
|
|
||||||
Annotation: currB.Annotation,
|
|
||||||
root: annotationA.root,
|
|
||||||
}
|
|
||||||
if currA != nil {
|
|
||||||
currA.prev = prevA
|
|
||||||
}
|
|
||||||
currA, currB = prevA, currB.prev
|
|
||||||
if headA == nil {
|
|
||||||
headA = currA
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
currA.prev = annotationA
|
//
|
||||||
return context.WithValue(ctxA, annotationKey(0), headA)
|
|
||||||
}
|
|
||||||
|
|
||||||
// MergeAnnotations sequentially merges the annotation data of the passed in
|
// MergeAnnotations sequentially merges the annotation data of the passed in
|
||||||
// Contexts into the first passed in one. Data from a Context overwrites
|
// Contexts into the first passed in Context. Data from a Context overwrites
|
||||||
// overlapping data on all passed in Contexts to the left of it. All other
|
// overlapping data on all passed in Contexts to the left of it. All other
|
||||||
// aspects of the first Context remain the same, and that Context is returned
|
// aspects of the first Context remain the same, and that Context is returned
|
||||||
// with the new set of Annotation data.
|
// with the new set of Annotation data.
|
||||||
//
|
func MergeAnnotations(ctx context.Context, ctxs ...context.Context) context.Context {
|
||||||
// NOTE this will panic if no Contexts are passed in.
|
aa := Annotations{}
|
||||||
func MergeAnnotations(ctxs ...context.Context) context.Context {
|
tmp := Annotations{}
|
||||||
return MergeAnnotationsInto(ctxs[0], ctxs[1:]...)
|
EvaluateAnnotations(ctx, aa)
|
||||||
}
|
|
||||||
|
|
||||||
// MergeAnnotationsInto is a convenience function which works like
|
|
||||||
// MergeAnnotations.
|
|
||||||
func MergeAnnotationsInto(ctx context.Context, ctxs ...context.Context) context.Context {
|
|
||||||
for _, ctxB := range ctxs {
|
for _, ctxB := range ctxs {
|
||||||
ctx = mergeAnnotations(ctx, ctxB)
|
EvaluateAnnotations(ctxB, tmp)
|
||||||
|
for k, v := range tmp {
|
||||||
|
aa[k] = v
|
||||||
|
delete(tmp, k)
|
||||||
}
|
}
|
||||||
return ctx
|
}
|
||||||
|
return context.WithValue(ctx, ctxKeyAnnotation(0), &el{annotator: aa})
|
||||||
}
|
}
|
||||||
|
@ -7,28 +7,37 @@ import (
|
|||||||
"github.com/mediocregopher/mediocre-go-lib/mtest/massert"
|
"github.com/mediocregopher/mediocre-go-lib/mtest/massert"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type testAnnotator [2]string
|
||||||
|
|
||||||
|
func (t testAnnotator) Annotate(aa Annotations) {
|
||||||
|
aa[t[0]] = t[1]
|
||||||
|
}
|
||||||
|
|
||||||
func TestAnnotate(t *T) {
|
func TestAnnotate(t *T) {
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
ctx = Annotate(ctx, "a", "foo")
|
ctx = Annotate(ctx, "a", "foo")
|
||||||
ctx = Annotate(ctx, "b", "bar")
|
ctx = Annotate(ctx, "b", "bar")
|
||||||
ctx = Annotate(ctx, "b", "BAR")
|
ctx = WithAnnotator(ctx, testAnnotator{"b", "BAR"})
|
||||||
|
|
||||||
|
aa := Annotations{}
|
||||||
|
EvaluateAnnotations(ctx, aa)
|
||||||
|
|
||||||
annotations := Annotations(ctx)
|
|
||||||
massert.Require(t,
|
massert.Require(t,
|
||||||
massert.Length(annotations, 2),
|
massert.Equal(Annotations{
|
||||||
massert.HasValue(annotations, Annotation{Key: "a", Value: "foo"}),
|
"a": "foo",
|
||||||
massert.HasValue(annotations, Annotation{Key: "b", Value: "BAR"}),
|
"b": "BAR",
|
||||||
|
}, aa),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestAnnotationsStringMap(t *T) {
|
func TestAnnotationsStringMap(t *T) {
|
||||||
type A int
|
type A int
|
||||||
type B int
|
type B int
|
||||||
aa := AnnotationSet{
|
aa := Annotations{
|
||||||
{Key: 0, Value: "zero"},
|
0: "zero",
|
||||||
{Key: 1, Value: "one"},
|
1: "one",
|
||||||
{Key: A(2), Value: "two"},
|
A(2): "two",
|
||||||
{Key: B(2), Value: "TWO"},
|
B(2): "TWO",
|
||||||
}
|
}
|
||||||
|
|
||||||
massert.Require(t,
|
massert.Require(t,
|
||||||
@ -48,11 +57,14 @@ func TestMergeAnnotations(t *T) {
|
|||||||
ctxB = Annotate(ctxB, 1, "ONE", 2, "TWO")
|
ctxB = Annotate(ctxB, 1, "ONE", 2, "TWO")
|
||||||
|
|
||||||
ctx := MergeAnnotations(ctxA, ctxB)
|
ctx := MergeAnnotations(ctxA, ctxB)
|
||||||
|
aa := Annotations{}
|
||||||
|
EvaluateAnnotations(ctx, aa)
|
||||||
|
|
||||||
err := massert.Equal(map[string]string{
|
err := massert.Equal(map[string]string{
|
||||||
"0": "ZERO",
|
"0": "ZERO",
|
||||||
"1": "ONE",
|
"1": "ONE",
|
||||||
"2": "TWO",
|
"2": "TWO",
|
||||||
}, Annotations(ctx).StringMap()).Assert()
|
}, aa.StringMap()).Assert()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
72
mlog/cmp.go
72
mlog/cmp.go
@ -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]),
|
|
||||||
)
|
|
||||||
|
|
||||||
}
|
|
304
mlog/mlog.go
304
mlog/mlog.go
@ -11,14 +11,20 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"io"
|
"io"
|
||||||
|
"io/ioutil"
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/mediocregopher/mediocre-go-lib/mctx"
|
"github.com/mediocregopher/mediocre-go-lib/mctx"
|
||||||
"github.com/mediocregopher/mediocre-go-lib/merr"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Null is an instance of Logger which will write all Messages to /dev/null.
|
||||||
|
var Null = NewLogger(&LoggerOpts{
|
||||||
|
MessageHandler: NewMessageHandler(ioutil.Discard),
|
||||||
|
})
|
||||||
|
|
||||||
// Truncate is a helper function to truncate a string to a given size. It will
|
// Truncate is a helper function to truncate a string to a given size. It will
|
||||||
// add 3 trailing elipses, so the returned string will be at most size+3
|
// add 3 trailing elipses, so the returned string will be at most size+3
|
||||||
// characters long
|
// characters long
|
||||||
@ -37,33 +43,32 @@ type Level interface {
|
|||||||
// String gives the string form of the level, e.g. "INFO" or "ERROR"
|
// String gives the string form of the level, e.g. "INFO" or "ERROR"
|
||||||
String() string
|
String() string
|
||||||
|
|
||||||
// Uint gives an integer indicator of the severity of the level, with zero
|
// Int gives an integer indicator of the severity of the level, with zero
|
||||||
// being most severe. If a Level with Uint of zero is logged then the Logger
|
// being most severe. If a Level with a negative Int is logged then the
|
||||||
// implementation provided by this package will exit the process (i.e. zero
|
// Logger implementation provided by this package will exit the process.
|
||||||
// is used as Fatal).
|
Int() int
|
||||||
Uint() uint
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type level struct {
|
type level struct {
|
||||||
s string
|
s string
|
||||||
i uint
|
i int
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l level) String() string {
|
func (l level) String() string {
|
||||||
return l.s
|
return l.s
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l level) Uint() uint {
|
func (l level) Int() int {
|
||||||
return l.i
|
return l.i
|
||||||
}
|
}
|
||||||
|
|
||||||
// All pre-defined log levels
|
// All pre-defined log levels
|
||||||
var (
|
var (
|
||||||
DebugLevel Level = level{s: "DEBUG", i: 40}
|
LevelDebug Level = level{s: "DEBUG", i: 40}
|
||||||
InfoLevel Level = level{s: "INFO", i: 30}
|
LevelInfo Level = level{s: "INFO", i: 30}
|
||||||
WarnLevel Level = level{s: "WARN", i: 20}
|
LevelWarn Level = level{s: "WARN", i: 20}
|
||||||
ErrorLevel Level = level{s: "ERROR", i: 10}
|
LevelError Level = level{s: "ERROR", i: 10}
|
||||||
FatalLevel Level = level{s: "FATAL", i: 0}
|
LevelFatal Level = level{s: "FATAL", i: -1}
|
||||||
)
|
)
|
||||||
|
|
||||||
// LevelFromString takes a string describing one of the pre-defined Levels (e.g.
|
// LevelFromString takes a string describing one of the pre-defined Levels (e.g.
|
||||||
@ -72,15 +77,15 @@ var (
|
|||||||
func LevelFromString(s string) Level {
|
func LevelFromString(s string) Level {
|
||||||
switch strings.TrimSpace(strings.ToUpper(s)) {
|
switch strings.TrimSpace(strings.ToUpper(s)) {
|
||||||
case "DEBUG":
|
case "DEBUG":
|
||||||
return DebugLevel
|
return LevelDebug
|
||||||
case "INFO":
|
case "INFO":
|
||||||
return InfoLevel
|
return LevelInfo
|
||||||
case "WARN":
|
case "WARN":
|
||||||
return WarnLevel
|
return LevelWarn
|
||||||
case "ERROR":
|
case "ERROR":
|
||||||
return ErrorLevel
|
return LevelError
|
||||||
case "FATAL":
|
case "FATAL":
|
||||||
return FatalLevel
|
return LevelFatal
|
||||||
default:
|
default:
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@ -88,106 +93,186 @@ func LevelFromString(s string) Level {
|
|||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
// Message describes a message to be logged, after having already resolved the
|
// Message describes a message to be logged.
|
||||||
// KVer
|
|
||||||
type Message struct {
|
type Message struct {
|
||||||
|
Context context.Context
|
||||||
Level
|
Level
|
||||||
Description string
|
Description string
|
||||||
Contexts []context.Context
|
Annotators []mctx.Annotator
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handler is a function which can process Messages in some way.
|
// FullMessage extends Message to contain loggable properties not provided
|
||||||
|
// directly by the user.
|
||||||
|
type FullMessage struct {
|
||||||
|
Message
|
||||||
|
Time time.Time
|
||||||
|
Namespace []string
|
||||||
|
}
|
||||||
|
|
||||||
|
// MessageHandler is a type which can process Messages in some way.
|
||||||
//
|
//
|
||||||
// NOTE that Logger does not handle thread-safety, that must be done inside the
|
// NOTE that Logger does not handle thread-safety, that must be done inside the
|
||||||
// Handler if necessary.
|
// MessageHandler if necessary.
|
||||||
type Handler func(msg Message) error
|
type MessageHandler interface {
|
||||||
|
Handle(FullMessage) error
|
||||||
|
|
||||||
// MessageJSON is the type used to encode Messages to JSON in DefaultHandler
|
// Sync flushes any buffered data to the handler's output, e.g. a file or
|
||||||
type MessageJSON struct {
|
// network connection. If the handler doesn't buffer data then this will be
|
||||||
|
// a no-op.
|
||||||
|
Sync() error
|
||||||
|
}
|
||||||
|
|
||||||
|
type messageHandler struct {
|
||||||
|
l sync.Mutex
|
||||||
|
out io.Writer
|
||||||
|
enc *json.Encoder
|
||||||
|
aa mctx.Annotations
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewMessageHandler initializes and returns a MessageHandler which will write
|
||||||
|
// all messages to the given io.Writer in a thread-safe way. If the io.Writer
|
||||||
|
// also implements a Sync or Flush method then that will be called when Sync is
|
||||||
|
// called on the returned MessageHandler.
|
||||||
|
func NewMessageHandler(out io.Writer) MessageHandler {
|
||||||
|
return &messageHandler{
|
||||||
|
out: out,
|
||||||
|
enc: json.NewEncoder(out),
|
||||||
|
aa: mctx.Annotations{},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type messageJSON struct {
|
||||||
|
TimeDate string `json:"td"`
|
||||||
|
Timestamp int64 `json:"ts"`
|
||||||
Level string `json:"level"`
|
Level string `json:"level"`
|
||||||
|
Namespace []string `json:"ns,omitempty"`
|
||||||
Description string `json:"descr"`
|
Description string `json:"descr"`
|
||||||
|
LevelInt int `json:"level_int"`
|
||||||
|
|
||||||
// key -> value
|
// key -> value
|
||||||
Annotations map[string]string `json:"annotations,omitempty"`
|
Annotations map[string]string `json:"annotations,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// DefaultHandler initializes and returns a Handler which will write all
|
const msgTimeFormat = "06/01/02 15:04:05.000000"
|
||||||
// messages to os.Stderr in a thread-safe way. This is the Handler which
|
|
||||||
// NewLogger will use automatically.
|
func (h *messageHandler) Handle(msg FullMessage) error {
|
||||||
func DefaultHandler() Handler {
|
h.l.Lock()
|
||||||
return defaultHandler(os.Stderr)
|
defer h.l.Unlock()
|
||||||
|
|
||||||
|
mctx.EvaluateAnnotations(msg.Context, h.aa)
|
||||||
|
for _, annotator := range msg.Annotators {
|
||||||
|
annotator.Annotate(h.aa)
|
||||||
}
|
}
|
||||||
|
|
||||||
func defaultHandler(out io.Writer) Handler {
|
msgJSON := messageJSON{
|
||||||
l := new(sync.Mutex)
|
TimeDate: msg.Time.UTC().Format(msgTimeFormat),
|
||||||
enc := json.NewEncoder(out)
|
Timestamp: msg.Time.UnixNano(),
|
||||||
return func(msg Message) error {
|
|
||||||
l.Lock()
|
|
||||||
defer l.Unlock()
|
|
||||||
|
|
||||||
msgJSON := MessageJSON{
|
|
||||||
Level: msg.Level.String(),
|
Level: msg.Level.String(),
|
||||||
|
LevelInt: msg.Level.Int(),
|
||||||
|
Namespace: msg.Namespace,
|
||||||
Description: msg.Description,
|
Description: msg.Description,
|
||||||
}
|
Annotations: h.aa.StringMap(),
|
||||||
if len(msg.Contexts) > 0 {
|
|
||||||
ctx := mctx.MergeAnnotations(msg.Contexts...)
|
|
||||||
msgJSON.Annotations = mctx.Annotations(ctx).StringMap()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return enc.Encode(msgJSON)
|
for k := range h.aa {
|
||||||
}
|
delete(h.aa, k)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Logger directs Messages to an internal Handler and provides convenient
|
return h.enc.Encode(msgJSON)
|
||||||
// methods for creating and modifying its own behavior. All methods are
|
}
|
||||||
// thread-safe.
|
|
||||||
|
func (h *messageHandler) Sync() error {
|
||||||
|
h.l.Lock()
|
||||||
|
defer h.l.Unlock()
|
||||||
|
if s, ok := h.out.(interface{ Sync() error }); ok {
|
||||||
|
return s.Sync()
|
||||||
|
} else if f, ok := h.out.(interface{ Flush() error }); ok {
|
||||||
|
return f.Flush()
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// LoggerOpts are optional parameters to NewLogger. All fields are optional. A
|
||||||
|
// nil value of LoggerOpts is equivalent to an empty one.
|
||||||
|
type LoggerOpts struct {
|
||||||
|
// MessageHandler is the MessageHandler which will be used to process
|
||||||
|
// Messages.
|
||||||
|
//
|
||||||
|
// Defaults to NewMessageHandler(os.Stderr).
|
||||||
|
MessageHandler MessageHandler
|
||||||
|
|
||||||
|
// MaxLevel indicates the maximum log level which should be handled. See the
|
||||||
|
// Level interface for more.
|
||||||
|
//
|
||||||
|
// Defaults to LevelInfo.Int().
|
||||||
|
MaxLevel int
|
||||||
|
|
||||||
|
// Now returns the current time.Time whenever it is called.
|
||||||
|
//
|
||||||
|
// Defaults to time.Now.
|
||||||
|
Now func() time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *LoggerOpts) withDefaults() *LoggerOpts {
|
||||||
|
out := new(LoggerOpts)
|
||||||
|
if o != nil {
|
||||||
|
*out = *o
|
||||||
|
}
|
||||||
|
|
||||||
|
if out.MessageHandler == nil {
|
||||||
|
out.MessageHandler = NewMessageHandler(os.Stderr)
|
||||||
|
}
|
||||||
|
|
||||||
|
if out.MaxLevel == 0 {
|
||||||
|
out.MaxLevel = LevelInfo.Int()
|
||||||
|
}
|
||||||
|
|
||||||
|
if out.Now == nil {
|
||||||
|
out.Now = time.Now
|
||||||
|
}
|
||||||
|
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
// Logger creates and directs Messages to an internal MessageHandler. All
|
||||||
|
// methods are thread-safe.
|
||||||
type Logger struct {
|
type Logger struct {
|
||||||
|
opts *LoggerOpts
|
||||||
l *sync.RWMutex
|
l *sync.RWMutex
|
||||||
h Handler
|
ns []string
|
||||||
maxLevel uint
|
|
||||||
|
|
||||||
testMsgWrittenCh chan struct{} // only initialized/used in tests
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewLogger initializes and returns a new instance of Logger which will write
|
// NewLogger initializes and returns a new instance of Logger.
|
||||||
// to the DefaultHandler.
|
func NewLogger(opts *LoggerOpts) *Logger {
|
||||||
func NewLogger() *Logger {
|
|
||||||
return &Logger{
|
return &Logger{
|
||||||
|
opts: opts.withDefaults(),
|
||||||
l: new(sync.RWMutex),
|
l: new(sync.RWMutex),
|
||||||
h: DefaultHandler(),
|
|
||||||
maxLevel: InfoLevel.Uint(),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clone returns an identical instance of the Logger which can be modified
|
// Close cleans up all resources held by the Logger.
|
||||||
// independently of the original.
|
func (l *Logger) Close() error {
|
||||||
func (l *Logger) Clone() *Logger {
|
if err := l.opts.MessageHandler.Sync(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *Logger) clone() *Logger {
|
||||||
l2 := *l
|
l2 := *l
|
||||||
l2.l = new(sync.RWMutex)
|
l2.l = new(sync.RWMutex)
|
||||||
|
l2.ns = make([]string, len(l.ns), len(l.ns)+1)
|
||||||
|
copy(l2.ns, l.ns)
|
||||||
return &l2
|
return &l2
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetMaxLevel sets the Logger to not log any messages with a higher Level.Uint
|
// WithNamespace returns a clone of the Logger with the given value appended to
|
||||||
// value than of the one given.
|
// its namespace array. The namespace array is included in every FullMessage
|
||||||
func (l *Logger) SetMaxLevel(lvl Level) {
|
// which is handled by Logger's MessageHandler.
|
||||||
l.l.Lock()
|
func (l *Logger) WithNamespace(name string) *Logger {
|
||||||
defer l.l.Unlock()
|
l = l.clone()
|
||||||
l.maxLevel = lvl.Uint()
|
l.ns = append(l.ns, name)
|
||||||
}
|
return l
|
||||||
|
|
||||||
// SetHandler sets the Logger to use the given Handler in order to process
|
|
||||||
// Messages.
|
|
||||||
func (l *Logger) SetHandler(h Handler) {
|
|
||||||
l.l.Lock()
|
|
||||||
defer l.l.Unlock()
|
|
||||||
l.h = h
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handler returns the Handler currently in use by the Logger.
|
|
||||||
func (l *Logger) Handler() Handler {
|
|
||||||
l.l.RLock()
|
|
||||||
defer l.l.RUnlock()
|
|
||||||
return l.h
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Log can be used to manually log a message of some custom defined Level.
|
// Log can be used to manually log a message of some custom defined Level.
|
||||||
@ -198,54 +283,59 @@ func (l *Logger) Log(msg Message) {
|
|||||||
l.l.RLock()
|
l.l.RLock()
|
||||||
defer l.l.RUnlock()
|
defer l.l.RUnlock()
|
||||||
|
|
||||||
if l.maxLevel < msg.Level.Uint() {
|
if l.opts.MaxLevel < msg.Level.Int() {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := l.h(msg); err != nil {
|
fullMsg := FullMessage{
|
||||||
go l.Error("Logger.Handler returned error", merr.Context(err))
|
Message: msg,
|
||||||
|
Time: l.opts.Now(),
|
||||||
|
Namespace: l.ns,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := l.opts.MessageHandler.Handle(fullMsg); err != nil {
|
||||||
|
// TODO log the error
|
||||||
|
go l.Error(context.Background(), "MessageHandler.Handle returned error")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if l.testMsgWrittenCh != nil {
|
if msg.Level.Int() < 0 {
|
||||||
l.testMsgWrittenCh <- struct{}{}
|
l.opts.MessageHandler.Sync()
|
||||||
}
|
|
||||||
|
|
||||||
if msg.Level.Uint() == 0 {
|
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func mkMsg(lvl Level, descr string, ctxs ...context.Context) Message {
|
func mkMsg(ctx context.Context, lvl Level, descr string, annotators ...mctx.Annotator) Message {
|
||||||
return Message{
|
return Message{
|
||||||
|
Context: ctx,
|
||||||
Level: lvl,
|
Level: lvl,
|
||||||
Description: descr,
|
Description: descr,
|
||||||
Contexts: ctxs,
|
Annotators: annotators,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Debug logs a DebugLevel message.
|
// Debug logs a LevelDebug message.
|
||||||
func (l *Logger) Debug(descr string, ctxs ...context.Context) {
|
func (l *Logger) Debug(ctx context.Context, descr string, annotators ...mctx.Annotator) {
|
||||||
l.Log(mkMsg(DebugLevel, descr, ctxs...))
|
l.Log(mkMsg(ctx, LevelDebug, descr, annotators...))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Info logs a InfoLevel message.
|
// Info logs a LevelInfo message.
|
||||||
func (l *Logger) Info(descr string, ctxs ...context.Context) {
|
func (l *Logger) Info(ctx context.Context, descr string, annotators ...mctx.Annotator) {
|
||||||
l.Log(mkMsg(InfoLevel, descr, ctxs...))
|
l.Log(mkMsg(ctx, LevelInfo, descr, annotators...))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Warn logs a WarnLevel message.
|
// Warn logs a LevelWarn message.
|
||||||
func (l *Logger) Warn(descr string, ctxs ...context.Context) {
|
func (l *Logger) Warn(ctx context.Context, descr string, annotators ...mctx.Annotator) {
|
||||||
l.Log(mkMsg(WarnLevel, descr, ctxs...))
|
l.Log(mkMsg(ctx, LevelWarn, descr, annotators...))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Error logs a ErrorLevel message.
|
// Error logs a LevelError message.
|
||||||
func (l *Logger) Error(descr string, ctxs ...context.Context) {
|
func (l *Logger) Error(ctx context.Context, descr string, annotators ...mctx.Annotator) {
|
||||||
l.Log(mkMsg(ErrorLevel, descr, ctxs...))
|
l.Log(mkMsg(ctx, LevelError, descr, annotators...))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fatal logs a FatalLevel message. A Fatal message automatically stops the
|
// Fatal logs a LevelFatal message. A Fatal message automatically stops the
|
||||||
// process with an os.Exit(1)
|
// process with an os.Exit(1) if the default MessageHandler is used.
|
||||||
func (l *Logger) Fatal(descr string, ctxs ...context.Context) {
|
func (l *Logger) Fatal(ctx context.Context, descr string, annotators ...mctx.Annotator) {
|
||||||
l.Log(mkMsg(FatalLevel, descr, ctxs...))
|
l.Log(mkMsg(ctx, LevelFatal, descr, annotators...))
|
||||||
}
|
}
|
||||||
|
@ -3,6 +3,7 @@ package mlog
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
. "testing"
|
. "testing"
|
||||||
"time"
|
"time"
|
||||||
@ -21,18 +22,17 @@ func TestTruncate(t *T) {
|
|||||||
|
|
||||||
func TestLogger(t *T) {
|
func TestLogger(t *T) {
|
||||||
buf := new(bytes.Buffer)
|
buf := new(bytes.Buffer)
|
||||||
h := defaultHandler(buf)
|
now := time.Now().UTC()
|
||||||
|
td, ts := now.Format(msgTimeFormat), fmt.Sprint(now.UnixNano())
|
||||||
|
|
||||||
l := NewLogger()
|
l := NewLogger(&LoggerOpts{
|
||||||
l.SetHandler(h)
|
MessageHandler: NewMessageHandler(buf),
|
||||||
l.testMsgWrittenCh = make(chan struct{}, 10)
|
Now: func() time.Time { return now },
|
||||||
|
})
|
||||||
|
|
||||||
assertOut := func(expected string) massert.Assertion {
|
assertOut := func(expected string) massert.Assertion {
|
||||||
select {
|
expected = strings.ReplaceAll(expected, "<TD>", td)
|
||||||
case <-l.testMsgWrittenCh:
|
expected = strings.ReplaceAll(expected, "<TS>", ts)
|
||||||
case <-time.After(1 * time.Second):
|
|
||||||
return massert.Errorf("waited too long for msg to write")
|
|
||||||
}
|
|
||||||
out, err := buf.ReadString('\n')
|
out, err := buf.ReadString('\n')
|
||||||
return massert.All(
|
return massert.All(
|
||||||
massert.Nil(err),
|
massert.Nil(err),
|
||||||
@ -40,41 +40,38 @@ func TestLogger(t *T) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Default max level should be INFO
|
|
||||||
l.Debug("foo")
|
|
||||||
l.Info("bar")
|
|
||||||
l.Warn("baz")
|
|
||||||
l.Error("buz")
|
|
||||||
massert.Require(t,
|
|
||||||
assertOut(`{"level":"INFO","descr":"bar"}`),
|
|
||||||
assertOut(`{"level":"WARN","descr":"baz"}`),
|
|
||||||
assertOut(`{"level":"ERROR","descr":"buz"}`),
|
|
||||||
)
|
|
||||||
|
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
|
|
||||||
l.SetMaxLevel(WarnLevel)
|
// Default max level should be INFO
|
||||||
l.Debug("foo")
|
l.Debug(ctx, "foo")
|
||||||
l.Info("bar")
|
l.Info(ctx, "bar")
|
||||||
l.Warn("baz")
|
l.Warn(ctx, "baz")
|
||||||
l.Error("buz", mctx.Annotate(ctx, "a", "b", "c", "d"))
|
l.Error(ctx, "buz")
|
||||||
massert.Require(t,
|
massert.Require(t,
|
||||||
assertOut(`{"level":"WARN","descr":"baz"}`),
|
assertOut(`{"td":"<TD>","ts":<TS>,"level":"INFO","descr":"bar","level_int":30}`),
|
||||||
assertOut(`{"level":"ERROR","descr":"buz","annotations":{"a":"b","c":"d"}}`),
|
assertOut(`{"td":"<TD>","ts":<TS>,"level":"WARN","descr":"baz","level_int":20}`),
|
||||||
|
assertOut(`{"td":"<TD>","ts":<TS>,"level":"ERROR","descr":"buz","level_int":10}`),
|
||||||
)
|
)
|
||||||
|
|
||||||
l2 := l.Clone()
|
// annotate context
|
||||||
l2.SetMaxLevel(InfoLevel)
|
ctx = mctx.Annotate(ctx, "foo", "bar")
|
||||||
l2.SetHandler(func(msg Message) error {
|
l.Info(ctx, "bar")
|
||||||
msg.Description = strings.ToUpper(msg.Description)
|
|
||||||
return h(msg)
|
|
||||||
})
|
|
||||||
l2.Info("bar")
|
|
||||||
l2.Warn("baz")
|
|
||||||
l.Error("buz")
|
|
||||||
massert.Require(t,
|
massert.Require(t,
|
||||||
assertOut(`{"level":"INFO","descr":"BAR"}`),
|
assertOut(`{"td":"<TD>","ts":<TS>,"level":"INFO","descr":"bar","level_int":30,"annotations":{"foo":"bar"}}`),
|
||||||
assertOut(`{"level":"WARN","descr":"BAZ"}`),
|
)
|
||||||
assertOut(`{"level":"ERROR","descr":"buz"}`),
|
|
||||||
|
// add other annotations
|
||||||
|
l.Info(ctx, "bar", mctx.Annotations{
|
||||||
|
"foo": "BAR",
|
||||||
|
})
|
||||||
|
massert.Require(t,
|
||||||
|
assertOut(`{"td":"<TD>","ts":<TS>,"level":"INFO","descr":"bar","level_int":30,"annotations":{"foo":"BAR"}}`),
|
||||||
|
)
|
||||||
|
|
||||||
|
// add namespace
|
||||||
|
l = l.WithNamespace("ns")
|
||||||
|
l.Info(ctx, "bar")
|
||||||
|
massert.Require(t,
|
||||||
|
assertOut(`{"td":"<TD>","ts":<TS>,"level":"INFO","ns":["ns"],"descr":"bar","level_int":30,"annotations":{"foo":"bar"}}`),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
127
mrun/hook.go
127
mrun/hook.go
@ -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),
|
|
||||||
)
|
|
||||||
}
|
|
117
mrun/mrun.go
117
mrun/mrun.go
@ -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
Block a user