-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathbuilder_request.go
More file actions
266 lines (222 loc) · 7.72 KB
/
builder_request.go
File metadata and controls
266 lines (222 loc) · 7.72 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
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
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
package httpclient
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"net/textproto"
"net/url"
"strings"
)
// RequestBuilder provides a fluent interface for building HTTP requests.
// Configure method, URL, headers, body through method chaining.
//
// Not thread-safe. Each instance builds and executes a single request.
type RequestBuilder struct {
builderError error
client Doer
method string
url url.URL
header http.Header
body io.Reader
bodyToMarshal any
bodyMarshaler func(any) ([]byte, error)
overrideFunc RequestOverrideFunc
}
// RequestOverrideFunc modifies an http.Request just before execution.
// Useful for authentication, signatures, or dynamic headers.
//
// Returns modified request and error. Errors cause request failure.
type RequestOverrideFunc func(req *http.Request) (*http.Request, error)
// NewRequest creates a RequestBuilder for the HTTP method and endpoint URL.
//
// Method should be valid (GET, POST, PUT, DELETE, etc.).
// Endpoint should be complete URL (containing scheme, host, endpoint, ...).
//
// Parse errors are captured and returned when Request() or Do() is called.
func NewRequest(method, endpoint string) *RequestBuilder {
builder := &RequestBuilder{
client: http.DefaultClient,
method: method,
header: make(http.Header),
}
endpointURL, err := url.Parse(endpoint)
if err != nil {
builder.builderError = fmt.Errorf("unable to parse endpoint url %q: %v", endpoint, err)
} else {
builder.url = *endpointURL
}
return builder
}
// Client sets the HTTP client for request execution.
// Must implement Doer interface (http.Client does).
// Defaults to http.DefaultClient. Allows custom clients with timeouts, transports.
func (b *RequestBuilder) Client(client Doer) *RequestBuilder {
b.client = client
return b
}
// SetHeader sets HTTP header values, replacing existing ones.
// Header names are canonicalized.
func (b *RequestBuilder) SetHeader(key, value string, values ...string) *RequestBuilder {
key = textproto.CanonicalMIMEHeaderKey(key)
b.header[key] = append([]string{value}, values...)
return b
}
// SetHeaders merges multiple headers, replacing existing values for same keys.
// Other headers remain unchanged.
//
// Equivalent to calling SetHeader for each provided header.
func (b *RequestBuilder) SetHeaders(header http.Header) *RequestBuilder {
for key, values := range header {
b.header[key] = values
}
return b
}
// AddHeader appends values to header, preserving existing ones.
// Header names are canonicalized.
// Creates header if missing, appends if exists.
func (b *RequestBuilder) AddHeader(key, value string, values ...string) *RequestBuilder {
key = textproto.CanonicalMIMEHeaderKey(key)
b.header[key] = append(b.header[key], append([]string{value}, values...)...)
return b
}
// AddHeaders appends all provided header values to existing ones.
// Use SetHeaders() to replace entirely.
func (b *RequestBuilder) AddHeaders(header http.Header) *RequestBuilder {
for key, values := range header {
b.header[key] = append(b.header[key], values...)
}
return b
}
// SetQueryParam sets query parameter values, replacing existing ones.
// Multiple values can be provided for the same parameter.
func (b *RequestBuilder) SetQueryParam(key, value string, values ...string) *RequestBuilder {
query := b.url.Query()
query[key] = append([]string{value}, values...)
b.url.RawQuery = query.Encode()
return b
}
// SetQueryParams merges query parameters, replacing existing ones.
// Does not append; replaces entirely.
func (b *RequestBuilder) SetQueryParams(params url.Values) *RequestBuilder {
query := b.url.Query()
for key, values := range params {
query[key] = values
}
b.url.RawQuery = query.Encode()
return b
}
// AddQueryParam appends values to query parameter, preserving existing ones.
// Creates parameter if missing, appends if exists.
func (b *RequestBuilder) AddQueryParam(key, value string, values ...string) *RequestBuilder {
query := b.url.Query()
for _, value := range append([]string{value}, values...) {
query.Add(key, value)
}
b.url.RawQuery = query.Encode()
return b
}
// AddQueryParams appends all provided parameter values to existing ones.
// Use SetQueryParams() to replace entirely.
func (b *RequestBuilder) AddQueryParams(params url.Values) *RequestBuilder {
query := b.url.Query()
for key, values := range params {
for _, value := range values {
query.Add(key, value)
}
}
b.url.RawQuery = query.Encode()
return b
}
// PathReplacer replaces pattern occurrences in URL path with replacement.
// Keeps URLs readable and searchable.
// Example: NewRequest("PUT", "/users/{userID}/email").PathReplacer("{userID}", userID).
func (b *RequestBuilder) PathReplacer(pattern, replaceWith string) *RequestBuilder {
b.url.Path = strings.ReplaceAll(b.url.Path, pattern, replaceWith)
return b
}
// SendForm sets request body to form values as application/x-www-form-urlencoded.
// Sets appropriate Content-Type header.
//
// Used for HTML forms and form-encoded API endpoints.
func (b *RequestBuilder) SendForm(values url.Values) *RequestBuilder {
b.body = strings.NewReader(values.Encode())
b.SetHeader("Content-Type", "application/x-www-form-urlencoded")
return b
}
// SendJSON sets object as JSON request body and Content-Type header.
//
// Marshaling is lazy - happens during execution, not when called.
// Object must be JSON-serializable. Marshal errors are returned during execution.
func (b *RequestBuilder) SendJSON(obj any) *RequestBuilder {
b.bodyToMarshal = obj
b.bodyMarshaler = json.Marshal
b.SetHeader("Content-Type", "application/json")
return b
}
// Send sets io.Reader as request body with Content-Type: application/octet-stream.
//
// Useful for binary data, file uploads, or custom content.
func (b *RequestBuilder) Send(body io.Reader) *RequestBuilder {
b.body = body
b.SetHeader("Content-Type", "application/octet-stream")
return b
}
// SetOverrideFunc sets function called before request execution.
// Receives built request, returns modified request and error.
// Useful for authentication, signing, dynamic modifications.
func (b *RequestBuilder) SetOverrideFunc(overrideFunc RequestOverrideFunc) *RequestBuilder {
b.overrideFunc = overrideFunc
return b
}
// Request builds and returns the http.Request.
func (b *RequestBuilder) Request(ctx context.Context) (*http.Request, error) {
if b.builderError != nil {
return nil, b.builderError
}
if b.bodyToMarshal != nil {
if b.body != nil {
return nil, errors.New("body to marshal is set but body is already set")
}
if b.bodyMarshaler == nil {
return nil, errors.New("body to marshal is set but body marshaller is unset")
}
raw, err := b.bodyMarshaler(b.bodyToMarshal)
if err != nil {
return nil, fmt.Errorf("unable to marshal body: %w", err)
}
b.body = bytes.NewReader(raw)
}
req, err := http.NewRequestWithContext(ctx, b.method, b.url.String(), b.body)
if err != nil {
return nil, fmt.Errorf("unable to create request %s %s: %w", b.method, b.url.String(), err)
}
for header, value := range b.header {
req.Header[header] = value
}
if b.overrideFunc != nil {
if req, err = b.overrideFunc(req); err != nil {
return nil, fmt.Errorf("unable to override request: %w", err)
}
}
return req, nil
}
// Do builds, executes request and returns ResponseBuilder.
func (b *RequestBuilder) Do(ctx context.Context) *ResponseBuilder {
responseBuilder := newResponse()
req, err := b.Request(ctx)
if err != nil {
responseBuilder.builderError = fmt.Errorf("unable to create request: %w", err)
return responseBuilder
}
resp, err := b.client.Do(req)
if err != nil {
responseBuilder.builderError = fmt.Errorf("unable to execute %s %s request: %w", req.Method, req.URL.String(), err)
return responseBuilder
}
responseBuilder.resp = resp
return responseBuilder
}