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.
//
// Possible CLI options are generated by joining the Path to a Param, and its
// name, with dashes. For example:
// Possible CLI options are generated by joining a Param's Path and Name with
// dashes. For example:
//
// cfg := mcfg.New().Child("foo").Child("bar")
// addr := cfg.ParamString("addr", "", "Some address")
// ctx := mctx.New()
// 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"
//
// 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
func (cli SourceCLI) Parse(cfg *Cfg) ([]ParamValue, error) {
func (cli SourceCLI) Parse(params []Param) ([]ParamValue, error) {
args := cli.Args
if cli.Args == nil {
args = os.Args[1:]
}
pM, err := cli.cliParams(cfg)
pM, err := cli.cliParams(params)
if err != nil {
return nil, err
}
@ -117,9 +119,9 @@ func (cli SourceCLI) Parse(cfg *Cfg) ([]ParamValue, error) {
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{}
for _, p := range cfg.allParams() {
for _, p := range params {
key := strings.Join(append(p.Path, p.Name), cliKeyJoin)
m[cliKeyPrefix+key] = p
}

View File

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

View File

@ -13,12 +13,15 @@ import (
// underscores and making all characters uppercase, as well as changing all
// dashes to underscores.
//
// cfg := mcfg.New().Child("foo").Child("bar")
// addr := cfg.ParamString("srv-addr", "", "Some address")
// ctx := mctx.New()
// 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"
//
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,
// 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
func (env SourceEnv) Parse(cfg *Cfg) ([]ParamValue, error) {
func (env SourceEnv) Parse(params []Param) ([]ParamValue, error) {
kvs := env.Env
if kvs == nil {
kvs = os.Environ()
}
pM := map[string]Param{}
for _, p := range cfg.allParams() {
for _, p := range params {
name := env.expectedName(p.Path, p.Name)
pM[name] = p
}

View File

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

View File

@ -3,133 +3,80 @@
package mcfg
import (
"context"
"encoding/json"
"fmt"
"strings"
"sort"
"github.com/mediocregopher/mediocre-go-lib/mctx"
)
// TODO Sources:
// - Env file
// - JSON file
// - YAML file
// Hook describes a function which can have other Hook functions appended to it
// via the Then method. A Hook is expected to return context.Canceled on context
// cancellation.
type Hook func(context.Context) error
// Nop returns a Hook which does nothing
func Nop() Hook {
return func(context.Context) error { return nil }
type ctxCfg struct {
path []string
params map[string]Param
}
// Then modifies the called upon Hook such that it will first perform whatever
// it's original functionality was, and then if that doesn't return an error it
// will subsequently perform the given Hook.
func (h *Hook) Then(h2 Hook) {
oldh := *h
*h = func(ctx context.Context) error {
if err := oldh(ctx); err != nil {
return err
} else if err := ctx.Err(); err != nil {
// in case the previous hook doesn't respect the context
return err
type ctxKey int
func get(ctx mctx.Context) *ctxCfg {
return mctx.GetSetMutableValue(ctx, true, ctxKey(0),
func(interface{}) interface{} {
return &ctxCfg{
path: mctx.Path(ctx),
params: map[string]Param{},
}
return h2(ctx)
},
).(*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:]
}
}
// Also modifies the called upon Hook such that it will perform the original
// functionality at the same time as the given Hook, wait for both to complete,
// and return an error if there is one.
func (h *Hook) Also(h2 Hook) {
oldh := *h
*h = func(ctx context.Context) error {
errCh := make(chan error, 1)
go func() {
errCh <- oldh(ctx)
}()
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
return err
} else if err := <-errCh; err != nil {
return err
}
return err
})
}
// returns all Params gathered by recursively retrieving them from this Context
// and its children. Returned Params are sorted according to their Path and
// Name.
func collectParams(ctx mctx.Context) []Param {
var params []Param
var visit func(mctx.Context)
visit = func(ctx mctx.Context) {
for _, param := range get(ctx).params {
params = append(params, param)
}
}
// Cfg describes a set of configuration parameters and run-time behaviors.
// Parameters are defined using the Param* methods, and run-time behaviors by
// 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(),
for _, childCtx := range mctx.Children(ctx) {
visit(childCtx)
}
}
// 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]
visit(ctx)
sortParams(params)
return params
}
// 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)
func populate(params []Param, src Source) error {
pvs, err := src.Parse(params)
if err != nil {
return err
}
@ -142,11 +89,11 @@ func (c *Cfg) populateParams(src Source) error {
}
// check for required params
for _, p := range c.allParams() {
if !p.Required {
for _, param := range params {
if !param.Required {
continue
} else if _, ok := pvM[p.hash()]; !ok {
return fmt.Errorf("param %q is required", p.fullName())
} else if _, ok := pvM[param.hash()]; !ok {
return fmt.Errorf("param %q is required", param.fullName())
}
}
@ -155,57 +102,12 @@ func (c *Cfg) populateParams(src Source) error {
return err
}
}
return nil
}
func (c *Cfg) runPreBlock(ctx context.Context, src Source) error {
if err := c.populateParams(src); err != nil {
return err
}
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
// Populate uses the Source to populate the values of all Params which were
// added to the given mctx.Context, and all of its children.
func Populate(ctx mctx.Context, src Source) error {
return populate(collectParams(ctx), src)
}

View File

@ -1,96 +1,21 @@
package mcfg
import (
"context"
. "testing"
"time"
"github.com/mediocregopher/mediocre-go-lib/mctx"
"github.com/stretchr/testify/assert"
)
func TestHook(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) {
func TestPopulate(t *T) {
{
cfg := New()
a := cfg.ParamInt("a", 0, "")
cfgChild := cfg.Child("foo")
b := cfgChild.ParamInt("b", 0, "")
c := cfgChild.ParamInt("c", 0, "")
ctx := mctx.New()
a := Int(ctx, "a", 0, "")
ctxChild := mctx.ChildOf(ctx, "foo")
b := Int(ctxChild, "b", 0, "")
c := Int(ctxChild, "c", 0, "")
err := cfg.populateParams(SourceCLI{
err := Populate(ctx, SourceCLI{
Args: []string{"--a=1", "--foo-b=2"},
})
assert.NoError(t, err)
@ -100,18 +25,18 @@ func TestPopulateParams(t *T) {
}
{ // test that required params are enforced
cfg := New()
a := cfg.ParamInt("a", 0, "")
cfgChild := cfg.Child("foo")
b := cfgChild.ParamInt("b", 0, "")
c := cfgChild.ParamRequiredInt("c", "")
ctx := mctx.New()
a := Int(ctx, "a", 0, "")
ctxChild := mctx.ChildOf(ctx, "foo")
b := Int(ctxChild, "b", 0, "")
c := RequiredInt(ctxChild, "c", "")
err := cfg.populateParams(SourceCLI{
err := Populate(ctx, SourceCLI{
Args: []string{"--a=1", "--foo-b=2"},
})
assert.Error(t, err)
err = cfg.populateParams(SourceCLI{
err = Populate(ctx, SourceCLI{
Args: []string{"--a=1", "--foo-b=2", "--foo-c=3"},
})
assert.NoError(t, err)
@ -120,25 +45,3 @@ func TestPopulateParams(t *T) {
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"
"strings"
"github.com/mediocregopher/mediocre-go-lib/mctx"
"github.com/mediocregopher/mediocre-go-lib/mtime"
)
// Param is a configuration parameter which can be added to a Cfg. The Param
// will exist relative to a Cfg's Path. For example, a Param with name "addr"
// under a Cfg with Path of []string{"foo","bar"} will be setabble on the CLI
// via "--foo-bar-addr". Other configuration Sources may treat the path/name
// differently, however.
// Param is a configuration parameter which can be populated by Populate. The
// Param will exist as part of an mctx.Context, relative to its Path. For
// example, a Param with name "addr" under an mctx.Context with Path of
// []string{"foo","bar"} will be setabble 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 Cfg instance
// How the parameter will be identified within an mctx.Context.
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
// 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
// the parameter's value with double-quotes
// 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
@ -31,8 +36,7 @@ type Param struct {
// differently.
IsBool bool
// If true then the parameter _must_ be set by at least one configuration
// source
// 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
@ -74,111 +78,118 @@ func (p Param) hash() string {
return p.fullName() + "/" + hStr
}
// ParamAdd adds the given Param to the Cfg. It will panic if a Param of the
// same Name already exists in the Cfg.
func (c *Cfg) ParamAdd(p Param) {
p.Name = strings.ToLower(p.Name)
if _, ok := c.Params[p.Name]; ok {
panic(fmt.Sprintf("Cfg.Path:%#v name:%q already exists", c.Path, p.Name))
// MustAdd adds the given Param to the mctx.Context. It will panic if a Param of
// the same Name already exists in the mctx.Context.
func MustAdd(ctx mctx.Context, param Param) {
param.Name = strings.ToLower(param.Name)
param.Path = mctx.Path(ctx)
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
c.Params[p.Name] = p
cfg.params[param.Name] = param
}
func (c *Cfg) allParams() []Param {
params := make([]Param, 0, len(c.Params))
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 {
// Int64 returns an *int64 which will be populated once Populate is run.
func Int64(ctx mctx.Context, name string, defaultVal int64, usage string) *int64 {
i := defaultVal
c.ParamAdd(Param{Name: name, Usage: usage, Into: &i})
MustAdd(ctx, Param{Name: name, Usage: usage, Into: &i})
return &i
}
// ParamRequiredInt64 returns an *int64 which will be populated once the Cfg is
// run, and which must be supplied by a configuration Source
func (c *Cfg) ParamRequiredInt64(name string, usage string) *int64 {
// RequiredInt64 returns an *int64 which will be populated once Populate is run,
// and which must be supplied by a configuration Source.
func RequiredInt64(ctx mctx.Context, name string, usage string) *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
}
// ParamInt returns an *int which will be populated once the Cfg is run
func (c *Cfg) ParamInt(name string, defaultVal int, usage string) *int {
// Int returns an *int which will be populated once Populate is run.
func Int(ctx mctx.Context, name string, defaultVal int, usage string) *int {
i := defaultVal
c.ParamAdd(Param{Name: name, Usage: usage, Into: &i})
MustAdd(ctx, Param{Name: name, Usage: usage, Into: &i})
return &i
}
// ParamRequiredInt returns an *int which will be populated once the Cfg is run,
// and which must be supplied by a configuration Source
func (c *Cfg) ParamRequiredInt(name string, usage string) *int {
// RequiredInt returns an *int which will be populated once Populate is run, and
// which must be supplied by a configuration Source.
func RequiredInt(ctx mctx.Context, name string, usage string) *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
}
// ParamString returns a *string which will be populated once the Cfg is run
func (c *Cfg) ParamString(name, defaultVal, usage string) *string {
// String returns a *string which will be populated once Populate is run.
func String(ctx mctx.Context, name, defaultVal, usage string) *string {
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
}
// ParamRequiredString returns a *string which will be populated once the Cfg is
// run, and which must be supplied by a configuration Source
func (c *Cfg) ParamRequiredString(name, usage string) *string {
// RequiredString returns a *string which will be populated once Populate is
// run, and which must be supplied by a configuration Source.
func RequiredString(ctx mctx.Context, name, usage string) *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
}
// ParamBool returns a *bool which will be populated once the Cfg is run, and
// which defaults to false if unconfigured
// Bool returns a *bool which will be populated once Populate is run, 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 (c *Cfg) ParamBool(name, usage string) *bool {
func Bool(ctx mctx.Context, name, usage string) *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
}
// ParamTS returns an *mtime.TS which will be populated once the Cfg is run
func (c *Cfg) ParamTS(name string, defaultVal mtime.TS, usage string) *mtime.TS {
// TS returns an *mtime.TS which will be populated once Populate is run.
func TS(ctx mctx.Context, name string, defaultVal mtime.TS, usage string) *mtime.TS {
t := defaultVal
c.ParamAdd(Param{Name: name, Usage: usage, Into: &t})
MustAdd(ctx, Param{Name: name, Usage: usage, Into: &t})
return &t
}
// ParamDuration returns an *mtime.Duration which will be populated once the Cfg
// is run
func (c *Cfg) ParamDuration(name string, defaultVal mtime.Duration, usage string) *mtime.Duration {
// RequiredTS returns an *mtime.TS which will be populated once Populate is run,
// and which must be supplied by a configuration Source.
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
c.ParamAdd(Param{Name: name, Usage: usage, IsString: true, Into: &d})
MustAdd(ctx, Param{Name: name, Usage: usage, IsString: true, Into: &d})
return &d
}
// ParamJSON reads the parameter value as a JSON value and unmarshals it into
// the given interface{} (which should be a pointer). The receiver (into) is
// also used to determine the default value.
func (c *Cfg) ParamJSON(name string, into interface{}, usage string) {
c.ParamAdd(Param{Name: name, Usage: usage, Into: into})
// RequiredDuration returns an *mtime.Duration which will be populated once
// Populate is run, and which must be supplied by a configuration Source.
func RequiredDuration(ctx mctx.Context, name string, defaultVal mtime.Duration, usage string) *mtime.Duration {
var d mtime.Duration
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
// into the given interface{} (which should be a pointer).
func (c *Cfg) ParamRequiredJSON(name string, into interface{}, usage string) {
c.ParamAdd(Param{Name: name, Required: true, Usage: usage, Into: into})
// JSON reads the parameter value as a JSON value and unmarshals it into the
// given interface{} (which should be a pointer). The receiver (into) is also
// used to determine the default value.
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
// Source
// Source.
type ParamValue struct {
Param
Value json.RawMessage
}
// Source parses ParamValues out of a particular configuration source. The
// returned []ParamValue may contain duplicates of the same Param's value.
// Source parses ParamValues out of a particular configuration source, given a
// 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 {
Parse(*Cfg) ([]ParamValue, error)
Parse([]Param) ([]ParamValue, error)
}
// Sources combines together multiple Source instances into one. It will call
// Parse on each element individually. Later Sources take precedence over
// previous ones in the slice.
// Parse on each element individually. Values from later Sources take precedence
// over previous ones.
type Sources []Source
// 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
for _, s := range ss {
innerPVs, err := s.Parse(c)
innerPVs, err := s.Parse(params)
if err != nil {
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
// 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), "-")`.
// Values will be parsed in the same way that SourceEnv parses its variables.
// 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.
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))
for _, p := range c.allParams() {
for _, p := range params {
if v, ok := m[p.fullName()]; ok {
pvs = append(pvs, ParamValue{
Param: p,

View File

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