From 372fadb6f7e7163b921b49c23f7c19c0ef5b081a Mon Sep 17 00:00:00 2001 From: Emile Pels Date: Sat, 16 Mar 2019 16:49:47 +0100 Subject: [PATCH 1/4] Add voice error reader --- client.go | 18 +++++++++ voice/testdata/error.json | 9 +++++ voice/testdata/errors.json | 13 +++++++ voice/voice.go | 53 ++++++++++++++++++++++++++ voice/voice_test.go | 78 ++++++++++++++++++++++++++++++++++++++ 5 files changed, 171 insertions(+) create mode 100644 voice/testdata/error.json create mode 100644 voice/testdata/errors.json create mode 100644 voice/voice_test.go diff --git a/client.go b/client.go index 518c683..d980fae 100644 --- a/client.go +++ b/client.go @@ -32,6 +32,9 @@ const ( // httpClientTimeout is used to limit http.Client waiting time. httpClientTimeout = 15 * time.Second + + // voiceHost is the host name for the Voice API. + voiceHost = "voice.messagebird.com" ) var ( @@ -55,6 +58,11 @@ const ( contentTypeFormURLEncoded contentType = "application/x-www-form-urlencoded" ) +// errorReader reads the provided byte slice into an appropriate error. +type errorReader func([]byte) error + +var voiceErrorReader errorReader + // New creates a new MessageBird client object. func New(accessKey string) *Client { return &Client{ @@ -65,6 +73,12 @@ func New(accessKey string) *Client { } } +// SetVoiceErrorReader takes an errorReader that must parse raw JSON errors +// returned from the Voice API. +func SetVoiceErrorReader(r errorReader) { + voiceErrorReader = r +} + // Request is for internal use only and unstable. func (c *Client) Request(v interface{}, method, path string, data interface{}) error { if !strings.HasPrefix(path, "https://") && !strings.HasPrefix(path, "http://") { @@ -135,6 +149,10 @@ func (c *Client) Request(v interface{}, method, path string, data interface{}) e return ErrUnexpectedResponse default: // Anything else than a 200/201/204/500 should be a JSON error. + if uri.Host == voiceHost && voiceErrorReader != nil { + return voiceErrorReader(responseBody) + } + var errorResponse ErrorResponse if err := json.Unmarshal(responseBody, &errorResponse); err != nil { return err diff --git a/voice/testdata/error.json b/voice/testdata/error.json new file mode 100644 index 0000000..e65bd9f --- /dev/null +++ b/voice/testdata/error.json @@ -0,0 +1,9 @@ +{ + "data": null, + "errors": [ + { + "code": 13, + "message": "some-error" + } + ] +} diff --git a/voice/testdata/errors.json b/voice/testdata/errors.json new file mode 100644 index 0000000..37cc528 --- /dev/null +++ b/voice/testdata/errors.json @@ -0,0 +1,13 @@ +{ + "data": null, + "errors": [ + { + "code": 11, + "message": "some-error" + }, + { + "code": 15, + "message": "other-error" + } + ] +} diff --git a/voice/voice.go b/voice/voice.go index ed42a62..6c4fd07 100644 --- a/voice/voice.go +++ b/voice/voice.go @@ -1,3 +1,56 @@ package voice +import ( + "encoding/json" + "fmt" + "strings" + + "github.com/messagebird/go-rest-api" +) + const apiRoot = "https://voice.messagebird.com" + +type ErrorResponse struct { + Errors []Error +} + +type Error struct { + Code int + Message string +} + +func init() { + // The Voice API returns errors in a format that slightly differs from other + // APIs. Here we instruct package messagebird to use our custom + // voice.errorReader func, which has access to voice.ErrorResponse, to + // unmarshal those. Package messagebird must not import the voice package to + // safeguard against import cycles, so it can not use voice.ErrorResponse + // directly. + messagebird.SetVoiceErrorReader(errorReader) +} + +// errorReader takes a []byte representation of a Voice API JSON error and +// parses it to a voice.ErrorResponse. +func errorReader(b []byte) error { + var er ErrorResponse + if err := json.Unmarshal(b, &er); err != nil { + return fmt.Errorf("encoding/json: Unmarshal: %v", err) + } + return er +} + +func (e ErrorResponse) Error() string { + if len(e.Errors) == 1 { + return e.Errors[0].Error() + } + + errStrings := make([]string, len(e.Errors)) + for i, v := range e.Errors { + errStrings[i] = v.Error() + } + return strings.Join(errStrings, "; ") +} + +func (e Error) Error() string { + return fmt.Sprintf("code: %d, message: %q", e.Code, e.Message) +} diff --git a/voice/voice_test.go b/voice/voice_test.go new file mode 100644 index 0000000..006c59e --- /dev/null +++ b/voice/voice_test.go @@ -0,0 +1,78 @@ +package voice + +import ( + "testing" + + "github.com/messagebird/go-rest-api/internal/mbtest" +) + +func TestErrorReader(t *testing.T) { + t.Run("Single error", func(t *testing.T) { + b := mbtest.Testdata(t, "error.json") + err := errorReader(b).(ErrorResponse) + + if count := len(err.Errors); count != 1 { + t.Fatalf("Got %d, expected 1", count) + } + + if err.Errors[0].Code != 13 { + t.Errorf("Got %d, expected 13", err.Errors[0].Code) + } + if err.Errors[0].Message != "some-error" { + t.Errorf("Got %q, expected some-error", err.Errors[0].Message) + } + }) + + t.Run("Multiple errors", func(t *testing.T) { + b := mbtest.Testdata(t, "errors.json") + err := errorReader(b).(ErrorResponse) + + if count := len(err.Errors); count != 2 { + t.Fatalf("Got %d, expected 2", count) + } + + if err.Errors[0].Code != 11 { + t.Errorf("Got %d, expected 11", err.Errors[0].Code) + } + if err.Errors[0].Message != "some-error" { + t.Errorf("Got %q, expected some-error", err.Errors[0].Message) + } + if err.Errors[1].Code != 15 { + t.Errorf("Got %d, expected 15", err.Errors[1].Code) + } + if err.Errors[1].Message != "other-error" { + t.Errorf("Got %q, expected other-error", err.Errors[1].Message) + } + }) + + t.Run("Invalid JSON", func(t *testing.T) { + b := []byte("clearly not json") + _, ok := errorReader(b).(ErrorResponse) + + if ok { + // If the data b is not JSON, we expect a "generic" errorString + // (from fmt.Errorf), but we somehow got our own ErrorResponse back. + t.Fatalf("Got ErrorResponse, expected errorString") + } + }) +} + +func TestErrorResponseError(t *testing.T) { + err := ErrorResponse{ + []Error{ + { + Code: 1, + Message: "foo", + }, + { + Code: 2, + Message: "bar", + }, + }, + } + + expect := `code: 1, message: "foo"; code: 2, message: "bar"` + if actual := err.Error(); actual != expect { + t.Fatalf("Got %q, expected %q", actual, expect) + } +} From 2a6f168935e3b641b3981966d75b7294e29e52a4 Mon Sep 17 00:00:00 2001 From: Emile Pels Date: Sat, 16 Mar 2019 17:31:54 +0100 Subject: [PATCH 2/4] Add voice.ErrorResponse with example to README. Also update some existing sections of the README --- README.md | 85 ++++++++++++++++++++++++++++++++++++++++++------------- 1 file changed, 65 insertions(+), 20 deletions(-) diff --git a/README.md b/README.md index 41a37cc..c38309d 100644 --- a/README.md +++ b/README.md @@ -26,43 +26,88 @@ Here is a quick example on how to get started. Assuming the **go get** installat import "github.com/messagebird/go-rest-api" ``` -Then, create an instance of **messagebird.Client**: +Then, create an instance of **messagebird.Client**. It can be used to access the MessageBird APIs. ```go -client := messagebird.New("test_gshuPaZoeEG6ovbc8M79w0QyM") -``` +// Access keys can be managed through our dashboard. +accessKey := "your-access-key" -Now you can query the API for information or send data. For example, if we want to request our balance information you'd do something like this: +// Create a client. +client := messagebird.New(accessKey) -```go -// Request the balance information, returned as a Balance object. +// Request the balance information, returned as a balance.Balance object. balance, err := balance.Read(client) if err != nil { - switch errResp := err.(type) { - case messagebird.ErrorResponse: - for _, mbError := range errResp.Errors { - fmt.Printf("Error: %#v\n", mbError) - } - } - + // Handle error. return } -fmt.Println(" payment :", balance.Payment) -fmt.Println(" type :", balance.Type) -fmt.Println(" amount :", balance.Amount) +// Display the results. +fmt.Println("Payment: ", balance.Payment) +fmt.Println("Type:", balance.Type) +fmt.Println("Amount:", balance.Amount) ``` This will give you something like: -```shell + +```bash $ go run example.go - payment : prepaid - type : credits - amount : 9 +Payment: prepaid +Type: credits +Amount: 9 ``` Please see the other examples for a complete overview of all the available API calls. +Errors +------ +When something goes wrong, our APIs can return more than a single error. They are therefore returned by the client as "error responses" that contain a slice of errors. + +It is important to notice that the Voice API returns errors with a format that slightly differs from other APIs. +For this reason, errors returned by the `voice` package are of type `voice.ErrorResponse`. It contains `voice.Error` structs. All other packages return `messagebird.ErrorResponse` structs that contain a slice of `messagebird.Error`. + +An example of "simple" error handling is shown in the example above. Let's look how we can gain more in-depth insight in what exactly went wrong: + +```go +import "github.com/messagebird/go-rest-api" +import "github.com/messagebird/go-rest-api/sms" + +// ... + +_, err := sms.Read(client, "some-id") +if err != nil { + mbErr, ok := err.(messagebird.ErrorResponse) + if !ok { + // A non-MessageBird error occurred (no connection, perhaps?) + return err + } + + fmt.Println("Code:", mbErr.Errors[0].Code) + fmt.Println("Description:", mbErr.Errors[0].Description) + fmt.Println("Parameter:", mbErr.Errors[0].Parameter) +} +``` + +`voice.ErrorResponse` is very similar, except that it holds `voice.Error` structs - those contain only `Code` and `Message` (not description!) fields: + +```go +import "github.com/messagebird/go-rest-api/voice" + +// ... + +_, err := voice.CallFlowByID(client, "some-id") +if err != nil { + vErr, ok := err.(voice.ErrorResponse) + if !ok { + // A non-MessageBird (Voice) error occurred (no connection, perhaps?) + return err + } + + fmt.Println("Code:", vErr.Errors[0].Code) + fmt.Println("Message:", vErr.Errors[0].Message) +} +``` + Documentation ------------- Complete documentation, instructions, and examples are available at: From db2cf5ecb119de3187429ca08af4299b3129d85b Mon Sep 17 00:00:00 2001 From: Emile Pels Date: Sat, 16 Mar 2019 17:36:42 +0100 Subject: [PATCH 3/4] Add link to dashboard to README --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index c38309d..ae391b6 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ This repository contains the open source Go client for MessageBird's REST API. D Requirements ------------ - [Sign up](https://www.messagebird.com/en/signup) for a free MessageBird account -- Create a new access key in the developers sections +- Create a new access key in the [dashboard](https://dashboard.messagebird.com/en-us/developers/access). - An application written in Go to make use of this API Installation From 9e497bacbc111ca31a97476bb182ff428760df24 Mon Sep 17 00:00:00 2001 From: Emile Pels Date: Sat, 16 Mar 2019 17:47:48 +0100 Subject: [PATCH 4/4] Remove quick exit from Error(). It does not have much benefit and only obscures code --- voice/voice.go | 4 ---- 1 file changed, 4 deletions(-) diff --git a/voice/voice.go b/voice/voice.go index 6c4fd07..721264d 100644 --- a/voice/voice.go +++ b/voice/voice.go @@ -40,10 +40,6 @@ func errorReader(b []byte) error { } func (e ErrorResponse) Error() string { - if len(e.Errors) == 1 { - return e.Errors[0].Error() - } - errStrings := make([]string, len(e.Errors)) for i, v := range e.Errors { errStrings[i] = v.Error()