/
config.go
347 lines (292 loc) · 9.49 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
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
package config
import (
"context"
"fmt"
"reflect"
"sync"
"time"
"github.com/DoNewsCode/core/contract"
"github.com/DoNewsCode/core/events"
"github.com/knadh/koanf"
"github.com/knadh/koanf/providers/confmap"
"github.com/mitchellh/mapstructure"
)
// KoanfAdapter is a implementation of contract.Config based on Koanf (https://github.com/knadh/koanf).
type KoanfAdapter struct {
layers []ProviderSet
validators []Validator
watcher contract.ConfigWatcher
dispatcher contract.Dispatcher
delimiter string
rwlock sync.RWMutex
K *koanf.Koanf
}
// ProviderSet is a configuration layer formed by a parser and a provider.
type ProviderSet struct {
Parser koanf.Parser
Provider koanf.Provider
}
// Option is the functional option type for KoanfAdapter
type Option func(option *KoanfAdapter)
// WithProviderLayer is an option for *KoanfAdapter that adds a layer to the bottom of the configuration stack.
// This option can be used multiple times, thus forming the whole stack. The layer on top has higher priority.
func WithProviderLayer(provider koanf.Provider, parser koanf.Parser) Option {
return func(option *KoanfAdapter) {
option.layers = append(option.layers, ProviderSet{Provider: provider, Parser: parser})
}
}
// WithWatcher is an option for *KoanfAdapter that adds a config watcher. The watcher should notify the configurations
// whenever a reload event is triggered.
func WithWatcher(watcher contract.ConfigWatcher) Option {
return func(option *KoanfAdapter) {
option.watcher = watcher
}
}
// WithDelimiter changes the default delimiter of Koanf. See Koanf's doc to learn more about delimiters.
func WithDelimiter(delimiter string) Option {
return func(option *KoanfAdapter) {
option.delimiter = delimiter
}
}
// WithDispatcher changes the default dispatcher of Koanf.
func WithDispatcher(dispatcher contract.Dispatcher) Option {
return func(option *KoanfAdapter) {
option.dispatcher = dispatcher
}
}
// WithValidators changes the validators of Koanf.
func WithValidators(validators ...Validator) Option {
return func(option *KoanfAdapter) {
option.validators = validators
}
}
// NewConfig creates a new *KoanfAdapter.
func NewConfig(options ...Option) (*KoanfAdapter, error) {
adapter := KoanfAdapter{delimiter: "."}
for _, f := range options {
f(&adapter)
}
adapter.K = koanf.New(adapter.delimiter)
if err := adapter.Reload(); err != nil {
return nil, err
}
return &adapter, nil
}
// Reload reloads the whole configuration stack. It reloads layer by layer, so if
// an error occurred, Reload will return early and abort the rest of the
// reloading.
func (k *KoanfAdapter) Reload() error {
tmp := koanf.New(".")
for i := len(k.layers) - 1; i >= 0; i-- {
err := tmp.Load(k.layers[i].Provider, k.layers[i].Parser)
if err != nil {
return fmt.Errorf("unable to load config %w", err)
}
}
for _, f := range k.validators {
if err := f(tmp.Raw()); err != nil {
return fmt.Errorf("validation failed: %w", err)
}
}
k.rwlock.Lock()
k.K = tmp
k.rwlock.Unlock()
if k.dispatcher != nil {
k.dispatcher.Dispatch(context.Background(), events.OnReload, events.OnReloadPayload{NewConf: k})
}
return nil
}
// Watch uses the internal watcher to watch the configuration reload signals.
// This function should be registered in the run group. If the watcher is nil,
// this call will block until context expired.
func (k *KoanfAdapter) Watch(ctx context.Context) error {
if k.watcher == nil {
<-ctx.Done()
return ctx.Err()
}
return k.watcher.Watch(ctx, k.Reload)
}
// Unmarshal unmarshals a given key path into the given struct using the mapstructure lib.
// If no path is specified, the whole map is unmarshalled. `koanf` is the struct field tag used to match field names.
func (k *KoanfAdapter) Unmarshal(path string, o interface{}) error {
k.rwlock.RLock()
defer k.rwlock.RUnlock()
return k.K.UnmarshalWithConf(path, o, koanf.UnmarshalConf{
Tag: "json",
DecoderConfig: &mapstructure.DecoderConfig{
Result: o,
ErrorUnused: true,
WeaklyTypedInput: true,
DecodeHook: mapstructure.ComposeDecodeHookFunc(
mapstructure.StringToTimeDurationHookFunc(),
stringToConfigDurationHookFunc(),
),
},
})
}
// Route cuts the config map at a given key path into a sub map and returns a new contract.ConfigAccessor instance
// with the cut config map loaded. For instance, if the loaded config has a path that looks like parent.child.sub.a.b,
// `Route("parent.child")` returns a new contract.ConfigAccessor instance with the config map `sub.a.b` where
// everything above `parent.child` are cut out.
func (k *KoanfAdapter) Route(s string) contract.ConfigAccessor {
k.rwlock.RLock()
defer k.rwlock.RUnlock()
return &KoanfAdapter{
K: k.K.Cut(s),
}
}
// String returns the string value of a given key path or "" if the path does not exist or if the value is not a valid string
func (k *KoanfAdapter) String(s string) string {
k.rwlock.RLock()
defer k.rwlock.RUnlock()
return k.K.String(s)
}
// Int returns the int value of a given key path or 0 if the path does not exist or if the value is not a valid int.
func (k *KoanfAdapter) Int(s string) int {
k.rwlock.RLock()
defer k.rwlock.RUnlock()
return k.K.Int(s)
}
// Strings returns the []string slice value of a given key path or an empty []string slice if the path does not exist
// or if the value is not a valid string slice.
func (k *KoanfAdapter) Strings(s string) []string {
k.rwlock.RLock()
defer k.rwlock.RUnlock()
return k.K.Strings(s)
}
// Bool returns the bool value of a given key path or false if the path does not exist or if the value is not a valid bool representation.
// Accepted string representations of bool are the ones supported by strconv.ParseBool.
func (k *KoanfAdapter) Bool(s string) bool {
k.rwlock.RLock()
defer k.rwlock.RUnlock()
return k.K.Bool(s)
}
// Get returns the raw, uncast interface{} value of a given key path in the config map. If the key path does not exist, nil is returned.
func (k *KoanfAdapter) Get(s string) interface{} {
k.rwlock.RLock()
defer k.rwlock.RUnlock()
return k.K.Get(s)
}
// Float64 returns the float64 value of a given key path or 0 if the path does not exist or if the value is not a valid float64.
func (k *KoanfAdapter) Float64(s string) float64 {
k.rwlock.RLock()
defer k.rwlock.RUnlock()
return k.K.Float64(s)
}
// Duration returns the time.Duration value of a given key path or its zero value if the path does not exist or if the value is not a valid float64.
func (k *KoanfAdapter) Duration(s string) time.Duration {
k.rwlock.RLock()
defer k.rwlock.RUnlock()
return k.K.Duration(s)
}
// MapAdapter implements ConfigUnmarshaler and ConfigRouter.
// It is primarily used for testing
type MapAdapter map[string]interface{}
func (m MapAdapter) Unmarshal(path string, o interface{}) (err error) {
k := koanf.New(".")
if err := k.Load(confmap.Provider(m, "."), nil); err != nil {
return err
}
return k.UnmarshalWithConf(path, o, koanf.UnmarshalConf{
Tag: "json",
DecoderConfig: &mapstructure.DecoderConfig{
Result: o,
ErrorUnused: true,
WeaklyTypedInput: true,
DecodeHook: mapstructure.ComposeDecodeHookFunc(
mapstructure.StringToTimeDurationHookFunc(),
stringToConfigDurationHookFunc(),
),
},
})
}
// Route implements contract.ConfigRouter
func (m MapAdapter) Route(s string) contract.ConfigUnmarshaler {
var v interface{}
v = m
if s != "" {
v = m[s]
}
switch x := v.(type) {
case map[string]interface{}:
return MapAdapter(x)
case MapAdapter:
return x
default:
panic(fmt.Sprintf("value at path %s is not a valid Router", s))
}
}
func stringToConfigDurationHookFunc() mapstructure.DecodeHookFunc {
return func(
f reflect.Type,
t reflect.Type,
data interface{}) (interface{}, error) {
if t != reflect.TypeOf(Duration{}) {
return data, nil
}
var val string
switch f.Kind() {
case reflect.Float64, reflect.Int:
val = fmt.Sprintf("%v", data)
case reflect.String:
val = fmt.Sprintf(`"%v"`, data)
default:
return nil, fmt.Errorf("expected a %s, should be float64/int/string, got '%s'", t.String(), f.String())
}
d := Duration{}
err := d.UnmarshalJSON([]byte(val))
if err != nil {
return nil, err
}
return d, nil
}
}
type wrappedConfigAccessor struct {
unmarshaler contract.ConfigUnmarshaler
}
func (w wrappedConfigAccessor) Unmarshal(path string, o interface{}) error {
return w.unmarshaler.Unmarshal(path, o)
}
func (w wrappedConfigAccessor) String(s string) string {
var o string
w.unmarshaler.Unmarshal(s, &o)
return o
}
func (w wrappedConfigAccessor) Int(s string) int {
var o int
w.unmarshaler.Unmarshal(s, &o)
return o
}
func (w wrappedConfigAccessor) Strings(s string) []string {
var o []string
w.unmarshaler.Unmarshal(s, &o)
return o
}
func (w wrappedConfigAccessor) Bool(s string) bool {
var o bool
w.unmarshaler.Unmarshal(s, &o)
return o
}
func (w wrappedConfigAccessor) Get(s string) interface{} {
var o interface{}
w.unmarshaler.Unmarshal(s, &o)
return o
}
func (w wrappedConfigAccessor) Float64(s string) float64 {
var o float64
w.unmarshaler.Unmarshal(s, &o)
return o
}
func (w wrappedConfigAccessor) Duration(s string) time.Duration {
var dur Duration
w.unmarshaler.Unmarshal(s, &dur)
return dur.Duration
}
// WithAccessor upgrade contract.ConfigUnmarshaler to contract.ConfigAccessor by
// wrapping the original unmarshaler.
func WithAccessor(unmarshaler contract.ConfigUnmarshaler) contract.ConfigAccessor {
if accessor, ok := unmarshaler.(contract.ConfigAccessor); ok {
return accessor
}
return wrappedConfigAccessor{unmarshaler: unmarshaler}
}