-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathconfig.go
More file actions
196 lines (177 loc) · 6.53 KB
/
config.go
File metadata and controls
196 lines (177 loc) · 6.53 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
// Package config provides functionality for loading application configuration from environment variables.
// It supports loading environment variables from .env files and processing them into Go structs.
//
// This package uses the functional options pattern to configure the loading process.
// It leverages godotenv for loading environment variables from files and envconfig for
// processing environment variables into Go structs.
//
// Example usage:
//
// type AppConfig struct {
// DatabaseURL string `envconfig:"DATABASE_URL" required:"true"`
// Port int `envconfig:"PORT" default:"8080"`
// }
//
// func main() {
// var cfg AppConfig
// err := config.Load(
// config.WithPrefix("APP"),
// config.WithEnvFiles(".env", ".env.local"),
// config.WithConfigs(&cfg),
// )
// if err != nil {
// log.Fatalf("Failed to load config: %v", err)
// }
// // Use cfg...
// }
package config
import (
"errors"
"fmt"
"regexp"
"github.com/joho/godotenv"
"github.com/kelseyhightower/envconfig"
)
var (
// missingValueRx is a regular expression used to extract the name of a missing environment variable
// from error messages produced by the envconfig package. It captures the variable name in a named
// capture group called "var".
missingValueRx = regexp.MustCompile(`required key (?P<var>[A-Z0-9_]+) missing value`)
// ErrMissingEnvVar is returned by the Load function when a required environment variable is missing.
// This error is wrapped with the name of the missing variable to provide more context.
// Applications can use errors.Is to check for this specific error type.
ErrMissingEnvVar = errors.New("config: a required environment variable is missing")
)
// option is an internal struct that holds the configuration options for the Load function.
// It is populated by the functional options passed to Load.
type option struct {
// prefix is the environment variable prefix used by envconfig
prefix string
// envFiles is a list of .env file paths to load environment variables from
envFiles []string
// configs is a list of struct pointers to populate with configuration values
configs []any
}
// Option is a function type that modifies the internal option struct.
// It implements the functional options pattern for configuring the Load function.
type Option func(*option)
// WithPrefix returns an Option that sets the environment variable prefix for envconfig.
// The prefix is used to filter environment variables and can be used to namespace
// configuration for different parts of an application.
//
// For example, with a prefix of "APP", envconfig will look for environment variables
// like "APP_DATABASE_URL" instead of just "DATABASE_URL".
//
// If not specified, no prefix is used.
func WithPrefix(prefix string) Option {
return func(o *option) {
o.prefix = prefix
}
}
// WithConfigs returns an Option that adds one or more configuration structs to be populated.
// Each config parameter should be a pointer to a struct that envconfig will populate
// with values from environment variables.
//
// The struct fields should be tagged with `envconfig:"ENV_VAR_NAME"` to specify
// which environment variable to use for each field. Additional tags like `required:"true"`
// and `default:"value"` can be used to configure the behavior.
//
// See the envconfig package documentation for more details on available tags and behavior.
func WithConfigs(configs ...any) Option {
return func(o *option) {
o.configs = configs
}
}
// WithEnvFiles returns an Option that adds one or more .env files to load environment
// variables from. The files are loaded in the order they are specified, with later files
// taking precedence over earlier ones.
//
// This is useful for loading default configuration from a checked-in .env file
// and then overriding it with a local .env file that is not checked into version control.
//
// If a specified file doesn't exist, it is silently ignored.
func WithEnvFiles(paths ...string) Option {
return func(o *option) {
o.envFiles = append(o.envFiles, paths...)
}
}
// Load loads configuration from environment variables into the provided configuration structs.
// It applies the provided options to customize the loading process.
//
// The function performs the following steps:
// 1. Loads environment variables from any .env files specified with WithEnvFiles
// 2. Processes each configuration struct specified with WithConfigs using the envconfig package
//
// If a required environment variable is missing, Load returns an ErrMissingEnvVar error
// wrapped with the name of the missing variable.
//
// Example:
//
// type Config struct {
// DatabaseURL string `envconfig:"DATABASE_URL" required:"true"`
// Port int `envconfig:"PORT" default:"8080"`
// }
//
// var cfg Config
// err := config.Load(
// config.WithPrefix("APP"),
// config.WithEnvFiles(".env"),
// config.WithConfigs(&cfg),
// )
//
// Parameters:
// - opts: Zero or more Option functions to configure the loading process
//
// Returns:
// - error: nil if successful, or an error if configuration loading failed
func Load(opts ...Option) error {
o := option{}
for i := range opts {
opts[i](&o)
}
_ = godotenv.Overload(o.envFiles...)
for i := range o.configs {
if err := envconfig.Process(o.prefix, o.configs[i]); err != nil {
if m := rxNamedExtract(missingValueRx, err.Error()); m != nil {
return fmt.Errorf("%s | %w", m["var"], ErrMissingEnvVar)
}
return fmt.Errorf("config: error processing via envconfig | %w", err)
}
}
return nil
}
// rxNamedExtract extracts named capture groups from a regular expression match.
// It returns a map where the keys are the capture group names and the values are the matched strings.
// If the regular expression doesn't match the string, or if there are no named capture groups,
// the function returns nil.
//
// Parameters:
// - rx: The compiled regular expression with named capture groups
// - s: The string to match against the regular expression
//
// Returns:
// - map[string]string: A map of capture group names to matched strings, or nil if no match
func rxNamedExtract(rx *regexp.Regexp, s string) map[string]string {
// the item at index zero is always the full match without captures
names := rx.SubexpNames()
if len(names) < 1 {
return nil
}
names = names[1:]
sm := rx.FindStringSubmatch(s)
if len(sm) < 1 {
return nil
}
sm = sm[1:]
// assert that length of names == length of sm
// see stdlib regexp implementation for details
out := make(map[string]string)
for i := range names {
if names[i] == "" {
// unnamed capture group
continue
}
out[names[i]] = sm[i]
}
return out
}