-
Notifications
You must be signed in to change notification settings - Fork 0
/
string_map_unmarshal.go
279 lines (240 loc) · 9.1 KB
/
string_map_unmarshal.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
package scold
import (
"encoding"
"fmt"
"reflect"
"strconv"
"time"
"github.com/hashicorp/go-multierror"
)
const (
// ErrNotAStructLike is issued when a destination type doesn't behave like
// a struct. struct and *struct types have the same syntax for manipulating
// them, so they are considered struct-like.
ErrNotAStructLike = StringError("not a struct-like")
// ErrUnknownField is issued when the deserialization destination cannot be
// found in a struct-like via reflection.
ErrUnknownField = StringWarning("unknown field")
// ErrNegativePositiveDuration is issued when PositiveDuration is attempted
// to be initialized with negative value.
ErrNegativePositiveDuration = StringError("PositiveDuration accepts only positive durations")
// ErrDurationWithoutSuffix is issued when PositiveDuration is unmarshalled
// with a value that doesn't have a unit suffix.
ErrDurationWithoutSuffix = StringWarning("durations without a unit suffix like \"s\" or \"us\" are deprecated and discouraged. Please, specify the unit explicitly, like \"10.0s\" for 10 seconds")
// ErrDurationBadSyntax is issued when PositiveDuration is unmarshalled
// with a value that cannot be interpreted as a duration.
ErrDurationBadSyntax = StringError("bad syntax. Correct values could be \"1s\" or \"12.3ms\"")
)
var intParsers = map[reflect.Kind]int{
reflect.Int: 0,
reflect.Int8: 8,
reflect.Int16: 16,
reflect.Int32: 32,
reflect.Int64: 64,
}
var uintParsers = map[reflect.Kind]int{
reflect.Uint: 0,
reflect.Uint8: 8,
reflect.Uint16: 16,
reflect.Uint32: 32,
reflect.Uint64: 64,
}
var floatParsers = map[reflect.Kind]int{
reflect.Float32: 32,
reflect.Float64: 64,
}
var complexParsers = map[reflect.Kind]int{
reflect.Complex64: 64,
reflect.Complex128: 128,
}
// PositiveDuration is a wrapper around time.Duration that allows
// StringMapsUnmarshal to parse it from a string and that forbids negative
// durations. Implements encoding.TextUnmarshaler.
type PositiveDuration struct{ time.Duration }
// NewPositiveDuration returns a PositiveDuration with the specified value.
// Panics if value is negative.
func NewPositiveDuration(dur time.Duration) PositiveDuration {
if dur < 0 {
panic(ErrNegativePositiveDuration)
}
return PositiveDuration{dur}
}
// UnmarshalText will delegate parsing to built-in time.ParseDuration and,
// hence, accept the same format as time.ParseDuration. It will also
// reject negative durations.
func (d *PositiveDuration) UnmarshalText(b []byte) error {
dur, err := time.ParseDuration(string(b))
if err != nil {
num, err := strconv.ParseFloat(string(b), 64)
if err != nil {
return ErrDurationBadSyntax
}
if num < 0 {
return ErrNegativePositiveDuration
}
*d = PositiveDuration{time.Duration(num * float64(time.Second))}
return ErrDurationWithoutSuffix
}
if dur.Nanoseconds() < 0 {
return ErrNegativePositiveDuration
}
*d = PositiveDuration{dur}
return nil
}
// StringMapUnmarshal accepts a string map and for each key-value pair tries
// to find an identically named field in the provided object, parse the
// string value according to the field's type and assign the parsed value
// to it.
//
// The field's type should be: int (any flavor), uint (any flavor), string,
// bool, any struct or a pointer to a struct. The structs should implement
// encoding.TextUnmarshaler standard interface to be parsed by this function,
// otherwise NotUnmarshalableTypeError is issued.
//
// If the destination object is not struct or a pointer to a struct,
// ErrNotAStructLike is issued.
//
// If a map's key cannot be mapped to a field within the destination object,
// FieldError wrapping ErrUnknownField is issued.
//
// If a map's value cannot be parsed to the destination type, a FieldError
// wrapping a NotValueOfTypeError is issued.
//
// StringMapUnmarshal makes sure that if any error occurs during unmarshaling
// of a field, the field's previous value is retained.
//
// This function will accumulate all produced errors and return an instance of
// *multierror.Error type.
//
// Since a key must match the field's name perfectly, and this function operates
// only on exported fields, this would mean that only capitalized keys would
// be accepted. This may not be desired. An array of transformers can be
// supplied to change the field matching behavior. Each map's key will be
// fed to one transformer at a time in the order they were passed to this
// function, until a match is found. The transformer's job, then, is to
// convert an arbitrary string to a possible exported field's name in the
// destination object. If a transformer succeeds, the successive transformers
// are not applied. If the field still could not be found, a FieldError
// wrapping ErrUnknownField is issued.
func StringMapUnmarshal(kvm map[string]string, data interface{}, transformers ...func(string) string) error {
val := reflect.ValueOf(data)
if val.Kind() == reflect.Ptr {
val = val.Elem()
}
if val.Kind() != reflect.Struct {
panic(ErrNotAStructLike)
}
var errs *multierror.Error
for k, v := range kvm {
var field reflect.Value
if len(transformers) == 0 {
field = val.FieldByName(k)
} else {
for _, transformer := range transformers {
nameCandidate := transformer(k)
field = val.FieldByName(nameCandidate)
if field.IsValid() {
break
}
}
}
if !field.IsValid() {
errs = multierror.Append(errs, &FieldError{k, ErrUnknownField})
continue
}
// A user-defined type should satisfy encoding.TextUnmarshaler
var unmarshalDest encoding.TextUnmarshaler
var isDeserializable bool
if field.Kind() == reflect.Ptr {
unmarshalDest, isDeserializable = field.Interface().(encoding.TextUnmarshaler)
} else {
unmarshalDest, isDeserializable = field.Addr().Interface().(encoding.TextUnmarshaler)
}
if isDeserializable {
// Make sure that the field is allocated, so that the FromString
// method had a place to write back to. But also remember if we
// changed the field, so to revert it back if error occurs.
wasNil := false
if field.Kind() == reflect.Ptr && field.IsNil() {
field.Set(reflect.New(field.Type().Elem()))
unmarshalDest = field.Interface().(encoding.TextUnmarshaler)
wasNil = true
}
err := unmarshalDest.UnmarshalText([]byte(v))
if err != nil {
var typeName string
if field.Kind() == reflect.Ptr {
typeName = field.Type().Elem().Name()
} else {
typeName = field.Type().Name()
}
errs = multierror.Append(errs, &FieldError{k, &NotValueOfTypeError{typeName, v, err}})
if wasNil {
field.Set(reflect.Zero(field.Type()))
}
}
continue
}
if bitSize, found := intParsers[field.Kind()]; found {
parsed, err := strconv.ParseInt(v, 10, bitSize)
if err != nil {
errs = multierror.Append(errs, &FieldError{k, &NotValueOfTypeError{field.Kind().String(), v, nil}})
continue
}
// This is the minimum I succeeded to decrease the boilerplate...
if field.Kind() == reflect.Int {
field.Set(reflect.ValueOf(int(parsed)))
} else if field.Kind() == reflect.Int8 {
field.Set(reflect.ValueOf(int8(parsed)))
} else if field.Kind() == reflect.Int16 {
field.Set(reflect.ValueOf(int16(parsed)))
} else if field.Kind() == reflect.Int32 {
field.Set(reflect.ValueOf(int32(parsed)))
} else if field.Kind() == reflect.Int64 {
field.Set(reflect.ValueOf(int64(parsed)))
}
} else if bitSize, found := uintParsers[field.Kind()]; found {
parsed, err := strconv.ParseUint(v, 10, bitSize)
if err != nil {
errs = multierror.Append(errs, &FieldError{k, &NotValueOfTypeError{field.Kind().String(), v, nil}})
continue
}
if field.Kind() == reflect.Uint {
field.Set(reflect.ValueOf(uint(parsed)))
} else if field.Kind() == reflect.Uint8 {
field.Set(reflect.ValueOf(uint8(parsed)))
} else if field.Kind() == reflect.Uint16 {
field.Set(reflect.ValueOf(uint16(parsed)))
} else if field.Kind() == reflect.Uint32 {
field.Set(reflect.ValueOf(uint32(parsed)))
} else if field.Kind() == reflect.Uint64 {
field.Set(reflect.ValueOf(uint64(parsed)))
}
} else if bitSize, found := floatParsers[field.Kind()]; found {
parsed, err := strconv.ParseFloat(v, bitSize)
if err != nil {
errs = multierror.Append(errs, &FieldError{k, &NotValueOfTypeError{field.Kind().String(), v, nil}})
continue
}
if field.Kind() == reflect.Float32 {
field.Set(reflect.ValueOf(float32(parsed)))
} else if field.Kind() == reflect.Float64 {
field.Set(reflect.ValueOf(float64(parsed)))
}
} else if _, found := complexParsers[field.Kind()]; found {
panic("Unmarshaling complex numbers from strings is not implemented")
} else if field.Kind() == reflect.Bool {
parsed, err := strconv.ParseBool(v)
if err != nil {
errs = multierror.Append(errs, &FieldError{k, &NotValueOfTypeError{field.Kind().String(), v, nil}})
continue
}
field.Set(reflect.ValueOf(parsed))
} else if field.Kind() == reflect.String {
field.Set(reflect.ValueOf(v))
} else {
panic(&NotTextUnmarshalableTypeError{Field: k, Type: field.Kind(), TypeName: fmt.Sprintf("%T", field.Interface())})
}
}
return errs.ErrorOrNil()
}