Skip to content

Commit

Permalink
Merge pull request #8 from j-low/update-error-handling-and-types-form…
Browse files Browse the repository at this point in the history
…atting

feat: update error handling, chore: format types
  • Loading branch information
j-low authored Jan 17, 2025
2 parents d494bc8 + 7475fa1 commit 5ff2832
Show file tree
Hide file tree
Showing 14 changed files with 411 additions and 372 deletions.
50 changes: 28 additions & 22 deletions common/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,21 +6,27 @@ import (
"github.com/google/uuid"
)

const (
ProductTypePhysical = "PHYSICAL"
ProductTypeDigital = "DIGITAL"
)

type Config struct {
APIKey string
UserAgent string
Client *http.Client
IdempotencyKey *uuid.UUID
APIKey string
UserAgent string
Client *http.Client
IdempotencyKey *uuid.UUID
}

type QueryParams struct {
Cursor string
Filter string
ModifiedAfter string
Cursor string
Filter string
ModifiedAfter string
ModifiedBefore string
SortDirection string
SortField string
Status string
SortDirection string
SortField string
Status string
Type string
}

type Pagination struct {
Expand All @@ -30,25 +36,25 @@ type Pagination struct {
}

type APIError struct {
Type string
Type string
Subtype string
Message string
Detail string
Detail string
}

type Address struct {
FirstName string `json:"firstName"`
LastName string `json:"lastName"`
Address1 string `json:"address1"`
Address2 string `json:"address2,omitempty"`
City string `json:"city"`
State string `json:"state"`
PostalCode string `json:"postalCode"`
CountryCode string `json:"countryCode"`
Phone string `json:"phone"`
FirstName string `json:"firstName"`
LastName string `json:"lastName"`
Address1 string `json:"address1"`
Address2 string `json:"address2,omitempty"`
City string `json:"city"`
State string `json:"state"`
PostalCode string `json:"postalCode"`
CountryCode string `json:"countryCode"`
Phone string `json:"phone"`
}

type Amount struct {
Currency string `json:"currency"`
Value string `json:"value"`
}
}
74 changes: 52 additions & 22 deletions common/util.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,34 @@ package common
import (
"encoding/json"
"fmt"
"strings"
"time"
)

