Skip to content

Commit

Permalink
Merge pull request #20 from ksysoev/errors_refactoring
Browse files Browse the repository at this point in the history
Add error handling and parsing for API responses
  • Loading branch information
ksysoev authored Aug 19, 2024
2 parents 19ea35e + 79a1866 commit 4c0c58a
Show file tree
Hide file tree
Showing 5 changed files with 83 additions and 27 deletions.
13 changes: 6 additions & 7 deletions api.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import (
"bytes"
"context"
"encoding/json"
"fmt"
"log"
"net/url"
"strconv"
Expand Down Expand Up @@ -86,15 +85,15 @@ func NewDerivAPI(endpoint string, appID int, lang, origin string, opts ...APIOpt
}

if urlEnpoint.Scheme != "wss" && urlEnpoint.Scheme != "ws" {
return nil, fmt.Errorf("invalid endpoint scheme")
return nil, ErrInvalidSchema
}

if appID < 1 {
return nil, fmt.Errorf("invalid app id")
return nil, ErrInvalidAppID
}

if lang == "" || len(lang) != 2 {
return nil, fmt.Errorf("invalid language")
return nil, ErrInvalidLanguage
}

query := urlEnpoint.Query()
Expand Down Expand Up @@ -357,7 +356,7 @@ func (api *Client) Send(ctx context.Context, reqID int, request any) (chan []byt
case <-ctx.Done():
return nil, ctx.Err()
case <-api.ctx.Done():
return nil, fmt.Errorf("connection closed")
return nil, ErrConnectionClosed
case api.reqChan <- req:
return respChan, nil
}
Expand All @@ -375,13 +374,13 @@ func (api *Client) SendRequest(ctx context.Context, reqID int, request, response

select {
case <-api.ctx.Done():
return fmt.Errorf("connection closed")
return ErrConnectionClosed
case <-ctx.Done():
return ctx.Err()
case responseJSON, ok := <-respChan:
if !ok {
api.logDebugf("Connection closed while waiting for response for request %d", reqID)
return fmt.Errorf("connection closed")
return ErrConnectionClosed
}

if err := parseError(responseJSON); err != nil {
Expand Down
34 changes: 26 additions & 8 deletions errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,32 +2,50 @@ package deriv

import (
"encoding/json"
"fmt"
)

var (
ErrConnectionClosed = fmt.Errorf("connection closed")
ErrEmptySubscriptionID = fmt.Errorf("subscription ID is empty")
ErrInvalidSchema = fmt.Errorf("invalid endpoint scheme")
ErrInvalidAppID = fmt.Errorf("invalid app ID")
ErrInvalidLanguage = fmt.Errorf("invalid language")
)

// APIError represents an error returned by the Deriv API service.
type APIError struct {
Details map[string]interface{} `json:"details"`
Code string `json:"code"`
Message string `json:"message"`
Details *json.RawMessage `json:"details"`
Code string `json:"code"`
Message string `json:"message"`
}

// Error returns the error message associated with the APIError.
func (e *APIError) Error() string {
return e.Message
}

// APIErrorResponse represents an error response returned by the Deriv API service.
type APIErrorResponse struct {
// ParseDetails parses the details field of the APIError into the provided value.
func (e *APIError) ParseDetails(v any) error {
if e.Details == nil {
return nil
}

return json.Unmarshal(*e.Details, v)
}

// apiErrorResponse represents an error response returned by the Deriv API service.
type apiErrorResponse struct {
// Error is the APIError associated with the response.
Error APIError `json:"error"`
}

// parseError parses a JSON error response from the Deriv API service into an error.
// If the response is not a valid JSON-encoded APIErrorResponse, an error is returned.
// If the APIErrorResponse contains a non-empty APIError, it is returned as an error.
// If the response is not a valid JSON-encoded apiErrorResponse, an error is returned.
// If the apiErrorResponse contains a non-empty APIError, it is returned as an error.
// Otherwise, nil is returned.
func parseError(rawResponse []byte) error {
var errorResponse APIErrorResponse
var errorResponse apiErrorResponse

err := json.Unmarshal(rawResponse, &errorResponse)
if err != nil {
Expand Down
51 changes: 47 additions & 4 deletions errors_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,13 @@ import (
"testing"
)

var expectedDetails json.RawMessage = []byte(`{"TestKey":"TestValue"}`)

func TestAPIError_Error(t *testing.T) {
err := &APIError{
Code: "test-code",
Message: "test-message",
Details: map[string]interface{}{"test-key": "test-value"},
Details: &expectedDetails,
}

expected := err.Message
Expand All @@ -22,11 +24,11 @@ func TestAPIError_Error(t *testing.T) {
}

func TestParseError_ValidResponse(t *testing.T) {
errorResponse := APIErrorResponse{
errorResponse := apiErrorResponse{
Error: APIError{
Code: "test-code",
Message: "test-message",
Details: map[string]interface{}{"test-key": "test-value"},
Details: &expectedDetails,
},
}

Expand Down Expand Up @@ -65,7 +67,7 @@ func TestParseError_EmptyErrorResponse(t *testing.T) {
}

func TestParseError_EmptyAPIError(t *testing.T) {
errorResponse := APIErrorResponse{
errorResponse := apiErrorResponse{
Error: APIError{},
}

Expand All @@ -80,3 +82,44 @@ func TestParseError_EmptyAPIError(t *testing.T) {
t.Errorf("parseError() returned %v, expected %v", actual, nil)
}
}
func TestAPIError_ParseDetails_ValidDetails(t *testing.T) {
details := struct {
TestKey string `json:"TestKey"`
}{}

apiErr := &APIError{
Code: "test-code",
Message: "test-message",
Details: &expectedDetails,
}

if err := apiErr.ParseDetails(&details); err != nil {
t.Errorf("Expected no error, got %v", err)
}

if details.TestKey != "TestValue" {
t.Errorf("ParseDetails() did not parse details correctly, expected %q, got %q", "test-value", details.TestKey)
}
}

func TestAPIError_ParseDetails_EmptyDetails(t *testing.T) {
details := struct {
TestKey string `json:"TestKey"`
}{}

apiErr := &APIError{
Code: "test-code",
Message: "test-message",
Details: nil,
}

err := apiErr.ParseDetails(&details)

if err != nil {
t.Errorf("Expected no error, got %v", err)
}

if details.TestKey != "" {
t.Errorf("ParseDetails() did not handle empty details correctly, expected %q, got %q", "", details.TestKey)
}
}
5 changes: 2 additions & 3 deletions subscriptions.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ package deriv
import (
"context"
"encoding/json"
"fmt"
"sync"

"github.com/ksysoev/deriv-api/schema"
Expand Down Expand Up @@ -49,7 +48,7 @@ func parseSubsciption(rawResponse []byte) (SubscriptionResponse, error) {
}

if sub.Subscription.ID == "" {
return sub, fmt.Errorf("subscription ID is empty")
return sub, ErrEmptySubscriptionID
}

return sub, nil
Expand Down Expand Up @@ -125,7 +124,7 @@ func (s *Subsciption[initResp, Resp]) Start(reqID int, request any) (initResp, e
if !ok {
s.API.logDebugf("Connection closed while waiting for response for request %d", reqID)

return resp, fmt.Errorf("connection closed")
return resp, ErrConnectionClosed
}

subResp, err := parseSubsciption(initResponse)
Expand Down
7 changes: 2 additions & 5 deletions subscriptions_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@ package deriv

import (
"context"
"errors"
"fmt"
"reflect"
"testing"
"time"
Expand Down Expand Up @@ -57,15 +55,14 @@ func TestParseSubscription_EmptyInput(t *testing.T) {

func TestParseSubscription_EmptySubscriptionData(t *testing.T) {
input := []byte(`{}`)
expectedErr := fmt.Errorf("subscription ID is empty")

_, err := parseSubsciption(input)
if err == nil {
t.Errorf("Expected an error, but got nil")
}

if errors.Is(err, expectedErr) {
t.Errorf("Expected %+v, but got %+v", expectedErr, err)
if err != ErrEmptySubscriptionID {
t.Errorf("Expected '%+v', but got '%+v'", ErrEmptySubscriptionID, err)
}
}

Expand Down

0 comments on commit 4c0c58a

Please sign in to comment.