diff --git a/mcfg/cli.go b/mcfg/cli.go new file mode 100644 index 0000000..658ed1d --- /dev/null +++ b/mcfg/cli.go @@ -0,0 +1,195 @@ +package mcfg + +import ( + "encoding/json" + "fmt" + "io" + "os" + "reflect" + "sort" + "strings" +) + +// 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: +// +// cfg := mcfg.New().Child("foo").Child("bar") +// addr := cfg.ParamString("addr", "", "Some address") +// // the CLI option to fill addr will be "--foo-bar-addr" +// +// If the "-h" option is seen then a help page will be printed to +// stdout and the process will exit. Since all normally-defined parameters must +// being with double-dash ("--") they won't ever conflict with the help option. +// +type SourceCLI struct { + Args []string // if nil then os.Args[1:] is used + + DisableHelpPage bool +} + +const ( + cliKeyJoin = "-" + cliKeyPrefix = "--" + cliValSep = "=" + cliHelpArg = "-h" +) + +// Parse implements the method for the Source interface +func (cli SourceCLI) Parse(cfg *Cfg) ([]ParamValue, error) { + args := cli.Args + if cli.Args == nil { + args = os.Args[1:] + } + + pvM, err := cli.cliParamVals(cfg) + if err != nil { + return nil, err + } + pvs := make([]ParamValue, 0, len(args)) + var ( + key string + pv ParamValue + pvOk bool + pvStrVal string + pvStrValOk bool + ) + for _, arg := range args { + if pvOk { + pvStrVal = arg + pvStrValOk = true + } else if !cli.DisableHelpPage && arg == cliHelpArg { + cli.printHelp(os.Stdout, pvM) + os.Stdout.Sync() + os.Exit(1) + } else { + for key, pv = range pvM { + if arg == key { + pvOk = true + break + } + + prefix := key + cliValSep + if !strings.HasPrefix(arg, prefix) { + continue + } + pvOk = true + pvStrVal = strings.TrimPrefix(arg, prefix) + pvStrValOk = true + break + } + if !pvOk { + return nil, fmt.Errorf("unexpected config parameter %q", arg) + } + } + + // pvOk is always true at this point, and so pv is filled in + + if pv.IsBool { + // if it's a boolean we don't expect there to be a following value, + // it's just a flag + if pvStrValOk { + return nil, fmt.Errorf("param %q is a boolean and cannot have a value", arg) + } + pv.Value = json.RawMessage("true") + + } else if !pvStrValOk { + // everything else should have a value. if pvStrVal isn't filled it + // means the next arg should be one. Continue the loop, it'll get + // filled with the next one (hopefully) + continue + + } else if pv.IsString && (pvStrVal == "" || pvStrVal[0] != '"') { + pv.Value = json.RawMessage(`"` + pvStrVal + `"`) + + } else { + pv.Value = json.RawMessage(pvStrVal) + } + + pvs = append(pvs, pv) + key = "" + pv = ParamValue{} + pvOk = false + pvStrVal = "" + pvStrValOk = false + } + if pvOk && !pvStrValOk { + return nil, fmt.Errorf("param %q expected a value", key) + } + return pvs, nil +} + +func (cli SourceCLI) cliParamVals(cfg *Cfg) (map[string]ParamValue, error) { + m := map[string]ParamValue{} + for _, param := range cfg.Params { + key := cliKeyPrefix + if len(cfg.Path) > 0 { + key += strings.Join(cfg.Path, cliKeyJoin) + cliKeyJoin + } + key += param.Name + m[key] = ParamValue{ + Param: param, + Path: cfg.Path, + } + } + + for _, child := range cfg.Children { + childM, err := cli.cliParamVals(child) + if err != nil { + return nil, err + } + for key, pv := range childM { + if _, ok := m[key]; ok { + return nil, fmt.Errorf("multiple params use the same CLI arg %q", key) + } + m[key] = pv + } + } + + return m, nil +} + +func (cli SourceCLI) printHelp(w io.Writer, pvM map[string]ParamValue) { + type pvEntry struct { + arg string + ParamValue + } + + pvA := make([]pvEntry, 0, len(pvM)) + for arg, pv := range pvM { + pvA = append(pvA, pvEntry{arg: arg, ParamValue: pv}) + } + + sort.Slice(pvA, func(i, j int) bool { + return pvA[i].arg < pvA[j].arg + }) + + fmtDefaultVal := func(ptr interface{}) string { + if ptr == nil { + return "" + } + val := reflect.Indirect(reflect.ValueOf(ptr)) + zero := reflect.Zero(val.Type()) + if reflect.DeepEqual(val.Interface(), zero.Interface()) { + return "" + } else if val.Type().Kind() == reflect.String { + return fmt.Sprintf("%q", val.Interface()) + } + return fmt.Sprint(val.Interface()) + } + + for _, pvE := range pvA { + fmt.Fprintf(w, "\n%s", pvE.arg) + if pvE.IsBool { + fmt.Fprintf(w, " (Flag)") + } else if defVal := fmtDefaultVal(pvE.Into); defVal != "" { + fmt.Fprintf(w, " (Default: %s)", defVal) + } + fmt.Fprintf(w, "\n") + if pvE.Usage != "" { + fmt.Fprintln(w, "\t"+pvE.Usage) + } + } + fmt.Fprintf(w, "\n") +} diff --git a/mcfg/cli_test.go b/mcfg/cli_test.go new file mode 100644 index 0000000..0a834a0 --- /dev/null +++ b/mcfg/cli_test.go @@ -0,0 +1,313 @@ +package mcfg + +import ( + "bytes" + "encoding/json" + "strconv" + . "testing" + + "github.com/mediocregopher/mediocre-go-lib/mtest" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// * dimension +// - dimension value +// +// * Cfg path +// - New() +// - New.Child("a") +// - New.Child("a-b") +// - New.Child("a=b") +// * Param name +// - normal +// - w/ "-" +// - w/ "=" ? +// * Param type +// - bool +// - non-bool +// * non-bool type +// - int +// - string +// * Str value +// - empty +// - normal +// - w/ - +// - w/ = +// * Value format +// - w/ = +// - w/o = + +func combinate(slices ...[]string) [][]string { + out := [][]string{{}} + for _, slice := range slices { + if len(slice) == 0 { + continue + } + prev := out + out = make([][]string, 0, len(prev)*len(slice)) + for _, prevSet := range prev { + for _, sliceElem := range slice { + prevSetCp := make([]string, len(prevSet), len(prevSet)+1) + copy(prevSetCp, prevSet) + prevSetCp = append(prevSetCp, sliceElem) + out = append(out, prevSetCp) + } + } + } + return out +} + +func TestCombinate(t *T) { + type combTest struct { + args [][]string + exp [][]string + } + + tests := []combTest{ + { + args: [][]string{}, + exp: [][]string{{}}, + }, + { + args: [][]string{{"a"}}, + exp: [][]string{{"a"}}, + }, + { + args: [][]string{{"a"}, {"b"}}, + exp: [][]string{{"a", "b"}}, + }, + { + args: [][]string{{"a", "aa"}, {"b"}}, + exp: [][]string{ + {"a", "b"}, + {"aa", "b"}, + }, + }, + { + args: [][]string{{"a"}, {"b", "bb"}}, + exp: [][]string{ + {"a", "b"}, + {"a", "bb"}, + }, + }, + { + args: [][]string{{"a", "aa"}, {"b", "bb"}}, + exp: [][]string{ + {"a", "b"}, + {"aa", "b"}, + {"a", "bb"}, + {"aa", "bb"}, + }, + }, + } + + for i, test := range tests { + msgAndArgs := []interface{}{"test:%d args:%v", i, test.args} + got := combinate(test.args...) + assert.Len(t, got, len(test.exp), msgAndArgs...) + for _, expHas := range test.exp { + assert.Contains(t, got, expHas, msgAndArgs...) + } + } +} + +func TestSourceCLI(t *T) { + var ( + paths = []string{ + "root", + "child", + "childDash", + "childEq", + } + + paramNames = []string{ + "normal", + "wDash", + "wEq", + } + + isBool = []string{ + "isBool", + "isNotBool", + } + + nonBoolTypes = []string{ + "int", + "str", + } + + nonBoolFmts = []string{ + "wEq", + "woEq", + } + + nonBoolStrValues = []string{ + "empty", + "normal", + "wDash", + "wEq", + } + ) + + type cliTest struct { + path string + name string + isBool bool + nonBoolType string + nonBoolStrValue string + nonBoolFmt string + + // it's kinda hacky to make this a pointer, but it makes the code a lot + // easier to read later + exp *ParamValue + } + + var tests []cliTest + for _, comb := range combinate(paths, paramNames, isBool) { + var test cliTest + test.path = comb[0] + test.name = comb[1] + test.isBool = comb[2] == "isBool" + if test.isBool { + tests = append(tests, test) + continue + } + + for _, nonBoolComb := range combinate(nonBoolTypes, nonBoolFmts) { + test.nonBoolType = nonBoolComb[0] + test.nonBoolFmt = nonBoolComb[1] + if test.nonBoolType != "str" { + tests = append(tests, test) + continue + } + for _, nonBoolStrValue := range nonBoolStrValues { + test.nonBoolStrValue = nonBoolStrValue + tests = append(tests, test) + } + } + } + + childName := mtest.RandHex(8) + childDashName := mtest.RandHex(4) + "-" + mtest.RandHex(4) + childEqName := mtest.RandHex(4) + "=" + mtest.RandHex(4) + + var args []string + rootCfg := New() + childCfg := rootCfg.Child(childName) + childDashCfg := rootCfg.Child(childDashName) + childEqCfg := rootCfg.Child(childEqName) + + for i := range tests { + var pv ParamValue + tests[i].exp = &pv + + switch tests[i].name { + case "normal": + pv.Name = mtest.RandHex(8) + case "wDash": + pv.Name = mtest.RandHex(4) + "-" + mtest.RandHex(4) + case "wEq": + pv.Name = mtest.RandHex(4) + "=" + mtest.RandHex(4) + } + + pv.IsBool = tests[i].isBool + pv.IsString = !tests[i].isBool && tests[i].nonBoolType == "str" + + var arg string + switch tests[i].path { + case "root": + rootCfg.ParamAdd(pv.Param) + arg = "--" + pv.Name + case "child": + childCfg.ParamAdd(pv.Param) + pv.Path = append(pv.Path, childName) + arg = "--" + childName + "-" + pv.Name + case "childDash": + childDashCfg.ParamAdd(pv.Param) + pv.Path = append(pv.Path, childDashName) + arg = "--" + childDashName + "-" + pv.Name + case "childEq": + childEqCfg.ParamAdd(pv.Param) + pv.Path = append(pv.Path, childEqName) + arg = "--" + childEqName + "-" + pv.Name + } + + if pv.IsBool { + pv.Value = json.RawMessage("true") + args = append(args, arg) + continue + } + + var val string + switch tests[i].nonBoolType { + case "int": + val = strconv.Itoa(mtest.Rand.Int()) + pv.Value = json.RawMessage(val) + case "str": + switch tests[i].nonBoolStrValue { + case "empty": + // ez + case "normal": + val = mtest.RandHex(8) + case "wDash": + val = mtest.RandHex(4) + "-" + mtest.RandHex(4) + case "wEq": + val = mtest.RandHex(4) + "=" + mtest.RandHex(4) + } + pv.Value = json.RawMessage(`"` + val + `"`) + } + + switch tests[i].nonBoolFmt { + case "wEq": + arg += "=" + val + args = append(args, arg) + case "woEq": + args = append(args, arg, val) + } + } + + src := SourceCLI{Args: args} + pvals, err := src.Parse(rootCfg) + require.NoError(t, err) + assert.Len(t, pvals, len(tests)) + + for _, test := range tests { + assert.Contains(t, pvals, *test.exp) + } + + // an extra bogus param or value should generate an error + src = SourceCLI{Args: append(args, "foo")} + _, err = src.Parse(rootCfg) + require.Error(t, err) + +} + +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", "", "") + src := SourceCLI{} + + buf := new(bytes.Buffer) + pvM, err := src.cliParamVals(cfg) + require.NoError(t, err) + SourceCLI{}.printHelp(buf, pvM) + + exp := ` +--bar (Flag) + Test bool param + +--baz (Default: "baz") + Test string param + +--baz2 + +--foo (Default: 5) + Test int param + +` + assert.Equal(t, exp, buf.String()) +} diff --git a/mcfg/mcfg.go b/mcfg/mcfg.go new file mode 100644 index 0000000..c567459 --- /dev/null +++ b/mcfg/mcfg.go @@ -0,0 +1,266 @@ +// Package mcfg provides a simple foundation for complex service/binary +// configuration, initialization, and destruction +package mcfg + +import ( + "context" + "encoding/json" + "fmt" + "sync" + "time" +) + +// TODO Sources: +// - Env +// - 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 } +} + +// 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 + } + return h2(ctx) + } +} + +// TODO Having Also here might be more confusing than it's worth, since Child +// effectively does the same thing wrt Hook handling + +// 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) + if err := <-errCh; err != nil { + return err + } + return err + } +} + +// ParamValue describes a value for a parameter which has been parsed by a +// Source +type ParamValue struct { + Param + Path []string // nil if root + Value json.RawMessage +} + +// Source parses ParamValues out of a particular configuration source +type Source interface { + Parse(*Cfg) ([]ParamValue, error) +} + +// 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 + + // Default 2 minutes. Timeout within which the Start Hook (and the Start + // Hooks of all children of this Cfg) must complete. + StartTimeout time.Duration + + // Stop hook is performed on interrupt signal, and should stop all + // go-routines and close all resource handlers created during Start + Stop Hook + + // Default 30 seconds. Timeout within which the Stop Hook (and the Stop + // Hooks of all children of this Cfg) must complete. + StopTimeout time.Duration +} + +// 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(), + StartTimeout: 2 * time.Minute, + Stop: Nop(), + StopTimeout: 30 * time.Second, + } +} + +func (c *Cfg) populateParams(src Source) error { + pvs, err := src.Parse(c) + if err != nil { + return err + } + + // first dedupe the params. We use this param struct as the key by which to + // dedupe by. Its use depends on the json.Marshaler always ordering fields + // in a marshaled struct the same way, which isn't the best assumption but + // it's ok for now + type param struct { + Path []string `json:",omitempty"` + Name string + } + + pvM := map[string]ParamValue{} + for _, pv := range pvs { + keyB, err := json.Marshal(param{Path: pv.Path, Name: pv.Name}) + if err != nil { + return err + } + pvM[string(keyB)] = pv + } + + // check for required params, again using the param struct and the existing + // pvM + var requiredParams func(*Cfg) []param + requiredParams = func(c *Cfg) []param { + var out []param + for _, p := range c.Params { + if !p.Required { + continue + } + out = append(out, param{Path: c.Path, Name: p.Name}) + } + for _, child := range c.Children { + out = append(out, requiredParams(child)...) + } + return out + } + + for _, reqP := range requiredParams(c) { + keyB, err := json.Marshal(reqP) + if err != nil { + return err + } else if _, ok := pvM[string(keyB)]; !ok { + return fmt.Errorf("param %s is required but wasn't populated by any configuration source", keyB) + } + } + + for _, pv := range pvM { + if pv.Into == nil { + continue + } + if err := json.Unmarshal(pv.Value, pv.Into); err != nil { + return err + } + } + return nil +} + +// Run blocks while performing all steps of a Cfg run. The steps, in order, are; +// * Populate all configuration parameters +// * Recursively perform Start hooks, depth first +// * Block till the passed in context is cancelled +// * Recursively perform Stop hooks, depth first +// +// If any step returns an error then everything returns that error immediately. +// +// A caveat about Run is that the error case doesn't leave a lot of room for a +// proper cleanup. If you care about that sort of thing you'll need to handle it +// yourself. +func (c *Cfg) Run(ctx context.Context, src Source) error { + if err := c.populateParams(src); err != nil { + return err + } + + startCtx, cancel := context.WithTimeout(ctx, c.StartTimeout) + err := c.startHooks(startCtx) + cancel() + if err != nil { + return err + } + + <-ctx.Done() + + stopCtx, cancel := context.WithTimeout(context.Background(), c.StopTimeout) + defer cancel() + return c.stopHooks(stopCtx) +} + +func (c *Cfg) startHooks(ctx context.Context) error { + return c.recurseHooks(ctx, func(c *Cfg) Hook { return c.Start }) +} + +func (c *Cfg) stopHooks(ctx context.Context) error { + return c.recurseHooks(ctx, func(c *Cfg) Hook { return c.Stop }) +} + +func (c *Cfg) recurseHooks(ctx context.Context, pickHook func(*Cfg) Hook) error { + var wg sync.WaitGroup + wg.Add(len(c.Children)) + errCh := make(chan error, len(c.Children)) + for name := range c.Children { + childCfg := c.Children[name] + go func() { + defer wg.Done() + if err := childCfg.recurseHooks(ctx, pickHook); err != nil { + errCh <- err + } + }() + } + wg.Wait() + close(errCh) + if err := <-errCh; err != nil { + return err + } + + return pickHook(c)(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 { + 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 + return c2 +} diff --git a/mcfg/mcfg_test.go b/mcfg/mcfg_test.go new file mode 100644 index 0000000..d2aa7e5 --- /dev/null +++ b/mcfg/mcfg_test.go @@ -0,0 +1,122 @@ +package mcfg + +import ( + "context" + . "testing" + "time" + + "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(nil) + }() + + 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(nil) + }() + + // both channels should get written to, then closed, then errCh should + // get written to + assert.True(t, <-aCh) + assert.True(t, <-bCh) + // make sure errCh isn't being written to till both channels are written + select { + case <-errCh: + assert.Fail(t, "errCh shouldn't be written to yet") + case <-time.After(250 * time.Millisecond): + close(aCh) + close(bCh) + } + assert.Nil(t, <-errCh) + } +} + +func TestPopulateParams(t *T) { + { + cfg := New() + a := cfg.ParamInt("a", 0, "") + cfgChild := cfg.Child("foo") + b := cfgChild.ParamInt("b", 0, "") + c := cfgChild.ParamInt("c", 0, "") + + err := cfg.populateParams(SourceCLI{ + Args: []string{"--a=1", "--foo-b=2"}, + }) + assert.NoError(t, err) + assert.Equal(t, 1, *a) + assert.Equal(t, 2, *b) + assert.Equal(t, 0, *c) + } + + { // 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", "") + + err := cfg.populateParams(SourceCLI{ + Args: []string{"--a=1", "--foo-b=2"}, + }) + assert.Error(t, err) + + err = cfg.populateParams(SourceCLI{ + Args: []string{"--a=1", "--foo-b=2", "--foo-c=3"}, + }) + assert.NoError(t, err) + assert.Equal(t, 1, *a) + assert.Equal(t, 2, *b) + assert.Equal(t, 3, *c) + } +} diff --git a/mcfg/param.go b/mcfg/param.go new file mode 100644 index 0000000..6ed357b --- /dev/null +++ b/mcfg/param.go @@ -0,0 +1,115 @@ +package mcfg + +import ( + "fmt" + + "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. +type Param struct { + // How the parameter will be identified within a Cfg instance + Name string + // A helpful description of how a parameter is expected to be used + Usage string + + // If the parameter's value is expected to be read as a go string. This is + // used for configuration sources like CLI which will automatically escape + // the parameter's value with double-quotes + IsString bool + + // If the parameter's value is expected to be a boolean. This is used for + // configuration sources like CLI which treat boolean parameters (aka flags) + // differently. + IsBool bool + + // If true then the parameter _must_ be set by at least one configuration + // source + Required bool + + // The pointer/interface into which the configuration value will be + // json.Unmarshal'd. The value being pointed to also determines the default + // value of the parameter. + Into interface{} +} + +// 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) { + if _, ok := c.Params[p.Name]; ok { + panic(fmt.Sprintf("Cfg.Path:%#v name:%q already exists", c.Path, p.Name)) + } + c.Params[p.Name] = p +} + +// ParamInt64 returns an *int64 which will be populated once the Cfg is run +func (c *Cfg) ParamInt64(name string, defaultVal int64, usage string) *int64 { + i := defaultVal + c.ParamAdd(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 { + var i int64 + c.ParamAdd(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 { + i := defaultVal + c.ParamAdd(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 { + var i int + c.ParamAdd(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 { + s := defaultVal + c.ParamAdd(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 { + var s string + c.ParamAdd(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 +func (c *Cfg) ParamBool(name, usage string) *bool { + var b bool + c.ParamAdd(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 { + t := defaultVal + c.ParamAdd(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 { + d := defaultVal + c.ParamAdd(Param{Name: name, Usage: usage, IsString: true, Into: &d}) + return &d +}