From 6e8338a5f8790688981cd9d1a37988990117120e Mon Sep 17 00:00:00 2001 From: Brian Picciano Date: Tue, 8 Jan 2019 14:21:55 -0500 Subject: [PATCH] mcfg: refactor to remove Child and Hook stuff, and use mctx instead --- mcfg/cli.go | 18 ++-- mcfg/cli_test.go | 15 +-- mcfg/env.go | 13 ++- mcfg/env_test.go | 2 +- mcfg/mcfg.go | 230 +++++++++++++------------------------------- mcfg/mcfg_test.go | 127 +++--------------------- mcfg/param.go | 155 +++++++++++++++-------------- mcfg/source.go | 27 +++--- mcfg/source_test.go | 69 ++++++------- 9 files changed, 241 insertions(+), 415 deletions(-) diff --git a/mcfg/cli.go b/mcfg/cli.go index 9a9ced6..fcf2968 100644 --- a/mcfg/cli.go +++ b/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 } diff --git a/mcfg/cli_test.go b/mcfg/cli_test.go index 50db500..e0f0114 100644 --- a/mcfg/cli_test.go +++ b/mcfg/cli_test.go @@ -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 { diff --git a/mcfg/env.go b/mcfg/env.go index 0b6ce3b..dfe4cde 100644 --- a/mcfg/env.go +++ b/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 } diff --git a/mcfg/env_test.go b/mcfg/env_test.go index 30f8593..35da5ee 100644 --- a/mcfg/env_test.go +++ b/mcfg/env_test.go @@ -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) diff --git a/mcfg/mcfg.go b/mcfg/mcfg.go index 5a0da5e..24f3c6d 100644 --- a/mcfg/mcfg.go +++ b/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) } diff --git a/mcfg/mcfg_test.go b/mcfg/mcfg_test.go index 2b49e85..15e5043 100644 --- a/mcfg/mcfg_test.go +++ b/mcfg/mcfg_test.go @@ -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()) -} diff --git a/mcfg/param.go b/mcfg/param.go index b8476f9..e699881 100644 --- a/mcfg/param.go +++ b/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}) } diff --git a/mcfg/source.go b/mcfg/source.go index 959f210..152f654 100644 --- a/mcfg/source.go +++ b/mcfg/source.go @@ -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, diff --git a/mcfg/source_test.go b/mcfg/source_test.go index 5591514..4779a0d 100644 --- a/mcfg/source_test.go +++ b/mcfg/source_test.go @@ -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",