mcfg: refactor to remove Child and Hook stuff, and use mctx instead
This commit is contained in:
parent
57bd022093
commit
6e8338a5f8
18
mcfg/cli.go
18
mcfg/cli.go
@ -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
|
||||
}
|
||||
|
@ -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 {
|
||||
|
13
mcfg/env.go
13
mcfg/env.go
@ -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
|
||||
}
|
||||
|
@ -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)
|
||||
|
230
mcfg/mcfg.go
230
mcfg/mcfg.go
@ -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{},
|
||||
}
|
||||
},
|
||||
).(*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
|
||||
// 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
|
||||
// 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)
|
||||
}
|
||||
|
||||
for _, childCtx := range mctx.Children(ctx) {
|
||||
visit(childCtx)
|
||||
}
|
||||
return err
|
||||
}
|
||||
visit(ctx)
|
||||
|
||||
sortParams(params)
|
||||
return params
|
||||
}
|
||||
|
||||
// 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(),
|
||||
}
|
||||
}
|
||||
|
||||
// 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)
|
||||
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)
|
||||
}
|
||||
|
@ -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())
|
||||
}
|
||||
|
155
mcfg/param.go
155
mcfg/param.go
@ -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})
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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",
|
||||
|
Loading…
Reference in New Issue
Block a user