mirror of
https://github.com/ncarlier/webhookd.git
synced 2025-04-06 15:35:55 +00:00
feat(): refactoring of the config flag system
This commit is contained in:
parent
d2189cfd6c
commit
6a011272fd
3
main.go
3
main.go
|
@ -11,6 +11,7 @@ import (
|
|||
|
||||
"github.com/ncarlier/webhookd/pkg/api"
|
||||
"github.com/ncarlier/webhookd/pkg/config"
|
||||
configflag "github.com/ncarlier/webhookd/pkg/config/flag"
|
||||
"github.com/ncarlier/webhookd/pkg/logger"
|
||||
"github.com/ncarlier/webhookd/pkg/notification"
|
||||
"github.com/ncarlier/webhookd/pkg/server"
|
||||
|
@ -19,7 +20,7 @@ import (
|
|||
|
||||
func main() {
|
||||
conf := &config.Config{}
|
||||
config.HydrateFromFlags(conf)
|
||||
configflag.Bind(conf, "WHD")
|
||||
|
||||
flag.Parse()
|
||||
|
||||
|
|
|
@ -1,62 +0,0 @@
|
|||
package config
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"reflect"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
// ErrInvalidSpecification indicates that a specification is of the wrong type.
|
||||
var ErrInvalidSpecification = errors.New("specification must be a struct pointer")
|
||||
|
||||
// HydrateFromFlags hydrate object form flags
|
||||
func HydrateFromFlags(conf interface{}) error {
|
||||
rv := reflect.ValueOf(conf)
|
||||
for rv.Kind() == reflect.Ptr || rv.Kind() == reflect.Interface {
|
||||
rv = rv.Elem()
|
||||
}
|
||||
typ := rv.Type()
|
||||
|
||||
for i := 0; i < typ.NumField(); i++ {
|
||||
fieldType := typ.Field(i)
|
||||
field := rv.Field(i)
|
||||
|
||||
var key, desc, val string
|
||||
if tag, ok := fieldType.Tag.Lookup("flag"); ok {
|
||||
key = tag
|
||||
} else {
|
||||
continue
|
||||
}
|
||||
if tag, ok := fieldType.Tag.Lookup("desc"); ok {
|
||||
desc = tag
|
||||
}
|
||||
if tag, ok := fieldType.Tag.Lookup("default"); ok {
|
||||
val = tag
|
||||
}
|
||||
|
||||
switch fieldType.Type.Kind() {
|
||||
case reflect.String:
|
||||
field.SetString(val)
|
||||
ptr, _ := field.Addr().Interface().(*string)
|
||||
setFlagEnvString(ptr, key, desc, val)
|
||||
case reflect.Bool:
|
||||
bVal, err := strconv.ParseBool(val)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
field.SetBool(bVal)
|
||||
ptr, _ := field.Addr().Interface().(*bool)
|
||||
setFlagEnvBool(ptr, key, desc, bVal)
|
||||
case reflect.Int:
|
||||
i64Val, err := strconv.ParseInt(val, 10, 32)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
iVal := int(i64Val)
|
||||
field.SetInt(i64Val)
|
||||
ptr, _ := field.Addr().Interface().(*int)
|
||||
setFlagEnvInt(ptr, key, desc, iVal)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
118
pkg/config/flag/bind.go
Normal file
118
pkg/config/flag/bind.go
Normal file
|
@ -0,0 +1,118 @@
|
|||
package configflag
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/ncarlier/webhookd/pkg/strcase"
|
||||
)
|
||||
|
||||
// Bind conf struct tags with flags
|
||||
func Bind(conf interface{}, prefix string) error {
|
||||
rv := reflect.ValueOf(conf)
|
||||
for rv.Kind() == reflect.Ptr || rv.Kind() == reflect.Interface {
|
||||
rv = rv.Elem()
|
||||
}
|
||||
typ := rv.Type()
|
||||
|
||||
for i := 0; i < typ.NumField(); i++ {
|
||||
fieldType := typ.Field(i)
|
||||
field := rv.Field(i)
|
||||
|
||||
var key, desc, val string
|
||||
// Get field key from struct tag
|
||||
if tag, ok := fieldType.Tag.Lookup("flag"); ok {
|
||||
key = tag
|
||||
} else {
|
||||
continue
|
||||
}
|
||||
// Get field description from struct tag
|
||||
if tag, ok := fieldType.Tag.Lookup("desc"); ok {
|
||||
desc = tag
|
||||
}
|
||||
// Get field value from struct tag
|
||||
if tag, ok := fieldType.Tag.Lookup("default"); ok {
|
||||
val = tag
|
||||
}
|
||||
|
||||
// Get field value and description from environment variable
|
||||
if fieldType.Type.Kind() == reflect.Slice {
|
||||
val = getEnvValue(prefix, key+"s", val)
|
||||
desc = getEnvDesc(prefix, key+"s", desc)
|
||||
} else {
|
||||
val = getEnvValue(prefix, key, val)
|
||||
desc = getEnvDesc(prefix, key, desc)
|
||||
}
|
||||
|
||||
// Get field value by reflection from struct definition
|
||||
// And bind value to command line flag
|
||||
switch fieldType.Type.Kind() {
|
||||
case reflect.String:
|
||||
field.SetString(val)
|
||||
ptr, _ := field.Addr().Interface().(*string)
|
||||
flag.StringVar(ptr, key, val, desc)
|
||||
case reflect.Bool:
|
||||
bVal, err := strconv.ParseBool(val)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Invalid boolean value for %s: %v", key, err)
|
||||
}
|
||||
field.SetBool(bVal)
|
||||
ptr, _ := field.Addr().Interface().(*bool)
|
||||
flag.BoolVar(ptr, key, bVal, desc)
|
||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||
if field.Kind() == reflect.Int64 && field.Type().PkgPath() == "time" && field.Type().Name() == "Duration" {
|
||||
d, err := time.ParseDuration(val)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Invalid duration value for %s: %v", key, err)
|
||||
}
|
||||
field.SetInt(int64(d))
|
||||
ptr, _ := field.Addr().Interface().(*time.Duration)
|
||||
flag.DurationVar(ptr, key, d, desc)
|
||||
} else {
|
||||
i64Val, err := strconv.ParseInt(val, 0, fieldType.Type.Bits())
|
||||
if err != nil {
|
||||
return fmt.Errorf("Invalid number value for %s: %v", key, err)
|
||||
}
|
||||
field.SetInt(i64Val)
|
||||
ptr, _ := field.Addr().Interface().(*int)
|
||||
flag.IntVar(ptr, key, int(i64Val), desc)
|
||||
}
|
||||
case reflect.Slice:
|
||||
sliceType := field.Type().Elem()
|
||||
if sliceType.Kind() == reflect.String {
|
||||
if len(strings.TrimSpace(val)) != 0 {
|
||||
vals := strings.Split(val, ",")
|
||||
sl := make([]string, len(vals), len(vals))
|
||||
for i, v := range vals {
|
||||
sl[i] = v
|
||||
}
|
||||
field.Set(reflect.ValueOf(sl))
|
||||
ptr, _ := field.Addr().Interface().(*[]string)
|
||||
af := newArrayFlags(ptr)
|
||||
flag.Var(af, key, desc)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func getEnvKey(prefix, key string) string {
|
||||
return strcase.ToScreamingSnake(prefix + "_" + key)
|
||||
}
|
||||
|
||||
func getEnvValue(prefix, key, fallback string) string {
|
||||
if value, ok := os.LookupEnv(getEnvKey(prefix, key)); ok {
|
||||
return value
|
||||
}
|
||||
return fallback
|
||||
}
|
||||
|
||||
func getEnvDesc(prefix, key, desc string) string {
|
||||
return fmt.Sprintf("%s (env: %s)", desc, getEnvKey(prefix, key))
|
||||
}
|
36
pkg/config/flag/test/bind_test.go
Normal file
36
pkg/config/flag/test/bind_test.go
Normal file
|
@ -0,0 +1,36 @@
|
|||
package test
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/ncarlier/webhookd/pkg/assert"
|
||||
configflag "github.com/ncarlier/webhookd/pkg/config/flag"
|
||||
)
|
||||
|
||||
type sampleConfig struct {
|
||||
Label string `flag:"label" desc:"String parameter" default:"foo"`
|
||||
Override string `flag:"override" desc:"String parameter to override" default:"bar"`
|
||||
Count int `flag:"count" desc:"Number parameter" default:"2"`
|
||||
Debug bool `flag:"debug" desc:"Boolean parameter" default:"false"`
|
||||
Timer time.Duration `flag:"timer" desc:"Duration parameter" default:"30s"`
|
||||
Array []string `flag:"array" desc:"Array parameter" default:"foo,bar"`
|
||||
OverrideArray []string `flag:"override-array" desc:"Array parameter to override" default:"foo"`
|
||||
}
|
||||
|
||||
func TestFlagBinding(t *testing.T) {
|
||||
conf := &sampleConfig{}
|
||||
err := configflag.Bind(conf, "FOO")
|
||||
flag.CommandLine.Parse([]string{"-override", "test", "-override-array", "a", "-override-array", "b"})
|
||||
assert.Nil(t, err, "error should be nil")
|
||||
assert.Equal(t, "foo", conf.Label, "")
|
||||
assert.Equal(t, "test", conf.Override, "")
|
||||
assert.Equal(t, 2, conf.Count, "")
|
||||
assert.Equal(t, false, conf.Debug, "")
|
||||
assert.Equal(t, time.Second*30, conf.Timer, "")
|
||||
assert.Equal(t, 2, len(conf.Array), "")
|
||||
assert.Equal(t, "foo", conf.Array[0], "")
|
||||
assert.Equal(t, 2, len(conf.OverrideArray), "")
|
||||
assert.Equal(t, "a", conf.OverrideArray[0], "")
|
||||
}
|
37
pkg/config/flag/types.go
Normal file
37
pkg/config/flag/types.go
Normal file
|
@ -0,0 +1,37 @@
|
|||
package configflag
|
||||
|
||||
import "strings"
|
||||
|
||||
// arrayFlags contains an array of command flags
|
||||
type arrayFlags struct {
|
||||
items *[]string
|
||||
reset bool
|
||||
}
|
||||
|
||||
func newArrayFlags(items *[]string) *arrayFlags {
|
||||
return &arrayFlags{
|
||||
items: items,
|
||||
reset: true,
|
||||
}
|
||||
}
|
||||
|
||||
// Values return the values of a flag array
|
||||
func (i *arrayFlags) Values() []string {
|
||||
return *i.items
|
||||
}
|
||||
|
||||
// String return the string value of a flag array
|
||||
func (i *arrayFlags) String() string {
|
||||
return strings.Join(i.Values(), ",")
|
||||
}
|
||||
|
||||
// Set is used to add a value to the flag array
|
||||
func (i *arrayFlags) Set(value string) error {
|
||||
if i.reset {
|
||||
i.reset = false
|
||||
*i.items = []string{value}
|
||||
} else {
|
||||
*i.items = append(*i.items, value)
|
||||
}
|
||||
return nil
|
||||
}
|
|
@ -1,69 +0,0 @@
|
|||
package config
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/ncarlier/webhookd/pkg/strcase"
|
||||
)
|
||||
|
||||
const envPrefix = "WHD"
|
||||
|
||||
// setFlagEnvString set string value from flag or env with fallback
|
||||
func setFlagEnvString(p *string, key, desc, fallback string) {
|
||||
if val := envValue(key); val != nil {
|
||||
fallback = *val
|
||||
}
|
||||
flag.StringVar(p, key, fallback, envDesc(key, desc))
|
||||
}
|
||||
|
||||
// setFlagEnvBool set bool value from flag or env with fallback
|
||||
func setFlagEnvBool(p *bool, key, desc string, fallback bool) {
|
||||
if val := envValue(key); val != nil {
|
||||
fallback, _ = strconv.ParseBool(*val)
|
||||
}
|
||||
flag.BoolVar(p, key, fallback, envDesc(key, desc))
|
||||
}
|
||||
|
||||
// setFlagEnvInt set int value from flag or env with fallback
|
||||
func setFlagEnvInt(p *int, key, desc string, fallback int) {
|
||||
if val := envValue(key); val != nil {
|
||||
fallback, _ = strconv.Atoi(*val)
|
||||
}
|
||||
flag.IntVar(p, key, fallback, envDesc(key, desc))
|
||||
}
|
||||
|
||||
// setFlagEnvDuration set duration value form flag or env with fallback
|
||||
func setFlagEnvDuration(p *time.Duration, key, desc string, fallback time.Duration) {
|
||||
if val := envValue(key); val != nil {
|
||||
fallback, _ = time.ParseDuration(*val)
|
||||
}
|
||||
flag.DurationVar(p, key, fallback, envDesc(key, desc))
|
||||
}
|
||||
|
||||
// setFlagString set string value from flag with fallback
|
||||
func setFlagString(p *string, key, desc, fallback string) {
|
||||
flag.StringVar(p, key, fallback, desc)
|
||||
}
|
||||
|
||||
// setFlagBool set bool value from flag with fallback
|
||||
func setFlagBool(p *bool, key, desc string, fallback bool) {
|
||||
flag.BoolVar(p, key, fallback, desc)
|
||||
}
|
||||
|
||||
func envDesc(key, desc string) string {
|
||||
envKey := strings.ToUpper(strcase.ToSnake(key))
|
||||
return fmt.Sprintf("%s (env: %s_%s)", desc, envPrefix, envKey)
|
||||
}
|
||||
|
||||
func envValue(key string) *string {
|
||||
envKey := strings.ToUpper(strcase.ToSnake(key))
|
||||
if value, ok := os.LookupEnv(envPrefix + "_" + envKey); ok {
|
||||
return &value
|
||||
}
|
||||
return nil
|
||||
}
|
|
@ -1,21 +0,0 @@
|
|||
package config_test
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"testing"
|
||||
|
||||
"github.com/ncarlier/webhookd/pkg/assert"
|
||||
"github.com/ncarlier/webhookd/pkg/config"
|
||||
)
|
||||
|
||||
func TestFlagBuilder(t *testing.T) {
|
||||
flag.Parse()
|
||||
conf := &config.Config{}
|
||||
err := config.HydrateFromFlags(conf)
|
||||
assert.Nil(t, err, "error should be nil")
|
||||
assert.Equal(t, ":8080", conf.ListenAddr, "")
|
||||
assert.Equal(t, 2, conf.NbWorkers, "")
|
||||
assert.Equal(t, 10, conf.Timeout, "")
|
||||
assert.Equal(t, "scripts", conf.ScriptDir, "")
|
||||
assert.Equal(t, false, conf.Debug, "")
|
||||
}
|
Loading…
Reference in New Issue
Block a user