From 795adf76a4a3b1eea5e187433033ff87e35bf22a Mon Sep 17 00:00:00 2001 From: Roel Schut Date: Thu, 20 Jul 2023 15:34:36 +0200 Subject: [PATCH] Add method to retrieve parcel documents Because this endpoints returns binary data representing a pdf/zpl/png, the Request method could not be used in its current form. Therefore I split the method into two methods, one for creating a new request (NewRequest) and one for validating the response from the api (ValidateResponse), while keeping Request backwards compatible. - add NewRequest function - add ValidateResponse function - add Document struct - add GetDocument method to parcel.Client --- README.md | 1 + parcel.go | 30 ++++++++++++++++ parcel/client.go | 49 +++++++++++++++++++++++--- sendcloud.go | 91 ++++++++++++++++++++++++++++++++++-------------- 4 files changed, 140 insertions(+), 31 deletions(-) diff --git a/README.md b/README.md index dc82238..f322dd7 100644 --- a/README.md +++ b/README.md @@ -7,6 +7,7 @@ An API-client for Sendcloud written in Golang. This package currently supports: - parcels +- parcel documents - labels - methods - addresses diff --git a/parcel.go b/parcel.go index 1d207b4..d619eb7 100644 --- a/parcel.go +++ b/parcel.go @@ -316,3 +316,33 @@ func (l *LabelData) SetResponse(body []byte) error { *l = body return nil } + +// DocumentFormat is any of the formats a Document can be in. +type DocumentFormat string + +func (df DocumentFormat) String() string { return string(df) } + +func (df DocumentFormat) Name() string { + switch df { + case DocumentPdf: + return "pdf" + case DocumentZpl: + return "zpl" + case DocumentPng: + return "png" + default: + return "unknown" + } +} + +const ( + DocumentPdf DocumentFormat = "application/pdf" + DocumentZpl DocumentFormat = "application/zpl" + DocumentPng DocumentFormat = "image/png" +) + +// Document represents a document file that can be downloaded from the api. +type Document struct { + Format DocumentFormat + Body []byte +} diff --git a/parcel/client.go b/parcel/client.go index 1453b64..6b247ba 100644 --- a/parcel/client.go +++ b/parcel/client.go @@ -1,13 +1,17 @@ package parcel import ( + "context" "crypto/hmac" "crypto/sha256" "encoding/hex" "encoding/json" "errors" sendcloud "github.com/afosto/sendcloud-go" + "io/ioutil" + "net/http" "strconv" + "time" ) type Client struct { @@ -22,7 +26,7 @@ func New(apiKey string, apiSecret string) *Client { } } -//Create a new parcel +// Create a new parcel func (c *Client) New(params *sendcloud.ParcelParams) (*sendcloud.Parcel, error) { parcel := sendcloud.ParcelResponseContainer{} err := sendcloud.Request("POST", "/api/v2/parcels", params, c.apiKey, c.apiSecret, &parcel) @@ -34,7 +38,7 @@ func (c *Client) New(params *sendcloud.ParcelParams) (*sendcloud.Parcel, error) return r, nil } -//Return a single parcel +// Return a single parcel func (c *Client) Get(parcelID int64) (*sendcloud.Parcel, error) { parcel := sendcloud.ParcelResponseContainer{} err := sendcloud.Request("GET", "/api/v2/parcels/"+strconv.Itoa(int(parcelID)), nil, c.apiKey, c.apiSecret, &parcel) @@ -46,7 +50,7 @@ func (c *Client) Get(parcelID int64) (*sendcloud.Parcel, error) { return r, nil } -//Get a label as bytes based on the url that references the PDF +// Get a label as bytes based on the url that references the PDF func (c *Client) GetLabel(labelURL string) ([]byte, error) { data := &sendcloud.LabelData{} err := sendcloud.Request("GET", labelURL, nil, c.apiKey, c.apiSecret, data) @@ -56,7 +60,7 @@ func (c *Client) GetLabel(labelURL string) ([]byte, error) { return *data, nil } -//Validate and read the incoming webhook +// Validate and read the incoming webhook func (c *Client) ReadParcelWebhook(payload []byte, signature string) (*sendcloud.Parcel, error) { hash := hmac.New(sha256.New, []byte(c.apiSecret)) hash.Write(payload) @@ -74,3 +78,40 @@ func (c *Client) ReadParcelWebhook(payload []byte, signature string) (*sendcloud return parcelResponse.GetResponse().(*sendcloud.Parcel), nil } + +// GetDocument retrieves the parcel document of parcelID with type docType from the api. +// https://api.sendcloud.dev/docs/sendcloud-public-api/parcel-documents/operations/get-a-parcel-document +func (c *Client) GetDocument(ctx context.Context, parcelID int64, docTyp string, fmt sendcloud.DocumentFormat, dpi int) (*sendcloud.Document, error) { + uri := "/api/v2/parcels/" + strconv.Itoa(int(parcelID)) + "/documents/" + docTyp + if dpi > 0 { + uri += "?dpi=" + strconv.Itoa(dpi) + } + + req, err := sendcloud.NewRequest(ctx, "GET", uri, nil, c.apiKey, c.apiSecret) + if err != nil { + return nil, err + } + + if fmt != "" { + req.Header.Set("accept", fmt.String()) + } + + client := http.Client{Timeout: 30 * time.Second} + response, err := client.Do(req) + if err != nil { + return nil, err + } + defer response.Body.Close() + + if err = sendcloud.ValidateResponse(response); err != nil { + return nil, err + } + + doc := sendcloud.Document{ + Format: sendcloud.DocumentFormat(response.Header.Get("content-type")), + } + if doc.Body, err = ioutil.ReadAll(response.Body); err != nil { + return nil, err + } + return &doc, nil +} diff --git a/sendcloud.go b/sendcloud.go index 64b910e..f12b4c6 100644 --- a/sendcloud.go +++ b/sendcloud.go @@ -2,6 +2,7 @@ package sendcloud import ( "bytes" + "context" "encoding/json" "fmt" "io/ioutil" @@ -37,27 +38,25 @@ func (e *Error) Error() string { return fmt.Sprintf("request %s resulted in error code %d: %s", e.Request, e.Code, e.Message) } -// Send a request to Sendcloud with given method, path, payload and credentials -func Request(method string, uri string, payload Payload, apiKey string, apiSecret string, r Response) error { - client := http.Client{ - Timeout: 30 * time.Second, - } +// NewRequest creates and prepares a *http.Request with the given method, url, +// payload and credentials, so it's ready to be sent to Sendcloud. +func NewRequest(ctx context.Context, method, uri string, payload Payload, apiKey, apiSecret string) (*http.Request, error) { var request *http.Request var err error if payload == nil { - request, err = http.NewRequest(method, getUrl(uri), nil) + request, err = http.NewRequestWithContext(ctx, method, getUrl(uri), nil) if err != nil { - return err + return nil, err } } else { body, err := json.Marshal(payload.GetPayload()) if err != nil { - return err + return nil, err } - request, err = http.NewRequest(method, getUrl(uri), bytes.NewBuffer(body)) + request, err = http.NewRequestWithContext(ctx, method, getUrl(uri), bytes.NewBuffer(body)) if err != nil { - return err + return nil, err } } @@ -66,39 +65,70 @@ func Request(method string, uri string, payload Payload, apiKey string, apiSecre } request.Header.Set("User-Agent", "Sendcloud-Go/0.1 ("+apiKey+")") request.SetBasicAuth(apiKey, apiSecret) + return request, nil +} + +// Request sends a request to Sendcloud with given method, path, payload and credentials. +func Request(method, uri string, payload Payload, apiKey, apiSecret string, r Response) error { + request, err := NewRequest(context.Background(), method, uri, payload, apiKey, apiSecret) + if err != nil { + return err + } + client := http.Client{Timeout: 30 * time.Second} response, err := client.Do(request) if err != nil { return err } defer response.Body.Close() + + if err = ValidateResponse(response); err != nil { + return err + } + body, err := ioutil.ReadAll(response.Body) if err != nil { return err } - if response.StatusCode > 299 || response.StatusCode < 200 { - if !strings.Contains(response.Header.Get("content-type"), "application/json") { - return &Error{ - Code: response.StatusCode, - Message: string(body), - } - } + return r.SetResponse(body) +} - // Return error response - errResponse := ErrorResponse{} - err = json.Unmarshal(body, &errResponse) - if err != nil { - return err - } +// ValidateResponse validates a received response from Sendcloud. It is valid +// and returns nil when the status code is between 200 and 299. +func ValidateResponse(response *http.Response) error { + if response.StatusCode >= 200 && response.StatusCode <= 299 { + return nil + } + + body, err := ioutil.ReadAll(response.Body) + if err != nil { + return err + } + if !strings.Contains(response.Header.Get("content-type"), "application/json") { return &Error{ Code: response.StatusCode, - Request: errResponse.Error.Request, - Message: errResponse.Error.Message, + Request: requestFromResponse(response), + Message: string(body), } } - err = r.SetResponse(body) - return err + + var errResponse ErrorResponse + if err = json.Unmarshal(body, &errResponse); err != nil { + return err + } + if errResponse.Error.Request == "" { + errResponse.Error.Request = requestFromResponse(response) + } + if errResponse.Error.Message == "" { + errResponse.Error.Message = string(body) + } + + return &Error{ + Code: response.StatusCode, + Request: errResponse.Error.Request, + Message: errResponse.Error.Message, + } } // Return the full URL @@ -112,3 +142,10 @@ func getUrl(uri string) string { return url } + +func requestFromResponse(resp *http.Response) string { + if resp.Request != nil && resp.Request.URL != nil { + return resp.Request.URL.String() + } + return "" +}