-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathmain.go
215 lines (183 loc) · 6.49 KB
/
main.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
// Package config parsed toml files in specific order to generate a validated struct
package config
import (
errors "errors"
fmt "fmt"
io "io"
fs "io/fs"
os "os"
path "path"
reflect "reflect"
strings "strings"
environment "github.com/beckend/go-config/pkg/environment"
file "github.com/beckend/go-config/pkg/file"
reflection "github.com/beckend/go-config/pkg/reflection"
singletons "github.com/beckend/go-config/pkg/singletons"
validation "github.com/beckend/go-config/pkg/validation"
walkertype "github.com/beckend/go-config/pkg/walker-type"
validator "github.com/go-playground/validator/v10"
flatten "github.com/jeremywohl/flatten"
envutil "github.com/gookit/goutil/envutil"
jsoniter "github.com/json-iterator/go"
conditional "github.com/mileusna/conditional"
mapstructure "github.com/mitchellh/mapstructure"
)
var json = jsoniter.ConfigCompatibleWithStandardLibrary
type Config struct {
ErrorsValidation *validator.ValidationErrors
}
type OnConfigBeforeValidationOptions struct {
ConfigUnmarshal interface{}
}
type LoadConfigsOptionsTOML struct {
FileToJSON func(string) ([]byte, error)
StringToJSON func(string) ([]byte, error)
BytesToJSON func([]byte) ([]byte, error)
ReaderToJSON func(io.Reader) ([]byte, error)
FileReaderToJSON func(file fs.File, closeFile bool) ([]byte, error)
FileReaderCallbackToJSON func(getFileCallback func() (fs.File, error)) ([]byte, error)
}
type LoadConfigsOptions struct {
FilesLoaded []string
ConfigJSONMergedBytes []byte
RunEnv string
TOML *LoadConfigsOptionsTOML
}
type (
// Allows user to have a shot on the config before it validats
OnConfigBeforeValidation func(options *OnConfigBeforeValidationOptions) error
// Allow user to load configs, user has to return a []byte which has been through json.Marshal into []byte
// The payload is in the end going to be json.Unmarshaled
LoadConfigs func(options *LoadConfigsOptions) ([][]byte, error)
)
type NewOptions struct {
OnConfigBeforeValidation OnConfigBeforeValidation
LoadConfigs LoadConfigs
EnvKeyRunEnv string
PathConfigs string
ConfigUnmarshal interface{}
}
// New read configurations with priority, the later overrides the previous
func New(options *NewOptions) (*Config, error) {
var (
_, envKeyUserExists = os.LookupEnv(options.EnvKeyRunEnv)
envRun = environment.GetEnv(conditional.String(envKeyUserExists, options.EnvKeyRunEnv, "RUN_ENV"), "")
filesToBeMerged []string
filesToLoad []string
)
if options.PathConfigs != "" {
// the order to load is base, env specific, then local, where the next overrides the previous values
filesToLoad = append(filesToLoad, path.Join(options.PathConfigs, "base.toml"))
if envRun != "" {
filesToLoad = append(filesToLoad, path.Join(options.PathConfigs, envRun+".toml"))
}
filesToLoad = append(filesToLoad, path.Join(options.PathConfigs, "local.toml"))
for _, pathFile := range filesToLoad {
if _, err := os.Stat(pathFile); err == nil {
filesToBeMerged = append(filesToBeMerged, pathFile)
}
}
}
bytesJSONMerged, err := file.TOMLFilesToMergedJSON(filesToBeMerged)
if err != nil {
return nil, err
}
if options.LoadConfigs != nil {
byteSlicesUser, err := options.LoadConfigs(&LoadConfigsOptions{
FilesLoaded: filesToLoad,
ConfigJSONMergedBytes: bytesJSONMerged,
RunEnv: envRun,
TOML: &LoadConfigsOptionsTOML{
FileToJSON: file.TOMLFileToJSON,
BytesToJSON: file.TOMLBytesToJSON,
StringToJSON: file.TOMLStringToJSON,
ReaderToJSON: file.TOMLReaderToJSON,
FileReaderToJSON: file.TOMLFileReaderToJSON,
FileReaderCallbackToJSON: file.TOMLFileReaderCallbackToJSON,
},
})
if err != nil {
return nil, err
}
// prepend bytesJSONMerged into byteSlicesUser so the user files overrides the initial config
byteSlicesUser = append([][]byte{bytesJSONMerged}, byteSlicesUser...)
bytesJSONMerged, err = file.TOMLBytesToMergedJSON(byteSlicesUser)
if err != nil {
return nil, err
}
}
// convert to a generic map interface to replace env variables
var configMap map[string]interface{}
err = json.Unmarshal(bytesJSONMerged, &configMap)
if err != nil {
return nil, err
}
configMapped := walkertype.Walk(&walkertype.WalkOptions{
Object: configMap,
OnKind: func(oosvo *walkertype.OnKindOptions) *walkertype.OnKindWalkReturn {
if oosvo.CaseKind == reflect.String {
oosvo.Copy.SetString(envutil.ParseEnvValue(oosvo.Original.String()))
return &walkertype.OnKindWalkReturn{
Handled: true,
}
}
return &walkertype.OnKindWalkReturn{
Handled: false,
}
},
})
if err != nil {
return nil, err
}
mapstructure.Decode(configMapped, &options.ConfigUnmarshal)
if options.OnConfigBeforeValidation != nil {
err = options.OnConfigBeforeValidation(&OnConfigBeforeValidationOptions{
ConfigUnmarshal: options.ConfigUnmarshal,
})
if err != nil {
return nil, err
}
}
// validator cannot handlle unamed types such as "var result map[string]interface{}", it needs a struct
if !reflection.HasElement([]string{"*", ""}, reflection.GetType(options.ConfigUnmarshal)) {
errsValidation := singletons.New().Validation.ValidateStruct(validation.ValidatorUtilsValidateStructOptions{
PrefixError: "Config struct validation error - ",
TheStruct: options.ConfigUnmarshal,
PanicOnError: false,
})
if errsValidation != nil && len(*errsValidation) > 0 {
err = errors.New("config struct validation failed")
} else {
err = nil
}
if err == nil {
err = validateStructNoUnmappedEnvVariables(options.ConfigUnmarshal)
}
return &Config{
ErrorsValidation: errsValidation,
}, err
}
return &Config{
ErrorsValidation: nil,
}, err
}
func validateStructNoUnmappedEnvVariables(input interface{}) (err error) {
var interfaceMapped map[string]interface{}
inrec, err := json.Marshal(input)
if err != nil {
return err
}
err = json.Unmarshal(inrec, &interfaceMapped)
if err != nil {
return err
}
interfaceFlat, err := flatten.Flatten(interfaceMapped, "", flatten.DotStyle)
for key, value := range interfaceFlat {
valueString := fmt.Sprintf("%v", value)
if strings.HasPrefix(valueString, "${") && strings.HasSuffix(valueString, "}") {
fmt.Printf("key: %s - environment variable not replaced: %s\n", key, valueString)
err = errors.New("validation failed due to missing replacement(s) of environment variable")
}
}
return err
}