-
-
Notifications
You must be signed in to change notification settings - Fork 23
/
Copy pathhttpin.go
187 lines (168 loc) · 6.98 KB
/
httpin.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
// Package httpin helps decoding an HTTP request to a custom struct by binding
// data with querystring (query params), HTTP headers, form data, JSON/XML
// payloads, URL path params, and file uploads (multipart/form-data).
package httpin
import (
"context"
"errors"
"io"
"net/http"
"reflect"
"github.com/ggicci/httpin/core"
"github.com/ggicci/httpin/internal"
)
type contextKey int
const (
// Input is the key to get the input object from Request.Context() injected by httpin. e.g.
//
// input := r.Context().Value(httpin.Input).(*InputStruct)
Input contextKey = iota
)
// Option is a collection of options for creating a Core instance.
var Option coreOptions = coreOptions{
WithErrorHandler: core.WithErrorHandler,
WithMaxMemory: core.WithMaxMemory,
WithNestedDirectivesEnabled: core.WithNestedDirectivesEnabled,
}
// New calls core.New to create a new Core instance. Which is responsible for both:
//
// - decoding an HTTP request to an instance of the inputStruct;
// - and encoding an instance of the inputStruct to an HTTP request.
//
// Note that the Core instance is bound to the given specific type, it will not
// work for other types. If you want to decode/encode other types, you need to
// create another Core instance. Or directly use the following functions, which are
// just shortcuts of Core's methods, so you don't need to create a Core instance:
// - httpin.Decode(): decode an HTTP request to an instance of the inputStruct.
// - httpin.NewRequest() to encode an instance of the inputStruct to an HTTP request.
//
// For best practice, we would recommend using httpin.NewInput() to create an
// HTTP middleware for a specific input type. The middleware can be bound to an
// API, chained with other middlewares, and also reused in other APIs. You even
// don't need to call the Deocde() method explicitly, the middleware will do it
// for you and put the decoded instance to the request's context.
func New(inputStruct any, opts ...core.Option) (*core.Core, error) {
return core.New(inputStruct, opts...)
}
// File is the builtin type of httpin to manupulate file uploads. On the server
// side, it is used to represent a file in a multipart/form-data request. On the
// client side, it is used to represent a file to be uploaded.
type File = core.File
// UploadFile is a helper function to create a File instance from a file path.
// It is useful when you want to upload a file from the local file system.
func UploadFile(path string) *File {
return core.UploadFile(path)
}
// UploadStream is a helper function to create a File instance from a io.Reader. It
// is useful when you want to upload a file from a stream.
func UploadStream(r io.ReadCloser) *File {
return core.UploadStream(r)
}
// DecodeTo decodes an HTTP request and populates input with data from the HTTP request.
// The input must be a pointer to a struct instance. For example:
//
// input := &InputStruct{}
// if err := DecodeTo(req, input); err != nil { ... }
//
// input is now populated with data from the request.
func DecodeTo(req *http.Request, input any, opts ...core.Option) error {
co, err := New(internal.DereferencedType(input), opts...)
if err != nil {
return err
}
return co.DecodeTo(req, input)
}
// Decode decodes an HTTP request to an instance of T and returns its pointer
// (*T). T must be a struct type. For example:
//
// if user, err := Decode[User](req); err != nil { ... }
// // now user is a *User instance, which has been populated with data from the request.
func Decode[T any](req *http.Request, opts ...core.Option) (*T, error) {
rt := internal.TypeOf[T]()
if rt.Kind() != reflect.Struct {
return nil, errors.New("generic type T must be a struct type")
}
co, err := New(rt, opts...)
if err != nil {
return nil, err
}
if v, err := co.Decode(req); err != nil {
return nil, err
} else {
return v.(*T), nil
}
}
// NewRequest wraps NewRequestWithContext using context.Background(), see NewRequestWithContext.
func NewRequest(method, url string, input any, opts ...core.Option) (*http.Request, error) {
return NewRequestWithContext(context.Background(), method, url, input)
}
// NewRequestWithContext turns the given input into an HTTP request. The input
// must be a struct instance. And its fields' "in" tags define how to bind the
// data from the struct to the HTTP request. Use it as the replacement of
// http.NewRequest().
//
// addUserPayload := &AddUserRequest{...}
// addUserRequest, err := NewRequestWithContext(context.Background(), "GET", "http://example.com", addUserPayload)
// http.DefaultClient.Do(addUserRequest)
func NewRequestWithContext(ctx context.Context, method, url string, input any, opts ...core.Option) (*http.Request, error) {
co, err := New(input, opts...)
if err != nil {
return nil, err
}
return co.NewRequestWithContext(ctx, method, url, input)
}
// NewInput creates an HTTP middleware handler. Which is a function that takes
// in an http.Handler and returns another http.Handler.
//
// The middleware created by NewInput is to add the decoding function to an
// existing http.Handler. This functionality will decode the HTTP request into a
// struct instance and put its pointer to the request's context. So that the
// next hop can get the decoded struct instance from the request's context.
//
// We recommend using https://github.com/justinas/alice to chain your
// middlewares. If you're using some popular web frameworks, they may have
// already provided a middleware chaining mechanism.
//
// For example:
//
// type ListUsersRequest struct {
// Page int `in:"query=page,page_index,index"`
// PerPage int `in:"query=per_page,page_size"`
// }
//
// func ListUsersHandler(rw http.ResponseWriter, r *http.Request) {
// input := r.Context().Value(httpin.Input).(*ListUsersRequest)
// // ...
// }
//
// func init() {
// http.Handle("/users", alice.New(httpin.NewInput(&ListUsersRequest{})).ThenFunc(ListUsersHandler))
// }
func NewInput(inputStruct any, opts ...core.Option) func(http.Handler) http.Handler {
co, err := New(inputStruct, opts...)
internal.PanicOnError(err)
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
// Here we read the request and decode it to fill our structure.
// Once failed, the request should end here.
input, err := co.Decode(r)
if err != nil {
co.GetErrorHandler()(rw, r, err)
return
}
// We put the `input` to the request's context, and it will pass to the next hop.
ctx := context.WithValue(r.Context(), Input, input)
next.ServeHTTP(rw, r.WithContext(ctx))
})
}
}
type coreOptions struct {
// WithErrorHandler overrides the default error handler.
WithErrorHandler func(core.ErrorHandler) core.Option
// WithMaxMemory overrides the default maximum memory size (32MB) when reading
// the request body. See https://pkg.go.dev/net/http#Request.ParseMultipartForm
// for more details.
WithMaxMemory func(int64) core.Option
// WithNestedDirectivesEnabled enables/disables nested directives.
WithNestedDirectivesEnabled func(bool) core.Option
}