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  }