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:
Brian P 2021-02-07 16:04:41 -07:00
parent c20f884d68
commit 3e2713a850
24 changed files with 348 additions and 2888 deletions

4
go.mod
View File

@ -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

View File

@ -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")
}
}
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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)
}
}

View File

@ -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
}

View File

@ -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)
}
}

View File

@ -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(&param)
}
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...)
}

View File

@ -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
}

View File

@ -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),
)
}

View File

@ -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)
}
}

View File

@ -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),
)
}

View File

@ -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
}

View File

@ -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{}),
)
}

View File

@ -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)
return ctx
} }
// Annotated is a shortcut for calling Annotate with a context.Background(). // Annotations is a set of key/value pairs representing a set of annotations. It
func Annotated(kvs ...interface{}) context.Context { // implements the Annotator interface along with other useful post-processing
return Annotate(context.Background(), kvs...) // 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
}
} }
// AnnotationSet describes a set of unique Annotation values which were // StringMap formats each of the key/value pairs into strings using fmt.Sprint.
// retrieved off a Context via the Annotations function. An AnnotationSet has a // If any two keys format to the same string, then type information will be
// 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})
} }

View File

@ -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)
} }

View File

@ -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
}

View File

@ -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]),
)
}

View File

@ -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 DefaultHandler() Handler {
return defaultHandler(os.Stderr)
}
func defaultHandler(out io.Writer) Handler { func (h *messageHandler) Handle(msg FullMessage) error {
l := new(sync.Mutex) h.l.Lock()
enc := json.NewEncoder(out) defer h.l.Unlock()
return func(msg Message) error {
l.Lock()
defer l.Unlock()
msgJSON := MessageJSON{ mctx.EvaluateAnnotations(msg.Context, h.aa)
for _, annotator := range msg.Annotators {
annotator.Annotate(h.aa)
}
msgJSON := messageJSON{
TimeDate: msg.Time.UTC().Format(msgTimeFormat),
Timestamp: msg.Time.UnixNano(),
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)
} }
return h.enc.Encode(msgJSON)
} }
// Logger directs Messages to an internal Handler and provides convenient func (h *messageHandler) Sync() error {
// methods for creating and modifying its own behavior. All methods are h.l.Lock()
// thread-safe. 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...))
} }

View File

@ -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"}}`),
) )
} }

View File

@ -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)
}

View File

@ -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),
)
}

View File

@ -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
}

View File

@ -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)
}
})
})
}