This repository has been archived by the owner on Oct 2, 2020. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 5
/
Copy pathdecode.go
208 lines (187 loc) · 6.73 KB
/
decode.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
// Copyright (c) 2017 Uber Technologies, Inc.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
// Package mapdecode implements a generic interface{} decoder. It allows
// implementing custom YAML/JSON decoding logic only once. Instead of
// implementing the same UnmarshalYAML and UnmarshalJSON twice, you can
// implement Decode once, parse the YAML/JSON input into a
// map[string]interface{} and decode it using this package.
//
// var data map[string]interface{}
// if err := json.Decode(&data, input); err != nil {
// log.Fatal(err)
// }
//
// var result MyStruct
// if err := mapdecode.Decode(&result, data); err != nil {
// log.Fatal(err)
// }
//
// This also makes it possible to implement custom markup parsing and
// deserialization strategies that get decoded into a user-provided struct.
package mapdecode
import (
"fmt"
"reflect"
"github.com/uber-go/mapdecode/internal/mapstructure"
"go.uber.org/multierr"
)
const _defaultTagName = "mapdecode"
type options struct {
TagName string
IgnoreUnused bool
Unmarshaler unmarshaler
FieldHooks []FieldHookFunc
DecodeHooks []DecodeHookFunc
}
// Option customizes the behavior of Decode.
type Option func(*options)
// TagName changes the name of the struct tag under which field names are
// expected.
func TagName(name string) Option {
return func(o *options) {
o.TagName = name
}
}
// IgnoreUnused specifies whether we should ignore unused attributes in YAML.
// By default, decoding will fail if an unused attribute is encountered.
func IgnoreUnused(ignore bool) Option {
return func(o *options) {
o.IgnoreUnused = ignore
}
}
// FieldHook registers a hook to be called when a struct field is being
// decoded by the system.
//
// This hook will be called with information about the target field of a
// struct and the source data that will fill it.
//
// Field hooks may return a value of the same type as the source data or the
// same type as the target. Other value decoding hooks will still be executed
// on this field.
//
// Multiple field hooks may be specified by providing this option multiple
// times. Hooks are exected in-order, feeding values from one hook to the
// next.
func FieldHook(f FieldHookFunc) Option {
return func(o *options) {
o.FieldHooks = append(o.FieldHooks, f)
}
}
// DecodeHook registers a hook to be called before a value is decoded by the
// system.
//
// This hook will be called with information about the target type and the
// source data that will fill it.
//
// Multiple decode hooks may be specified by providing this option multiple
// times. Hooks are exected in-order, feeding values from one hook to the
// next.
func DecodeHook(f DecodeHookFunc) Option {
return func(o *options) {
o.DecodeHooks = append(o.DecodeHooks, f)
}
}
// unmarshaler defines a scheme that allows users to do custom unmarshalling.
// The default scheme is _decoderUnmarshaler where we expect users to
// implement the Decoder interface.
type unmarshaler struct {
// Interface that the type must implement for Unmarshal to be called.
Interface reflect.Type
// Unmarshal will be called with a Value that implements the interface
// specified above and a function to decode the underlying data into
// another shape. This is analogous to the Into type.
Unmarshal func(reflect.Value, func(interface{}) error) error
}
// Decode from src into dest where dest is a pointer to the value being
// decoded.
//
// Primitives are mapped as-is with pointers created or dereferenced as
// necessary. Maps and slices use Decode recursively for each of their items.
// For structs, the source must be a map[string]interface{} or
// map[interface{}]interface{}. Each key in the map calls Decode recursively
// with the field of the struct that has a name similar to the key (case
// insensitive match).
//
// var item struct{ Key, Value string }
// err := Decode(&item, map[string]string{"key": "some key", "Value": "some value"})
//
// The name of the field in the map may be customized with the `mapdecode`
// tag. (Use the TagName option to change the name of the tag.)
//
// var item struct {
// Key string `mapdecode:"name"`
// Value string
// }
// var item struct{ Key, Value string }
// err := Decode(&item, map[string]string{"name": "token", "Value": "some value"})
//
// The destination type or any subtype may implement the Decoder interface to
// customize how it gets decoded.
func Decode(dest, src interface{}, os ...Option) error {
opts := options{
TagName: _defaultTagName,
Unmarshaler: _decoderUnmarshaler,
}
for _, o := range os {
o(&opts)
}
return decodeFrom(&opts, src)(dest)
}
// decodeFrom builds a decode Into function that reads the given value into
// the destination.
func decodeFrom(opts *options, src interface{}) Into {
return func(dest interface{}) error {
var fieldHooks FieldHookFunc
hooks := opts.DecodeHooks
// fieldHook goes first because it may replace the source data map.
if len(opts.FieldHooks) > 0 {
fieldHooks = composeFieldHooks(opts.FieldHooks)
}
hooks = append(
hooks,
unmarshalerHook(opts),
// durationHook must come before the strconvHook
// because the Kind of time.Duration is Int64.
durationHook,
strconvHook,
)
cfg := mapstructure.DecoderConfig{
ErrorUnused: !opts.IgnoreUnused,
Result: dest,
SquashEmbedded: true,
DecodeHook: fromDecodeHookFunc(
supportPointers(composeDecodeHooks(hooks)),
),
FieldHook: mapstructure.FieldHookFunc(fieldHooks),
TagName: opts.TagName,
}
decoder, err := mapstructure.NewDecoder(&cfg)
if err != nil {
return fmt.Errorf("failed to set up decoder: %v", err)
}
if err := decoder.Decode(src); err != nil {
if merr, ok := err.(*mapstructure.Error); ok {
return multierr.Combine(merr.WrappedErrors()...)
}
return err
}
return nil
}
}