func ParseErrorResponse(body []byte, statusCode int) error {
func ParseErrorResponse(endpoint string, url string, body []byte, statusCode int) error {
var apiError APIError

if err := json.Unmarshal(body, &apiError); err != nil {
return fmt.Errorf("unexpected status code: %d, body: %s", statusCode, string(body))
}

errorMessage := fmt.Sprintf("status: %d, type: %s, subtype: %s, message: %s, detail: %s",
statusCode, apiError.Type, apiError.Subtype, apiError.Message, apiError.Detail)
errorFormat := "%s url: %s: status: %d, type: %s"
errorArgs := []interface{}{endpoint, url, statusCode, apiError.Type}

if apiError.Subtype != "" {
errorFormat += ", subtype: %s"
errorArgs = append(errorArgs, apiError.Subtype)
}

errorFormat += ", message: %s"
errorArgs = append(errorArgs, apiError.Message)

if apiError.Detail != "" {
errorFormat += ", detail: %s"
errorArgs = append(errorArgs, apiError.Detail)
}

return fmt.Errorf(errorMessage)
return fmt.Errorf(errorFormat, errorArgs...)
}

func SetUserAgent(userAgent string) string {
Expand All @@ -27,35 +41,51 @@ func SetUserAgent(userAgent string) string {
}
}

func ValidateQueryParams(queryParams QueryParams) error {
if queryParams.Cursor != "" {
if queryParams.Filter != "" || queryParams.ModifiedAfter != "" || queryParams.ModifiedBefore != "" ||
queryParams.SortDirection != "" || queryParams.SortField != "" || queryParams.Status != "" {
func ValidateQueryParams(params QueryParams) error {
if params.Cursor != "" {
if params.Filter != "" || params.ModifiedAfter != "" || params.ModifiedBefore != "" ||
params.SortDirection != "" || params.SortField != "" || params.Status != "" {
return fmt.Errorf("cannot use cursor alongside other query parameters")
}
} else {
if err := validateModifiedBeforeAfterQueryParams(queryParams.ModifiedAfter, queryParams.ModifiedBefore); err != nil {
return fmt.Errorf("invalid modifiedAfter or modifiedBefore: %w", err)
if params.ModifiedAfter != "" && params.ModifiedBefore == "" || params.ModifiedAfter == "" && params.ModifiedBefore != "" {
return fmt.Errorf("modifiedAfter and modifiedBefore must both be specified together or not at all")
}
if params.ModifiedAfter != "" {
if _, err := time.Parse(time.RFC3339, params.ModifiedAfter); err != nil {
return fmt.Errorf("modifiedAfter is not a valid ISO 8601 UTC date-time string: %w", err)
}
}
if params.ModifiedBefore != "" {
if _, err := time.Parse(time.RFC3339, params.ModifiedBefore); err != nil {
return fmt.Errorf("modifiedBefore is not a valid ISO 8601 UTC date-time string: %w", err)
}
}
if params.Type != "" {
if err := validateTypeParam(params.Type); err != nil {
return fmt.Errorf("invalid type: %w", err)
}
}
}

return nil
}

func validateModifiedBeforeAfterQueryParams(modifiedAfter, modifiedBefore string) error {
if modifiedAfter != "" && modifiedBefore == "" || modifiedAfter == "" && modifiedBefore != "" {
return fmt.Errorf("modifiedAfter and modifiedBefore must both be specified together or not at all")
}
if modifiedAfter != "" {
if _, err := time.Parse(time.RFC3339, modifiedAfter); err != nil {
return fmt.Errorf("modifiedAfter is not a valid ISO 8601 UTC date-time string: %w", err)
func validateTypeParam(productType string) error {
types := strings.Split(productType, ",")
validTypes := make(map[string]bool)

for _, t := range types {
t = strings.TrimSpace(t)
if t != ProductTypePhysical && t != ProductTypeDigital {
return fmt.Errorf("type must be either 'PHYSICAL' or 'DIGITAL' (or both comma-separated), got: %s", productType)
}
validTypes[t] = true
}
if modifiedBefore != "" {
if _, err := time.Parse(time.RFC3339, modifiedBefore); err != nil {
return fmt.Errorf("modifiedBefore is not a valid ISO 8601 UTC date-time string: %w", err)
}

if len(validTypes) != len(types) {
return fmt.Errorf("duplicate types found in: %s", productType)
}

return nil
}
}
21 changes: 11 additions & 10 deletions inventory/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ func RetrieveAllInventory(ctx context.Context, config *common.Config, params com
}

if resp.StatusCode != http.StatusOK {
return nil, common.ParseErrorResponse(body, resp.StatusCode)
return nil, common.ParseErrorResponse("RetrieveAllInventory", u.String(), body, resp.StatusCode)
}

var response RetrieveAllInventoryResponse
Expand All @@ -70,7 +70,8 @@ func RetrieveSpecificInventory(ctx context.Context, config *common.Config, inven
}

idsPath := strings.Join(inventoryIDs, ",")
endpoint := fmt.Sprintf("https://api.squarespace.com/%s/commerce/inventory/%s", InventoryAPIVersion, idsPath)
baseURL := fmt.Sprintf("https://api.squarespace.com/%s/commerce/inventory", InventoryAPIVersion)
endpoint := fmt.Sprintf("%s/%s", baseURL, idsPath)

u, err := url.Parse(endpoint)
if err != nil {
Expand All @@ -97,7 +98,7 @@ func RetrieveSpecificInventory(ctx context.Context, config *common.Config, inven
}

if resp.StatusCode != http.StatusOK {
return nil, common.ParseErrorResponse(body, resp.StatusCode)
return nil, common.ParseErrorResponse("RetrieveSpecificInventory", u.String(), body, resp.StatusCode)
}

var response RetrieveSpecificInventoryResponse
Expand All @@ -108,17 +109,17 @@ func RetrieveSpecificInventory(ctx context.Context, config *common.Config, inven
return &response, nil
}

func AdjustStockQuantities(ctx context.Context, config *common.Config, request AdjustStockQuantitiesRequest) error {
func AdjustStockQuantities(ctx context.Context, config *common.Config, request AdjustStockQuantitiesRequest) (status int, err error) {
url := fmt.Sprintf("https://api.squarespace.com/%s/commerce/inventory/adjustments", InventoryAPIVersion)

reqBody, err := json.Marshal(request)
if err != nil {
return fmt.Errorf("failed to marshal request body: %w", err)
return http.StatusBadRequest, fmt.Errorf("failed to marshal request body: %w", err)
}

req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(reqBody))
if err != nil {
return fmt.Errorf("failed to create request: %w", err)
return http.StatusBadRequest, fmt.Errorf("failed to create request: %w", err)
}

req.Header.Set("Authorization", "Bearer " + config.APIKey)
Expand All @@ -131,17 +132,17 @@ func AdjustStockQuantities(ctx context.Context, config *common.Config, request A

resp, err := config.Client.Do(req)
if err != nil {
return fmt.Errorf("failed to adjust stock quantities: %w", err)
return http.StatusBadRequest, fmt.Errorf("failed to adjust stock quantities: %w", err)
}
defer resp.Body.Close()

if resp.StatusCode == http.StatusNoContent {
return nil
return http.StatusNoContent, nil
}

body, err := io.ReadAll(resp.Body)
if err != nil {
return fmt.Errorf("failed to read response body: %w", err)
return http.StatusBadRequest, fmt.Errorf("failed to read response body: %w", err)
}
return common.ParseErrorResponse(body, resp.StatusCode)
return resp.StatusCode, common.ParseErrorResponse("AdjustStockQuantities", url, body, resp.StatusCode)
}
24 changes: 12 additions & 12 deletions inventory/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,27 +7,27 @@ const (
)

type RetrieveAllInventoryResponse struct {
Inventory []InventoryRecord `json:"inventory"`
Pagination common.Pagination `json:"pagination"`
Inventory []InventoryRecord `json:"inventory"`
Pagination common.Pagination `json:"pagination"`
}

type RetrieveSpecificInventoryResponse struct {
Inventory []InventoryRecord `json:"inventory"`
Inventory []InventoryRecord `json:"inventory"`
}

type AdjustStockQuantitiesRequest struct {
IncrementOperations []QuantityOperation `json:"incrementOperations,omitempty"`
DecrementOperations []QuantityOperation `json:"decrementOperations,omitempty"`
SetFiniteOperations []QuantityOperation `json:"setFiniteOperations,omitempty"`
SetUnlimitedOperations []string `json:"setUnlimitedOperations,omitempty"`
IncrementOperations []QuantityOperation `json:"incrementOperations,omitempty"`
DecrementOperations []QuantityOperation `json:"decrementOperations,omitempty"`
SetFiniteOperations []QuantityOperation `json:"setFiniteOperations,omitempty"`
SetUnlimitedOperations []string `json:"setUnlimitedOperations,omitempty"`
}

type InventoryRecord struct {
VariantID string `json:"variantId"`
SKU string `json:"sku"`
Descriptor string `json:"descriptor"`
IsUnlimited bool `json:"isUnlimited"`
Quantity int `json:"quantity"`
VariantID string `json:"variantId"`
SKU string `json:"sku"`
Descriptor string `json:"descriptor"`
IsUnlimited bool `json:"isUnlimited"`
Quantity int `json:"quantity"`
}

type QuantityOperation struct {
Expand Down
20 changes: 10 additions & 10 deletions orders/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ func CreateOrder(ctx context.Context, config *common.Config, request CreateOrder
}

if resp.StatusCode != http.StatusCreated {
return nil, common.ParseErrorResponse(body, resp.StatusCode)
return nil, common.ParseErrorResponse("CreateOrder", url, body, resp.StatusCode)
}

var response Order
Expand All @@ -56,17 +56,17 @@ func CreateOrder(ctx context.Context, config *common.Config, request CreateOrder
return &response, nil
}

func FulfillOrder(ctx context.Context, config *common.Config, orderID string, request FulfillOrderRequest) error {
func FulfillOrder(ctx context.Context, config *common.Config, orderID string, request FulfillOrderRequest) (status int, err error) {
url := fmt.Sprintf("https://api.squarespace.com/%s/commerce/orders/%s/fulfillments", OrdersAPIVersion, orderID)

reqBody, err := json.Marshal(request)
if err != nil {
return fmt.Errorf("failed to marshal request body: %w", err)
return http.StatusBadRequest, fmt.Errorf("failed to marshal request body: %w", err)
}

req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(reqBody))
if err != nil {
return fmt.Errorf("failed to create request: %w", err)
return http.StatusBadRequest, fmt.Errorf("failed to create request: %w", err)
}

req.Header.Set("Authorization", "Bearer " + config.APIKey)
Expand All @@ -75,19 +75,19 @@ func FulfillOrder(ctx context.Context, config *common.Config, orderID string, re

resp, err := config.Client.Do(req)
if err != nil {
return fmt.Errorf("failed to fulfill order: %w", err)
return http.StatusBadRequest, fmt.Errorf("failed to fulfill order: %w", err)
}
defer resp.Body.Close()

if resp.StatusCode != http.StatusNoContent {
body, readErr := io.ReadAll(resp.Body)
if readErr != nil {
return fmt.Errorf("failed to read response body: %w", readErr)
return http.StatusBadRequest, fmt.Errorf("failed to read response body: %w", readErr)
}
return common.ParseErrorResponse(body, resp.StatusCode)
return resp.StatusCode, common.ParseErrorResponse("FulfillOrder", url, body, resp.StatusCode)
}

return nil
return http.StatusNoContent, nil
}

func RetrieveAllOrders(ctx context.Context, config *common.Config, params common.QueryParams) (*RetrieveAllOrdersResponse, error) {
Expand Down Expand Up @@ -136,7 +136,7 @@ func RetrieveAllOrders(ctx context.Context, config *common.Config, params common
}

if resp.StatusCode != http.StatusOK {
return nil, common.ParseErrorResponse(body, resp.StatusCode)
return nil, common.ParseErrorResponse("RetrieveAllOrders", u.String(), body, resp.StatusCode)
}

var response RetrieveAllOrdersResponse
Expand Down Expand Up @@ -170,7 +170,7 @@ func RetrieveSpecificOrder(ctx context.Context, config *common.Config, orderID s
}

if resp.StatusCode != http.StatusOK {
return nil, common.ParseErrorResponse(body, resp.StatusCode)
return nil, common.ParseErrorResponse("RetrieveSpecificOrder", url, body, resp.StatusCode)
}

var response Order
Expand Down
Loading

0 comments on commit 5ff2832

Please sign in to comment.