implement mcfg package, which includes both configuration, runtime setup, and organizing both into heirarchies
This commit is contained in:
parent
3f2f00d367
commit
3d43caba18
195
mcfg/cli.go
Normal file
195
mcfg/cli.go
Normal 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
313
mcfg/cli_test.go
Normal 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
266
mcfg/mcfg.go
Normal 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
122
mcfg/mcfg_test.go
Normal 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
115
mcfg/param.go
Normal 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
|
||||
}
|
Loading…
Reference in New Issue
Block a user