forked from gobuffalo/buffalo
/
binding.go
170 lines (142 loc) · 3.93 KB
/
binding.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
package binding
import (
"encoding/json"
"encoding/xml"
"net/http"
"strings"
"sync"
"time"
"github.com/gobuffalo/pop/nulls"
"github.com/gobuffalo/x/httpx"
"github.com/monoculum/formam"
"github.com/pkg/errors"
)
// Binder takes a request and binds it to an interface.
// If there is a problem it should return an error.
type Binder func(*http.Request, interface{}) error
// CustomTypeDecoder converts a custom type from the request insto its exact type.
type CustomTypeDecoder func([]string) (interface{}, error)
// binders is a map of the defined content-type related binders.
var binders = map[string]Binder{}
var decoder *formam.Decoder
var lock = &sync.Mutex{}
var timeFormats = []string{
"2006-01-02T15:04:05Z07:00",
"01/02/2006",
"2006-01-02",
"2006-01-02T03:04",
time.ANSIC,
time.UnixDate,
time.RubyDate,
time.RFC822,
time.RFC822Z,
time.RFC850,
time.RFC1123,
time.RFC1123Z,
time.RFC3339,
time.RFC3339Nano,
time.Kitchen,
time.Stamp,
time.StampMilli,
time.StampMicro,
time.StampNano,
}
// RegisterTimeFormats allows to add custom time layouts that
// the binder will be able to use for decoding.
func RegisterTimeFormats(layouts ...string) {
timeFormats = append(timeFormats, layouts...)
}
// RegisterCustomDecorder allows to define custom type decoders.
func RegisterCustomDecorder(fn CustomTypeDecoder, types []interface{}, fields []interface{}) {
rawFunc := (func([]string) (interface{}, error))(fn)
decoder.RegisterCustomType(rawFunc, types, fields)
}
// Register maps a request Content-Type (application/json)
// to a Binder.
func Register(contentType string, fn Binder) {
lock.Lock()
defer lock.Unlock()
binders[strings.ToLower(contentType)] = fn
}
// Exec will bind the interface to the request.Body. The type of binding
// is dependent on the "Content-Type" for the request. If the type
// is "application/json" it will use "json.NewDecoder". If the type
// is "application/xml" it will use "xml.NewDecoder". The default
// binder is "https://github.com/monoculum/formam".
func Exec(req *http.Request, value interface{}) error {
ct := httpx.ContentType(req)
if ct == "" {
return errors.New("blank content type")
}
if b, ok := binders[ct]; ok {
return b(req, value)
}
return errors.Errorf("could not find a binder for %s", ct)
}
func init() {
decoder = formam.NewDecoder(&formam.DecoderOptions{
TagName: "form",
IgnoreUnknownKeys: true,
})
decoder.RegisterCustomType(func(vals []string) (interface{}, error) {
return parseTime(vals)
}, []interface{}{time.Time{}}, nil)
decoder.RegisterCustomType(func(vals []string) (interface{}, error) {
var ti nulls.Time
t, err := parseTime(vals)
if err != nil {
return ti, errors.WithStack(err)
}
ti.Time = t
ti.Valid = true
return ti, nil
}, []interface{}{nulls.Time{}}, nil)
sb := func(req *http.Request, i interface{}) error {
err := req.ParseForm()
if err != nil {
return errors.WithStack(err)
}
if err := decoder.Decode(req.Form, i); err != nil {
return errors.WithStack(err)
}
return nil
}
binders["application/html"] = sb
binders["text/html"] = sb
binders["application/x-www-form-urlencoded"] = sb
binders["html"] = sb
}
func init() {
jb := func(req *http.Request, value interface{}) error {
return json.NewDecoder(req.Body).Decode(value)
}
binders["application/json"] = jb
binders["text/json"] = jb
binders["json"] = jb
}
func init() {
xb := func(req *http.Request, value interface{}) error {
return xml.NewDecoder(req.Body).Decode(value)
}
binders["application/xml"] = xb
binders["text/xml"] = xb
binders["xml"] = xb
}
func parseTime(vals []string) (time.Time, error) {
var t time.Time
var err error
// don't try to parse empty time values, it will raise an error
if len(vals) == 0 || vals[0] == "" {
return t, nil
}
for _, layout := range timeFormats {
t, err = time.Parse(layout, vals[0])
if err == nil {
return t, nil
}
}
if err != nil {
return t, errors.WithStack(err)
}
return t, nil
}