mcfg: refactor to remove Child and Hook stuff, and use mctx instead

This commit is contained in:
Brian Picciano 2019-01-08 14:21:55 -05:00
parent 57bd022093
commit 6e8338a5f8
9 changed files with 241 additions and 415 deletions

View File

@ -11,11 +11,13 @@ import (
// SourceCLI is a Source which will parse configuration from the CLI. // SourceCLI is a Source which will parse configuration from the CLI.
// //
// Possible CLI options are generated by joining the Path to a Param, and its // Possible CLI options are generated by joining a Param's Path and Name with
// name, with dashes. For example: // dashes. For example:
// //
// cfg := mcfg.New().Child("foo").Child("bar") // ctx := mctx.New()
// addr := cfg.ParamString("addr", "", "Some address") // ctx = mctx.ChildOf(ctx, "foo")
// ctx = mctx.ChildOf(ctx, "bar")
// addr := mcfg.String(ctx, "addr", "", "Some address")
// // the CLI option to fill addr will be "--foo-bar-addr" // // the CLI option to fill addr will be "--foo-bar-addr"
// //
// If the "-h" option is seen then a help page will be printed to // If the "-h" option is seen then a help page will be printed to
@ -41,13 +43,13 @@ const (
) )
// Parse implements the method for the Source interface // Parse implements the method for the Source interface
func (cli SourceCLI) Parse(cfg *Cfg) ([]ParamValue, error) { func (cli SourceCLI) Parse(params []Param) ([]ParamValue, error) {
args := cli.Args args := cli.Args
if cli.Args == nil { if cli.Args == nil {
args = os.Args[1:] args = os.Args[1:]
} }
pM, err := cli.cliParams(cfg) pM, err := cli.cliParams(params)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -117,9 +119,9 @@ func (cli SourceCLI) Parse(cfg *Cfg) ([]ParamValue, error) {
return pvs, nil return pvs, nil
} }
func (cli SourceCLI) cliParams(cfg *Cfg) (map[string]Param, error) { func (cli SourceCLI) cliParams(params []Param) (map[string]Param, error) {
m := map[string]Param{} m := map[string]Param{}
for _, p := range cfg.allParams() { for _, p := range params {
key := strings.Join(append(p.Path, p.Name), cliKeyJoin) key := strings.Join(append(p.Path, p.Name), cliKeyJoin)
m[cliKeyPrefix+key] = p m[cliKeyPrefix+key] = p
} }

View File

@ -6,6 +6,7 @@ import (
. "testing" . "testing"
"time" "time"
"github.com/mediocregopher/mediocre-go-lib/mctx"
"github.com/mediocregopher/mediocre-go-lib/mrand" "github.com/mediocregopher/mediocre-go-lib/mrand"
"github.com/mediocregopher/mediocre-go-lib/mtest/mchk" "github.com/mediocregopher/mediocre-go-lib/mtest/mchk"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
@ -13,15 +14,15 @@ import (
) )
func TestSourceCLIHelp(t *T) { func TestSourceCLIHelp(t *T) {
cfg := New() ctx := mctx.New()
cfg.ParamInt("foo", 5, "Test int param") Int(ctx, "foo", 5, "Test int param")
cfg.ParamBool("bar", "Test bool param") Bool(ctx, "bar", "Test bool param")
cfg.ParamString("baz", "baz", "Test string param") String(ctx, "baz", "baz", "Test string param")
cfg.ParamString("baz2", "", "") String(ctx, "baz2", "", "")
src := SourceCLI{} src := SourceCLI{}
buf := new(bytes.Buffer) buf := new(bytes.Buffer)
pM, err := src.cliParams(cfg) pM, err := src.cliParams(collectParams(ctx))
require.NoError(t, err) require.NoError(t, err)
SourceCLI{}.printHelp(buf, pM) SourceCLI{}.printHelp(buf, pM)
@ -71,7 +72,7 @@ func TestSourceCLI(t *T) {
s := ss.(state) s := ss.(state)
p := a.Params.(params) p := a.Params.(params)
s.srcCommonState = s.srcCommonState.applyCfgAndPV(p.srcCommonParams) s.srcCommonState = s.srcCommonState.applyCtxAndPV(p.srcCommonParams)
if !p.unset { if !p.unset {
arg := cliKeyPrefix arg := cliKeyPrefix
if len(p.path) > 0 { if len(p.path) > 0 {

View File

@ -13,12 +13,15 @@ import (
// underscores and making all characters uppercase, as well as changing all // underscores and making all characters uppercase, as well as changing all
// dashes to underscores. // dashes to underscores.
// //
// cfg := mcfg.New().Child("foo").Child("bar") // ctx := mctx.New()
// addr := cfg.ParamString("srv-addr", "", "Some address") // ctx = mctx.ChildOf(ctx, "foo")
// ctx = mctx.ChildOf(ctx, "bar")
// addr := mcfg.String(ctx, "srv-addr", "", "Some address")
// // the Env option to fill addr will be "FOO_BAR_SRV_ADDR" // // the Env option to fill addr will be "FOO_BAR_SRV_ADDR"
// //
type SourceEnv struct { type SourceEnv struct {
Env []string // in the format key=value // 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, // If set then all expected Env options must be prefixed with this string,
// which will be uppercased and have dashes replaced with underscores like // which will be uppercased and have dashes replaced with underscores like
@ -37,14 +40,14 @@ func (env SourceEnv) expectedName(path []string, name string) string {
} }
// Parse implements the method for the Source interface // Parse implements the method for the Source interface
func (env SourceEnv) Parse(cfg *Cfg) ([]ParamValue, error) { func (env SourceEnv) Parse(params []Param) ([]ParamValue, error) {
kvs := env.Env kvs := env.Env
if kvs == nil { if kvs == nil {
kvs = os.Environ() kvs = os.Environ()
} }
pM := map[string]Param{} pM := map[string]Param{}
for _, p := range cfg.allParams() { for _, p := range params {
name := env.expectedName(p.Path, p.Name) name := env.expectedName(p.Path, p.Name)
pM[name] = p pM[name] = p
} }

View File

@ -34,7 +34,7 @@ func TestSourceEnv(t *T) {
Apply: func(ss mchk.State, a mchk.Action) (mchk.State, error) { Apply: func(ss mchk.State, a mchk.Action) (mchk.State, error) {
s := ss.(state) s := ss.(state)
p := a.Params.(params) p := a.Params.(params)
s.srcCommonState = s.srcCommonState.applyCfgAndPV(p.srcCommonParams) s.srcCommonState = s.srcCommonState.applyCtxAndPV(p.srcCommonParams)
if !p.unset { if !p.unset {
kv := strings.Join(append(p.path, p.name), "_") kv := strings.Join(append(p.path, p.name), "_")
kv = strings.Replace(kv, "-", "_", -1) kv = strings.Replace(kv, "-", "_", -1)

View File

@ -3,133 +3,80 @@
package mcfg package mcfg
import ( import (
"context"
"encoding/json" "encoding/json"
"fmt" "fmt"
"strings" "sort"
"github.com/mediocregopher/mediocre-go-lib/mctx"
) )
// TODO Sources: // TODO Sources:
// - Env file
// - JSON file // - JSON file
// - YAML file // - YAML file
// Hook describes a function which can have other Hook functions appended to it type ctxCfg struct {
// via the Then method. A Hook is expected to return context.Canceled on context path []string
// cancellation. params map[string]Param
type Hook func(context.Context) error
// Nop returns a Hook which does nothing
func Nop() Hook {
return func(context.Context) error { return nil }
} }
// Then modifies the called upon Hook such that it will first perform whatever type ctxKey int
// it's original functionality was, and then if that doesn't return an error it
// will subsequently perform the given Hook. func get(ctx mctx.Context) *ctxCfg {
func (h *Hook) Then(h2 Hook) { return mctx.GetSetMutableValue(ctx, true, ctxKey(0),
oldh := *h func(interface{}) interface{} {
*h = func(ctx context.Context) error { return &ctxCfg{
if err := oldh(ctx); err != nil { path: mctx.Path(ctx),
return err params: map[string]Param{},
} else if err := ctx.Err(); err != nil { }
// in case the previous hook doesn't respect the context },
return err ).(*ctxCfg)
}
func sortParams(params []Param) {
sort.Slice(params, func(i, j int) bool {
a, b := params[i], params[j]
aPath, bPath := a.Path, b.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:]
}
} }
return h2(ctx) })
}
} }
// Also modifies the called upon Hook such that it will perform the original // returns all Params gathered by recursively retrieving them from this Context
// functionality at the same time as the given Hook, wait for both to complete, // and its children. Returned Params are sorted according to their Path and
// and return an error if there is one. // Name.
func (h *Hook) Also(h2 Hook) { func collectParams(ctx mctx.Context) []Param {
oldh := *h var params []Param
*h = func(ctx context.Context) error {
errCh := make(chan error, 1) var visit func(mctx.Context)
go func() { visit = func(ctx mctx.Context) {
errCh <- oldh(ctx) for _, param := range get(ctx).params {
}() params = append(params, param)
err := h2(ctx) // don't immediately return this, wait for h2 or ctx }
if err := ctx.Err(); err != nil {
// in case the previous hook doesn't respect the context for _, childCtx := range mctx.Children(ctx) {
return err visit(childCtx)
} else if err := <-errCh; err != nil {
return err
} }
return err
} }
visit(ctx)
sortParams(params)
return params
} }
// Cfg describes a set of configuration parameters and run-time behaviors. func populate(params []Param, src Source) error {
// Parameters are defined using the Param* methods, and run-time behaviors by pvs, err := src.Parse(params)
// the Hook fields on this struct.
//
// Each Cfg can have child Cfg's spawned off of it using the Child method, which
// allows for namespacing related params/behaviors into heirarchies.
type Cfg struct {
// Read-only. The set of names passed into Child methods used to generate
// this Cfg and all of its parents. Path will be nil if this Cfg was created
// with New and not a Child call.
//
// Examples:
// New().Path is nil
// New().Child("foo").Path is []string{"foo"}
// New().Child("foo").Child("bar").Path is []string{"foo", "bar"}
Path []string
// Read-only. The set of children spawned off of this Cfg via the Child
// method, keyed by the children's names.
Children map[string]*Cfg
// Read-only. The set of Params which have been added to the Cfg instance
// via its Add method.
Params map[string]Param
// Start hook is performed after configuration variables have been parsed
// and populated. All Start hooks are expected to run in a finite amount of
// time, any long running processes spun off from them should do so in a
// separate go-routine
Start Hook
// Stop hook is performed on interrupt signal, and should stop all
// go-routines and close all resource handlers created during Start
Stop Hook
}
// New initializes and returns an empty Cfg with default values filled in
func New() *Cfg {
return &Cfg{
Children: map[string]*Cfg{},
Params: map[string]Param{},
Start: Nop(),
Stop: Nop(),
}
}
// IsRoot returns true if this is the root instance of a Cfg (i.e. the one
// returned by New)
func (c *Cfg) IsRoot() bool {
return len(c.Path) == 0
}
// Name returns the name given to this instance when it was created via Child.
// if this instance was created via New (i.e. it is the root instance) then
// empty string is returned.
func (c *Cfg) Name() string {
if c.IsRoot() {
return ""
}
return c.Path[len(c.Path)-1]
}
// FullName returns a string representing the full path of the instance.
func (c *Cfg) FullName() string {
return "/" + strings.Join(c.Path, "/")
}
func (c *Cfg) populateParams(src Source) error {
pvs, err := src.Parse(c)
if err != nil { if err != nil {
return err return err
} }
@ -142,11 +89,11 @@ func (c *Cfg) populateParams(src Source) error {
} }
// check for required params // check for required params
for _, p := range c.allParams() { for _, param := range params {
if !p.Required { if !param.Required {
continue continue
} else if _, ok := pvM[p.hash()]; !ok { } else if _, ok := pvM[param.hash()]; !ok {
return fmt.Errorf("param %q is required", p.fullName()) return fmt.Errorf("param %q is required", param.fullName())
} }
} }
@ -155,57 +102,12 @@ func (c *Cfg) populateParams(src Source) error {
return err return err
} }
} }
return nil return nil
} }
func (c *Cfg) runPreBlock(ctx context.Context, src Source) error { // Populate uses the Source to populate the values of all Params which were
if err := c.populateParams(src); err != nil { // added to the given mctx.Context, and all of its children.
return err func Populate(ctx mctx.Context, src Source) error {
} return populate(collectParams(ctx), src)
return c.Start(ctx)
}
// StartRun blocks while performing all steps of a Cfg run. The steps, in order,
// are:
// * Populate all configuration parameters
// * Perform Start hooks
//
// If any step returns an error then everything returns that error immediately.
func (c *Cfg) StartRun(ctx context.Context, src Source) error {
return c.runPreBlock(ctx, src)
}
// StartTestRun is like StartRun, except it's intended to only be used during
// tests to initialize other entities which are going to actually be tested. It
// assumes all default configuration param values, and will return after the
// Start hook has completed. It will panic on any errors.
func (c *Cfg) StartTestRun() {
if err := c.runPreBlock(context.Background(), nil); err != nil {
panic(err)
}
}
// StopRun blocks while calling the Stop hook of the Cfg, returning any error
// that it does.
func (c *Cfg) StopRun(ctx context.Context) error {
return c.Stop(ctx)
}
// Child returns a sub-Cfg of the callee with the given name. The name will be
// prepended to all configuration options created in the returned sub-Cfg, and
// must not be empty.
func (c *Cfg) Child(name string) *Cfg {
name = strings.ToLower(name)
if _, ok := c.Children[name]; ok {
panic(fmt.Sprintf("child Cfg named %q already exists", name))
}
c2 := New()
c2.Path = make([]string, 0, len(c.Path)+1)
c2.Path = append(c2.Path, c.Path...)
c2.Path = append(c2.Path, name)
c.Children[name] = c2
c.Start.Then(func(ctx context.Context) error { return c2.Start(ctx) })
c.Stop.Then(func(ctx context.Context) error { return c2.Stop(ctx) })
return c2
} }

View File

@ -1,96 +1,21 @@
package mcfg package mcfg
import ( import (
"context"
. "testing" . "testing"
"time"
"github.com/mediocregopher/mediocre-go-lib/mctx"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
func TestHook(t *T) { func TestPopulate(t *T) {
{ // test Then
aCh := make(chan bool)
bCh := make(chan bool)
h := Nop()
h.Then(func(context.Context) error {
aCh <- true
<-aCh
return nil
})
h.Then(func(context.Context) error {
bCh <- true
<-bCh
return nil
})
errCh := make(chan error)
go func() {
errCh <- h(context.Background())
}()
assert.True(t, <-aCh)
// make sure bCh isn't being written to till aCh is closed
select {
case <-bCh:
assert.Fail(t, "bCh shouldn't be written to yet")
case <-time.After(250 * time.Millisecond):
close(aCh)
}
assert.True(t, <-bCh)
// make sure errCh isn't being written to till bCh is closed
select {
case <-errCh:
assert.Fail(t, "errCh shouldn't be written to yet")
case <-time.After(250 * time.Millisecond):
close(bCh)
}
assert.Nil(t, <-errCh)
}
{ // test Also
aCh := make(chan bool)
bCh := make(chan bool)
h := Nop()
h.Also(func(context.Context) error {
aCh <- true
<-aCh
return nil
})
h.Also(func(context.Context) error {
bCh <- true
<-bCh
return nil
})
errCh := make(chan error)
go func() {
errCh <- h(context.Background())
}()
// both channels should get written to, then closed, then errCh should
// get written to
assert.True(t, <-aCh)
assert.True(t, <-bCh)
// make sure errCh isn't being written to till both channels are written
select {
case <-errCh:
assert.Fail(t, "errCh shouldn't be written to yet")
case <-time.After(250 * time.Millisecond):
close(aCh)
close(bCh)
}
assert.Nil(t, <-errCh)
}
}
func TestPopulateParams(t *T) {
{ {
cfg := New() ctx := mctx.New()
a := cfg.ParamInt("a", 0, "") a := Int(ctx, "a", 0, "")
cfgChild := cfg.Child("foo") ctxChild := mctx.ChildOf(ctx, "foo")
b := cfgChild.ParamInt("b", 0, "") b := Int(ctxChild, "b", 0, "")
c := cfgChild.ParamInt("c", 0, "") c := Int(ctxChild, "c", 0, "")
err := cfg.populateParams(SourceCLI{ err := Populate(ctx, SourceCLI{
Args: []string{"--a=1", "--foo-b=2"}, Args: []string{"--a=1", "--foo-b=2"},
}) })
assert.NoError(t, err) assert.NoError(t, err)
@ -100,18 +25,18 @@ func TestPopulateParams(t *T) {
} }
{ // test that required params are enforced { // test that required params are enforced
cfg := New() ctx := mctx.New()
a := cfg.ParamInt("a", 0, "") a := Int(ctx, "a", 0, "")
cfgChild := cfg.Child("foo") ctxChild := mctx.ChildOf(ctx, "foo")
b := cfgChild.ParamInt("b", 0, "") b := Int(ctxChild, "b", 0, "")
c := cfgChild.ParamRequiredInt("c", "") c := RequiredInt(ctxChild, "c", "")
err := cfg.populateParams(SourceCLI{ err := Populate(ctx, SourceCLI{
Args: []string{"--a=1", "--foo-b=2"}, Args: []string{"--a=1", "--foo-b=2"},
}) })
assert.Error(t, err) assert.Error(t, err)
err = cfg.populateParams(SourceCLI{ err = Populate(ctx, SourceCLI{
Args: []string{"--a=1", "--foo-b=2", "--foo-c=3"}, Args: []string{"--a=1", "--foo-b=2", "--foo-c=3"},
}) })
assert.NoError(t, err) assert.NoError(t, err)
@ -120,25 +45,3 @@ func TestPopulateParams(t *T) {
assert.Equal(t, 3, *c) assert.Equal(t, 3, *c)
} }
} }
func TestChild(t *T) {
cfg := New()
assert.True(t, cfg.IsRoot())
assert.Equal(t, "", cfg.Name())
assert.Equal(t, "/", cfg.FullName())
foo := cfg.Child("foo")
assert.False(t, foo.IsRoot())
assert.Equal(t, "foo", foo.Name())
assert.Equal(t, "/foo", foo.FullName())
bar := cfg.Child("bar")
assert.False(t, bar.IsRoot())
assert.Equal(t, "bar", bar.Name())
assert.Equal(t, "/bar", bar.FullName())
foo2 := foo.Child("foo2")
assert.False(t, foo2.IsRoot())
assert.Equal(t, "foo2", foo2.Name())
assert.Equal(t, "/foo/foo2", foo2.FullName())
}

View File

@ -7,23 +7,28 @@ import (
"fmt" "fmt"
"strings" "strings"
"github.com/mediocregopher/mediocre-go-lib/mctx"
"github.com/mediocregopher/mediocre-go-lib/mtime" "github.com/mediocregopher/mediocre-go-lib/mtime"
) )
// Param is a configuration parameter which can be added to a Cfg. The Param // Param is a configuration parameter which can be populated by Populate. The
// will exist relative to a Cfg's Path. For example, a Param with name "addr" // Param will exist as part of an mctx.Context, relative to its Path. For
// under a Cfg with Path of []string{"foo","bar"} will be setabble on the CLI // example, a Param with name "addr" under an mctx.Context with Path of
// via "--foo-bar-addr". Other configuration Sources may treat the path/name // []string{"foo","bar"} will be setabble on the CLI via "--foo-bar-addr". Other
// differently, however. // 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 { type Param struct {
// How the parameter will be identified within a Cfg instance // How the parameter will be identified within an mctx.Context.
Name string Name string
// A helpful description of how a parameter is expected to be used
// A helpful description of how a parameter is expected to be used.
Usage string Usage string
// If the parameter's value is expected to be read as a go string. This is // 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 escape // used for configuration sources like CLI which will automatically add
// the parameter's value with double-quotes // double-quotes around the value if they aren't already there.
IsString bool IsString bool
// If the parameter's value is expected to be a boolean. This is used for // If the parameter's value is expected to be a boolean. This is used for
@ -31,8 +36,7 @@ type Param struct {
// differently. // differently.
IsBool bool IsBool bool
// If true then the parameter _must_ be set by at least one configuration // If true then the parameter _must_ be set by at least one Source.
// source
Required bool Required bool
// The pointer/interface into which the configuration value will be // The pointer/interface into which the configuration value will be
@ -74,111 +78,118 @@ func (p Param) hash() string {
return p.fullName() + "/" + hStr return p.fullName() + "/" + hStr
} }
// ParamAdd adds the given Param to the Cfg. It will panic if a Param of the // MustAdd adds the given Param to the mctx.Context. It will panic if a Param of
// same Name already exists in the Cfg. // the same Name already exists in the mctx.Context.
func (c *Cfg) ParamAdd(p Param) { func MustAdd(ctx mctx.Context, param Param) {
p.Name = strings.ToLower(p.Name) param.Name = strings.ToLower(param.Name)
if _, ok := c.Params[p.Name]; ok { param.Path = mctx.Path(ctx)
panic(fmt.Sprintf("Cfg.Path:%#v name:%q already exists", c.Path, p.Name))
cfg := get(ctx)
if _, ok := cfg.params[param.Name]; ok {
panic(fmt.Sprintf("Context Path:%#v Name:%q already exists", param.Path, param.Name))
} }
p.Path = c.Path cfg.params[param.Name] = param
c.Params[p.Name] = p
} }
func (c *Cfg) allParams() []Param { // Int64 returns an *int64 which will be populated once Populate is run.
params := make([]Param, 0, len(c.Params)) func Int64(ctx mctx.Context, name string, defaultVal int64, usage string) *int64 {
for _, p := range c.Params {
params = append(params, p)
}
for _, child := range c.Children {
params = append(params, child.allParams()...)
}
return params
}
// ParamInt64 returns an *int64 which will be populated once the Cfg is run
func (c *Cfg) ParamInt64(name string, defaultVal int64, usage string) *int64 {
i := defaultVal i := defaultVal
c.ParamAdd(Param{Name: name, Usage: usage, Into: &i}) MustAdd(ctx, Param{Name: name, Usage: usage, Into: &i})
return &i return &i
} }
// ParamRequiredInt64 returns an *int64 which will be populated once the Cfg is // RequiredInt64 returns an *int64 which will be populated once Populate is run,
// run, and which must be supplied by a configuration Source // and which must be supplied by a configuration Source.
func (c *Cfg) ParamRequiredInt64(name string, usage string) *int64 { func RequiredInt64(ctx mctx.Context, name string, usage string) *int64 {
var i int64 var i int64
c.ParamAdd(Param{Name: name, Required: true, Usage: usage, Into: &i}) MustAdd(ctx, Param{Name: name, Required: true, Usage: usage, Into: &i})
return &i return &i
} }
// ParamInt returns an *int which will be populated once the Cfg is run // Int returns an *int which will be populated once Populate is run.
func (c *Cfg) ParamInt(name string, defaultVal int, usage string) *int { func Int(ctx mctx.Context, name string, defaultVal int, usage string) *int {
i := defaultVal i := defaultVal
c.ParamAdd(Param{Name: name, Usage: usage, Into: &i}) MustAdd(ctx, Param{Name: name, Usage: usage, Into: &i})
return &i return &i
} }
// ParamRequiredInt returns an *int which will be populated once the Cfg is run, // RequiredInt returns an *int which will be populated once Populate is run, and
// and which must be supplied by a configuration Source // which must be supplied by a configuration Source.
func (c *Cfg) ParamRequiredInt(name string, usage string) *int { func RequiredInt(ctx mctx.Context, name string, usage string) *int {
var i int var i int
c.ParamAdd(Param{Name: name, Required: true, Usage: usage, Into: &i}) MustAdd(ctx, Param{Name: name, Required: true, Usage: usage, Into: &i})
return &i return &i
} }
// ParamString returns a *string which will be populated once the Cfg is run // String returns a *string which will be populated once Populate is run.
func (c *Cfg) ParamString(name, defaultVal, usage string) *string { func String(ctx mctx.Context, name, defaultVal, usage string) *string {
s := defaultVal s := defaultVal
c.ParamAdd(Param{Name: name, Usage: usage, IsString: true, Into: &s}) MustAdd(ctx, Param{Name: name, Usage: usage, IsString: true, Into: &s})
return &s return &s
} }
// ParamRequiredString returns a *string which will be populated once the Cfg is // RequiredString returns a *string which will be populated once Populate is
// run, and which must be supplied by a configuration Source // run, and which must be supplied by a configuration Source.
func (c *Cfg) ParamRequiredString(name, usage string) *string { func RequiredString(ctx mctx.Context, name, usage string) *string {
var s string var s string
c.ParamAdd(Param{Name: name, Required: true, Usage: usage, IsString: true, Into: &s}) MustAdd(ctx, Param{Name: name, Required: true, Usage: usage, IsString: true, Into: &s})
return &s return &s
} }
// ParamBool returns a *bool which will be populated once the Cfg is run, and // Bool returns a *bool which will be populated once Populate is run, and which
// which defaults to false if unconfigured // defaults to false if unconfigured.
// //
// The default behavior of all Sources is that a boolean parameter will be set // The default behavior of all Sources is that a boolean parameter will be set
// to true unless the value is "", 0, or false. In the case of the CLI Source // to true unless the value is "", 0, or false. In the case of the CLI Source
// the value will also be true when the parameter is used with no value at all, // the value will also be true when the parameter is used with no value at all,
// as would be expected. // as would be expected.
func (c *Cfg) ParamBool(name, usage string) *bool { func Bool(ctx mctx.Context, name, usage string) *bool {
var b bool var b bool
c.ParamAdd(Param{Name: name, Usage: usage, IsBool: true, Into: &b}) MustAdd(ctx, Param{Name: name, Usage: usage, IsBool: true, Into: &b})
return &b return &b
} }
// ParamTS returns an *mtime.TS which will be populated once the Cfg is run // TS returns an *mtime.TS which will be populated once Populate is run.
func (c *Cfg) ParamTS(name string, defaultVal mtime.TS, usage string) *mtime.TS { func TS(ctx mctx.Context, name string, defaultVal mtime.TS, usage string) *mtime.TS {
t := defaultVal t := defaultVal
c.ParamAdd(Param{Name: name, Usage: usage, Into: &t}) MustAdd(ctx, Param{Name: name, Usage: usage, Into: &t})
return &t return &t
} }
// ParamDuration returns an *mtime.Duration which will be populated once the Cfg // RequiredTS returns an *mtime.TS which will be populated once Populate is run,
// is run // and which must be supplied by a configuration Source.
func (c *Cfg) ParamDuration(name string, defaultVal mtime.Duration, usage string) *mtime.Duration { func RequiredTS(ctx mctx.Context, name, usage string) *mtime.TS {
var t mtime.TS
MustAdd(ctx, Param{Name: name, Required: true, Usage: usage, Into: &t})
return &t
}
// Duration returns an *mtime.Duration which will be populated once
// Populate is run.
func Duration(ctx mctx.Context, name string, defaultVal mtime.Duration, usage string) *mtime.Duration {
d := defaultVal d := defaultVal
c.ParamAdd(Param{Name: name, Usage: usage, IsString: true, Into: &d}) MustAdd(ctx, Param{Name: name, Usage: usage, IsString: true, Into: &d})
return &d return &d
} }
// ParamJSON reads the parameter value as a JSON value and unmarshals it into // RequiredDuration returns an *mtime.Duration which will be populated once
// the given interface{} (which should be a pointer). The receiver (into) is // Populate is run, and which must be supplied by a configuration Source.
// also used to determine the default value. func RequiredDuration(ctx mctx.Context, name string, defaultVal mtime.Duration, usage string) *mtime.Duration {
func (c *Cfg) ParamJSON(name string, into interface{}, usage string) { var d mtime.Duration
c.ParamAdd(Param{Name: name, Usage: usage, Into: into}) MustAdd(ctx, Param{Name: name, Required: true, Usage: usage, IsString: true, Into: &d})
return &d
} }
// ParamRequiredJSON reads the parameter value as a JSON value and unmarshals it // JSON reads the parameter value as a JSON value and unmarshals it into the
// into the given interface{} (which should be a pointer). // given interface{} (which should be a pointer). The receiver (into) is also
func (c *Cfg) ParamRequiredJSON(name string, into interface{}, usage string) { // used to determine the default value.
c.ParamAdd(Param{Name: name, Required: true, Usage: usage, Into: into}) func JSON(ctx mctx.Context, name string, into interface{}, usage string) {
MustAdd(ctx, Param{Name: name, Usage: usage, Into: into})
}
// RequiredJSON reads the parameter value as a JSON value and unmarshals it into
// the given interface{} (which should be a pointer). The value must be supplied
// by a configuration Source.
func RequiredJSON(ctx mctx.Context, name string, into interface{}, usage string) {
MustAdd(ctx, Param{Name: name, Required: true, Usage: usage, Into: into})
} }

View File

@ -5,28 +5,30 @@ import (
) )
// ParamValue describes a value for a parameter which has been parsed by a // ParamValue describes a value for a parameter which has been parsed by a
// Source // Source.
type ParamValue struct { type ParamValue struct {
Param Param
Value json.RawMessage Value json.RawMessage
} }
// Source parses ParamValues out of a particular configuration source. The // Source parses ParamValues out of a particular configuration source, given a
// returned []ParamValue may contain duplicates of the same Param's value. // sorted set of possible Params to parse. The returned []ParamValue may contain
// duplicates of the same Param's value. in which case the later value takes
// precedence.
type Source interface { type Source interface {
Parse(*Cfg) ([]ParamValue, error) Parse([]Param) ([]ParamValue, error)
} }
// Sources combines together multiple Source instances into one. It will call // Sources combines together multiple Source instances into one. It will call
// Parse on each element individually. Later Sources take precedence over // Parse on each element individually. Values from later Sources take precedence
// previous ones in the slice. // over previous ones.
type Sources []Source type Sources []Source
// Parse implements the method for the Source interface. // Parse implements the method for the Source interface.
func (ss Sources) Parse(c *Cfg) ([]ParamValue, error) { func (ss Sources) Parse(params []Param) ([]ParamValue, error) {
var pvs []ParamValue var pvs []ParamValue
for _, s := range ss { for _, s := range ss {
innerPVs, err := s.Parse(c) innerPVs, err := s.Parse(params)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -37,13 +39,14 @@ func (ss Sources) Parse(c *Cfg) ([]ParamValue, error) {
// SourceMap implements the Source interface by mapping parameter names to // SourceMap implements the Source interface by mapping parameter names to
// values for them. The names are comprised of the path and name of a Param // values for them. The names are comprised of the path and name of a Param
// joined by "-" characters, i.e. `strings.Join(append(p.Path, p.Name), "-")`. // joined by "-" characters, i.e. `strings.Join(append(param.Path, param.Name),
// Values will be parsed in the same way that SourceEnv parses its variables. // "-")`. Values will be parsed in the same way that SourceEnv parses its
// variables.
type SourceMap map[string]string type SourceMap map[string]string
func (m SourceMap) Parse(c *Cfg) ([]ParamValue, error) { func (m SourceMap) Parse(params []Param) ([]ParamValue, error) {
pvs := make([]ParamValue, 0, len(m)) pvs := make([]ParamValue, 0, len(m))
for _, p := range c.allParams() { for _, p := range params {
if v, ok := m[p.fullName()]; ok { if v, ok := m[p.fullName()]; ok {
pvs = append(pvs, ParamValue{ pvs = append(pvs, ParamValue{
Param: p, Param: p,

View File

@ -6,6 +6,7 @@ import (
. "testing" . "testing"
"time" "time"
"github.com/mediocregopher/mediocre-go-lib/mctx"
"github.com/mediocregopher/mediocre-go-lib/mrand" "github.com/mediocregopher/mediocre-go-lib/mrand"
"github.com/mediocregopher/mediocre-go-lib/mtest/massert" "github.com/mediocregopher/mediocre-go-lib/mtest/massert"
) )
@ -15,8 +16,8 @@ import (
// all the code they share // all the code they share
type srcCommonState struct { type srcCommonState struct {
cfg *Cfg ctx mctx.Context
availCfgs []*Cfg availCtxs []mctx.Context
expPVs []ParamValue expPVs []ParamValue
// each specific test should wrap this to add the Source itself // each specific test should wrap this to add the Source itself
@ -24,22 +25,22 @@ type srcCommonState struct {
func newSrcCommonState() srcCommonState { func newSrcCommonState() srcCommonState {
var scs srcCommonState var scs srcCommonState
scs.cfg = New() scs.ctx = mctx.New()
{ {
a := scs.cfg.Child("a") a := mctx.ChildOf(scs.ctx, "a")
b := scs.cfg.Child("b") b := mctx.ChildOf(scs.ctx, "b")
c := scs.cfg.Child("c") c := mctx.ChildOf(scs.ctx, "c")
ab := a.Child("b") ab := mctx.ChildOf(a, "b")
bc := b.Child("c") bc := mctx.ChildOf(b, "c")
abc := ab.Child("c") abc := mctx.ChildOf(ab, "c")
scs.availCfgs = []*Cfg{scs.cfg, a, b, c, ab, bc, abc} scs.availCtxs = []mctx.Context{scs.ctx, a, b, c, ab, bc, abc}
} }
return scs return scs
} }
type srcCommonParams struct { type srcCommonParams struct {
name string name string
availCfgI int // not technically needed, but finding the cfg easier availCtxI int // not technically needed, but makes finding the ctx easier
path []string path []string
isBool bool isBool bool
nonBoolType string // "int", "str", "duration", "json" nonBoolType string // "int", "str", "duration", "json"
@ -55,9 +56,9 @@ func (scs srcCommonState) next() srcCommonParams {
p.name = mrand.Hex(8) p.name = mrand.Hex(8)
} }
p.availCfgI = mrand.Intn(len(scs.availCfgs)) p.availCtxI = mrand.Intn(len(scs.availCtxs))
thisCfg := scs.availCfgs[p.availCfgI] thisCtx := scs.availCtxs[p.availCtxI]
p.path = thisCfg.Path p.path = mctx.Path(thisCtx)
p.isBool = mrand.Intn(2) == 0 p.isBool = mrand.Intn(2) == 0
if !p.isBool { if !p.isBool {
@ -93,22 +94,22 @@ func (scs srcCommonState) next() srcCommonParams {
return p return p
} }
// adds the new cfg param to the cfg, and if the param is expected to be set in // adds the new param to the ctx, and if the param is expected to be set in
// the Source adds it to the expected ParamValues as well // the Source adds it to the expected ParamValues as well
func (scs srcCommonState) applyCfgAndPV(p srcCommonParams) srcCommonState { func (scs srcCommonState) applyCtxAndPV(p srcCommonParams) srcCommonState {
thisCfg := scs.availCfgs[p.availCfgI] thisCtx := scs.availCtxs[p.availCtxI]
cfgP := Param{ ctxP := Param{
Name: p.name, Name: p.name,
IsString: p.nonBoolType == "str" || p.nonBoolType == "duration", IsString: p.nonBoolType == "str" || p.nonBoolType == "duration",
IsBool: p.isBool, IsBool: p.isBool,
// the Sources don't actually care about the other fields of Param, // the Sources don't actually care about the other fields of Param,
// those are only used by Cfg once it has all ParamValues together // those are only used by Populate once it has all ParamValues together
} }
thisCfg.ParamAdd(cfgP) MustAdd(thisCtx, ctxP)
cfgP = thisCfg.Params[p.name] // get it back out to get any added fields ctxP = get(thisCtx).params[p.name] // get it back out to get any added fields
if !p.unset { if !p.unset {
pv := ParamValue{Param: cfgP} pv := ParamValue{Param: ctxP}
if p.isBool { if p.isBool {
pv.Value = json.RawMessage("true") pv.Value = json.RawMessage("true")
} else { } else {
@ -130,7 +131,7 @@ func (scs srcCommonState) applyCfgAndPV(p srcCommonParams) srcCommonState {
// given a Source asserts that it's Parse method returns the expected // given a Source asserts that it's Parse method returns the expected
// ParamValues // ParamValues
func (scs srcCommonState) assert(s Source) error { func (scs srcCommonState) assert(s Source) error {
gotPVs, err := s.Parse(scs.cfg) gotPVs, err := s.Parse(collectParams(scs.ctx))
if err != nil { if err != nil {
return err return err
} }
@ -141,12 +142,12 @@ func (scs srcCommonState) assert(s Source) error {
} }
func TestSources(t *T) { func TestSources(t *T) {
cfg := New() ctx := mctx.New()
a := cfg.ParamRequiredInt("a", "") a := RequiredInt(ctx, "a", "")
b := cfg.ParamRequiredInt("b", "") b := RequiredInt(ctx, "b", "")
c := cfg.ParamRequiredInt("c", "") c := RequiredInt(ctx, "c", "")
err := cfg.populateParams(Sources{ err := Populate(ctx, Sources{
SourceCLI{Args: []string{"--a=1", "--b=666"}}, SourceCLI{Args: []string{"--a=1", "--b=666"}},
SourceEnv{Env: []string{"B=2", "C=3"}}, SourceEnv{Env: []string{"B=2", "C=3"}},
}) })
@ -159,13 +160,13 @@ func TestSources(t *T) {
} }
func TestSourceMap(t *T) { func TestSourceMap(t *T) {
cfg := New() ctx := mctx.New()
a := cfg.ParamRequiredInt("a", "") a := RequiredInt(ctx, "a", "")
foo := cfg.Child("foo") foo := mctx.ChildOf(ctx, "foo")
b := foo.ParamRequiredString("b", "") b := RequiredString(foo, "b", "")
c := foo.ParamBool("c", "") c := Bool(foo, "c", "")
err := cfg.populateParams(SourceMap{ err := Populate(ctx, SourceMap{
"a": "4", "a": "4",
"foo-b": "bbb", "foo-b": "bbb",
"foo-c": "1", "foo-c": "1",