Skip to content

Commit

Permalink
Merge pull request #3 from tylerb/master
Browse files Browse the repository at this point in the history
Add "required" annotation
  • Loading branch information
joeshaw committed Jun 26, 2014
2 parents 061b1d2 + d302bcd commit 264b8cb
Show file tree
Hide file tree
Showing 3 changed files with 89 additions and 8 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ type Config struct {

AWS struct {
ID string `env:"AWS_ACCESS_KEY_ID"`
Secret string `env:"AWS_SECRET_ACCESS_KEY"`
Secret string `env:"AWS_SECRET_ACCESS_KEY,required"`
}
}
```
Expand Down
51 changes: 44 additions & 7 deletions envdecode.go
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
// envdecode is a package for populating structs from environment
// Package envdecode is a package for populating structs from environment
// variables, using struct tags.
package envdecode

import (
"errors"
"fmt"
"log"
"os"
"reflect"
"strconv"
Expand All @@ -14,6 +16,16 @@ import (
// Decode is invalid. Target must be a non-nil pointer to a struct.
var ErrInvalidTarget = errors.New("target must be non-nil pointer to struct")

// FailureFunc is called when an error is encountered during a MustDecode
// operation. It prints the error and terminates the process.
//
// This variable can be assigned to another function of the user-programmer's
// design, allowing for graceful recovery of the problem, such as loading
// from a backup configuration file.
var FailureFunc = func(err error) {
log.Fatalf("envdecode: an error was encountered while decoding: %v\n", err)
}

// Decode environment variables into the provided target. The target
// must be a non-nil pointer to a struct. Fields in the struct must
// be exported, and tagged with an "env" struct tag with a value
Expand Down Expand Up @@ -59,15 +71,31 @@ func Decode(target interface{}) error {

parts := strings.Split(tag, ",")
env := os.Getenv(parts[0])
if env == "" {
for _, o := range parts[1:] {
if strings.HasPrefix(o, "default=") {
env = o[8:]
break
}

required := false
hasDefault := false
defaultValue := ""

for _, o := range parts[1:] {
if !required {
required = strings.HasPrefix(o, "required")
}
if strings.HasPrefix(o, "default=") {
hasDefault = true
defaultValue = o[8:]
}
}

if required && hasDefault {
panic(`"default" and "required" may not be specified in the same annotation`)
}
if env == "" && required {
return fmt.Errorf("the environment variable \"%s\" is missing", parts[0])
}
if env == "" {
env = defaultValue
}

if env == "" {
continue
}
Expand Down Expand Up @@ -107,3 +135,12 @@ func Decode(target interface{}) error {

return nil
}

// MustDecode calls Decode and terminates the process if any errors
// are encountered.
func MustDecode(target interface{}) {
err := Decode(target)
if err != nil {
FailureFunc(err)
}
}
44 changes: 44 additions & 0 deletions envdecode_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,13 @@ type testConfig struct {
Default int `env:"TEST_UNSET,asdf=asdf,default=1234"`
}

type testConfigRequired struct {
Required string `env:"TEST_REQUIRED,required"`
}
type testConfigRequiredDefault struct {
RequiredDefault string `env:"TEST_REQUIRED_DEFAULT,required,default=test"`
}

func TestDecode(t *testing.T) {
os.Setenv("TEST_STRING", "foo")
os.Setenv("TEST_INT64", fmt.Sprintf("%d", -(1<<50)))
Expand Down Expand Up @@ -105,6 +112,19 @@ func TestDecode(t *testing.T) {
if tc.Default != 1234 {
t.Fatalf("Expected 1234, got %d", tc.Default)
}

os.Setenv("TEST_REQUIRED", "required")
var tcr testConfigRequired

err = Decode(&tcr)
if err != nil {
t.Fatal(err)
}

if tcr.Required != "required" {
t.Fatalf("Expected \"required\", got %s", tcr.Required)
}

}

func TestDecodeErrors(t *testing.T) {
Expand All @@ -125,6 +145,30 @@ func TestDecodeErrors(t *testing.T) {
if err != ErrInvalidTarget {
t.Fatal("Should have gotten an error decoding to a nil pointer")
}

var tcr testConfigRequired
os.Clearenv()
err = Decode(&tcr)
if err == nil {
t.Fatal("An error was expected but recieved:", err)
}

missing := false
FailureFunc = func(err error) {
missing = true
}
MustDecode(&tcr)
if !missing {
t.Fatal("The FailureFunc should have been called but it was not")
}

var tcrd testConfigRequiredDefault
defer func() {
if r := recover(); r != nil {
}
}()
err = Decode(&tcrd)
t.Fatal("This should not have been reached. A panic should have occured.")
}

func ExampleDecode() {
Expand Down

0 comments on commit 264b8cb

Please sign in to comment.