-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathloader.go
More file actions
509 lines (457 loc) · 19.7 KB
/
loader.go
File metadata and controls
509 lines (457 loc) · 19.7 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
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
package gonfig
import (
"fmt"
"io"
"os"
)
// Error is a custom error type based on a string.
// It represents an error that is constant and does not change at runtime.
type Error string
// Config holds the configuration options for loading settings using various parsers such as defaults,
// environment variables, and command-line flags.
//
// Fields:
// - SkipDefaults: If true, the loader will skip loading configurations from the 'default' tags in struct fields.
// - SkipEnv: If true, the loader will skip loading configurations from environment variables.
// - SkipFlags: If true, the loader will skip loading configurations from command-line flags.
//
// - EnvPrefix: A string that specifies a prefix for filtering environment variables. Only variables
// starting with this prefix will be considered.
//
// - LoaderOrder: Defines the default order in which the parsers are executed:
// 1. Defaults (from struct tags)
// 2. Config source (pre-scanning flags for a configuration path)
// 3. Custom loaders (e.g., file loaders like JSON, YAML, TOML)
// 4. Environment variables (overrides file values)
// 5. Flags (the highest priority, overrides all previous)
//
// - Envs: A slice of environment variables to be used for parsing. If left nil, the loader will
// default to using `os.Environ()`.
//
// - Args: A slice of command-line arguments to be used for parsing. If left nil, the loader will
// default to using `os.Args`. The user can explicitly parse this if required.
//
// `loader` struct:
// - Embeds `Config` to inherit its configuration options.
// - `groups`: A map that holds the different `Parser` implementations, indexed by `ParserType`.
//
// Types:
// - `LoaderOption`: A function type used to apply custom options to the `loader`.
// - `ParserType`: Represents the type of parser (e.g., "defaults", "env", "flags").
//
// Constants:
// - `ParserDefaults`: Represents the default parser that loads configurations based on struct tags.
// - `ParserFlags`: Represents the parser that loads configurations from command-line flags.
// - `ParserEnv`: Represents the parser that loads configurations from environment variables.
//
// Example Usage:
// Custom parsers can be injected using `LoaderOption` functions, such as `WithCustomParser`.
type Config struct {
SkipDefaults bool // SkipDefaults set to true will not load config from the 'default' tag.
SkipEnv bool // SkipEnv set to true will not load config from environment variables.
SkipFlags bool // SkipFlags set to true will not load config from flag parameters.
EnvPrefix string // EnvPrefix for environment variables.
// Envs hold the environment variable from which envs will be parsed.
// By default, it is nil and then os.Environ() will be used.
Envs []string
// Args hold the command-line arguments from which flags will be parsed.
// By default, it is nil and then os.Args will be used.
// Unless loader.Flags() will be explicitly parsed by the user.
Args []string
}
// Loader is responsible for managing the configuration loading process by coordinating different parsers.
// It embeds the `Config` struct and contains a map of `Parser` implementations.
//
// Fields:
//
// - Config: The embedded configuration settings that control how the loader behaves. This includes
// flags for skipping defaults, environment variables, or command-line flags, as well as any custom
// environment variables and arguments provided.
//
// - Groups: A map where the keys are `ParserType` values (such as "defaults", "env", and "flags"),
// and the values are `Parser` instances. This map allows the loader to invoke the correct parser
// based on the order specified in `LoaderOrder` from the `Config`.
//
// The `loader` is initialized with a set of defaults, and custom options can be added through
// `LoaderOption` functions. Each parser in the `groups` map is responsible for loading part of the
// configuration from its respective source (e.g., defaults, environment variables, or flags).
//
// Example:
// A `loader` might have parsers for environment variables and flags configured, and it would
// execute them in the order defined by `LoaderOrder`, applying each configuration in sequence.
type loader struct {
Config
output any
config string
buffer io.Writer
orders []ParserType
groups map[ParserType]Parser
exit func(int) // used for tests, to ignore os.Exit
}
// LoaderOption defines a function type used to customize the behavior of the loader.
// Each `LoaderOption` takes a pointer to a `loader` and returns an error if the customization fails.
//
// The purpose of `LoaderOption` is to allow for flexible configuration of the loader instance.
// Options can be applied to modify the loader’s behavior, such as adding custom parsers,
// modifying existing parsers, or changing configuration settings.
//
// Usage:
// A `LoaderOption` function is passed to the `New` function or similar, allowing for dynamic
// configuration. Multiple options can be combined, and each one will be applied to the loader
// sequentially.
//
// Example:
//
// func WithCustomSetting(setting string) LoaderOption {
// return func(l *loader) error {
// // modify loader based on custom setting
// l.Config.SomeSetting = setting
// return nil
// }
// }
type LoaderOption func(*loader) error
// ParserType represents the different types of parsers that can be used in the loader system.
// It is defined as a string to allow for flexible and extensible parser type definitions.
//
// Each value of `ParserType` corresponds to a specific source or method of configuration parsing.
// The parser types determine the priority and the sequence in which the parsers are applied.
//
// Constants:
//
// - ParserDefaults: Represents the default parser type that handles configuration values
// set by default values in the code or configuration. This parser is typically used to
// provide fallback values when other sources do not supply a value.
//
// - ParserFlags: Represents the parser type that handles command-line flags. This parser
// processes the command-line arguments passed to the program to configure various options.
//
// - ParserEnv: Represents the parser type that handles environment variables. This parser
// reads configuration values from environment variables, which can be used to configure
// the application in different deployment environments.
//
// To specify the order in which parsers should be applied, the default sequence is:
// Defaults -> Config-path -> File -> Env -> Flags.
//
// config := Config{
// LoaderOrder: []ParserType{ParserDefaults, ParserEnv, ParserFlags},
// }
type ParserType string
const (
// ParserDefaults Represents the default parser type that handles configuration values
// set by default values in the code or configuration. This parser is typically used to
// provide fallback values when other sources do not supply a value.
ParserDefaults ParserType = "defaults"
// ParserFlags Represents the parser type that handles command-line flags. This parser
// processes the command-line arguments passed to the program to configure various options.
ParserFlags ParserType = "flags"
// ParserEnv Represents the parser type that handles environment variables. This parser
// reads configuration values from environment variables, which can be used to configure
// the application in different deployment environments.
ParserEnv ParserType = "env"
// ParserConfigSet Represents the parser type that handles command-line flags. This parser
// processes the command-line arguments passed to the program to set the config path.
ParserConfigSet ParserType = "config-setter"
)
// Error implements the error interface for the Error type.
// It returns the error message as a string, which is the underlying value of the Error.
func (e Error) Error() string { return string(e) }
// WithCustomParser creates a LoaderOption that adds a custom parser to the loader's group of parsers.
// This function allows you to inject a parser into the loader, which will be used to handle a specific
// type of configuration source. The custom parser will be added to the loader's parser group, enabling
// it to be invoked during the configuration loading process.
//
// Parameters:
// - p: The custom parser to be added to the loader. The parser must implement the `Parser` interface.
// If the provided parser is `nil`, no action is taken and the function returns `nil`.
//
// Returns:
// - A `LoaderOption` function that, when applied to a `loader`, will add the custom parser to the loader's
// group of parsers.
//
// Example usage:
// If you have a custom parser that implements the `Parser` interface ands you want to include it in the
// loader's configuration process, you can use this function to add it:
//
// myParser := NewMyCustomParser() // Assume this returns a valid Parser
// option := WithCustomParser(myParser)
// loader := NewLoader(config, option)
//
// This will ensure that `myParser` is used by the loader to process configuration data.
func WithCustomParser(p Parser) LoaderOption {
return func(l *loader) error {
if p == nil {
return nil
}
l.groups[p.Type()] = p
l.orders = append(l.orders, p.Type())
return nil
}
}
// WithCustomParserInit allows the injection of a custom parser into the loader by using a provided
// `ParserInit` function. This function is useful for adding custom logic or additional parsers beyond the
// predefined ones.
//
// The function performs the following tasks:
// - Accepts a `ParserInit` function (`fabric`) that takes a `Config` and returns a `Parser` and an error.
// - Executes the `fabric` function with the loader's current `Config` to initialize the custom parser.
// - If the `fabric` function returns an error, it propagates that error immediately.
// - Otherwise, it adds the parser to the loader's group under the parser's type.
//
// Parameters:
// - fabric: A `ParserInit` function that returns a custom `Parser` and an error based on the `Config`.
//
// Returns:
// - A `LoaderOption` that applies the custom parser to the loader's parser group or returns an error if
// parser initialization fails.
func WithCustomParserInit(fabric ParserInit) LoaderOption {
return func(l *loader) error {
switch parser, err := fabric(l.Config); {
case err != nil:
return err
case parser == nil:
return nil
default:
l.groups[parser.Type()] = parser
l.orders = append(l.orders, parser.Type())
return nil
}
}
}
// WithOptions allows dynamic application of loader options by accepting either a slice of LoaderOption
// or a function that returns a slice of LoaderOption. It ensures flexibility in configuring the loader.
//
// The function performs the following tasks:
// - Accepts `options` as an argument of a type `any`, which can be either a `[]LoaderOption` or
// a `func() []LoaderOption`.
// - Uses a type switch to determine the type of `options` and converts it into a `[]LoaderOption`.
// - Applies each LoaderOption to the provided loader (`l`) by iterating over the result.
// - If an invalid type is passed to `options`, it returns an error with the message indicating the
// unexpected type.
// - If applying any option fails, it returns an error that includes the original error.
//
// Parameters:
// - options: Can either be a slice of `LoaderOption` or a function that returns a slice of `LoaderOption`.
//
// Returns:
// - A `LoaderOption` that applies the resolved list of options to a given loader, or an error if the
// `options` type is invalid or if any option fails during application.
func WithOptions(options any) LoaderOption {
return func(l *loader) error {
var result []LoaderOption
switch opts := options.(type) {
case []LoaderOption:
result = opts
case func() []LoaderOption:
result = opts()
default:
return fmt.Errorf("invalid options type: %T", opts)
}
for _, opt := range result {
if err := opt(l); err != nil {
return fmt.Errorf("could not init options: %w", err)
}
}
return nil
}
}
// WithCustomOutput sets a custom io.Writer as the loader's output destination.
//
// By default, the loader writes its output to os.Stdout. This LoaderOption allows
// overriding the output destination by providing a custom writer (e.g., a buffer,
// a file, or a mock implementation). Useful for testing or redirecting output in
// specific environments.
//
// Parameters:
// - writer: An implementation of io.Writer (e.g., os.Stdout, bytes.Buffer).
// If nil, the default (os.Stdout) remains unchanged.
//
// Returns:
// - A LoaderOption that applies the custom writer to the loader's output buffer.
func WithCustomOutput(writer io.Writer) LoaderOption {
return func(l *loader) error {
if writer != nil {
l.buffer = writer
}
return nil
}
}
// WithCustomExit provides an option to override the default exit behavior of the loader.
// This is useful for testing, where calling `os.Exit` would terminate the test process.
//
// By default, `loader.exit` is set to `os.Exit`, but this function allows replacing it
// with a custom exit function (e.g., a no-op or mock function).
//
// Parameters:
// - exit: A custom function that takes an exit code as an argument. If nil, the default behavior remains unchanged.
//
// Returns:
// - A LoaderOption function that applies the custom exit behavior to the loader.
func WithCustomExit(exit func(int)) LoaderOption {
return func(l *loader) error {
if exit == nil {
return nil
}
l.exit = exit
return nil
}
}
// WithConfig allows modifying the Config object using a custom handler function.
// This LoaderOption provides a way to customize configuration settings dynamically
// before the loading process begins.
//
// The provided handler function receives a pointer to the Config object, allowing
// modifications to be applied as needed.
//
// Parameters:
// - handler: A function that takes a *Config and applies custom modifications.
//
// Returns:
// - A LoaderOption that applies the given handler to modify the loader's Config.
func WithConfig(handler func(*Config)) LoaderOption {
return func(l *loader) error {
if handler != nil {
handler(&l.Config)
}
return nil
}
}
// WithDefaults sets default values for the loader's output structure.
//
// This function takes a tag-key and map of default values and applies them to the target structure
// using `decodeMapToStruct`. It ensures that any unset fields in the structure receive
// the specified default values before other parsing mechanisms (such as environment
// variables or configuration files) are applied.
//
// This function is useful when:
// - You want to provide fallback values for missing configurations.
// - You need to ensure a structure is always initialized with meaningful defaults.
//
// For example, you can set `path.to.key=value`, to unmarshal it for struct{Path struct {To struct{Key string}}}
//
// Parameters:
// - keyTag: allows using struct-tag to find field names.
// - defaults: A map where keys are field names (or tagged keys) and values are default values.
//
// Returns:
// - A `LoaderOption` function that applies the default values to the loader's output.
func WithDefaults(keyTag string, defaults map[string]any) LoaderOption {
return func(l *loader) error {
return decodeMapToStruct(l.output, defaults, keyTag)
}
}
// setLoaderDefaults initializes a loader with default values based on the provided configuration.
// It sets up the environment variables, command-line arguments, and the order in which parsers
// will be applied, ensuring defaults are in place if not explicitly provided in the Config.
//
// The function performs the following tasks:
// - If no environment variables are provided in the Config, it defaults to using `os.Environ()`.
// - If no arguments are provided in the Config, it defaults to `os.Args[1:]`.
// - If no loader order is defined, it sets a default order:
// Defaults -> Config Path -> File -> Env -> Flags.
// - Initializes a map of parsers (`parsers`), based on the Config options such as SkipDefaults,
// SkipEnv, and SkipFlags, to include or exclude certain parsers.
//
// Parameters:
// - c: The Config object that contains user-provided settings for environment variables, arguments,
// and parser control options.
//
// Returns:
// - A pointer to a `loader` struct, which contains the updated Config and the map of available parsers.
func setLoaderDefaults(c Config) *loader {
l := &loader{Config: c, exit: os.Exit, buffer: os.Stdout, groups: make(map[ParserType]Parser, 4)}
if l.Envs == nil {
l.Envs = os.Environ()
}
if l.Args == nil {
l.Args = os.Args[1:]
}
if !l.SkipDefaults {
l.groups[ParserDefaults] = newDefaultParser()
}
if !l.SkipEnv {
l.groups[ParserEnv] = newEnvLoader(l)
}
if !l.SkipFlags {
l.groups[ParserFlags] = newFlagsLoader(l)
l.groups[ParserConfigSet] = parseConfigPath(l)
}
return l
}
// New creates a new Parser based on the provided configuration and optional LoaderOptions.
// The function initializes a loader service (`svc`) with default settings from the provided
// configuration. Then it applies each LoaderOption to customize the service if any are provided.
//
// The function returns a `parserFunc` that, when called, will:
// - Apply all the LoaderOptions to the `svc`.
// - Iterate through the `LoaderOrder` and invoke the corresponding group parsers.
// If any parser fails or if a group parser is missing, the function returns an error.
//
// Parameters:
// - config: The Config object used to initialize the default settings for the loader.
// - options: A variadic number of LoaderOption functions to customize the loader.
//
// Returns:
// - A Parser that can be used to load and parse values into the provided target structure.
//
// nolint:ireturn
func New(config Config, options ...LoaderOption) Parser {
l := setLoaderDefaults(config)
// return a group parser with the following loading priority:
// 1. Defaults
// 2. Config path (pre-scan)
// 3. Custom orders (e.g., file loaders)
// 4. Envs (overrides file)
// 5. Flags (the highest priority)
return &parserFunc{call: wrapUsageLoader(l, func(v interface{}) error {
l.output = v
for _, option := range options {
if err := option(l); err != nil {
return fmt.Errorf("gonfig: could not init option: %w", err)
}
}
order := make([]ParserType, 0, len(l.groups)+4)
if !config.SkipDefaults { // 1. set defaults
order = append(order, ParserDefaults)
}
if !config.SkipFlags { // 2. set config path flag (pre-scan)
order = append(order, ParserConfigSet)
}
// 3. set custom loaders (e.g., file loaders like JSON/YAML/TOML)
order = append(order, l.orders...)
if !config.SkipEnv { // 4. set envs (overrides file)
order = append(order, ParserEnv)
}
if !config.SkipFlags { // 5. set final flags (the highest priority)
order = append(order, ParserFlags)
}
for _, typ := range order {
if setter, ok := l.groups[typ].(ParserConfigSetter); ok {
setter.SetConfigPath(l.config)
}
if err := l.groups[typ].Load(v); err != nil {
return fmt.Errorf("gonfig: could not load: %w", err)
}
}
if err := ValidateRequiredFields(v); err != nil {
return err
}
if validator, ok := v.(LoaderValidator); ok {
return validator.Validate()
}
return nil
})}
}
// Load initializes a new Parser with default settings and applies optional LoaderOptions.
// It then loads the provided target structure using the configured loader service.
//
// This function is shorthand for creating a new Parser with default Config and calling Load on it.
//
// Parameters:
// - v: The target structure where the configuration will be loaded.
// - options: Optional LoaderOptions to customize the behavior of the parser.
//
// Returns:
// - An error if the loading process fails, otherwise nil.
func Load(v any, options ...LoaderOption) error {
return New(Config{}, options...).Load(v)
}