Skip to content

Commit

Permalink
Merge pull request #1 from everettcaleb/improvements
Browse files Browse the repository at this point in the history
Improvements, new types, and refactoring
  • Loading branch information
everettcaleb committed Feb 9, 2019
2 parents 9ac4c9b + fefc1d7 commit 183f316
Show file tree
Hide file tree
Showing 6 changed files with 262 additions and 147 deletions.
46 changes: 34 additions & 12 deletions README.md
Expand Up @@ -27,15 +27,21 @@ require that a database connection string and Redis host are provided:
type appSettings struct {
Port int `env:"PORT"`
DBConnection string `env:"DB_CONNECTION" required:"true"`
RedisPort int `env:"REDIS_PORT"`
RedisHost string `env:"REDIS_HOST" required:"true"`
Redis redisSettings
}

type redisSettings struct {
Port int `env:"REDIS_PORT"`
Host string `env:"REDIS_HOST" required:"true"`
}

func main() {
// Initialize with defaults (if zero value is not desired)
settings := appSettings{
Port: 80,
RedisPort: 6379
Redis: redisSettings{
Port: 6379,
},
}

// Pull in environment variables
Expand All @@ -48,8 +54,8 @@ require that a database connection string and Redis host are provided:
// Print them out
fmt.Println("Port:", settings.Port)
fmt.Println("DB Connection:", settings.DBConnection)
fmt.Println("Redis Port:", settings.RedisPort)
fmt.Println("Redis Host:", settings.RedisHost)
fmt.Println("Redis Port:", settings.Redis.Port)
fmt.Println("Redis Host:", settings.Redis.Host)
}

This example will print out:
Expand All @@ -63,18 +69,34 @@ This example will print out:
Below is documentation for exported functions:

### `func Unmarshal(i interface{}) error`
`Unmarshal` dumps environment variable values into a `struct` (pass a `*struct`)
`Unmarshal` dumps environment variable values into exported fields of a `struct` (pass a `*struct`)
It will automatically look up environment variables that are named in a struct tag.
For example, if you tag a field in your struct with ```env:"MY_VAR"``` it will
be filled with the value of that environment variable. If you would like an error
to be returned if the field is not set, use the tag ```required:"true"```. For boolean,
fields, any of the following values are valid (any case): `"true"`, `"false"`, `"yes"`, `"no"`,
`"t"`, `"f"`, `"y"`, `"n"`, `"1"`, `"0"`. Empty/unset environment variable values will not overwrite
the struct fields that are already set.
fields, any of the following values are valid (any case): `"true"/"false"`, `"yes"/"no"`, `"on"/"off"`,
`"t"/"f"`, `"y"/"n"`, `"1"/"0"`. Empty/unset environment variable values will not overwrite
the struct fields that are already set. Any pointer fields will be dereferenced once (but are skipped if `nil`).
Fields of a struct kind are unmarshalled recursively. Slices are supported with ":" separated strings (only string slices are
supported though)

#### Supported Field Types
Currently, `Unmarshal` supports the following field types:

- `bool`
- `float32` and `float64`
- `int`, `int8`, `int16`, `int32`, and `int64`
- `[]string`
- `string`
- `struct`
- `uint`, `uint8`, `uint16`, `uint32`, and `uint64`

Note: user-defined types that are defined using one of the above built-in
types are also supported. For example:

type myCustomInt int

## TODO
Currently, `Unmarshal` doesn't support arrays, pointers, slices, or structs. I want to add support for
this later (slices will be comma separated probably).
The above is perfectly fine and will work as well.

