-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathhttpclt.go
More file actions
154 lines (135 loc) · 4.47 KB
/
httpclt.go
File metadata and controls
154 lines (135 loc) · 4.47 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
/*
* Copyright 2025 The Go-Spring Authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package httpclt
import (
"bytes"
"context"
"io"
"maps"
"net/http"
"github.com/go-spring/stdlib/jsonflow"
)
// QueryStringer defines the method to convert an object to a query string
// format (e.g., "key1=value1&key2=value2").
type QueryStringer interface {
QueryForm() (string, error)
}
// Metadata holds contextual information for an HTTP request.
type Metadata struct {
Target string // Service name or IP:PORT
Schema string // HTTP protocol (e.g., http, https)
Method string // HTTP method (GET, POST, etc.)
Pattern string // Request path, usually with placeholders (for REST)
RawPath string // Request path with placeholders processed
Query QueryStringer // Query string after the '?' part
Body any // Request body
Header http.Header // Request headers
Config map[string]string // Additional configuration options
}
// RequestOption is a function that modifies the Metadata.
type RequestOption func(meta *Metadata)
// CombineMetadata applies the given RequestOptions to the Metadata.
func CombineMetadata(meta Metadata, opts ...RequestOption) Metadata {
for _, opt := range opts {
opt(&meta)
}
return meta
}
// WithHeader is a RequestOption that adds the given HTTP headers to the Metadata.
func WithHeader(header http.Header) RequestOption {
return func(meta *Metadata) {
if meta.Header == nil {
meta.Header = http.Header{}
}
maps.Copy(meta.Header, header)
}
}
// WithConfig is a RequestOption that adds the given configuration map to the Metadata.
func WithConfig(config map[string]string) RequestOption {
return func(meta *Metadata) {
if meta.Config == nil {
meta.Config = map[string]string{}
}
maps.Copy(meta.Config, config)
}
}
// DoRequest is a function that performs the actual HTTP request.
var DoRequest = func(req *http.Request, meta Metadata, fn func(io.Reader) error) (*http.Response, error) {
resp, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
}
defer func() {
_ = resp.Body.Close()
}()
if err = fn(resp.Body); err != nil {
return nil, err
}
return resp, nil
}
// doRequest executes the HTTP request, preparing the body and metadata before sending the request.
func doRequest(ctx context.Context, meta Metadata, fn func(io.Reader) error) (*http.Response, error) {
if s, err := meta.Query.QueryForm(); err != nil {
return nil, err
} else if s != "" {
meta.RawPath += "?" + s
}
buf := bytes.NewBuffer(nil)
if v, ok := meta.Body.(interface{ EncodeForm() (string, error) }); ok {
if s, err := v.EncodeForm(); err != nil {
return nil, err
} else if s != "" {
buf.WriteString(s)
}
} else if meta.Body != nil {
if err := jsonflow.MarshalWrite(buf, meta.Body); err != nil {
return nil, err
}
}
req, err := http.NewRequestWithContext(ctx, meta.Method, meta.RawPath, buf)
if err != nil {
return nil, err
}
req.Host = meta.Target
req.URL.Host = meta.Target
req.URL.Scheme = meta.Schema
maps.Copy(req.Header, meta.Header)
return DoRequest(req, meta, fn)
}
// ResponseObject is an interface that can decode the response body from JSON using streaming.
type ResponseObject interface {
DecodeJSON(d jsonflow.Decoder) error
}
// ObjectResponse decodes the response body into a given object using streaming JSON parsing.
func ObjectResponse[T ResponseObject](ctx context.Context, o T, meta Metadata) (*http.Response, T, error) {
resp, err := doRequest(ctx, meta, func(r io.Reader) error {
return o.DecodeJSON(jsonflow.NewDecoder(r))
})
if err != nil {
return nil, o, err
}
return resp, o, nil
}
// JSONResponse decodes the response body into a generic JSON object.
func JSONResponse[T any](ctx context.Context, meta Metadata) (_ *http.Response, o T, _ error) {
resp, err := doRequest(ctx, meta, func(r io.Reader) error {
return jsonflow.UnmarshalRead(r, &o)
})
if err != nil {
return nil, o, err
}
return resp, o, nil
}