implement mcfg package, which includes both configuration, runtime setup, and organizing both into heirarchies

This commit is contained in:
Brian Picciano 2018-01-11 22:19:25 +00:00
parent 3f2f00d367
commit 3d43caba18
5 changed files with 1011 additions and 0 deletions

195
mcfg/cli.go Normal file
View File

@ -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")
}

313
mcfg/cli_test.go Normal file
View File

@ -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())
}

266
mcfg/mcfg.go Normal file
View File

@ -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
}

122
mcfg/mcfg_test.go Normal file
View File

@ -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)
}
}

115
mcfg/param.go Normal file
View File

@ -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
}