-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathenv.go
More file actions
264 lines (229 loc) · 7.2 KB
/
Copy pathenv.go
File metadata and controls
264 lines (229 loc) · 7.2 KB
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
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
// Package envparser is a simple no-dependency library for parsing environment variables in Go.
package envparser
import (
"errors"
"fmt"
"os"
"reflect"
"slices"
"strconv"
"strings"
)
var (
//nolint:gochecknoglobals
vars = make([]any, 0, 1)
//nolint:gochecknoglobals
nameMap map[string]bool
//nolint:gochecknoglobals
parsed = false
// ErrName indicates that the variable name is invalid. Variable names must be non-empty strings.
ErrName = errors.New("variable name is invalid")
// ErrNameExists indicates that a variable with the same name has already been registered. Variable names must be unique.
ErrNameExists = errors.New("variable name already exists")
// ErrRequired indicates that a required variable is missing from the environment.
ErrRequired = errors.New("variable is required")
// ErrCreateAndRequired indicates that a variable cannot be marked for creation and required at the same time. A variable
// that is marked for creation will be created with the default value if it does not exist in the environment, so it cannot be required.
ErrCreateAndRequired = errors.New(
"variable can't be marked for creation and required at the same time",
)
// ErrValidate indicates that the variable value failed validation. The error message will include the specific validation error.
ErrValidate = errors.New("variable validation failed")
// ErrAccepted indicates that the variable value is not in the list of accepted values. The error message will include the accepted values.
ErrAccepted = errors.New("variable value not in accepted values")
// ExitOnError ensures that encountered errors are printed to stderr and the
// program exits with code 1. If not set, errors are returned to the caller.
//nolint:gochecknoglobals
ExitOnError = true
//nolint:gochecknoglobals
exitFunc = os.Exit
// Prefix is the prefix used for environment variables. If set, all
// environment variables will be prefixed with this value. For example, if
// Prefix is set to "MYAPP", the environment variable "MYAPP_FOO" will be
// used for the variable "FOO". If not set, no prefix will be used.
// This is useful for namespacing environment variables in larger applications.
//nolint:gochecknoglobals
Prefix = ""
)
// Register a variable with the given options. Returns a pointer to the
// registered variable.
func Register[T TypeConstraint](opts *Opts[T]) *Var[T] {
v := &Var[T]{
name: opts.Name,
desc: opts.Desc,
value: opts.Value,
required: opts.Required,
create: opts.Create,
validate: opts.Validate,
acceptedValues: opts.AcceptedValues,
}
vars = append(vars, v)
return v
}
// Parse parses the environment variables registered with Register. If an error
// occurs, it will be returned. If ExitOnError is set, the program will exit
// with code 1 and print the error to stderr.
func Parse() error {
defer func() { parsed = true }()
nameMap = make(map[string]bool, len(vars))
errs := []error{}
for _, v := range vars {
var err error
switch v := v.(type) {
case *Var[int]:
err = check(v, parseInt)
case *Var[bool]:
err = check(v, parseBool)
case *Var[string]:
err = check(v, parseString)
case *Var[float64]:
err = check(v, parseFloat)
default:
panic("unsupported type")
}
if err != nil {
errs = append(errs, err)
}
}
nameMap = nil
if len(errs) > 0 {
if ExitOnError {
fmt.Fprint(os.Stderr, "Errors:\n")
for _, err := range errs {
fmt.Fprintf(os.Stderr, "%s\n", err.Error())
}
fmt.Fprintf(os.Stderr, "\n%s\n", Help())
exitFunc(1)
return nil // for testing purposes
}
return fmt.Errorf("failed to parse env vars: %w", errors.Join(errs...))
}
return nil
}
// Help returns a string with the help information for all registered
// environment variables. The help information includes the name, type,
// description, and default value (if applicable) for each variable.
func Help() string {
help := strings.Builder{}
help.WriteString("Environment variables:\n\n")
longest := 0
for _, v := range vars {
switch v := v.(type) {
case *Var[int]:
if l := metaLength(v); l > longest {
longest = l
}
case *Var[bool]:
if l := metaLength(v); l > longest {
longest = l
}
case *Var[string]:
if l := metaLength(v); l > longest {
longest = l
}
case *Var[float64]:
if l := metaLength(v); l > longest {
longest = l
}
default:
panic("unsupported type")
}
}
for _, v := range vars {
switch v := v.(type) {
case *Var[int]:
_, _ = help.WriteString(getHelpString(v, longest))
case *Var[bool]:
_, _ = help.WriteString(getHelpString(v, longest))
case *Var[string]:
_, _ = help.WriteString(getHelpString(v, longest))
case *Var[float64]:
_, _ = help.WriteString(getHelpString(v, longest))
default:
panic("unsupported type")
}
}
return help.String()
}
func metaLength[T TypeConstraint](v *Var[T]) int {
// Parentheses and spacing
const defaultPadding = 3
l := len(v.prefixedName()) + len(reflect.TypeOf(v.value).String()) + defaultPadding
if v.required {
l += len(", required")
}
return l
}
func getHelpString[T TypeConstraint](v *Var[T], longest int) string {
defaultInfo := ""
typeInfo := reflect.TypeOf(v.value).String()
if v.required {
typeInfo += ", required"
} else {
defaultInfo = fmt.Sprintf("(default: %v)", v.value)
}
name := fmt.Sprintf("%s (%s)", v.prefixedName(), typeInfo)
// <name> (<type>, [required]): <description> [(default: <value>)]\n
return fmt.Sprintf("%-*s: %s %s\n", longest, name, v.desc, defaultInfo)
}
func check[T TypeConstraint](v *Var[T], parser func(string) (T, error)) error {
value, exists := os.LookupEnv(v.prefixedName())
if err := generalCheck(v, exists); err != nil {
return err
}
if !exists && !v.create {
return nil
}
if !exists && v.create {
return os.Setenv(v.prefixedName(), fmt.Sprintf("%v", v.value))
}
parsedValue, err := parser(value)
if err != nil {
return err
}
// If both are set, accepted values take precedence
if v.acceptedValues != nil {
if !slices.Contains(v.acceptedValues, parsedValue) {
return fmt.Errorf("%w: %s %v", ErrAccepted, v.prefixedName(), v.acceptedValues)
}
} else if v.validate != nil {
//nolint:govet
if err := v.validate(parsedValue); err != nil {
return fmt.Errorf("%w: %w", fmt.Errorf("%w: %s", ErrValidate, v.prefixedName()), err)
}
}
v.value = parsedValue
return nil
}
func generalCheck[T TypeConstraint](v *Var[T], exists bool) error {
if v.name == "" {
return fmt.Errorf("%w: %s", ErrName, v.prefixedName())
}
if _, nameExists := nameMap[v.prefixedName()]; nameExists {
return fmt.Errorf("%w: %s", ErrNameExists, v.prefixedName())
}
nameMap[v.prefixedName()] = true
if v.required && !exists {
return fmt.Errorf("%w: %s", ErrRequired, v.prefixedName())
}
if v.required && v.create {
return fmt.Errorf("%w: %s", ErrCreateAndRequired, v.prefixedName())
}
return nil
}
func parseInt(value string) (int, error) {
i, err := strconv.ParseInt(value, 10, 0)
if err != nil {
return 0, err
}
return int(i), nil
}
func parseBool(value string) (bool, error) {
return strconv.ParseBool(value)
}
func parseString(value string) (string, error) {
return value, nil
}
func parseFloat(value string) (float64, error) {
return strconv.ParseFloat(value, 64)
}