-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathbind.go
More file actions
182 lines (165 loc) · 6.23 KB
/
bind.go
File metadata and controls
182 lines (165 loc) · 6.23 KB
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
package hx
import (
"errors"
"net/http"
"github.com/go-playground/validator/v10"
"github.com/struct0x/hx/internal/bind"
)
type BindOpt = bind.Opt
// WithPathValueFunc overrides the default way of extracting a path
// parameter from the request. The function receives the request and the
// name of the path variable and must return the value (or the empty string
// if the variable is not present).
func WithPathValueFunc(fn func(r *http.Request, name string) string) BindOpt {
return bind.WithPathValueFunc(fn)
}
// WithMaxFormMemoryMB configures the maximum size of multipart form data that will reside in memory.
// The rest of the data will be stored on disk in temporary files.
// This option is used when binding multipart form data and file uploads.
func WithMaxFormMemoryMB(maxFormMemoryMB int64) BindOpt {
return bind.WithMaxFormMemoryMB(maxFormMemoryMB)
}
// WithValidator configures a custom validator.Validate instance to be used for request validation.
// The validator will be used to validate struct fields with "validate" tags after binding.
// If not provided, a default validator will be used.
func WithValidator(v *validator.Validate) BindOpt {
return bind.WithValidator(v)
}
// Bind extracts data from an HTTP request into a destination struct.
// It supports binding from multiple sources including URL query parameters, path variables,
// headers, cookies, JSON body, and multipart file uploads.
//
// The destination must be a pointer to a struct. Fields in the struct are bound based on
// struct tags that specify the data source and field name:
//
// - `query:"name"` - binds from URL query parameters
// - `path:"name"` - binds from URL path variables
// - `header:"Name"` - binds from HTTP headers
// - `cookie:"name"` - binds from HTTP cookies
// - `json:"name"` - binds from JSON request body (application/json)
// - `file:"name"` - binds file uploads from multipart/form-data
//
// Supported field types include:
// - Basic types: string, bool, int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64, float32, float64
// - Slices of basic types (for multiple values)
// - Slices of strings (for headers and query parameters with multiple values)
// - http.Cookie (for cookie fields)
// - multipart.FileHeader (for single file uploads)
// - []*multipart.FileHeader (for multiple file uploads)
// - Types implementing encoding.TextUnmarshaler
// - Any type for json-tagged fields (unmarshaled via encoding/json)
//
// Options:
// - WithPathValueFunc: provides a custom function to extract path variables from the request.
// By default, uses http.Request.PathValue.
// - WithMaxFormMemoryMB: sets the maximum memory in megabytes for parsing multipart forms.
// Defaults to 32 MB if not specified.
// - WithValidator: sets a custom validator.Validate instance to be used for request validation.
// By default, a default validator is used.
//
// Returns:
// - nil if all fields are successfully bound
// - error if any field fails to bind, use BindProblem to create a structured error response
//
// Example usage:
//
// type UserRequest struct {
// ID int `path:"id"`
// Name string `json:"name"`
// Email string `json:"email"`
// Tags []string `query:"tags"`
// AuthToken string `header:"Authorization"`
// }
//
// func handler(ctx context.Context, r *http.Request) error {
// var req UserRequest
// if err := hx.Bind(r, &req); err != nil {
// return BindProblem(err, "Invalid user request",
// WithTypeURI("https://example.com/errors/invalid-user"))
// }
// // Use req...
// return nil
// }
func Bind[T any](r *http.Request, dst *T, opts ...BindOpt) error {
return bind.Bind(r, dst, opts...)
}
// BindProblem creates a structured error response by wrapping binding errors into a ProblemDetails.
// It takes an error from Bind, a summary message, and optional ProblemOpt options.
//
// The function handles different types of binding errors:
// - Structural errors (nil request, nil destination, invalid types) return 500 Internal Server Error
// - Validation errors return 400 Bad Request with detailed field errors in the extensions
// - Other errors return 500 Internal Server Error
//
// For validation errors, the response includes an "errors" field in the extensions containing
// an array of objects with "field" and "detail" properties for each validation error.
//
// Allowed ProblemOpt options:
// - WithTypeURI sets the Type field of ProblemDetails.
// - WithDetail sets the Detail field of ProblemDetails.
// - WithField adds a single field to the Extensions map of ProblemDetails.
// - WithFields sets multiple fields at once.
// - WithInstance sets the Instance field of ProblemDetails.
// Note: WithCause option is automatically added and will be ignored if provided manually.
//
// Example usage:
//
// var req UserRequest
// if err := Bind(r, &req); err != nil {
// return BindProblem(err, "Invalid user request",
// WithTypeURI("https://example.com/errors/invalid-user"))
// }
func BindProblem(err error, summary string, opts ...ProblemOpt) error {
pOpts := make([]ProblemOpt, len(opts), len(opts)+2)
copy(pOpts, opts)
pOpts = append(pOpts,
WithCause(err),
)
switch {
case errors.Is(err, bind.ErrNilRequest),
errors.Is(err, bind.ErrNilDestination),
errors.Is(err, bind.ErrNotAStruct),
errors.Is(err, bind.ErrExpectedStruct),
errors.Is(err, bind.ErrMultipleTags),
errors.Is(err, bind.ErrEmptyTag):
return Problem(
http.StatusInternalServerError,
summary,
pOpts...,
)
}
var bindErrs *bind.Errors
if errors.As(err, &bindErrs) {
fields := make([]struct {
Field string `json:"field"`
Detail string `json:"detail"`
}, 0, len(bindErrs.Errors))
for _, e := range bindErrs.Errors {
detail := e.Err.Error()
var fieldErr validator.FieldError
if errors.As(e.Err, &fieldErr) {
detail = fieldErr.ActualTag()
}
fields = append(fields, struct {
Field string `json:"field"`
Detail string `json:"detail"`
}{
Field: e.Field,
Detail: detail,
})
}
pOpts = append(pOpts,
WithField(F("errors", fields)),
)
return Problem(
http.StatusBadRequest,
summary,
pOpts...,
)
}
return Problem(
http.StatusInternalServerError,
summary,
pOpts...,
)
}