-
Notifications
You must be signed in to change notification settings - Fork 0
/
lookup.go
155 lines (125 loc) · 3.78 KB
/
lookup.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 env
import (
"errors"
"fmt"
"os"
"reflect"
"regexp"
"strconv"
"strings"
)
const (
envTag = "env"
envSep = ","
validatorArgsPattern = `(\w+)(?:=([-\w\s]+))?` // (e.g., min=0, expectedValues=development production)
requiredFlag = "required"
expectedValuesFlag = "expectedValues"
)
var (
// Regex to capture dynamic validation arguments
validatorArgsRegex = regexp.MustCompile(validatorArgsPattern)
)
// ValidatorRegistry holds registered validators for easy lookup.
var validatorRegistry = map[string]ValidatorFactory{
requiredFlag: newRequiredValidator,
expectedValuesFlag: newExpectedValueValidator,
}
// AddValidator allows users to add custom validators to the registry
func AddValidator(name string, fn ValidatorFactory) {
validatorRegistry[name] = fn
}
// Lookup loads environment variables into the provided struct.
func Lookup(m any) error {
return lookupValue(reflect.ValueOf(m).Elem())
}
// lookupValue recursively processes struct fields for environment variables.
func lookupValue(v reflect.Value) error {
var errs error
for i := 0; i < v.Type().NumField(); i++ {
field := v.Type().Field(i)
valueField := v.Field(i)
// Skip unexported fields or unsettable fields
if !v.Field(i).CanSet() {
continue
}
// Handle struct fields recursively
if v.Field(i).Kind() == reflect.Struct {
if err := lookupValue(valueField); err != nil {
errs = errors.Join(errs, err)
}
continue
}
// Read the env tag configuration
envName, validators := parseEnvTag(field.Tag.Get(envTag), envSep)
if envName == "" {
continue // No env tag, skip
}
// Read env from name
envValue := os.Getenv(envName)
// Validate required flags
for _, validator := range validators {
if err := validator.Validate(envName, envValue); err != nil {
errs = errors.Join(errs, err)
}
}
// Parse and set the environment variable value
if err := setFieldValue(valueField, envName, envValue); err != nil {
errs = errors.Join(errs, err)
}
}
return errs
}
// setFieldValue sets the value of a struct field based on the environment variable.
func setFieldValue(valueField reflect.Value, envName string, envValue string) error {
// Skip if envValue empty
if envValue == "" {
return nil
}
switch valueField.Kind() {
case reflect.String:
valueField.SetString(strings.TrimSpace(envValue))
case reflect.Int:
n, err := strconv.ParseInt(envValue, 10, 64)
if err != nil {
return fmt.Errorf("error parsing int for %s: %w", envName, err)
}
valueField.SetInt(n)
case reflect.Bool:
b, err := strconv.ParseBool(envValue)
if err != nil {
return fmt.Errorf("error parsing bool for %s: %w", envName, err)
}
valueField.SetBool(b)
default:
return fmt.Errorf("unsupported type %s for %s", valueField.Kind(), envName)
}
return nil
}
// parseEnvTag parses the tag and returns the environment variable name and a list of validators.
func parseEnvTag(tag string, sep string) (string, []Validator) {
if tag == "-" || tag == "" {
return "", nil
}
sepIndex := strings.Index(tag, sep)
if sepIndex == -1 {
return tag, nil
}
envName := tag[:sepIndex]
options := tag[sepIndex+len(sep):]
validators := make([]Validator, 0, strings.Count(options, sep)+1)
// Iterate over each validation option (after the env name)
for _, option := range strings.Split(options, sep) {
matches := validatorArgsRegex.FindStringSubmatch(option)
if len(matches) > 0 {
validatorName := matches[1]
validatorArgs := matches[2]
// Lookup the validator in the registry and create it
if createValidator, ok := validatorRegistry[validatorName]; ok {
validators = append(validators, createValidator(validatorArgs))
}
continue
}
validators = append(validators, validatorRegistry[option](""))
}
return envName, validators
}