Skip to content

Commit

Permalink
feat(gqlclient): require passing context (#10)
Browse files Browse the repository at this point in the history
* feat(gqlclient): require passing context

BREAKING CHANGE: all exposed methods now require a context.Context as
the first parameter to allow request cancelling. Respects the Go
guidelines as described in https://golang.org/pkg/context/.

Also adapt the default http timeout to 30 seconds.

Fix #7

* docs(readme): add context info to readme
  • Loading branch information
steebchen committed Jun 28, 2019
1 parent 407b9b8 commit ae05fd0
Show file tree
Hide file tree
Showing 5 changed files with 54 additions and 19 deletions.
24 changes: 17 additions & 7 deletions client.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package gqlclient

import (
"bytes"
"context"
"encoding/json"
"io/ioutil"
"net/http"
Expand All @@ -26,20 +27,20 @@ type Response struct {
}

// request is the payload for GraphQL queries
type request struct {
type gqlRequest struct {
Query string `json:"query"`
Variables map[string]interface{} `json:"variables,omitempty"`
OperationName string `json:"operationName,omitempty"`
}

// Send a GraphQL request to struct or map
func (c *Client) Send(dest interface{}, query string, variables interface{}) (*Response, error) {
func (c *Client) Send(ctx context.Context, dest interface{}, query string, variables interface{}) (*Response, error) {
unboxedVars, err := structs.StructToMap(variables)
if err != nil {
return nil, errors.Wrap(err, "StructToMap failed")
}

resp, err := c.Raw(query, unboxedVars)
resp, err := c.Raw(ctx, query, unboxedVars)
if err != nil {
return nil, err
}
Expand All @@ -56,20 +57,29 @@ func (c *Client) Send(dest interface{}, query string, variables interface{}) (*R

// Raw sends a basic GraphQL request without any struct types.
// Parameter `variables` can be either a map or a struct
func (c *Client) Raw(query string, variables map[string]interface{}) (*Response, error) {
func (c *Client) Raw(ctx context.Context, query string, variables map[string]interface{}) (*Response, error) {
var err error

req := &request{
payload := &gqlRequest{
Query: query,
Variables: variables,
}

requestBody, err := json.Marshal(req)
requestBody, err := json.Marshal(payload)
if err != nil {
return nil, errors.Wrap(err, "raw encode")
}

rawResponse, err := c.http.Post(c.url, "application/json", bytes.NewBuffer(requestBody))
req, err := http.NewRequest("post", c.url, bytes.NewBuffer(requestBody))

if err != nil {
return nil, err
}

req.Header.Set("content-type", "application/json")
req = req.WithContext(ctx)

rawResponse, err := c.http.Do(req)
if err != nil {
return nil, errors.Wrap(err, "raw post")
}
Expand Down
25 changes: 22 additions & 3 deletions client_test.go
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
package gqlclient

import (
"context"
"encoding/json"
"io/ioutil"
"net/http"
"net/http/httptest"
"os"
"testing"
"time"

"github.com/pkg/errors"
"github.com/stretchr/testify/require"
)

Expand Down Expand Up @@ -49,7 +53,7 @@ func TestClient_Send(t *testing.T) {

instance := New(mockServer(t).URL)

_, err := instance.Send(&structDest, query, variables)
_, err := instance.Send(context.Background(), &structDest, query, variables)

require.NoError(t, err)
require.Equal(t, structType{
Expand All @@ -63,7 +67,7 @@ func TestClient_Send(t *testing.T) {

instance := New(mockServer(t).URL)

_, err := instance.Send(&mapDest, query, variables)
_, err := instance.Send(context.Background(), &mapDest, query, variables)

require.NoError(t, err)
require.Equal(t, map[string]interface{}{
Expand All @@ -73,6 +77,21 @@ func TestClient_Send(t *testing.T) {
})
}

func TestClient_Send_context(t *testing.T) {
query := `query GetUser { user(id: $id) { id name } }`
variables := map[string]interface{}{
"id": "1",
}

instance := New(mockServer(t).URL)

ctx, _ := context.WithDeadline(context.Background(), time.Now())

_, err := instance.Raw(ctx, query, variables)

require.Equal(t, true, os.IsTimeout(errors.Cause(err)))
}

func TestClient_Send_Variations(t *testing.T) {
query := `query GetUser { user(id: $id) { id name } }`

Expand Down Expand Up @@ -133,7 +152,7 @@ func TestClient_Send_Variations(t *testing.T) {
t.Run(tt.name, func(t *testing.T) {
instance := New(mockServer(t).URL)
var dest map[string]interface{}
got, err := instance.Send(&dest, tt.args.query, tt.args.variables)
got, err := instance.Send(context.Background(), &dest, tt.args.query, tt.args.variables)

if !tt.wantErr {
require.NoError(t, err)
Expand Down
7 changes: 5 additions & 2 deletions gqlclient.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package gqlclient

import (
"net/http"
"time"
)

// Client is the GraphQL client which is returned by New()
Expand All @@ -14,8 +15,10 @@ type Client struct {
// New creates a graphql http
func New(url string) *Client {
c := &Client{
url: url,
http: http.DefaultClient,
url: url,
http: &http.Client{
Timeout: 30 * time.Second,
},
}

return c
Expand Down
5 changes: 3 additions & 2 deletions gqlclient_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package gqlclient

import (
"context"
"net/http"
"testing"

Expand All @@ -17,7 +18,7 @@ func TestNew(t *testing.T) {
Name string
}

resp, err := client.Send(&data, `query GetUser { user(id: $id) { id name } }`, map[string]interface{}{
resp, err := client.Send(context.Background(), &data, `query GetUser { user(id: $id) { id name } }`, map[string]interface{}{
"id": "1",
})

Expand All @@ -39,7 +40,7 @@ func TestClient_WithHTTPClient(t *testing.T) {
Name string
}

_, err := client.Send(&data, `query GetUser { user(id: $id) { id name } }`, map[string]interface{}{
_, err := client.Send(context.Background(), &data, `query GetUser { user(id: $id) { id name } }`, map[string]interface{}{
"id": "1",
})

Expand Down
12 changes: 7 additions & 5 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,16 +12,17 @@ Reasons to use gqlclient:

- Simple, familiar API
- Use strong Go types for variables and response data
- Simple error handling
- Receive a full GraphQL response with data, errors and extensions
- Respects `context.Context` cancellations and timeouts
- Supports GraphQL Errors with Extensions

*Note*: This package already works quite well, but it is under heavy development to work towards a v1.0 release. Before that, the API may have breaking changes even with minor versions.
*Note*: This package already works quite well, but it is under heavy development to work towards a v1.0 release. Before that, the API may have breaking changes even with minor versions.

Coming soon:

- Uploads
- Subscriptions
- More options (http headers, request context)
- More options (e.g. http headers)

## Installation

Expand All @@ -42,6 +43,7 @@ package main

import (
"log"
"context"
"github.com/steebchen/gqlclient"
)

Expand All @@ -68,7 +70,7 @@ func main() {
}
`

_, err := client.Send(&data, query, variables{
_, err := client.Send(context.Background(), &data, query, variables{
ID: "55bfed9275de7b060098b9bc",
})

Expand All @@ -88,7 +90,7 @@ func main() {
If you don't want to use structs, you use `Raw()` to use maps for both input (variables) and output (response data).

```go
resp, err := client.Raw(query, map[string]interface{}{
resp, err := client.Raw(context.Background(), query, map[string]interface{}{
"id": "55bfed9275de7b060098b9bc",
})

Expand Down

0 comments on commit ae05fd0

Please sign in to comment.