github.com/plutov/paypal/v4@v4.7.1/client.go (about)

     1  package paypal
     2  
     3  import (
     4  	"bytes"
     5  	"context"
     6  	"encoding/json"
     7  	"errors"
     8  	"fmt"
     9  	"io"
    10  	"net/http"
    11  	"net/http/httputil"
    12  	"time"
    13  )
    14  
    15  // NewClient returns new Client struct
    16  // APIBase is a base API URL, for testing you can use paypal.APIBaseSandBox
    17  func NewClient(clientID string, secret string, APIBase string) (*Client, error) {
    18  	if clientID == "" || secret == "" || APIBase == "" {
    19  		return nil, errors.New("ClientID, Secret and APIBase are required to create a Client")
    20  	}
    21  
    22  	return &Client{
    23  		Client:   &http.Client{},
    24  		ClientID: clientID,
    25  		Secret:   secret,
    26  		APIBase:  APIBase,
    27  	}, nil
    28  }
    29  
    30  // GetAccessToken returns struct of TokenResponse
    31  // No need to call SetAccessToken to apply new access token for current Client
    32  // Endpoint: POST /v1/oauth2/token
    33  func (c *Client) GetAccessToken(ctx context.Context) (*TokenResponse, error) {
    34  	buf := bytes.NewBuffer([]byte("grant_type=client_credentials"))
    35  	req, err := http.NewRequestWithContext(ctx, "POST", fmt.Sprintf("%s%s", c.APIBase, "/v1/oauth2/token"), buf)
    36  	if err != nil {
    37  		return &TokenResponse{}, err
    38  	}
    39  
    40  	req.Header.Set("Content-type", "application/x-www-form-urlencoded")
    41  
    42  	response := &TokenResponse{}
    43  	err = c.SendWithBasicAuth(req, response)
    44  
    45  	// Set Token fur current Client
    46  	if response.Token != "" {
    47  		c.Token = response
    48  		c.tokenExpiresAt = time.Now().Add(time.Duration(response.ExpiresIn) * time.Second)
    49  	}
    50  
    51  	return response, err
    52  }
    53  
    54  // SetHTTPClient sets *http.Client to current client
    55  func (c *Client) SetHTTPClient(client *http.Client) {
    56  	c.Client = client
    57  }
    58  
    59  // SetAccessToken sets saved token to current client
    60  func (c *Client) SetAccessToken(token string) {
    61  	c.Token = &TokenResponse{
    62  		Token: token,
    63  	}
    64  	c.tokenExpiresAt = time.Time{}
    65  }
    66  
    67  // SetLog will set/change the output destination.
    68  // If log file is set paypal will log all requests and responses to this Writer
    69  func (c *Client) SetLog(log io.Writer) {
    70  	c.Log = log
    71  }
    72  
    73  // SetReturnRepresentation enables verbose response
    74  // Verbose response: https://developer.paypal.com/docs/api/orders/v2/#orders-authorize-header-parameters
    75  func (c *Client) SetReturnRepresentation() {
    76  	c.returnRepresentation = true
    77  }
    78  
    79  // Send makes a request to the API, the response body will be
    80  // unmarshalled into v, or if v is an io.Writer, the response will
    81  // be written to it without decoding
    82  func (c *Client) Send(req *http.Request, v interface{}) error {
    83  	var (
    84  		err  error
    85  		resp *http.Response
    86  		data []byte
    87  	)
    88  
    89  	// Set default headers
    90  	req.Header.Set("Accept", "application/json")
    91  	req.Header.Set("Accept-Language", "en_US")
    92  
    93  	// Default values for headers
    94  	if req.Header.Get("Content-type") == "" {
    95  		req.Header.Set("Content-type", "application/json")
    96  	}
    97  	if c.returnRepresentation {
    98  		req.Header.Set("Prefer", "return=representation")
    99  	}
   100  
   101  	resp, err = c.Client.Do(req)
   102  	c.log(req, resp)
   103  
   104  	if err != nil {
   105  		return err
   106  	}
   107  	defer func(Body io.ReadCloser) error {
   108  		return Body.Close()
   109  	}(resp.Body)
   110  
   111  	if resp.StatusCode < 200 || resp.StatusCode > 299 {
   112  		errResp := &ErrorResponse{Response: resp}
   113  		data, err = io.ReadAll(resp.Body)
   114  
   115  		if err == nil && len(data) > 0 {
   116  			err := json.Unmarshal(data, errResp)
   117  			if err != nil {
   118  				return err
   119  			}
   120  		}
   121  
   122  		return errResp
   123  	}
   124  	if v == nil {
   125  		return nil
   126  	}
   127  
   128  	if w, ok := v.(io.Writer); ok {
   129  		_, err := io.Copy(w, resp.Body)
   130  		return err
   131  	}
   132  
   133  	return json.NewDecoder(resp.Body).Decode(v)
   134  }
   135  
   136  // SendWithAuth makes a request to the API and apply OAuth2 header automatically.
   137  // If the access token soon to be expired or already expired, it will try to get a new one before
   138  // making the main request
   139  // client.Token will be updated when changed
   140  func (c *Client) SendWithAuth(req *http.Request, v interface{}) error {
   141  	// c.Lock()
   142  	c.mu.Lock()
   143  	// Note: Here we do not want to `defer c.Unlock()` because we need `c.Send(...)`
   144  	// to happen outside of the locked section.
   145  
   146  	if c.Token == nil || (!c.tokenExpiresAt.IsZero() && time.Until(c.tokenExpiresAt) < RequestNewTokenBeforeExpiresIn) {
   147  		// c.Token will be updated in GetAccessToken call
   148  		if _, err := c.GetAccessToken(req.Context()); err != nil {
   149  			// c.Unlock()
   150  			c.mu.Unlock()
   151  			return err
   152  		}
   153  	}
   154  
   155  	req.Header.Set("Authorization", "Bearer "+c.Token.Token)
   156  	// Unlock the client mutex before sending the request, this allows multiple requests
   157  	// to be in progress at the same time.
   158  	// c.Unlock()
   159  	c.mu.Unlock()
   160  	return c.Send(req, v)
   161  }
   162  
   163  // SendWithBasicAuth makes a request to the API using clientID:secret basic auth
   164  func (c *Client) SendWithBasicAuth(req *http.Request, v interface{}) error {
   165  	req.SetBasicAuth(c.ClientID, c.Secret)
   166  
   167  	return c.Send(req, v)
   168  }
   169  
   170  // NewRequest constructs a request
   171  // Convert payload to a JSON
   172  func (c *Client) NewRequest(ctx context.Context, method, url string, payload interface{}) (*http.Request, error) {
   173  	var buf io.Reader
   174  	if payload != nil {
   175  		b, err := json.Marshal(&payload)
   176  		if err != nil {
   177  			return nil, err
   178  		}
   179  		buf = bytes.NewBuffer(b)
   180  	}
   181  	return http.NewRequestWithContext(ctx, method, url, buf)
   182  }
   183  
   184  // log will dump request and response to the log file
   185  func (c *Client) log(r *http.Request, resp *http.Response) {
   186  	if c.Log != nil {
   187  		var (
   188  			reqDump  string
   189  			respDump []byte
   190  		)
   191  
   192  		if r != nil {
   193  			reqDump = fmt.Sprintf("%s %s. Data: %s", r.Method, r.URL.String(), r.Form.Encode())
   194  		}
   195  		if resp != nil {
   196  			respDump, _ = httputil.DumpResponse(resp, true)
   197  		}
   198  
   199  		c.Log.Write([]byte(fmt.Sprintf("Request: %s\nResponse: %s\n", reqDump, string(respDump))))
   200  	}
   201  }