Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add error handling and parsing for API responses #20

Merged
merged 5 commits into from
Aug 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading