-
Notifications
You must be signed in to change notification settings - Fork 5
/
Copy pathenv.go
201 lines (178 loc) Β· 4.93 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
191
192
193
194
195
196
197
198
199
200
201
// Package env implements loading environment variables into a config struct.
package env
import (
"fmt"
"os"
"reflect"
"strings"
)
// Options are the options for the [Load] and [Usage] functions.
type Options struct {
Source Source // The source of environment variables. The default is [OS].
SliceSep string // The separator used to parse slice values. The default is space.
NameSep string // The separator used to concatenate environment variable names from nested struct tags. The default is an empty string.
}
// NotSetError is returned when required environment variables are not set.
type NotSetError struct {
Names []string
}
// Error implements the error interface.
func (e *NotSetError) Error() string {
if len(e.Names) == 1 {
return fmt.Sprintf("env: %s is required but not set", e.Names[0])
}
return fmt.Sprintf("env: %s are required but not set", strings.Join(e.Names, " "))
}
// Load loads environment variables into the given struct.
// cfg must be a non-nil struct pointer, otherwise Load panics.
// If opts is nil, the default [Options] are used.
//
// The struct fields must have the `env:"VAR"` struct tag,
// where VAR is the name of the corresponding environment variable.
// Unexported fields are ignored.
//
// The following types are supported:
// - int (any kind)
// - float (any kind)
// - bool
// - string
// - [time.Duration]
// - [encoding.TextUnmarshaler]
// - slices of any type above
// - nested structs of any depth
//
// See the [strconv].Parse* functions for the parsing rules.
// User-defined types can be used by implementing the [encoding.TextUnmarshaler] interface.
//
// Nested struct of any depth level are supported,
// allowing grouping of related environment variables.
// If a nested struct has the optional `env:"PREFIX"` tag,
// the environment variables declared by its fields are prefixed with PREFIX.
//
// Default values can be specified using the `default:"VALUE"` struct tag.
//
// The name of an environment variable can be followed by comma-separated options:
// - required: marks the environment variable as required
// - expand: expands the value of the environment variable using [os.Expand]
func Load(cfg any, opts *Options) error {
pv := reflect.ValueOf(cfg)
if !structPtr(pv) {
panic("env: cfg must be a non-nil struct pointer")
}
opts = setDefaultOptions(opts)
v := pv.Elem()
vars := parseVars(v, opts)
cache[v.Type()] = vars
var notset []string
for _, v := range vars {
value, ok := lookupEnv(opts.Source, v.Name, v.Expand)
if !ok {
if v.Required {
notset = append(notset, v.Name)
continue
}
if !v.hasDefaultTag {
continue // nothing to set.
}
value = v.Default
}
var err error
if kindOf(v.structField, reflect.Slice) && !implements(v.structField, unmarshalerIface) {
err = setSlice(v.structField, strings.Split(value, opts.SliceSep))
} else {
err = setValue(v.structField, value)
}
if err != nil {
return err
}
}
if len(notset) > 0 {
return &NotSetError{Names: notset}
}
return nil
}
func setDefaultOptions(opts *Options) *Options {
if opts == nil {
opts = new(Options)
}
if opts.Source == nil {
opts.Source = OS
}
if opts.SliceSep == "" {
opts.SliceSep = " "
}
return opts
}
func parseVars(v reflect.Value, opts *Options) []Var {
var vars []Var
for i := 0; i < v.NumField(); i++ {
field := v.Field(i)
if !field.CanSet() {
continue
}
tags := v.Type().Field(i).Tag
if kindOf(field, reflect.Struct) && !implements(field, unmarshalerIface) {
var prefix string
if value, ok := tags.Lookup("env"); ok {
prefix = value + opts.NameSep
}
for _, v := range parseVars(field, opts) {
v.Name = prefix + v.Name
vars = append(vars, v)
}
continue
}
value, ok := tags.Lookup("env")
if !ok {
continue
}
parts := strings.Split(value, ",")
name, options := parts[0], parts[1:]
if name == "" {
panic("env: empty tag name is not allowed")
}
var required, expand bool
for _, option := range options {
switch option {
case "required":
required = true
case "expand":
expand = true
default:
panic(fmt.Sprintf("env: invalid tag option `%s`", option))
}
}
defValue, defSet := tags.Lookup("default")
switch {
case defSet && required:
panic("env: `required` and `default` can't be used simultaneously")
case !defSet && !required:
defValue = fmt.Sprintf("%v", field.Interface())
}
vars = append(vars, Var{
Name: name,
Type: field.Type(),
Usage: tags.Get("usage"),
Default: defValue,
Required: required,
Expand: expand,
structField: field,
hasDefaultTag: defSet,
})
}
return vars
}
func lookupEnv(src Source, key string, expand bool) (string, bool) {
value, ok := src.LookupEnv(key)
if !ok {
return "", false
}
if !expand {
return value, true
}
mapping := func(key string) string {
v, _ := src.LookupEnv(key)
return v
}
return os.Expand(value, mapping), true
}