Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for environment variables #4

Merged
merged 5 commits into from
Nov 7, 2019
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 6 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ Offers a rich configuration file handler.

- Read configuration files with ease
- Bind CLI flags
- Bind environment variables
- Watch file (or files) and get notified if they change

## Basic Example
Expand All @@ -15,9 +16,10 @@ Call the `Load()` method to load a config.

```go
type MyConfig struct {
Key1 string `toml:"key1"`
Key2 string `toml:"key2"`
Port int `toml:"-" flag:"port"`
Key1 string `toml:"key1"`
Key2 string `toml:"key2"`
Port int `toml:"-" flag:"port"`
Secret string `toml:"-" flag:"-" env:"secret"`
}

_ = flag.Int("port", 8080, "Port to listen on") // <- notice no variable
Expand All @@ -28,6 +30,7 @@ Call the `Load()` method to load a config.

fmt.Printf("Loaded config: %#v\n", cfg)
// Port info is in cfg.Port, parsed from `-port` param
// Secret info is in cfg.Secret, parsed from `secret` environment variable
```

## File Watching
Expand Down
178 changes: 118 additions & 60 deletions config.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ package config
import (
"flag"
"fmt"
"os"
"reflect"
"strconv"

Expand All @@ -13,44 +14,67 @@ import (
"github.com/fatih/structs"
)

const (
envTag string = "env"
flagTag string = "flag"
)

// Load loads filepath into dst. It also handles "flag" binding.
func Load(filepath string, dst interface{}) error {
metadata, err := toml.DecodeFile(filepath, dst)
if err := bindEnvVariables(dst); err != nil {
return err
}

metadata, err := toml.DecodeFile(filepath, dst)
if err != nil {
return err
}

return bindFlags(dst, metadata)
}

// bindFlags will bind CLI flags to their respective elements in dst, defined by the struct-tag "flag".
func bindFlags(dst interface{}, metadata toml.MetaData) error {
// Iterate all fields
// bindEnvVariables will bind CLI flags to their respective elements in dst, defined by the struct-tag "env".
func bindEnvVariables(dst interface{}) error {
fields := structs.Fields(dst)
for _, field := range fields {
tag := field.Tag("flag")
tag := field.Tag(envTag)
if tag == "" || tag == "-" {
// Maybe it's nested?
ok, dstElem := isNestedStruct(dst, field)
if !ok {
continue
}

dstElem := reflect.ValueOf(dst).Elem().FieldByName(field.Name())
if err := bindEnvVariables(dstElem.Addr().Interface()); err != nil {
return err
}

if dstElem.Kind() == reflect.Ptr {
if dstElem.IsNil() {
// Create new non-nil ptr
dstElem.Set(reflect.New(dstElem.Type().Elem()))
}
continue
}

// Dereference
dstElem = dstElem.Elem()
}
fVal, ok := os.LookupEnv(tag)
if !ok {
return fmt.Errorf("env '%v' is not defined but given as env struct tag in %v.%v", tag, reflect.TypeOf(dst), field.Name())
}

if err := setDstElem(dst, field, fVal); err != nil {
return err
}
}
return nil
}

if dstElem.Kind() != reflect.Struct {
// bindFlags will bind CLI flags to their respective elements in dst, defined by the struct-tag "flag".
func bindFlags(dst interface{}, metadata toml.MetaData) error {
fields := structs.Fields(dst)
for _, field := range fields {
tag := field.Tag(flagTag)
if tag == "" || tag == "-" {
ok, dstElem := isNestedStruct(dst, field)
if !ok {
continue
}

err := bindFlags(dstElem.Addr().Interface(), metadata)
if err != nil {
if err := bindFlags(dstElem.Addr().Interface(), metadata); err != nil {
return err
}

Expand All @@ -65,71 +89,95 @@ func bindFlags(dst interface{}, metadata toml.MetaData) error {

useFlagDefaultValue := false
if !isFlagSet(tag) {
tomlHasKey := false
for _, key := range metadata.Keys() {
if strings.ToLower(key.String()) == strings.ToLower(tag) {
tomlHasKey = true
break
}
}
if tomlHasKey {
if tomlHasKey(metadata, tag) {
continue
} else {
useFlagDefaultValue = true
}
}

// CLI value

if flag.Lookup(tag) == nil {
return fmt.Errorf("flag '%v' is not defined but given as flag struct tag in %v.%v", tag, reflect.TypeOf(dst), field.Name())
}

fVal := flag.Lookup(tag).Value.String()
var fVal string
if useFlagDefaultValue {
fVal = flag.Lookup(tag).DefValue
} else {
fVal = flag.Lookup(tag).Value.String()
}

if err := setDstElem(dst, field, fVal); err != nil {
return err
}
}

// Destination
dstElem := reflect.ValueOf(dst).Elem().FieldByName(field.Name())
return nil
}

// Attempt to convert the flag input depending on type of destination
switch dstElem.Kind().String() {
case "bool":
if p, err := strconv.ParseBool(fVal); err != nil {
return err
} else {
dstElem.SetBool(p)
}
case "int":
if p, err := strconv.ParseInt(fVal, 10, 0); err != nil {
return err
} else {
dstElem.SetInt(p)
}
case "uint":
if p, err := strconv.ParseUint(fVal, 10, 0); err != nil {
return err
} else {
dstElem.SetUint(p)
}
case "float64":
if p, err := strconv.ParseFloat(fVal, 64); err != nil {
return err
} else {
dstElem.SetFloat(p)
}
case "string":
dstElem.SetString(fVal)
// isNestedStruct will check if destination element or its pointer is struct type
func isNestedStruct(dst interface{}, field *structs.Field) (bool, reflect.Value) {
dstElem := reflect.ValueOf(dst).Elem().FieldByName(field.Name())
if dstElem.Kind() == reflect.Ptr {
if dstElem.IsNil() {
// Create new non-nil ptr
dstElem.Set(reflect.New(dstElem.Type().Elem()))
}

// Dereference
dstElem = dstElem.Elem()
}

if dstElem.Kind() != reflect.Struct {
return false, dstElem
}

return true, dstElem
}

default:
return fmt.Errorf("Unhandled type %v for elem %v", dstElem.Kind().String(), field.Name())
// setDstElem will convert tag input to its real type
func setDstElem(dst interface{}, field *structs.Field, fVal string) error {
// Destination
dstElem := reflect.ValueOf(dst).Elem().FieldByName(field.Name())

// Attempt to convert the tag input depending on type of destination
switch dstElem.Kind().String() {
case "bool":
if p, err := strconv.ParseBool(fVal); err != nil {
return err
} else {
dstElem.SetBool(p)
}
case "int":
if p, err := strconv.ParseInt(fVal, 10, 0); err != nil {
return err
} else {
dstElem.SetInt(p)
}
case "uint":
if p, err := strconv.ParseUint(fVal, 10, 0); err != nil {
return err
} else {
dstElem.SetUint(p)
}
case "float64":
if p, err := strconv.ParseFloat(fVal, 64); err != nil {
return err
} else {
dstElem.SetFloat(p)
}
case "string":
dstElem.SetString(fVal)

default:
return fmt.Errorf("unhandled type %v for elem %v", dstElem.Kind().String(), field.Name())
}

return nil
}

// isFlagSet will check if flag is set
func isFlagSet(tag string) bool {
flagSet := false
flag.Visit(func(fl *flag.Flag) {
Expand All @@ -139,3 +187,13 @@ func isFlagSet(tag string) bool {
})
return flagSet
}

// tomlHasKey will check if the tag presents in toml metadata
func tomlHasKey(metadata toml.MetaData, tag string) bool {
for _, key := range metadata.Keys() {
if strings.ToLower(key.String()) == strings.ToLower(tag) {
return true
}
}
return false
}
Loading