-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathsnakecharmer.go
400 lines (349 loc) · 13.1 KB
/
snakecharmer.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
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
// Copyright 2013-2023 The SnakeCharmer Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package snakecharmer
import (
"errors"
"fmt"
"os"
"path/filepath"
"reflect"
"strings"
"github.com/mitchellh/mapstructure"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
// NewSnakeCharmer creates a new snakecharmer instance.
// charmer, err = NewSnakeCharmer(
//
// WithResultStruct(result),
// WithFieldTagName("snakecharmer"),
// WithViper(vpr),
// WithCobraCommand(cmd),
// WithConfigFilePath(defaultConfigFile),
// WithConfigFileType("yaml"),
// WithIgnoreUntaggedFields(true),
// WithDecoderConfigOption(
// func(dc *mapstructure.DecoderConfig) { dc.WeaklyTypedInput = true },
// ),
//
// )
func NewSnakeCharmer(opts ...CharmingOption) (*SnakeCharmer, error) {
sch := SnakeCharmer{
fieldTagName: "mapstructure",
envTagName: "env",
flagHelpTagName: "usage",
configFileType: "yaml",
configFilePath: "",
configFileBaseName: "config",
}
for _, opt := range opts {
if err := opt(&sch); err != nil {
return &sch, err
}
}
if sch.resultStruct == nil {
return &sch, fmt.Errorf("result struct <interface{}> is not set")
}
if sch.viper == nil {
sch.viper = viper.New()
}
if sch.cmd == nil {
return &sch, fmt.Errorf("cmd <*cobra.Command> is not set")
}
return &sch, nil
}
// SnakeCharmer helps to get Cobra and Viper work together.
// It uses a user defined Struct for reading field tags and default values.
// Please note, that the struct must be initialized (with the default values)
// before passing to the SnakeCharmer.
// It automatically creates flags and adds them to cobra PersistentFlags flagset.
// It also creates viper's config params, and sets their default values,
// binds viper's config param with a corresponding flag from the cobra flagset,
// binds viper's config param with a corresponding ENV var
// SnakeCharmer sets the following priority of values:
// 1. flags (if passed)
// 2. ENV variables (if env tag set)
// 3. config file (if used)
// 4. defaults (from user defined Struct)
type SnakeCharmer struct {
// resultStruct is a pointer to the struct that will contain
// the decoded values.
resultStruct interface{}
// The tag name that snakecharmer reads for field names.
// This defaults to "mapstructure"
fieldTagName string
// The tag name that snakecharmer reads for setting ENV var name.
// This defaults to "env"
envTagName string
// The tag name that snakecharmer reads for flag usage help.
// This defaults to "usage"
flagHelpTagName string
// The type that will be passed to viper.SetConfigType().
// REQUIRED in case if the config file does not have the extension or
// if the config file extension is not in the list of supported extensions.
// See viper.SupportedExts for full list of supported extensions.
// This defaults to "yaml"
configFileType string
// The config file path that will be passed to
// viper.AddConfigPath() if path is a directory,
// viper.SetConfigFile() if path is a file.
// This defaults to "", which means config file won't be used.
configFilePath string
// The base name of the config file (without extension)
// that will be passed to viper.SetConfigName().
// REQUIRED in case of the configFilePath is a directory, otherwise ignored.
// This defaults to "config"
configFileBaseName string
// The pointer to the viper.Viper instance
viper *viper.Viper
// The pointer to the cobra.Command instance
cmd *cobra.Command
// A slice of viper.DecoderConfigOption that will be passed to viper.Unmarshal
// to configure mapstructure.DecoderConfig options
// See https://pkg.go.dev/github.com/spf13/[email protected]#DecoderConfigOption
decoderConfigOptions []viper.DecoderConfigOption
// ignoreUntaggedFields ignores all struct fields without explicit
// fieldTagName, comparable to `mapstructure:"-"` as default behaviour.
ignoreUntaggedFields bool
}
// Set sets the snakecharmer options
func (sch *SnakeCharmer) Set(opts ...CharmingOption) error {
for _, opt := range opts {
if err := opt(sch); err != nil {
return err
}
}
return nil
}
// ResultStruct returns the pointer to the struct that contains the decoded values.
func (sch *SnakeCharmer) ResultStruct() interface{} { return sch.resultStruct }
// FieldTagName returns the tag name that snakecharmer reads for field names.
func (sch *SnakeCharmer) FieldTagName() string { return sch.fieldTagName }
// EnvTagName returns the tag name that snakecharmer reads for ENV var names.
func (sch *SnakeCharmer) EnvTagName() string { return sch.envTagName }
// FlagHelpTagName returns the tag name that snakecharmer reads for flag usage help.
func (sch *SnakeCharmer) FlagHelpTagName() string { return sch.flagHelpTagName }
// ConfigFileType returns the type that will be passed to viper.SetConfigType().
func (sch *SnakeCharmer) ConfigFileType() string { return sch.configFileType }
// ConfigFilePath returns the config file path that will be passed to
// viper.AddConfigPath() if path is a directory,
// viper.SetConfigFile() if path is a file.
func (sch *SnakeCharmer) ConfigFilePath() string { return sch.configFilePath }
// ConfigFileBaseName returns the base name of the config file (without extension)
// that will be passed to viper.SetConfigName().
func (sch *SnakeCharmer) ConfigFileBaseName() string { return sch.configFileBaseName }
// DecoderConfigOptions returns the slice of viper.DecoderConfigOption
// that will be passed to viper.Unmarshal()
func (sch *SnakeCharmer) DecoderConfigOptions() []viper.DecoderConfigOption {
return sch.decoderConfigOptions
}
// IgnoreUntaggedFields returns the SnakeCharmer.ignoreUntaggedFields value
// that will be passed as viper.DecoderConfigOption
func (sch *SnakeCharmer) IgnoreUntaggedFields() bool { return sch.ignoreUntaggedFields }
// AddFlags creates flags from tags of a given Result Struct.
// Adds flags to cobra PersistentFlags flagset,
// creates viper's config param and sets default value (viper.SetDefault()),
// binds viper's config param with a corresponding flag from the cobra flagset,
// binds viper's config param with a corresponding ENV var
func (sch *SnakeCharmer) AddFlags() { sch.addFlags(sch.resultStruct, "") }
func (sch *SnakeCharmer) addFlags(input interface{}, prefix string) {
var key, env, help string
var err error
v := reflect.ValueOf(input)
if v.Kind() == reflect.Ptr || v.Kind() == reflect.Interface {
if v.IsNil() {
panic("BUG: got nil input")
}
v = v.Elem()
}
if v.Kind() != reflect.Struct {
panic(fmt.Sprintf("BUG: invalid input type: %q", v.Kind().String()))
}
for i := 0; i < v.NumField(); i++ {
structField := v.Type().Field(i)
fieldValue := v.Field(i)
if fieldValue.Kind() == reflect.Ptr || fieldValue.Kind() == reflect.Interface {
if fieldValue.IsNil() {
panic(fmt.Sprintf("BUG: got nil for field: %s", structField.Name))
}
fieldValue = fieldValue.Elem()
}
fieldTag := structField.Tag.Get(sch.fieldTagName)
// TODO: handle if field doesn't have fieldTag tag
if len(fieldTag) == 0 {
if sch.ignoreUntaggedFields {
continue
} else {
panic(fmt.Sprintf("BUG: got untagged field: %s", structField.Name))
}
}
key = strings.Split(fieldTag, ",")[0]
if len(prefix) > 0 {
key = prefix + "." + key
}
if fieldValue.Kind() == reflect.Struct {
// Run addFlags recursively with prefix
sch.addFlags(fieldValue.Interface(), key)
continue
}
help = structField.Tag.Get(sch.flagHelpTagName)
if len(help) == 0 {
panic(fmt.Sprintf("BUG: %s tag is not specified for field: %q", sch.flagHelpTagName, structField.Name))
}
// Add Flag to cobra flagset and Set default viper config param
if err = sch.applySetting(fieldValue, key, help); err != nil {
panic(err.Error())
}
// Bind flag to viper.
// This overrides viper default setting
// with values from cobra flags.
err = sch.viper.BindPFlag(key, sch.cmd.PersistentFlags().Lookup(key))
if err != nil {
panic(err.Error())
}
env = structField.Tag.Get(sch.envTagName)
if len(env) > 0 {
// Bind env var to viper.
// This overrides viper default setting
// with values from ENV vars.
// Note: viper treats ENV variables as case sensitive.
err = sch.viper.BindEnv(key, env)
if err != nil {
panic(err.Error())
}
}
}
}
// UnmarshalExact unmarshals the config into a Struct,
// erroring if a field is nonexistent in the destination struct.
func (sch *SnakeCharmer) UnmarshalExact() (err error) {
if len(sch.configFilePath) > 0 {
if err = sch.mergeInConfigFile(); err != nil {
return err
}
}
if sch.fieldTagName != "mapstructure" {
sch.decoderConfigOptions = append(sch.decoderConfigOptions,
func(dc *mapstructure.DecoderConfig) { dc.TagName = sch.fieldTagName },
)
}
err = sch.viper.UnmarshalExact(sch.resultStruct, sch.decoderConfigOptions...)
if err != nil {
return fmt.Errorf("while unmarshalling config, flags, and env vars: %s", err.Error())
}
return nil
}
func (sch *SnakeCharmer) mergeInConfigFile() (err error) {
if len(sch.configFilePath) == 0 {
return fmt.Errorf("config file path is an empty string")
}
found, err := sch.findConfigFile()
if err != nil {
return fmt.Errorf("while finding config %q: %s", sch.configFilePath, err.Error())
}
if !found {
return nil
}
if err = sch.viper.ReadInConfig(); err != nil {
return fmt.Errorf("while reading config %q: %s", sch.configFilePath, err.Error())
}
return nil
}
func (sch *SnakeCharmer) findConfigFile() (bool, error) {
if len(sch.configFilePath) == 0 {
return false, fmt.Errorf("config file path is an empty string")
}
fileInfo, err := os.Stat(sch.configFilePath)
if err == nil {
// path exists
if fileInfo.IsDir() {
// path is a directory
sch.viper.AddConfigPath(sch.configFilePath) // path to look for the config file in
sch.viper.SetConfigName(sch.configFileBaseName) // name of config file (without extension)
} else {
// path is a file
fext := strings.TrimPrefix(filepath.Ext(sch.configFilePath), ".")
if len(fext) == 0 {
// REQUIRED since the config file does not have the extension in the name
sch.viper.SetConfigType(sch.configFileType)
sch.viper.SetConfigFile(sch.configFilePath)
} else if fileExtSupported(fext) {
// See viper.SupportedExts for full list of supported extensions
sch.viper.SetConfigFile(sch.configFilePath)
} else {
// REQUIRED since the config file extension is not in the list of supported extensions
sch.viper.SetConfigType(sch.configFileType)
sch.viper.SetConfigFile(sch.configFilePath)
}
}
return true, nil
} else if errors.Is(err, os.ErrNotExist) {
// path does *not* exist
return false, fmt.Errorf("no such file or directory: %q", sch.configFilePath)
} else {
// Schrodinger: file may or may not exist. See err for details.
// Therefore, do *NOT* use !os.IsNotExist(err) to test for file existence
return false, fmt.Errorf("schrodinger: %q may or may not exist: %s", sch.configFilePath, err.Error())
}
}
// This adds Flag to cobra flagset and sets default viper config param
func (sch *SnakeCharmer) applySetting(rv reflect.Value, name, help string) error {
switch rv.Kind() {
case reflect.Bool:
value := rv.Bool()
sch.cmd.PersistentFlags().Bool(name, value, help)
sch.viper.SetDefault(name, value)
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
value := rv.Uint()
sch.cmd.PersistentFlags().Uint64(name, value, help)
sch.viper.SetDefault(name, value)
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
value := rv.Int()
sch.cmd.PersistentFlags().Int64(name, value, help)
sch.viper.SetDefault(name, value)
case reflect.Float32, reflect.Float64:
value := rv.Float()
sch.cmd.PersistentFlags().Float64(name, value, help)
sch.viper.SetDefault(name, value)
case reflect.String:
value := rv.String()
sch.cmd.PersistentFlags().String(name, value, help)
sch.viper.SetDefault(name, value)
case reflect.Slice:
intf := rv.Interface()
value, ok := intf.([]string)
if !ok {
return fmt.Errorf("BUG: invalid type: %T for flag %q", intf, name)
}
if len(value) == 0 {
return fmt.Errorf("BUG: value of flag %q (%T) is nil or empty", name, intf)
}
sch.cmd.PersistentFlags().StringSlice(name, value, help)
sch.viper.SetDefault(name, value)
case reflect.Map:
intf := rv.Interface()
value, ok := intf.(map[string]string)
if !ok {
return fmt.Errorf("BUG: invalid type: %T for flag %q", intf, name)
}
if value == nil {
return fmt.Errorf("BUG: value of flag %q (%T) is nil or empty", name, intf)
}
sch.cmd.PersistentFlags().StringToString(name, value, help)
sch.viper.SetDefault(name, value)
default:
return fmt.Errorf("BUG: unsupported type: %q", rv.Kind().String())
}
return nil
}