-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathenv.go
190 lines (176 loc) · 4.96 KB
/
env.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
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
package qcl
import (
"errors"
"os"
"reflect"
"strings"
)
const env = "env"
type envConfig struct {
prefix string
structTag string
separator string
}
var defaultEnvConfig = &envConfig{
prefix: "",
structTag: "env",
separator: ",",
}
type envOption func(*envConfig)
var (
NotAMapError = errors.New("not a map")
NotASliceError = errors.New("not a slice")
ConfigTypeError = errors.New("config must be a pointer to a struct")
)
// UseEnv allows you to load configuration from environment variables. The environment variables are expected to be
// in all caps and separated by underscores. For example, a field named "FooBar" will be set by the environment
// variable "FOO_BAR".
//
// Example:
//
// export FOO_BAR=baz
//
// type Config struct {
// FooBar string
// }
//
// var defaultConfig Config
//
// ocl.Load(defaultConfig, ocl.UseEnv())
//
// will set the value of FooBar to the value of the environment variable "FOO_BAR".
func UseEnv(opts ...envOption) LoadOption {
envConf := defaultEnvConfig
for _, opt := range opts {
opt(envConf)
}
return func(o *LoadConfig) {
o.Sources = append(o.Sources, env)
o.Loaders[env] = loadFromEnv(envConf)
}
}
// WithEnvPrefix allows you to specify a prefix for environment variables. For example, if you specify "FOO" as the
// prefix, then an environment variable named "FOO_BAR" will be used to set a field named "Bar" in the config struct.
//
// Example:
//
// type Config struct {
// Bar string
// }
//
// WithEnvPrefix("FOO")
//
// will set the value of Bar to the value of the environment variable "FOO_BAR".
//
// The default is no prefix.
func WithEnvPrefix(prefix string) envOption {
return func(c *envConfig) {
c.prefix = prefix
}
}
// WithEnvStructTag allows you to specify a custom struct tag to use for environment variable names. By default, the loader
// looks for the "env" struct tag, but if that's not found the field name itself is used as the environment variable
// name and in either case, it is split on word boundaries. For example, a field named "FooBar" will be set by the
// environment variable "FOO_BAR" by default.
//
// Example:
//
// WithEnvStructTag("mytag")
//
// type Config struct {
// FooBar string `mytag:"FOO"` // FooBar will be set by the environment variable "FOO" instead of the default "FOO_BAR"
// }
//
// By default, the environment loader looks for a struct tag "env" and in the absence of a struct tag, will use the field
// name itself.
func WithEnvStructTag(tag string) envOption {
return func(c *envConfig) {
c.structTag = tag
}
}
// WithEnvSeparator allows you to specify a custom separator for environment variables that are setting iterables.
//
// Example:
//
// WithEnvSeparator(";")
//
// will allow an environment variable like
//
// export FOO="bar;baz"
//
// to set a field named "Foo" to a slice of strings with the values "bar" and "baz":
//
// type Config struct {
// Foo []string // foo will be set to []string{"bar", "baz"}
// }
//
// This also works for maps where the key/value pairs are separated by the separator.
//
// Example:
//
// export FOO="bar=baz;qux=quux"
//
// will set a field named "Foo" to a map with the key/value pairs bar=baz, and qux=quux.
//
// type Config struct {
// Foo map[string]string // foo will be set to map[string]string{"bar": "baz", "qux": "quux"}
// }
//
// The default separator is a comma (,)
func WithEnvSeparator(separator string) envOption {
return func(c *envConfig) {
c.separator = separator
}
}
func loadFromEnv(envConf *envConfig) Loader {
if envConf == nil {
envConf = defaultEnvConfig
}
if envConf.prefix != "" && !strings.HasSuffix(envConf.prefix, "_") {
envConf.prefix += "_"
}
return func(config any) error {
if reflect.TypeOf(config).Kind() != reflect.Ptr {
return ConfigTypeError
}
val := reflect.ValueOf(config).Elem()
typ := val.Type()
return envSetFields(val, typ, envConf.prefix, envConf.structTag, envConf.separator)
}
}
func envSetFields(val reflect.Value, typ reflect.Type, envPrefix, structTag, separator string) error {
for i := 0; i < typ.NumField(); i++ {
field := typ.Field(i)
fName := strings.Join(splitOnWordBoundaries(field.Name), "_")
if structTag != "" {
if tag, ok := field.Tag.Lookup(structTag); ok {
tag = strings.Split(strings.TrimSpace(tag), ",")[0]
fName = strings.Join(splitOnWordBoundaries(tag), "_")
}
}
if val := val.Field(i); val.CanSet() {
if field.Anonymous && field.Type.Kind() == reflect.Struct {
if err := envSetFields(val, field.Type, envPrefix, structTag, separator); err != nil {
return err
}
}
if val.Kind() == reflect.Ptr {
if val.IsNil() {
val.Set(reflect.New(val.Type().Elem()))
}
val = val.Elem()
}
if val.Kind() == reflect.Struct {
if err := envSetFields(val, val.Type(), envPrefix+fName+"_", structTag, separator); err != nil {
return err
}
}
if v := os.Getenv(strings.ToUpper(envPrefix + fName)); v != "" {
if err := setField(val, v, separator); err != nil {
return err
}
}
}
}
return nil
}