## Contributing
Feel free to contribute by forking and creating a pull request. If you find any issues please
Expand Down
141 changes: 62 additions & 79 deletions getenv.go
Expand Up @@ -10,130 +10,113 @@ import (

var env = os.Getenv

func parseFriendlyBool(s string) (bool, error) {
switch strings.ToLower(s) {
case "yes", "true", "t", "y", "1":
return true, nil
case "no", "false", "f", "n", "0":
return false, nil
default:
return false, fmt.Errorf("Expected boolean value, got: %s", s)
func setStringFromEnv(v reflect.Value, name string) bool {
s := env(name)
if len(s) == 0 {
return false
}
v.SetString(s)
return true
}

func getEnvString(name string) (string, bool) {
func setStringSliceFromEnv(v reflect.Value, name string) bool {
s := env(name)
return s, len(s) > 0
if len(s) == 0 {
return false
}
v.Set(reflect.ValueOf(strings.Split(s, ":")))
return true
}

func getEnvInt(name string, base int, bits int) (int64, bool, error) {
func setIntFromEnv(v reflect.Value, name string) (bool, error) {
s := env(name)
if len(s) == 0 {
return 0, false, nil
return false, nil
}

val, err := strconv.ParseInt(s, base, bits)
return val, err == nil, err
val, err := strconv.ParseInt(s, 10, bitsOf(v.Kind()))
if err != nil {
return false, err
}
v.SetInt(val)
return true, nil
}

func getEnvUint(name string, base int, bits int) (uint64, bool, error) {
func setUintFromEnv(v reflect.Value, name string) (bool, error) {
s := env(name)
if len(s) == 0 {
return 0, false, nil
return false, nil
}

val, err := strconv.ParseUint(s, base, bits)
return val, err == nil, err
val, err := strconv.ParseUint(s, 10, bitsOf(v.Kind()))
if err != nil {
return false, err
}
v.SetUint(val)
return true, nil
}

func getEnvFloat(name string, bits int) (float64, bool, error) {
func setFloatFromEnv(v reflect.Value, name string) (bool, error) {
s := env(name)
if len(s) == 0 {
return 0, false, nil
return false, nil
}

val, err := strconv.ParseFloat(s, bits)
return val, err == nil, err
val, err := strconv.ParseFloat(s, bitsOf(v.Kind()))
if err != nil {
return false, err
}
v.SetFloat(val)
return true, nil
}

func getEnvBool(name string) (bool, bool, error) {
func setBoolFromEnv(v reflect.Value, name string) (bool, error) {
s := env(name)
if len(s) == 0 {
return false, false, nil
return false, nil
}

b, err := parseFriendlyBool(s)
return b, err == nil, err
}

func getFieldTagBool(f *reflect.StructField, tag string, defaultValue bool) (bool, error) {
str, ok := f.Tag.Lookup(tag)
if ok {
v, err := parseFriendlyBool(str)
if err != nil {
return false, fmt.Errorf("Tag %s must have boolean string value", tag)
}
return v, nil
if err != nil {
return false, err
}
return defaultValue, nil
v.SetBool(b)
return true, nil
}

// bitsOf gets the number of bits of the reflected variable kind
func bitsOf(k reflect.Kind) int {
switch k {
case reflect.Int8, reflect.Uint8:
return 8
case reflect.Int16, reflect.Uint16:
return 16
case reflect.Float32, reflect.Int, reflect.Int32, reflect.Uint, reflect.Uint32:
return 32
case reflect.Float64, reflect.Int64, reflect.Uint64:
return 64
default:
return 0
}
}

// getEnvAndSet gets an environment variable by name and assigns it to the field belonging to value "obj"
// setValueFromEnv gets an environment variable by name and assigns it to a reflected value
// First return value is true if the field was set, false otherwise
// Second return value is an error for if something went wrong (invalid format for parsing, etc)
func getEnvAndSet(obj *reflect.Value, f *reflect.StructField, fieldIndex int, envName string) (bool, error) {
k := f.Type.Kind()
func setValueFromEnv(v reflect.Value, name string) (bool, error) {
k := v.Kind()

switch k {
case reflect.Bool:
v, isSet, err := getEnvBool(envName)
if isSet {
obj.Field(fieldIndex).SetBool(v)
}
return isSet, err
return setBoolFromEnv(v, name)

case reflect.Float32, reflect.Float64:
v, isSet, err := getEnvFloat(envName, bitsOf(k))
if isSet {
obj.Field(fieldIndex).SetFloat(v)
}
return isSet, err
return setFloatFromEnv(v, name)

case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
v, isSet, err := getEnvInt(envName, 10, bitsOf(k))
if isSet {
obj.Field(fieldIndex).SetInt(v)
return setIntFromEnv(v, name)

case reflect.Ptr:
return false, fmt.Errorf("invalid kind, pointer-to-pointer is not supported and single pointer is resolved by unmarshalStructValue")

case reflect.Slice:
if v.Type().Elem().Kind() == reflect.String {
return setStringSliceFromEnv(v, name), nil
}
return isSet, err
return false, fmt.Errorf("invalid kind, for slices only string slices are currently supported")

case reflect.String:
v, isSet := getEnvString(envName)
if isSet {
obj.Field(fieldIndex).SetString(v)
}
return isSet, nil
return setStringFromEnv(v, name), nil

case reflect.Struct:
return false, fmt.Errorf("invalid kind, struct must be processed by unmarshalStructValue")

case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
v, isSet, err := getEnvUint(envName, 10, bitsOf(k))
if isSet {
obj.Field(fieldIndex).SetUint(v)
}
return isSet, err
return setUintFromEnv(v, name)

default:
return false, fmt.Errorf("invalid kind")
Expand Down
49 changes: 49 additions & 0 deletions helpers.go
@@ -0,0 +1,49 @@
package envconfig

import (
"fmt"
"reflect"
"strings"
)

// getStructTagAsBool gets a struct tag (by name) as a boolean or returns a default value if not specified
func getStructTagAsBool(f *reflect.StructField, tag string, defaultValue bool) (bool, error) {
str, ok := f.Tag.Lookup(tag)
if ok {
v, err := parseFriendlyBool(str)
if err != nil {
return false, fmt.Errorf("Tag %s must have boolean string value", tag)
}
return v, nil
}
return defaultValue, nil
}

// bitsOf gets the number of bits of the reflected variable kind
func bitsOf(k reflect.Kind) int {
switch k {
case reflect.Int8, reflect.Uint8:
return 8
case reflect.Int16, reflect.Uint16:
return 16
case reflect.Float32, reflect.Int32, reflect.Uint32:
return 32
case reflect.Float64, reflect.Int, reflect.Int64, reflect.Uint, reflect.Uint64:
return 64
default:
return 0
}
}

// parseFriendlyBool parses a multitude of values as boolean case-insensitively: "yes"/"no", "true"/"false", "on"/"off",
// "t"/"f", "y"/"n", "1"/"0" to allow for more expressive configuration settings
func parseFriendlyBool(s string) (bool, error) {
switch strings.ToLower(s) {
case "yes", "true", "on", "t", "y", "1":
return true, nil
case "no", "false", "off", "f", "n", "0":
return false, nil
default:
return false, fmt.Errorf("Expected boolean value, got: %s", s)
}
}
File renamed without changes.

0 comments on commit 183f316

Please sign in to comment.