-
Notifications
You must be signed in to change notification settings - Fork 3
/
Copy pathconfig.go
327 lines (274 loc) · 9.39 KB
/
config.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
// Package swap is an agnostic config parser
// (supporting YAML, TOML, JSON and environment vars) and
// a toolbox factory with automatic configuration
// based on your build environment.
package swap
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"os"
"path/filepath"
"reflect"
"regexp"
"strings"
"text/template"
"github.com/BurntSushi/toml"
"gopkg.in/yaml.v3"
)
// todo: replace encoding/json with github.com/json-iterator/go
const (
// struct field tag key
sftConfigKey = "swapcp"
// return error if missing value
// eg.: `swap:"required"`
sffConfigRequired = "required"
// sffEnv environment var value can be in json format,
// it also overrides the default value.
// eg.: `swap:"env=env_var_name"`
sffConfigEnv = "env"
// set the default value
// eg.: `swap:"default=1"`
sffConfigDefault = "default"
)
var (
// files type regexp
regexpValidExt = regexp.MustCompile(`(?i)(.y(|a)ml|.toml|.json)`)
regexpYAML = regexp.MustCompile(`(?i)(.y(|a)ml)`)
regexpTOML = regexp.MustCompile(`(?i)(.toml)`)
regexpJSON = regexp.MustCompile(`(?i)(.json)`)
)
// Parse strictly parse only the specified config files
// in the exact order they are into the config interface, one by one.
// The latest files will override the former.
// Will also parse fmt template keys in configs and struct flags.
func Parse(config interface{}, files ...string) (err error) {
return ParseByEnv(config, nil, files...)
}
// ParseByEnv parse all the passed files plus all the matched ones
// for the given Environment (if not nil) into the config interface.
// Environment specific files will override generic files.
// The latest files passed will override the former.
// Will also parse fmt template keys and struct flags.
func ParseByEnv(config interface{}, env *Environment, files ...string) (err error) {
files, err = appendEnvFiles(env, files)
if err != nil {
return fmt.Errorf("no config file found for '%s': %s", strings.Join(files, " | "), err.Error())
}
if len(files) == 0 {
return fmt.Errorf("no config file found for '%s'", strings.Join(files, " | "))
}
if reflect.TypeOf(config).Kind() != reflect.Ptr {
return fmt.Errorf("the config argument should be a pointer: `%s`", reflect.TypeOf(config).String())
}
for _, file := range files {
if err = unmarshalFile(file, config); err != nil {
return err
}
if err = parseTemplateFile(file, config); err != nil {
return err
}
}
return parseConfigTags(config)
}
// File search ---------------------------------------------------------------------------------------------------------
// appendEnvFiles will search for the given file names in the given path
// returning all the eligible files (eg.: <path>/config.yaml or <path>/config.<environment>.json)
//
// Files name can also be passed without file extension,
// configFilesByEnv is semi-agnostic and will match any
// supported extension using the regex: `(?i)(.y(|a)ml|.toml|.json)`.
//
// The 'file' name will be searched as (in that order):
// - '<path>/<file>(.* || <the_provided_extension>)'
// - '<path>/<file>.<environment>(.* || <the_provided_extension>)'
//
// The latest found files will override previous.
func appendEnvFiles(env *Environment, files []string) (foundFiles []string, err error) {
for _, file := range files {
configPath, fileName := filepath.Split(file)
if len(configPath) == 0 {
configPath = "./"
}
ext := filepath.Ext(fileName)
extTrimmed := strings.TrimSuffix(fileName, ext)
if len(ext) == 0 {
ext = regexpValidExt.String() // search for any compatible file
}
format := "^%s%s$"
if !FileSearchCaseSensitive {
format = "(?i)(^%s)%s$"
}
// look for the config file in the config path (eg.: tool.yml)
regex := regexp.MustCompile(fmt.Sprintf(format, extTrimmed, ext))
var foundFile string
foundFile, err = walkConfigPath(configPath, regex)
if err != nil {
break
}
if len(foundFile) > 0 {
foundFiles = append(foundFiles, foundFile)
}
if env != nil {
// look for the env config file in the config path (eg.: tool.development.yml)
//regexEnv := regexp.MustCompile(fmt.Sprintf(format, fmt.Sprintf("%s.%s", extTrimmed, Env().ID()), ext))
regexEnv := regexp.MustCompile(fmt.Sprintf(format, fmt.Sprintf("%s.%s", extTrimmed, env.Tag()), ext))
foundFile, err = walkConfigPath(configPath, regexEnv)
if err != nil {
break
}
if len(foundFile) > 0 {
foundFiles = append(foundFiles, foundFile)
}
}
}
if err == nil && len(foundFiles) == 0 {
err = fmt.Errorf("no config file found for '%s'", strings.Join(files, " | "))
}
return
}
// walkConfigPath look for a file matching the passed regex skipping sub-directories.
func walkConfigPath(configPath string, regex *regexp.Regexp) (matchedFile string, err error) {
err = filepath.Walk(configPath, func(path string, info os.FileInfo, err error) error {
// nil if the path does not exist
if info == nil {
return filepath.SkipDir
}
if info.IsDir() && info.Name() != filepath.Base(configPath) {
return filepath.SkipDir
}
if !info.Mode().IsRegular() {
return nil
}
if regex.MatchString(info.Name()) {
matchedFile = path
}
return nil
})
return
}
// File parse ----------------------------------------------------------------------------------------------------------
func unmarshalFile(file string, config interface{}) (err error) {
var in []byte
if in, err = ioutil.ReadFile(file); err != nil {
return err
}
ext := filepath.Ext(file)
switch {
case regexpYAML.MatchString(ext):
err = unmarshalYAML(in, config)
case regexpTOML.MatchString(ext):
err = unmarshalTOML(in, config)
case regexpJSON.MatchString(ext):
err = unmarshalJSON(in, config)
default:
err = fmt.Errorf("unknown data format, can't unmarshal file: '%s'", file)
}
return
}
func unmarshalJSON(data []byte, config interface{}) (err error) {
return json.Unmarshal(data, config)
}
func unmarshalTOML(data []byte, config interface{}) (err error) {
_, err = toml.Decode(string(data), config)
return err
}
func unmarshalYAML(data []byte, config interface{}) (err error) {
return yaml.Unmarshal(data, config)
}
// parseTemplateFile parse all text/template placeholders
// (eg.: {{.Key}}) in config files.
func parseTemplateFile(file string, config interface{}) error {
tpl, err := template.ParseFiles(file)
if err != nil {
return err
}
var buf bytes.Buffer
if err = tpl.Execute(&buf, config); err != nil {
return err
}
ext := filepath.Ext(file)
switch {
case regexpYAML.MatchString(ext):
return unmarshalYAML(buf.Bytes(), config)
case regexpTOML.MatchString(ext):
return unmarshalTOML(buf.Bytes(), config)
case regexpJSON.MatchString(ext):
return unmarshalJSON(buf.Bytes(), config)
default:
return fmt.Errorf("unknown data format, can't unmarshal file: '%s'", file)
}
}
// Flags parse ---------------------------------------------------------------------------------------------------------
// parseConfigTags will process the struct field tags.
func parseConfigTags(elem interface{}) error {
elemValue := reflect.Indirect(reflect.ValueOf(elem))
switch elemValue.Kind() {
case reflect.Struct:
elemType := elemValue.Type()
//fmt.Printf("%sProcessing STRUCT: %s = %+v\n", indent, elemType.Name(), elem)
for i := 0; i < elemType.NumField(); i++ {
ft := elemType.Field(i)
fv := elemValue.Field(i)
if !fv.CanAddr() || !fv.CanInterface() {
//fmt.Printf("%sCan't addr or interface FIELD: CanAddr: %v, CanInterface: %v. -> %s = '%+v'\n", indent, fv.CanAddr(), fv.CanInterface(), ft.Name, fv.Interface())
continue
}
tag := ft.Tag.Get(sftConfigKey)
tagFields := strings.Split(tag, ",")
//fmt.Printf("\n%sProcessing FIELD: %s %s = %+v, tags: %s\n", indent, ft.Name, ft.Type.String(), fv.Interface(), tag)
for _, flag := range tagFields {
kv := strings.Split(flag, "=")
if kv[0] == sffConfigEnv {
if len(kv) == 2 {
if value := os.Getenv(kv[1]); len(value) > 0 {
//debugPrintf("Loading configuration for struct `%v`'s field `%v` from env %v...\n", elemType.Name(), ft.Name, kv[1])
if err := yaml.Unmarshal([]byte(value), fv.Addr().Interface()); err != nil {
return err
}
}
} else {
return fmt.Errorf("missing environment variable key value in tag: %s, must be someting like: `%s:\"env=env_var_name\"`",
sftConfigKey, flag)
}
}
if empty := reflect.DeepEqual(fv.Interface(), reflect.Zero(fv.Type()).Interface()); empty {
if kv[0] == sffConfigDefault {
if len(kv) == 2 {
if err := yaml.Unmarshal([]byte(kv[1]), fv.Addr().Interface()); err != nil {
return err
}
} else {
return fmt.Errorf("missing default value in tag: %s, must be someting like: `%s:\"default=true\"`",
sftConfigKey, flag)
}
} else if kv[0] == sffConfigRequired {
return errors.New(ft.Name + " is required")
}
}
}
switch fv.Kind() {
case reflect.Ptr, reflect.Struct, reflect.Slice, reflect.Map:
if err := parseConfigTags(fv.Addr().Interface()); err != nil {
return err
}
}
//fmt.Printf("%sProcessed FIELD: %s %s = %+v\n", indent, ft.Name, ft.Type.String(), fv.Interface())
}
case reflect.Slice:
for i := 0; i < elemValue.Len(); i++ {
if err := parseConfigTags(elemValue.Index(i).Addr().Interface()); err != nil {
return err
}
}
case reflect.Map:
for _, key := range elemValue.MapKeys() {
if err := parseConfigTags(elemValue.MapIndex(key).Interface()); err != nil {
return err
}
}
}
return nil
}