github.com/IBM-Cloud/bluemix-go@v0.0.0-20240423071914-9e96525baef4/rest/request.go (about) 1 package rest 2 3 import ( 4 "bytes" 5 "encoding/json" 6 "fmt" 7 "io" 8 "mime/multipart" 9 "net/http" 10 "net/textproto" 11 "net/url" 12 "strings" 13 ) 14 15 const ( 16 contentType = "Content-Type" 17 jsonContentType = "application/json" 18 formUrlEncodedContentType = "application/x-www-form-urlencoded" 19 ) 20 21 // File represents a file upload in the POST request 22 type File struct { 23 // File name 24 Name string 25 // File content 26 Content io.Reader 27 // Mime type, defaults to "application/octet-stream" 28 Type string 29 } 30 31 // Request is a REST request. It also acts like a HTTP request builder. 32 type Request struct { 33 method string 34 rawUrl string 35 header http.Header 36 37 queryParams url.Values 38 formParams url.Values 39 40 // files to upload 41 files map[string][]File 42 43 // custom request body 44 body interface{} 45 } 46 47 // NewRequest creates a new REST request with the given rawUrl. 48 func NewRequest(rawUrl string) *Request { 49 return &Request{ 50 rawUrl: rawUrl, 51 header: http.Header{}, 52 queryParams: url.Values{}, 53 formParams: url.Values{}, 54 files: make(map[string][]File), 55 } 56 } 57 58 // Method sets HTTP method of the request. 59 func (r *Request) Method(method string) *Request { 60 r.method = method 61 return r 62 } 63 64 // GetRequest creates a REST request with GET method and the given rawUrl. 65 func GetRequest(rawUrl string) *Request { 66 return NewRequest(rawUrl).Method("GET") 67 } 68 69 // HeadRequest creates a REST request with HEAD method and the given rawUrl. 70 func HeadRequest(rawUrl string) *Request { 71 return NewRequest(rawUrl).Method("HEAD") 72 } 73 74 // PostRequest creates a REST request with POST method and the given rawUrl. 75 func PostRequest(rawUrl string) *Request { 76 return NewRequest(rawUrl).Method("POST") 77 } 78 79 // PutRequest creates a REST request with PUT method and the given rawUrl. 80 func PutRequest(rawUrl string) *Request { 81 return NewRequest(rawUrl).Method("PUT") 82 } 83 84 // DeleteRequest creates a REST request with DELETE method and the given 85 // rawUrl. 86 func DeleteRequest(rawUrl string) *Request { 87 return NewRequest(rawUrl).Method("DELETE") 88 } 89 90 // PatchRequest creates a REST request with PATCH method and the given 91 // rawUrl. 92 func PatchRequest(rawUrl string) *Request { 93 return NewRequest(rawUrl).Method("PATCH") 94 } 95 96 // Creates a request with HTTP OPTIONS. 97 func OptionsRequest(rawUrl string) *Request { 98 return NewRequest(rawUrl).Method("OPTIONS") 99 } 100 101 // Add adds the key, value pair to the request header. It appends to any 102 // existing values associated with key. 103 func (r *Request) Add(key string, value string) *Request { 104 r.header.Add(http.CanonicalHeaderKey(key), value) 105 return r 106 } 107 108 // Del deletes the header as specified by the key. 109 func (r *Request) Del(key string) *Request { 110 r.header.Del(http.CanonicalHeaderKey(key)) 111 return r 112 } 113 114 // Set sets the header entries associated with key to the single element value. 115 // It replaces any existing values associated with key. 116 func (r *Request) Set(key string, value string) *Request { 117 r.header.Set(http.CanonicalHeaderKey(key), value) 118 return r 119 } 120 121 // Query appends the key, value pair to the request query which will be 122 // encoded as url query parameters on HTTP request's url. 123 func (r *Request) Query(key string, value string) *Request { 124 r.queryParams.Add(key, value) 125 return r 126 } 127 128 // Field appends the key, value pair to the form fields in the POST request. 129 func (r *Request) Field(key string, value string) *Request { 130 r.formParams.Add(key, value) 131 return r 132 } 133 134 // File appends a file upload item in the POST request. The file content will 135 // be consumed when building HTTP request (see Build()) and closed if it's 136 // also a ReadCloser type. 137 func (r *Request) File(name string, file File) *Request { 138 r.files[name] = append(r.files[name], file) 139 return r 140 } 141 142 // Body sets the request body. Accepted types are string, []byte, io.Reader, 143 // or structs to be JSON encodeded. 144 func (r *Request) Body(body interface{}) *Request { 145 r.body = body 146 return r 147 } 148 149 // Build builds a HTTP request according to the settings in the REST request. 150 func (r *Request) Build() (*http.Request, error) { 151 url, err := r.buildURL() 152 if err != nil { 153 return nil, err 154 } 155 156 body, err := r.buildBody() 157 if err != nil { 158 return nil, err 159 } 160 161 req, err := http.NewRequest(r.method, url, body) 162 if err != nil { 163 return req, err 164 } 165 166 for k, vs := range r.header { 167 for _, v := range vs { 168 req.Header.Add(k, v) 169 } 170 } 171 172 return req, nil 173 } 174 175 func (r *Request) buildURL() (string, error) { 176 if r.rawUrl == "" || len(r.queryParams) == 0 { 177 return r.rawUrl, nil 178 } 179 u, err := url.Parse(r.rawUrl) 180 if err != nil { 181 return "", err 182 } 183 q := u.Query() 184 for k, vs := range r.queryParams { 185 for _, v := range vs { 186 q.Add(k, v) 187 } 188 } 189 u.RawQuery = q.Encode() 190 return u.String(), nil 191 } 192 193 func (r *Request) buildBody() (io.Reader, error) { 194 if len(r.files) > 0 { 195 return r.buildFormMultipart() 196 } 197 198 if len(r.formParams) > 0 { 199 return r.buildFormFields() 200 } 201 202 return r.buildCustomBody() 203 } 204 205 func (r *Request) buildFormMultipart() (io.Reader, error) { 206 b := new(bytes.Buffer) 207 w := multipart.NewWriter(b) 208 defer w.Close() 209 210 for k, files := range r.files { 211 for _, f := range files { 212 defer func() { 213 if f, ok := f.Content.(io.ReadCloser); ok { 214 f.Close() 215 } 216 }() 217 218 p, err := createPartWriter(w, k, f) 219 if err != nil { 220 return nil, err 221 } 222 _, err = io.Copy(p, f.Content) 223 if err != nil { 224 return nil, err 225 } 226 } 227 } 228 229 for k, vs := range r.formParams { 230 for _, v := range vs { 231 err := w.WriteField(k, v) 232 if err != nil { 233 return nil, err 234 } 235 } 236 } 237 238 r.header.Set(contentType, w.FormDataContentType()) 239 return b, nil 240 } 241 242 func createPartWriter(w *multipart.Writer, fieldName string, f File) (io.Writer, error) { 243 h := make(textproto.MIMEHeader) 244 h.Set("Content-Disposition", 245 fmt.Sprintf(`form-data; name="%s"; filename="%s"`, 246 escapeQuotes(fieldName), escapeQuotes(f.Name))) 247 if f.Type != "" { 248 h.Set("Content-Type", f.Type) 249 } else { 250 h.Set("Content-Type", "application/octet-stream") 251 } 252 return w.CreatePart(h) 253 } 254 255 var quoteEscaper = strings.NewReplacer("\\", "\\\\", `"`, "\\\"") 256 257 func escapeQuotes(s string) string { 258 return quoteEscaper.Replace(s) 259 } 260 261 func (r *Request) buildFormFields() (io.Reader, error) { 262 r.header.Set(contentType, formUrlEncodedContentType) 263 return strings.NewReader(r.formParams.Encode()), nil 264 } 265 266 func (r *Request) buildCustomBody() (io.Reader, error) { 267 if r.body == nil { 268 return nil, nil 269 } 270 271 switch b := r.body; b.(type) { 272 case string: 273 return strings.NewReader(b.(string)), nil 274 case []byte: 275 return bytes.NewReader(b.([]byte)), nil 276 case io.Reader: 277 return b.(io.Reader), nil 278 default: 279 raw, err := json.Marshal(b) 280 if err != nil { 281 return nil, fmt.Errorf("Invalid JSON request: %v", err) 282 } 283 return bytes.NewReader(raw), nil 284 } 285 }