Skip to content

Commit 3b82e63

Browse files
committed
Merge branch 'ft/decode-config'
2 parents 5b98be5 + d144469 commit 3b82e63

File tree

8 files changed

+574
-80
lines changed

8 files changed

+574
-80
lines changed

config_decode.go

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
package kutils
2+
3+
import (
4+
"encoding"
5+
"reflect"
6+
"time"
7+
8+
"github.com/mitchellh/mapstructure"
9+
10+
"github.com/KyberNetwork/kutils/internal/json"
11+
)
12+
13+
// StringUnmarshalHookFunc converts string values using json.Unmarshaler or encoding.TextUnmarshaler
14+
// if the destination type implements it.
15+
func StringUnmarshalHookFunc() mapstructure.DecodeHookFuncValue {
16+
return func(f, t reflect.Value) (any, error) {
17+
if f.Kind() != reflect.String {
18+
return f.Interface(), nil
19+
}
20+
data := f.String()
21+
22+
var dest any
23+
if t.Kind() == reflect.Pointer {
24+
if t.IsNil() {
25+
dest = reflect.New(t.Type().Elem()).Interface()
26+
} else {
27+
dest = t.Interface()
28+
}
29+
} else if t.CanAddr() {
30+
dest = t.Addr().Interface()
31+
} else {
32+
dest = reflect.New(t.Type()).Interface()
33+
}
34+
35+
switch dest := dest.(type) {
36+
case encoding.TextUnmarshaler:
37+
return dest, dest.UnmarshalText([]byte(data))
38+
case json.Unmarshaler:
39+
quoted, _ := json.Marshal(data)
40+
return dest, dest.UnmarshalJSON(quoted)
41+
}
42+
return data, nil
43+
}
44+
}
45+
46+
// StringToTimeDurationHookFunc converts string to time.Duration. It also handles json.Number.
47+
func StringToTimeDurationHookFunc() mapstructure.DecodeHookFuncValue {
48+
durationType := reflect.TypeOf(time.Duration(0))
49+
return func(f, t reflect.Value) (any, error) {
50+
if f.Kind() != reflect.String {
51+
return f.Interface(), nil
52+
}
53+
if t.Type() != durationType {
54+
return f.Interface(), nil
55+
}
56+
57+
data := f.String()
58+
if data == "" {
59+
return time.Duration(0), nil
60+
}
61+
62+
lastChar := data[len(data)-1]
63+
if lastChar == '.' || '0' <= lastChar && lastChar <= '9' {
64+
return Atoi[time.Duration](data)
65+
}
66+
return time.ParseDuration(data)
67+
}
68+
}
69+
70+
// ConfigDecodeHook converts string to time.Duration and string to string slice like viper does.
71+
// It also contains StringUnmarshalHookFunc to account for fields implementing Unmarshal interfaces.
72+
// We also need special handling of string in StringToTimeDurationHookFunc hook in order to
73+
// process json.Number produced by JSONUnmarshal which uses UseNumber option.
74+
var ConfigDecodeHook = mapstructure.ComposeDecodeHookFunc(
75+
StringUnmarshalHookFunc(),
76+
StringToTimeDurationHookFunc(),
77+
mapstructure.StringToSliceHookFunc(","),
78+
)
79+
80+
// DecodeConfig unmarshalls the raw config value bytes into the specified destination with custom logic hooks.
81+
// It uses JSONUnmarshal which enables UseNumber option for maintaining number precision
82+
// and mapstructure to make use of custom hooks including a hook for making use of custom text/json unmarshaler
83+
// and hooks for time and string slice conversion hooks (same as viper config).
84+
func DecodeConfig(data []byte, dest any) error {
85+
var cfgMap any
86+
if err := JSONUnmarshal(data, &cfgMap); err != nil {
87+
return err
88+
}
89+
decoder, err := mapstructure.NewDecoder(&mapstructure.DecoderConfig{
90+
Result: dest,
91+
WeaklyTypedInput: true,
92+
DecodeHook: ConfigDecodeHook,
93+
})
94+
if err != nil {
95+
return err
96+
}
97+
return decoder.Decode(cfgMap)
98+
}

0 commit comments

Comments
 (0)