Skip to content

Commit

Permalink
Add support for environment variables (#4)
Browse files Browse the repository at this point in the history
* Add support for environment variables

* Add support for conversion between numeric types

* Prioritize env variables over config file

* Add env flag to example

* Update documentation
  • Loading branch information
sonmezonur authored Nov 7, 2019
1 parent 2afdad0 commit 3926ec7
Show file tree
Hide file tree
Showing 6 changed files with 508 additions and 83 deletions.
31 changes: 28 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,41 @@ 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

---

Uses the following precedence order:

* `flag`
* `env`
* `toml`


| flag | env | toml | result |
|:----:|:-----:|:-------------:|:---:|
| ☑ | ☑ | ☑ | **flag** |
| ☑ | ☑ | ☐ | **flag** |
| ☑ | ☐ | ☑ | **flag** |
| ☐ | ☑ | ☑ | **env** |
| ☑ | ☐ | ☐ | **flag** |
| ☐ | ☑ | ☐ | **env** |
| ☐ | ☐ | ☑ | **toml** |

If `flag` is set and not given, it will parse `env` or `toml` according to their precedence order (otherwise flag default).


## Basic Example

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 +52,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
7 changes: 4 additions & 3 deletions _example/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,10 @@ import (
)

type cfgType 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" env:"port"`
Secret string `env:"secret"`
}

func main() {
Expand Down
189 changes: 124 additions & 65 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,123 +14,171 @@ 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 != nil {
return err
}

if err := bindEnvVariables(dst); 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 {
continue
}

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
}

continue
}

// if config struct has "flag" tag in flags:
// if flag is set, use flag value
// else
// if toml file has key, use toml value
// else use flag default value
// if config struct has "flag" tag:
// if flag is set, use flag value
// else if env has key, use environment value
// else if toml file has key, use toml value
// else use flag default value

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 {
_, envHasKey := os.LookupEnv(tag)
if envHasKey || 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()
}

// Destination
dstElem := reflect.ValueOf(dst).Elem().FieldByName(field.Name())
if err := setDstElem(dst, field, fVal); err != nil {
return err
}
}

// 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)
return nil
}

// 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", "int8", "int16", "int32", "int64":
if p, err := strconv.ParseInt(fVal, 10, 0); err != nil {
return err
} else {
dstElem.SetInt(p)
}
case "uint", "uint8", "uint16", "uint32", "uint64", "uintptr":
if p, err := strconv.ParseUint(fVal, 10, 0); err != nil {
return err
} else {
dstElem.SetUint(p)
}
case "float64", "float32":
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 +188,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

0 comments on commit 3926ec7

Please sign in to comment.