264 lines
6.0 KiB
Go
264 lines
6.0 KiB
Go
package config
|
|
|
|
import (
|
|
"flag"
|
|
"fmt"
|
|
"os"
|
|
"reflect"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/fatih/camelcase"
|
|
"github.com/fatih/structs"
|
|
)
|
|
|
|
func newLoader(prefix string) *loader {
|
|
return &loader{CamelCase: true, DefaultTagName: "default", Prefix: prefix}
|
|
}
|
|
|
|
// loader satisfies the loader interface. It parses a struct's field tags
|
|
// and populates the each field with that given tag.
|
|
type loader struct {
|
|
// DefaultTagName is the default tag name for struct fields to define
|
|
// default values for a field. Example:
|
|
//
|
|
// // Field's default value is "koding".
|
|
// Name string `default:"koding"`
|
|
//
|
|
// The default value is "default" if it's not set explicitly.
|
|
DefaultTagName string
|
|
// Prefix prepends given string to every environment variable
|
|
// {STRUCTNAME}_FIELDNAME will be {PREFIX}_FIELDNAME
|
|
Prefix string
|
|
|
|
// CamelCase adds a separator for field names in camelcase form. A
|
|
// fieldname of "AccessKey" would generate a environment name of
|
|
// "STRUCTNAME_ACCESSKEY". If CamelCase is enabled, the environment name
|
|
// will be generated in the form of "STRUCTNAME_ACCESS_KEY"
|
|
CamelCase bool
|
|
}
|
|
|
|
func (l *loader) getPrefix(s *structs.Struct) string {
|
|
if l.Prefix != "" {
|
|
return l.Prefix
|
|
}
|
|
|
|
return s.Name()
|
|
}
|
|
|
|
func (l *loader) Load(s interface{}) error {
|
|
strct := structs.New(s)
|
|
prefix := l.getPrefix(strct)
|
|
if l.DefaultTagName == "" {
|
|
l.DefaultTagName = "default"
|
|
}
|
|
|
|
for _, field := range structs.Fields(s) {
|
|
|
|
if err := l.processTagField(l.DefaultTagName, field); err != nil {
|
|
return err
|
|
}
|
|
if err := l.processEnvField(prefix, field); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// processTagField gets tagName and the field, recursively checks if the field has the given
|
|
// tag, if yes, sets it otherwise ignores
|
|
func (l *loader) processTagField(tagName string, field *structs.Field) error {
|
|
switch field.Kind() {
|
|
case reflect.Struct:
|
|
for _, f := range field.Fields() {
|
|
if err := l.processTagField(tagName, f); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
default:
|
|
defaultVal := field.Tag(l.DefaultTagName)
|
|
if defaultVal == "" {
|
|
return nil
|
|
}
|
|
|
|
err := fieldSet(field, defaultVal)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// processEnvField gets leading name for the env variable and combines the current
|
|
// field's name and generates environment variable names recursively
|
|
func (l *loader) processEnvField(prefix string, field *structs.Field) error {
|
|
fieldName := l.getEnvFieldName(prefix, field.Name())
|
|
|
|
switch field.Kind() {
|
|
case reflect.Struct:
|
|
for _, f := range field.Fields() {
|
|
if err := l.processEnvField(fieldName, f); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
default:
|
|
v := os.Getenv(fieldName)
|
|
if v == "" {
|
|
return nil
|
|
}
|
|
|
|
if err := fieldSet(field, v); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// PrintEnvs prints the generated environment variables to the std out.
|
|
func (l *loader) PrintEnvs(s interface{}) {
|
|
strct := structs.New(s)
|
|
prefix := l.getPrefix(strct)
|
|
for _, field := range structs.Fields(s) {
|
|
l.printEnvField(prefix, field)
|
|
}
|
|
}
|
|
|
|
// printEnvField prints the field of the config struct for the flag.Usage
|
|
func (l *loader) printEnvField(prefix string, field *structs.Field) {
|
|
fieldName := l.getEnvFieldName(prefix, field.Name())
|
|
|
|
switch field.Kind() {
|
|
case reflect.Struct:
|
|
for _, f := range field.Fields() {
|
|
l.printEnvField(fieldName, f)
|
|
}
|
|
default:
|
|
fmt.Printf(" %s (default value: '%s')\n", fieldName, field.Tag(l.DefaultTagName))
|
|
}
|
|
}
|
|
|
|
// getEnvFieldName generates the field name combined with the prefix and the
|
|
// struct's field name
|
|
func (l *loader) getEnvFieldName(prefix string, name string) string {
|
|
fieldName := strings.ToUpper(name)
|
|
if l.CamelCase {
|
|
fieldName = strings.ToUpper(strings.Join(camelcase.Split(name), "_"))
|
|
}
|
|
|
|
return strings.ToUpper(prefix) + "_" + fieldName
|
|
}
|
|
|
|
// fieldSet sets field value from the given string value. It converts the
|
|
// string value in a sane way and is usefulf or environment variables or flags
|
|
// which are by nature in string types.
|
|
func fieldSet(field *structs.Field, v string) error {
|
|
switch f := field.Value().(type) {
|
|
case flag.Value:
|
|
if v := reflect.ValueOf(field.Value()); v.IsNil() {
|
|
typ := v.Type()
|
|
if typ.Kind() == reflect.Ptr {
|
|
typ = typ.Elem()
|
|
}
|
|
|
|
if err := field.Set(reflect.New(typ).Interface()); err != nil {
|
|
return err
|
|
}
|
|
|
|
f = field.Value().(flag.Value)
|
|
}
|
|
|
|
return f.Set(v)
|
|
}
|
|
|
|
// TODO: add support for other types
|
|
switch field.Kind() {
|
|
case reflect.Bool:
|
|
val, err := strconv.ParseBool(v)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := field.Set(val); err != nil {
|
|
return err
|
|
}
|
|
case reflect.Int:
|
|
i, err := strconv.Atoi(v)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := field.Set(i); err != nil {
|
|
return err
|
|
}
|
|
case reflect.String:
|
|
if err := field.Set(v); err != nil {
|
|
return err
|
|
}
|
|
case reflect.Slice:
|
|
switch t := field.Value().(type) {
|
|
case []string:
|
|
if err := field.Set(strings.Split(v, ",")); err != nil {
|
|
return err
|
|
}
|
|
case []int:
|
|
var list []int
|
|
for _, in := range strings.Split(v, ",") {
|
|
i, err := strconv.Atoi(in)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
list = append(list, i)
|
|
}
|
|
|
|
if err := field.Set(list); err != nil {
|
|
return err
|
|
}
|
|
default:
|
|
return fmt.Errorf("multiconfig: field '%s' of type slice is unsupported: %s (%T)",
|
|
field.Name(), field.Kind(), t)
|
|
}
|
|
case reflect.Float64:
|
|
f, err := strconv.ParseFloat(v, 64)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := field.Set(f); err != nil {
|
|
return err
|
|
}
|
|
case reflect.Int64:
|
|
switch t := field.Value().(type) {
|
|
case time.Duration:
|
|
d, err := time.ParseDuration(v)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := field.Set(d); err != nil {
|
|
return err
|
|
}
|
|
case int64:
|
|
p, err := strconv.ParseInt(v, 10, 0)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := field.Set(p); err != nil {
|
|
return err
|
|
}
|
|
default:
|
|
return fmt.Errorf("multiconfig: field '%s' of type int64 is unsupported: %s (%T)",
|
|
field.Name(), field.Kind(), t)
|
|
}
|
|
|
|
default:
|
|
return fmt.Errorf("multiconfig: field '%s' has unsupported type: %s", field.Name(), field.Kind())
|
|
}
|
|
|
|
return nil
|
|
}
|