github.com/schmorrison/Zoho@v1.1.4/http.go (about)

     1  package zoho
     2  
     3  import (
     4  	"bytes"
     5  	"encoding/json"
     6  	"fmt"
     7  	"io"
     8  	"io/ioutil"
     9  	"mime/multipart"
    10  	"net/http"
    11  	"net/url"
    12  	"os"
    13  	"path/filepath"
    14  	"reflect"
    15  	"strings"
    16  
    17  	"github.com/schmorrison/go-querystring/query"
    18  )
    19  
    20  // Endpoint defines the data required to interact with most Zoho REST api endpoints
    21  type Endpoint struct {
    22  	Method        HTTPMethod
    23  	URL           string
    24  	Name          string
    25  	ResponseData  interface{}
    26  	RequestBody   interface{}
    27  	URLParameters map[string]Parameter
    28  	Headers       map[string]string
    29  	BodyFormat    BodyFormat
    30  	Attachment    string
    31  }
    32  
    33  // Parameter is used to provide URL Parameters to zoho endpoints
    34  type Parameter string
    35  type BodyFormat string
    36  
    37  const (
    38  	JSON        = ""
    39  	JSON_STRING = "jsonString"
    40  	FILE        = "file"
    41  	URL         = "url" // Added new BodyFormat option
    42  )
    43  
    44  // HTTPRequest is the function which actually performs the request to a Zoho endpoint as specified by the provided endpoint
    45  func (z *Zoho) HTTPRequest(endpoint *Endpoint) (err error) {
    46  	if reflect.TypeOf(endpoint.ResponseData).Kind() != reflect.Ptr {
    47  		return fmt.Errorf("Failed, you must pass a pointer in the ResponseData field of endpoint")
    48  	}
    49  
    50  	// Load and renew access token if expired
    51  	err = z.CheckForSavedTokens()
    52  	if err == ErrTokenExpired {
    53  		err := z.RefreshTokenRequest()
    54  		if err != nil {
    55  			return fmt.Errorf("Failed to refresh the access token: %s: %s", endpoint.Name, err)
    56  		}
    57  	}
    58  
    59  	// Retrieve URL parameters
    60  	endpointURL := endpoint.URL
    61  	q := url.Values{}
    62  	for k, v := range endpoint.URLParameters {
    63  		if v != "" {
    64  			q.Set(k, string(v))
    65  		}
    66  	}
    67  
    68  	var (
    69  		req         *http.Request
    70  		reqBody     io.Reader
    71  		contentType string
    72  	)
    73  
    74  	// Has a body, likely a CRUD operation (still possibly JSONString)
    75  	if endpoint.BodyFormat == JSON || endpoint.BodyFormat == JSON_STRING {
    76  		if endpoint.RequestBody != nil {
    77  			// JSON Marshal the body
    78  			marshalledBody, err := json.Marshal(endpoint.RequestBody)
    79  			if err != nil {
    80  				return fmt.Errorf("Failed to create json from request body: %s", err)
    81  			}
    82  
    83  			reqBody = bytes.NewReader(marshalledBody)
    84  		}
    85  		contentType = "application/json; charset=UTF-8"
    86  	}
    87  
    88  	if endpoint.BodyFormat == JSON_STRING || endpoint.BodyFormat == FILE {
    89  		// Create a multipart form
    90  		var b bytes.Buffer
    91  		w := multipart.NewWriter(&b)
    92  
    93  		switch endpoint.BodyFormat {
    94  		case JSON_STRING:
    95  			// Use the form to create the proper field
    96  			fw, err := w.CreateFormField("JSONString")
    97  			if err != nil {
    98  				return err
    99  			}
   100  			// Copy the request body JSON into the field
   101  			if _, err = io.Copy(fw, reqBody); err != nil {
   102  				return err
   103  			}
   104  
   105  			// Close the multipart writer to set the terminating boundary
   106  			err = w.Close()
   107  			if err != nil {
   108  				return err
   109  			}
   110  
   111  		case FILE:
   112  			// Retreive the file contents
   113  			fileReader, err := os.Open(endpoint.Attachment)
   114  			if err != nil {
   115  				return err
   116  			}
   117  			defer fileReader.Close()
   118  			// Create the correct form field
   119  			part, err := w.CreateFormFile("attachment", filepath.Base(endpoint.Attachment))
   120  			if err != nil {
   121  				return err
   122  			}
   123  			// copy the file contents to the form
   124  			if _, err = io.Copy(part, fileReader); err != nil {
   125  				return err
   126  			}
   127  
   128  			err = w.Close()
   129  			if err != nil {
   130  				return err
   131  			}
   132  		}
   133  
   134  		reqBody = &b
   135  		contentType = w.FormDataContentType()
   136  	}
   137  
   138  	// New BodyFormat encoding option
   139  	if endpoint.BodyFormat == URL {
   140  		body, err := query.Values(endpoint.RequestBody) // send struct into the newly imported package
   141  		if err != nil {
   142  			return err
   143  		}
   144  
   145  		reqBody = strings.NewReader(body.Encode()) // write to body
   146  		contentType = "application/x-www-form-urlencoded; charset=UTF-8"
   147  	}
   148  
   149  	req, err = http.NewRequest(string(endpoint.Method), fmt.Sprintf("%s?%s", endpointURL, q.Encode()), reqBody)
   150  	if err != nil {
   151  		return fmt.Errorf("Failed to create a request for %s: %s", endpoint.Name, err)
   152  	}
   153  
   154  	req.Header.Set("Content-Type", contentType)
   155  
   156  	// Add global authorization header
   157  	req.Header.Add("Authorization", "Zoho-oauthtoken "+z.oauth.token.AccessToken)
   158  
   159  	// Add specific endpoint headers
   160  	for k, v := range endpoint.Headers {
   161  		req.Header.Add(k, v)
   162  	}
   163  
   164  	resp, err := z.client.Do(req)
   165  	if err != nil {
   166  		return fmt.Errorf("Failed to perform request for %s: %s", endpoint.Name, err)
   167  	}
   168  
   169  	defer resp.Body.Close()
   170  
   171  	body, err := ioutil.ReadAll(resp.Body)
   172  	if err != nil {
   173  		return fmt.Errorf("Failed to read body of response for %s: got status %s: %s", endpoint.Name, resolveStatus(resp), err)
   174  	}
   175  
   176  	dataType := reflect.TypeOf(endpoint.ResponseData).Elem()
   177  	data := reflect.New(dataType).Interface()
   178  
   179  	if len(body) > 0 { // Avoid failed to unmarshal if there is no result
   180  		err = json.Unmarshal(body, data)
   181  		if err != nil {
   182  			return fmt.Errorf("Failed to unmarshal data from response for %s: got status %s: %s", endpoint.Name, resolveStatus(resp), err)
   183  		}
   184  
   185  		// Search for hidden errors (appears on success response)
   186  		if bytes.Contains(body, []byte(`"status":"error"`)) {
   187  			return fmt.Errorf("%s", string(body))
   188  		}
   189  	}
   190  
   191  	endpoint.ResponseData = data
   192  
   193  	return nil
   194  }
   195  
   196  // HTTPStatusCode is a type for resolving the returned HTTP Status Code Content
   197  type HTTPStatusCode int
   198  
   199  // HTTPStatusCodes is a map of possible HTTP Status Code and Messages
   200  var HTTPStatusCodes = map[HTTPStatusCode]string{
   201  	200: "The API request is successful.",
   202  	201: "Request fulfilled for single record insertion.",
   203  	202: "Request fulfilled for multiple records insertion.",
   204  	204: "There is no content available for the request.",
   205  	304: "The requested page has not been modified. In case \"If-Modified-Since\" header is used for GET APIs",
   206  	400: "The request or the authentication considered is invalid.",
   207  	401: "Invalid API key provided.",
   208  	403: "No permission to do the operation.",
   209  	404: "Invalid request.",
   210  	405: "The specified method is not allowed.",
   211  	413: "The server did not accept the request while uploading a file, since the limited file size has exceeded.",
   212  	415: "The server did not accept the request while uploading a file, since the media/ file type is not supported.",
   213  	429: "Number of API requests per minute/day has exceeded the limit.",
   214  	500: "Generic error that is encountered due to an unexpected server error.",
   215  }
   216  
   217  func resolveStatus(r *http.Response) string {
   218  	if v, ok := HTTPStatusCodes[HTTPStatusCode(r.StatusCode)]; ok {
   219  		return v
   220  	}
   221  	return ""
   222  }
   223  
   224  // HTTPHeader is a type for defining possible HTTPHeaders that zoho request could return
   225  type HTTPHeader string
   226  
   227  const (
   228  	rateLimit          HTTPHeader = "X-RATELIMIT-LIMIT"
   229  	rateLimitRemaining HTTPHeader = "X-RATELIMIT-REMAINING"
   230  	rateLimitReset     HTTPHeader = "X-RATELIMIT-RESET"
   231  )
   232  
   233  func checkHeaders(r http.Response, header HTTPHeader) string {
   234  	value := r.Header.Get(string(header))
   235  
   236  	if value != "" {
   237  		return value
   238  	}
   239  	return ""
   240  }
   241  
   242  // HTTPMethod is a type for defining the possible HTTP request methods that can be used
   243  type HTTPMethod string
   244  
   245  const (
   246  	// HTTPGet is the GET method for http requests
   247  	HTTPGet HTTPMethod = "GET"
   248  	// HTTPPost is the POST method for http requests
   249  	HTTPPost HTTPMethod = "POST"
   250  	// HTTPPut is the PUT method for http requests
   251  	HTTPPut HTTPMethod = "PUT"
   252  	// HTTPDelete is the DELETE method for http requests
   253  	HTTPDelete HTTPMethod = "DELETE"
   254  )