-
Notifications
You must be signed in to change notification settings - Fork 13
/
gonfig.go
335 lines (287 loc) · 9.63 KB
/
gonfig.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
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
// Copyright (c) 2017 Steven Roose <[email protected]>.
// Use of this source code is governed by an MIT-style
// license that can be found in the LICENSE file.
package gonfig
import (
"fmt"
"path/filepath"
"reflect"
)
// Conf is used to specify the intended behavior of gonfig.
type Conf struct {
// ConfigFileVariable is the config variable that will be read before looking
// for a config file. If no value is specified in the environment variables
// of the command line flags, the default config file will be read.
// This flag should be defined in the config file struct and referred to here
// by its ID. The default value for this variable is obviously ignored.
ConfigFileVariable string
// FileDisable disabled reading config variables from the config file.
FileDisable bool
// FileDefaultFilename is the default filename to look for for the config
// file. If this is empty and no filename is explicitly provided, parsing
// a config file is skipped.
FileDefaultFilename string
// FileDecoder specifies the decoder function to be used for decoding the
// config file. The following decoders are provided, but the user can also
// specify a custom decoder function:
// - DecoderYAML
// - DecoderTOML
// - DecoderJSON
// If no decoder function is provided, gonfig tries to guess the function
// based on the file extension and otherwise tries them all in the above
// mentioned order.
FileDecoder FileDecoderFn
// FlagDisable disabled reading config variables from the command line flags.
FlagDisable bool
// FlagIgnoreUnknown ignores unknown command line flags instead of stopping
// with an error message.
FlagIgnoreUnknown bool
// EnvDisables disables reading config variables from the environment
// variables.
EnvDisable bool
// EnvPrefix is the prefix to use for the the environment variables.
// gonfig does not add an underscore after the prefix.
EnvPrefix string
// HelpDisable disables printing the help message when the --help or -h flag
// is provided. If this is false, an explicit --help flag will be added.
HelpDisable bool
// HelpMessage is the message printed before the list of the flags when the
// user sets the --help flag.
// The default is "Usage of [executable name]:".
HelpMessage string
// HelpDescription is the description to print for the help flag.
// By default, this is "show this help menu".
HelpDescription string
}
// setup is the struct that keeps track of the state of the program throughout
// the lifecycle of loading the configuration.
type setup struct {
conf *Conf
opts []*option // Holds all top-level options in the config struct.
allOpts []*option // Holds all options and all sub-options recursively.
// Some cached variables to avoid having to generate them twice.
configFilePath string
customConfigFile bool // Whether the config file is user-provided.
}
// findCustomConfigFile finds out where to look for the config file.
// It looks in the environment variables and the command line flags.
// It returns an absolute path to the config file.
func findCustomConfigFile(s *setup) (string, error) {
if s.conf.ConfigFileVariable == "" {
return "", nil
}
// Check if the config struct defined a variable for the config file.
var configOpt *option
for _, opt := range s.opts {
if opt.id == s.conf.ConfigFileVariable {
configOpt = opt
break
}
}
if configOpt == nil {
panic(fmt.Errorf("config variable name provided (%v), "+
"but not defined in config struct", s.conf.ConfigFileVariable))
}
// Look if the user specified a config file. We go in opposite priority
// and return as soon as we find one.
path, err := lookupConfigFileFlag(s, configOpt)
if err != nil {
return "", err
}
if path != "" {
return filepath.Abs(path)
}
path, err = lookupConfigFileEnv(s, configOpt)
if err != nil {
return "", err
}
if path != "" {
return filepath.Abs(path)
}
return "", nil
}
// setDefaults writes the default values in the field values if a default value
// has been provided.
func setDefaults(s *setup) error {
for _, opt := range s.allOpts {
if !opt.defaultSet {
continue
}
if opt.isParent {
// Default values should not be set for nested options.
return fmt.Errorf("default value specified for nested value '%v'",
opt.fullID())
}
if !isZero(opt.value) {
// The value has already set before calling gonfig. In this case,
// we don't touch it aymore.
continue
}
opt.defaultValue = reflect.New(opt.value.Type()).Elem()
if isSlice(opt.value) {
if err := parseSlice(opt.defaultValue, opt.defaul); err != nil {
return fmt.Errorf(
"error parsing default value for %v: %v", opt.fullID(), err)
}
} else {
if err := parseSimpleValue(opt.defaultValue, opt.defaul); err != nil {
return fmt.Errorf(
"error parsing default value for %v: %v", opt.fullID(), err)
}
}
if err := setValue(opt.value, opt.defaultValue); err != nil {
return fmt.Errorf("error setting default value for option "+
"'%v' to '%v': %v", opt.id, opt.defaultValue, err)
}
}
return nil
}
// Load loads the configuration of your program in the struct at c.
// Use conf to specify how gonfig should look for configuration variables.
//
// This method can panic if there was a problem in the configuration struct that
// is used (which should not happen at runtime), but will always try to produce
// an error instead if the user provided incorrect values.
//
// The recognised tags on the exported struct variables are:
// - id: the keyword identifier (defaults to lowercase of variable name)
// - default: the default value of the variable
// - short: the shorthand used for command line flags (like -h)
// - desc: the description of the config var, used in --help
// - opts: comma-separated flags. Supported flags are:
// - hidden: Hides the option from help outputs.
func Load(c interface{}, conf Conf) error {
s := &setup{
conf: &conf,
}
if err := inspectConfigStructure(s, c); err != nil {
panic(fmt.Errorf("error in config structure: %v", err))
}
if err := setDefaults(s); err != nil {
panic(fmt.Errorf("error in default values: %v", err))
}
// Parse in order of opposite priority: file, env, flags
if !s.conf.FileDisable {
filename, err := findCustomConfigFile(s)
if err != nil {
return err
}
if filename != "" {
s.customConfigFile = true
} else {
s.customConfigFile = false
if s.conf.FileDefaultFilename != "" {
filename, err = filepath.Abs(s.conf.FileDefaultFilename)
if err != nil {
return fmt.Errorf("failed to convert default config file "+
"location to an absolute path: %v", err)
}
}
}
if filename != "" {
s.configFilePath = filename
if err := parseFile(s); err != nil {
return err
}
}
}
if !s.conf.EnvDisable {
if err := parseEnv(s); err != nil {
return err
}
}
if !s.conf.FlagDisable {
if err := parseFlags(s); err != nil {
return err
}
}
return nil
}
// LoadRawFile loads the configuration of your program in the struct at c from
// the given raw config file contents.
// In this method, conf is only used to pass the FileDecoder option.
// Use conf to specify how gonfig should look for configuration variables.
//
// Read documentation of Load for effects.
func LoadRawFile(c interface{}, fileContent []byte, conf Conf) error {
conf.EnvDisable = true
conf.FlagDisable = true
return LoadWithRawFile(c, fileContent, conf)
}
// LoadWithRawFile loads the configuration of your program in the struct at c
// by using the given contents for the config file.
// Use conf to specify how gonfig should look for configuration variables.
// As opposed to LoadRawFile, in this method, the other config sources are also
// loaded.
//
// Read documentation of Load for effects.
func LoadWithRawFile(c interface{}, fileContent []byte, conf Conf) error {
s := &setup{
conf: &conf,
}
if err := inspectConfigStructure(s, c); err != nil {
panic(fmt.Errorf("config: error in structure: %v", err))
}
if err := setDefaults(s); err != nil {
panic(fmt.Errorf("config: error in default values: %v", err))
}
if s.conf.FileDisable {
panic("config: can't use LoadWithRawFile with DisableFile set to true")
}
if err := parseFileContent(s, fileContent); err != nil {
return err
}
if !s.conf.EnvDisable {
if err := parseEnv(s); err != nil {
return err
}
}
if !s.conf.FlagDisable {
if err := parseFlags(s); err != nil {
return err
}
}
return nil
}
// LoadWithMap loads the configuration of your program in the struct at c
// by using the given map. All other config sources will be ignored.
// Use conf to specify how gonfig should look for configuration variables.
//
// Read documentation of Load for effects.
func LoadMap(c interface{}, vars map[string]interface{}, conf Conf) error {
conf.EnvDisable = true
conf.FlagDisable = true
return LoadWithMap(c, vars, conf)
}
// LoadWithMap loads the configuration of your program in the struct at c
// by using the given map.
// Use conf to specify how gonfig should look for configuration variables.
// As opposed to LoadMap, in this method, the other config sources are also
// loaded.
//
// Read documentation of Load for effects.
func LoadWithMap(c interface{}, vars map[string]interface{}, conf Conf) error {
s := &setup{
conf: &conf,
}
if err := inspectConfigStructure(s, c); err != nil {
panic(fmt.Errorf("config: error in structure: %v", err))
}
if err := setDefaults(s); err != nil {
panic(fmt.Errorf("config: error in default values: %v", err))
}
if err := parseMapOpts(vars, s.allOpts); err != nil {
return err
}
if !s.conf.EnvDisable {
if err := parseEnv(s); err != nil {
return err
}
}
if !s.conf.FlagDisable {
if err := parseFlags(s); err != nil {
return err
}
}
return nil
}