Skip to content

Commit

Permalink
Merge pull request #14 from printeers/feature/retrieve_parcel_documents
Browse files Browse the repository at this point in the history
Add method to retrieve parcel documents
  • Loading branch information
roelofjan-elsinga authored Jan 29, 2024
2 parents f7c4aed + 795adf7 commit 1061427
Show file tree
Hide file tree
Showing 4 changed files with 140 additions and 31 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ An API-client for Sendcloud written in Golang.

This package currently supports:
- parcels
- parcel documents
- labels
- methods
- addresses
Expand Down
30 changes: 30 additions & 0 deletions parcel.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
49 changes: 45 additions & 4 deletions parcel/client.go
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -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)
Expand All @@ -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)
Expand All @@ -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)
Expand All @@ -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)
Expand All @@ -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
}
91 changes: 64 additions & 27 deletions sendcloud.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package sendcloud

import (
"bytes"
"context"
"encoding/json"
"fmt"
"io/ioutil"
Expand Down Expand Up @@ -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
}
}

Expand All @@ -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
Expand All @@ -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 ""
}

0 comments on commit 1061427

Please sign in to comment.