-
-
Notifications
You must be signed in to change notification settings - Fork 47
/
deserialization.go
238 lines (200 loc) · 6.11 KB
/
deserialization.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
package fuego
import (
"context"
"database/sql"
"encoding/json"
"encoding/xml"
"errors"
"fmt"
"io"
"log/slog"
"net/http"
"reflect"
"github.com/gorilla/schema"
"gopkg.in/yaml.v3"
)
// InTransformer is an interface for entities that can be transformed.
// Useful for example for trimming strings, changing case, etc.
// Can also raise an error if the entity is not valid.
type InTransformer interface {
InTransform(context.Context) error // InTransforms the entity.
}
var ReadOptions = readOptions{
DisallowUnknownFields: true,
MaxBodySize: maxBodySize,
}
// ReadJSON reads the request body as JSON.
// Can be used independently of Fuego framework.
// Customizable by modifying ReadOptions.
func ReadJSON[B any](context context.Context, input io.Reader) (B, error) {
return readJSON[B](context, input, ReadOptions)
}
// readJSON reads the request body as JSON.
// Can be used independently of framework using ReadJSON,
// or as a method of Context.
// It will also read strings.
func readJSON[B any](context context.Context, input io.Reader, options readOptions) (B, error) {
// Deserialize the request body.
dec := json.NewDecoder(input)
if options.DisallowUnknownFields {
dec.DisallowUnknownFields()
}
return read[B](context, dec)
}
// ReadXML reads the request body as XML.
// Can be used independently of Fuego framework.
// Customizable by modifying ReadOptions.
func ReadXML[B any](context context.Context, input io.Reader) (B, error) {
return readXML[B](context, input, ReadOptions)
}
// readXML reads the request body as XML.
// Can be used independently of framework using readXML,
// or as a method of Context.
func readXML[B any](context context.Context, input io.Reader, options readOptions) (B, error) {
dec := xml.NewDecoder(input)
if options.DisallowUnknownFields {
dec.Strict = true
}
return read[B](context, dec)
}
// ReadYAML reads the request body as YAML.
// Can be used independently of Fuego framework.
// Customizable by modifying ReadOptions.
func ReadYAML[B any](context context.Context, input io.Reader) (B, error) {
return readYAML[B](context, input, ReadOptions)
}
// readYAML reads the request body as YAML.
// Can be used independently of framework using ReadYAML,
// or as a method of Context.
func readYAML[B any](context context.Context, input io.Reader, options readOptions) (B, error) {
dec := yaml.NewDecoder(input)
if options.DisallowUnknownFields {
dec.KnownFields(true)
}
return read[B](context, dec)
}
type decoder interface {
Decode(v any) error
}
func read[B any](context context.Context, dec decoder) (B, error) {
var body B
err := dec.Decode(&body)
if err != nil && !errors.Is(err, io.EOF) {
return body, BadRequestError{
Title: "Decoding Failed",
Err: err,
Detail: "cannot decode request body: " + err.Error(),
}
}
slog.Debug("Decoded body", "body", body)
body, err = transform(context, body)
if err != nil {
return body, BadRequestError{
Title: "Transformation Failed",
Err: err,
Detail: "cannot transform request body: " + err.Error(),
}
}
err = validate(body)
if err != nil {
return body, err
}
return body, nil
}
// ReadString reads the request body as string.
// Can be used independently of Fuego framework.
// Customizable by modifying ReadOptions.
func ReadString[B ~string](context context.Context, input io.Reader) (B, error) {
return readString[B](context, input, ReadOptions)
}
func readString[B ~string](context context.Context, input io.Reader, _ readOptions) (B, error) {
// Read the request body.
readBody, err := io.ReadAll(input)
if err != nil {
return "", BadRequestError{
Err: err,
Detail: "cannot read request body: " + err.Error(),
}
}
body := B(readBody)
slog.Debug("Read body", "body", body)
return transform(context, body)
}
func convertSQLNullString(value string) reflect.Value {
v := sql.NullString{}
if err := v.Scan(value); err != nil {
return reflect.Value{}
}
return reflect.ValueOf(v)
}
func convertSQLNullBool(value string) reflect.Value {
v := sql.NullBool{}
if err := v.Scan(value); err != nil {
return reflect.Value{}
}
return reflect.ValueOf(v)
}
func newDecoder() *schema.Decoder {
decoder := schema.NewDecoder()
decoder.RegisterConverter(sql.NullString{}, convertSQLNullString)
decoder.RegisterConverter(sql.NullBool{}, convertSQLNullBool)
return decoder
}
// ReadURLEncoded reads the request body as HTML Form.
func ReadURLEncoded[B any](r *http.Request) (B, error) {
return readURLEncoded[B](r, ReadOptions)
}
// readURLEncoded reads the request body as HTML Form.
// Can be used independently of framework using [ReadURLEncoded],
// or as a method of Context.
func readURLEncoded[B any](r *http.Request, options readOptions) (B, error) {
var body B
err := r.ParseForm()
if err != nil {
return body, fmt.Errorf("cannot parse form: %w", err)
}
decoder := newDecoder()
decoder.IgnoreUnknownKeys(!options.DisallowUnknownFields)
err = decoder.Decode(&body, r.PostForm)
if err != nil {
return body, BadRequestError{
Detail: "cannot decode x-www-form-urlencoded request body: " + err.Error(),
Err: err,
Errors: []ErrorItem{
{Name: "form", Reason: "check that the form is valid, and that the content-type is correct"},
},
}
}
slog.Debug("Decoded body", "body", body)
body, err = transform(r.Context(), body)
if err != nil {
return body, BadRequestError{
Title: "Transformation Failed",
Detail: "cannot transform x-www-form-urlencoded request body: " + err.Error(),
Err: err,
Errors: []ErrorItem{
{Name: "transformation", Reason: "transformation failed"},
},
}
}
err = validate(body)
if err != nil {
return body, fmt.Errorf("cannot validate request body: %w", err)
}
return body, nil
}
// transforms the input if possible.
func transform[B any](ctx context.Context, body B) (B, error) {
if inTransformerBody, ok := any(&body).(InTransformer); ok {
err := inTransformerBody.InTransform(ctx)
if err != nil {
return body, BadRequestError{
Err: err,
Detail: "cannot transform request body: " + err.Error(),
}
}
body = *any(inTransformerBody).(*B)
slog.Debug("InTransformd body", "body", body)
}
return body, nil
}