forked from joeshaw/envdecode
-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathenvdecode.go
155 lines (130 loc) · 3.53 KB
/
envdecode.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
// Package envdecode is a package for populating structs from environment
// variables, using struct tags.
package envdecode
import (
"errors"
"fmt"
"log"
"os"
"reflect"
"strconv"
"strings"
)
// ErrInvalidTarget indicates that the target value passed to
// 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 that has at least one exported field with a valid env tag.")
// 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
// containing the name of the environment variable. Default values
// may be provided by appending ",default=value" to the struct tag.
func Decode(target interface{}) error {
s := reflect.ValueOf(target)
if s.Kind() != reflect.Ptr || s.IsNil() {
return ErrInvalidTarget
}
s = s.Elem()
if s.Kind() != reflect.Struct {
return ErrInvalidTarget
}
t := s.Type()
setFieldCount := 0
for i := 0; i < s.NumField(); i++ {
f := s.Field(i)
switch f.Kind() {
case reflect.Ptr:
if f.Elem().Kind() != reflect.Struct {
break
}
f = f.Elem()
fallthrough
case reflect.Struct:
ss := f.Addr().Interface()
Decode(ss)
}
if !f.CanSet() {
continue
}
tag := t.Field(i).Tag.Get("env")
if tag == "" {
continue
}
parts := strings.Split(tag, ",")
env := os.Getenv(parts[0])
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(`envdecode: "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
}
setFieldCount++
switch f.Kind() {
case reflect.Bool:
v, err := strconv.ParseBool(env)
if err == nil {
f.SetBool(v)
}
case reflect.Float32, reflect.Float64:
bits := f.Type().Bits()
v, err := strconv.ParseFloat(env, bits)
if err == nil {
f.SetFloat(v)
}
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
bits := f.Type().Bits()
v, err := strconv.ParseInt(env, 0, bits)
if err == nil {
f.SetInt(v)
}
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
bits := f.Type().Bits()
v, err := strconv.ParseUint(env, 0, bits)
if err == nil {
f.SetUint(v)
}
case reflect.String:
f.SetString(env)
}
}
// if we didn't do anything - the user probably did something
// wrong like leave all fields unexported.
if setFieldCount == 0 {
return ErrInvalidTarget
}
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)
}
}