cfgstruct.Bind: support nested config paths (#785)

this will allow some config cleanups in a future pr

Change-Id: Ie873bcee567a72956d9337dfc13ab6ba46c9d1a0
This commit is contained in:
JT Olio 2018-12-11 11:41:19 -07:00 committed by GitHub
parent ceb590fa67
commit 4a4f6ad53e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 74 additions and 9 deletions

View File

@ -46,8 +46,7 @@ var (
func init() { func init() {
rootCmd.AddCommand(setupCmd) rootCmd.AddCommand(setupCmd)
cfgstruct.Bind(setupCmd.Flags(), &setupCfg, cfgstruct.Bind(setupCmd.Flags(), &setupCfg,
cfgstruct.ConfDir(defaultConfDir), cfgstruct.ConfDir(defaultConfDir))
)
} }
func cmdSetup(cmd *cobra.Command, args []string) (err error) { func cmdSetup(cmd *cobra.Command, args []string) (err error) {

View File

@ -6,6 +6,7 @@ package cfgstruct
import ( import (
"fmt" "fmt"
"os" "os"
"path/filepath"
"reflect" "reflect"
"regexp" "regexp"
"strconv" "strconv"
@ -14,15 +15,33 @@ import (
) )
// BindOpt is an option for the Bind method // BindOpt is an option for the Bind method
type BindOpt func(vars map[string]string) type BindOpt func(vars map[string]confVar)
// ConfDir sets a variable for default options called $CONFDIR // ConfDir sets variables for default options called $CONFDIR and $CONFNAME.
func ConfDir(confdir string) BindOpt { func ConfDir(confdir string) BindOpt {
return BindOpt(func(vars map[string]string) { val := filepath.Clean(os.ExpandEnv(confdir))
vars["CONFDIR"] = os.ExpandEnv(confdir) return BindOpt(func(vars map[string]confVar) {
vars["CONFDIR"] = confVar{val: val, nested: false}
vars["CONFNAME"] = confVar{val: val, nested: false}
}) })
} }
// ConfDirNested sets variables for default options called $CONFDIR and $CONFNAME.
// ConfDirNested also appends the parent struct field name to the paths before
// descending into substructs.
func ConfDirNested(confdir string) BindOpt {
val := filepath.Clean(os.ExpandEnv(confdir))
return BindOpt(func(vars map[string]confVar) {
vars["CONFDIR"] = confVar{val: val, nested: true}
vars["CONFNAME"] = confVar{val: val, nested: true}
})
}
type confVar struct {
val string
nested bool
}
// Bind sets flags on a FlagSet that match the configuration struct // Bind sets flags on a FlagSet that match the configuration struct
// 'config'. This works by traversing the config struct using the 'reflect' // 'config'. This works by traversing the config struct using the 'reflect'
// package. // package.
@ -32,7 +51,7 @@ func Bind(flags FlagSet, config interface{}, opts ...BindOpt) {
panic(fmt.Sprintf("invalid config type: %#v. "+ panic(fmt.Sprintf("invalid config type: %#v. "+
"Expecting pointer to struct.", config)) "Expecting pointer to struct.", config))
} }
vars := map[string]string{} vars := map[string]confVar{}
for _, opt := range opts { for _, opt := range opts {
opt(vars) opt(vars)
} }
@ -44,13 +63,25 @@ var (
) )
func bindConfig(flags FlagSet, prefix string, val reflect.Value, func bindConfig(flags FlagSet, prefix string, val reflect.Value,
vars map[string]string) { vars map[string]confVar) {
if val.Kind() != reflect.Struct { if val.Kind() != reflect.Struct {
panic(fmt.Sprintf("invalid config type: %#v. Expecting struct.", panic(fmt.Sprintf("invalid config type: %#v. Expecting struct.",
val.Interface())) val.Interface()))
} }
typ := val.Type() typ := val.Type()
resolvedVars := make(map[string]string, len(vars))
{
structpath := strings.Replace(prefix, ".", "/", -1)
for k, v := range vars {
if !v.nested {
resolvedVars[k] = v.val
continue
}
resolvedVars[k] = filepath.Join(v.val, structpath)
}
}
for i := 0; i < typ.NumField(); i++ { for i := 0; i < typ.NumField(); i++ {
field := typ.Field(i) field := typ.Field(i)
fieldval := val.Field(i) fieldval := val.Field(i)
@ -107,7 +138,8 @@ func bindConfig(flags FlagSet, prefix string, val reflect.Value,
check(err) check(err)
flags.Float64Var(fieldaddr.(*float64), flagname, val, help) flags.Float64Var(fieldaddr.(*float64), flagname, val, help)
case reflect.TypeOf(string("")): case reflect.TypeOf(string("")):
flags.StringVar(fieldaddr.(*string), flagname, expand(vars, def), help) flags.StringVar(
fieldaddr.(*string), flagname, expand(resolvedVars, def), help)
case reflect.TypeOf(bool(false)): case reflect.TypeOf(bool(false)):
val, err := strconv.ParseBool(def) val, err := strconv.ParseBool(def)
check(err) check(err)

View File

@ -74,3 +74,37 @@ func TestBind(t *testing.T) {
assertEqual(c.Fields[0].AnotherInt, int(0)) assertEqual(c.Fields[0].AnotherInt, int(0))
assertEqual(c.Fields[3].AnotherInt, int(1)) assertEqual(c.Fields[3].AnotherInt, int(1))
} }
func TestConfDir(t *testing.T) {
f := flag.NewFlagSet("test", flag.PanicOnError)
var c struct {
String string `default:"-$CONFDIR+"`
MyStruct1 struct {
String string `default:"1${CONFDIR}2"`
MyStruct2 struct {
String string `default:"2${CONFDIR}3"`
}
}
}
Bind(f, &c, ConfDir("confpath"))
assertEqual(f.Lookup("string").DefValue, "-confpath+")
assertEqual(f.Lookup("my-struct1.string").DefValue, "1confpath2")
assertEqual(f.Lookup("my-struct1.my-struct2.string").DefValue, "2confpath3")
}
func TestNesting(t *testing.T) {
f := flag.NewFlagSet("test", flag.PanicOnError)
var c struct {
String string `default:"-$CONFDIR+"`
MyStruct1 struct {
String string `default:"1${CONFDIR}2"`
MyStruct2 struct {
String string `default:"2${CONFDIR}3"`
}
}
}
Bind(f, &c, ConfDirNested("confpath"))
assertEqual(f.Lookup("string").DefValue, "-confpath+")
assertEqual(f.Lookup("my-struct1.string").DefValue, "1confpath/my-struct12")
assertEqual(f.Lookup("my-struct1.my-struct2.string").DefValue, "2confpath/my-struct1/my-struct23")
}