Skip to content

Commit 1d68c4a

Browse files
authored
feat: decode extensions from response via BindExtensions option (#146)
* feat: decode extensions from response via BindExtensions option
1 parent 5a8b2ec commit 1d68c4a

File tree

7 files changed

+286
-162
lines changed

7 files changed

+286
-162
lines changed

README.md

Lines changed: 115 additions & 88 deletions
Large diffs are not rendered by default.

example/subscription/subscription_test.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -538,6 +538,9 @@ func testSubscription_LifeCycleEvents(t *testing.T, syncMode bool) {
538538
}
539539
}
540540

541+
// workaround for race condition
542+
time.Sleep(time.Second)
543+
541544
if atomic.LoadInt32(&wasConnected) != 1 {
542545
t.Fatalf("expected OnConnected event, got none")
543546
}

graphql.go

Lines changed: 76 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -47,61 +47,61 @@ func NewClient(url string, httpClient Doer) *Client {
4747
// Query executes a single GraphQL query request,
4848
// with a query derived from q, populating the response into it.
4949
// q should be a pointer to struct that corresponds to the GraphQL schema.
50-
func (c *Client) Query(ctx context.Context, q interface{}, variables map[string]interface{}, options ...Option) error {
50+
func (c *Client) Query(ctx context.Context, q any, variables map[string]any, options ...Option) error {
5151
return c.do(ctx, queryOperation, q, variables, options...)
5252
}
5353

5454
// NamedQuery executes a single GraphQL query request, with operation name
5555
//
5656
// Deprecated: this is the shortcut of Query method, with NewOperationName option
57-
func (c *Client) NamedQuery(ctx context.Context, name string, q interface{}, variables map[string]interface{}, options ...Option) error {
57+
func (c *Client) NamedQuery(ctx context.Context, name string, q any, variables map[string]any, options ...Option) error {
5858
return c.do(ctx, queryOperation, q, variables, append(options, OperationName(name))...)
5959
}
6060

6161
// Mutate executes a single GraphQL mutation request,
6262
// with a mutation derived from m, populating the response into it.
6363
// m should be a pointer to struct that corresponds to the GraphQL schema.
64-
func (c *Client) Mutate(ctx context.Context, m interface{}, variables map[string]interface{}, options ...Option) error {
64+
func (c *Client) Mutate(ctx context.Context, m any, variables map[string]any, options ...Option) error {
6565
return c.do(ctx, mutationOperation, m, variables, options...)
6666
}
6767

6868
// NamedMutate executes a single GraphQL mutation request, with operation name
6969
//
7070
// Deprecated: this is the shortcut of Mutate method, with NewOperationName option
71-
func (c *Client) NamedMutate(ctx context.Context, name string, m interface{}, variables map[string]interface{}, options ...Option) error {
71+
func (c *Client) NamedMutate(ctx context.Context, name string, m any, variables map[string]any, options ...Option) error {
7272
return c.do(ctx, mutationOperation, m, variables, append(options, OperationName(name))...)
7373
}
7474

7575
// Query executes a single GraphQL query request,
7676
// with a query derived from q, populating the response into it.
7777
// q should be a pointer to struct that corresponds to the GraphQL schema.
7878
// return raw bytes message.
79-
func (c *Client) QueryRaw(ctx context.Context, q interface{}, variables map[string]interface{}, options ...Option) ([]byte, error) {
79+
func (c *Client) QueryRaw(ctx context.Context, q any, variables map[string]any, options ...Option) ([]byte, error) {
8080
return c.doRaw(ctx, queryOperation, q, variables, options...)
8181
}
8282

8383
// NamedQueryRaw executes a single GraphQL query request, with operation name
8484
// return raw bytes message.
85-
func (c *Client) NamedQueryRaw(ctx context.Context, name string, q interface{}, variables map[string]interface{}, options ...Option) ([]byte, error) {
85+
func (c *Client) NamedQueryRaw(ctx context.Context, name string, q any, variables map[string]any, options ...Option) ([]byte, error) {
8686
return c.doRaw(ctx, queryOperation, q, variables, append(options, OperationName(name))...)
8787
}
8888

8989
// MutateRaw executes a single GraphQL mutation request,
9090
// with a mutation derived from m, populating the response into it.
9191
// m should be a pointer to struct that corresponds to the GraphQL schema.
9292
// return raw bytes message.
93-
func (c *Client) MutateRaw(ctx context.Context, m interface{}, variables map[string]interface{}, options ...Option) ([]byte, error) {
93+
func (c *Client) MutateRaw(ctx context.Context, m any, variables map[string]any, options ...Option) ([]byte, error) {
9494
return c.doRaw(ctx, mutationOperation, m, variables, options...)
9595
}
9696

9797
// NamedMutateRaw executes a single GraphQL mutation request, with operation name
9898
// return raw bytes message.
99-
func (c *Client) NamedMutateRaw(ctx context.Context, name string, m interface{}, variables map[string]interface{}, options ...Option) ([]byte, error) {
99+
func (c *Client) NamedMutateRaw(ctx context.Context, name string, m any, variables map[string]any, options ...Option) ([]byte, error) {
100100
return c.doRaw(ctx, mutationOperation, m, variables, append(options, OperationName(name))...)
101101
}
102102

103-
// buildAndRequest the common method that builds and send graphql request
104-
func (c *Client) buildAndRequest(ctx context.Context, op operationType, v interface{}, variables map[string]interface{}, options ...Option) ([]byte, *http.Response, io.Reader, Errors) {
103+
// buildQueryAndOptions the common method to build query and options
104+
func (c *Client) buildQueryAndOptions(op operationType, v any, variables map[string]any, options ...Option) (string, *constructOptionsOutput, error) {
105105
var query string
106106
var err error
107107
var optionOutput *constructOptionsOutput
@@ -110,18 +110,18 @@ func (c *Client) buildAndRequest(ctx context.Context, op operationType, v interf
110110
query, optionOutput, err = constructQuery(v, variables, options...)
111111
case mutationOperation:
112112
query, optionOutput, err = constructMutation(v, variables, options...)
113+
default:
114+
err = fmt.Errorf("invalid operation type: %v", op)
113115
}
114116

115117
if err != nil {
116-
return nil, nil, nil, Errors{newError(ErrGraphQLEncode, err)}
118+
return "", nil, Errors{newError(ErrGraphQLEncode, err)}
117119
}
118-
119-
data, _, resp, respBuf, errs := c.request(ctx, query, variables, optionOutput)
120-
return data, resp, respBuf, errs
120+
return query, optionOutput, nil
121121
}
122122

123123
// Request the common method that send graphql request
124-
func (c *Client) request(ctx context.Context, query string, variables map[string]interface{}, options *constructOptionsOutput) ([]byte, []byte, *http.Response, io.Reader, Errors) {
124+
func (c *Client) request(ctx context.Context, query string, variables map[string]any, options *constructOptionsOutput) ([]byte, []byte, *http.Response, io.Reader, Errors) {
125125
in := GraphQLRequestPayload{
126126
Query: query,
127127
Variables: variables,
@@ -248,35 +248,45 @@ func (c *Client) request(ctx context.Context, query string, variables map[string
248248

249249
// do executes a single GraphQL operation.
250250
// return raw message and error
251-
func (c *Client) doRaw(ctx context.Context, op operationType, v interface{}, variables map[string]interface{}, options ...Option) ([]byte, error) {
252-
data, _, _, err := c.buildAndRequest(ctx, op, v, variables, options...)
253-
if len(err) > 0 {
254-
return data, err
251+
func (c *Client) doRaw(ctx context.Context, op operationType, v any, variables map[string]any, options ...Option) ([]byte, error) {
252+
query, optionsOutput, err := c.buildQueryAndOptions(op, v, variables, options...)
253+
if err != nil {
254+
return nil, err
255+
}
256+
data, _, _, _, errs := c.request(ctx, query, variables, optionsOutput)
257+
if len(errs) > 0 {
258+
return data, errs
255259
}
260+
256261
return data, nil
257262
}
258263

259264
// do executes a single GraphQL operation and unmarshal json.
260-
func (c *Client) do(ctx context.Context, op operationType, v interface{}, variables map[string]interface{}, options ...Option) error {
261-
data, resp, respBuf, errs := c.buildAndRequest(ctx, op, v, variables, options...)
262-
return c.processResponse(v, data, resp, respBuf, errs)
265+
func (c *Client) do(ctx context.Context, op operationType, v any, variables map[string]any, options ...Option) error {
266+
query, optionsOutput, err := c.buildQueryAndOptions(op, v, variables, options...)
267+
if err != nil {
268+
return err
269+
}
270+
data, extData, resp, respBuf, errs := c.request(ctx, query, variables, optionsOutput)
271+
272+
return c.processResponse(v, data, optionsOutput.extensions, extData, resp, respBuf, errs)
263273
}
264274

265275
// Executes a pre-built query and unmarshals the response into v. Unlike the Query method you have to specify in the query the
266276
// fields that you want to receive as they are not inferred from v. This method is useful if you need to build the query dynamically.
267-
func (c *Client) Exec(ctx context.Context, query string, v interface{}, variables map[string]interface{}, options ...Option) error {
277+
func (c *Client) Exec(ctx context.Context, query string, v any, variables map[string]any, options ...Option) error {
268278
optionsOutput, err := constructOptions(options)
269279
if err != nil {
270280
return err
271281
}
272282

273-
data, _, resp, respBuf, errs := c.request(ctx, query, variables, optionsOutput)
274-
return c.processResponse(v, data, resp, respBuf, errs)
283+
data, extData, resp, respBuf, errs := c.request(ctx, query, variables, optionsOutput)
284+
return c.processResponse(v, data, optionsOutput.extensions, extData, resp, respBuf, errs)
275285
}
276286

277287
// Executes a pre-built query and returns the raw json message. Unlike the Query method you have to specify in the query the
278288
// fields that you want to receive as they are not inferred from the interface. This method is useful if you need to build the query dynamically.
279-
func (c *Client) ExecRaw(ctx context.Context, query string, variables map[string]interface{}, options ...Option) ([]byte, error) {
289+
func (c *Client) ExecRaw(ctx context.Context, query string, variables map[string]any, options ...Option) ([]byte, error) {
280290
optionsOutput, err := constructOptions(options)
281291
if err != nil {
282292
return nil, err
@@ -289,10 +299,10 @@ func (c *Client) ExecRaw(ctx context.Context, query string, variables map[string
289299
return data, nil
290300
}
291301

292-
// Executes a pre-built query and returns the raw json message and a map with extensions (values also as raw json objects). Unlike the
302+
// ExecRawWithExtensions execute a pre-built query and returns the raw json message and a map with extensions (values also as raw json objects). Unlike the
293303
// Query method you have to specify in the query the fields that you want to receive as they are not inferred from the interface. This method
294304
// is useful if you need to build the query dynamically.
295-
func (c *Client) ExecRawWithExtensions(ctx context.Context, query string, variables map[string]interface{}, options ...Option) ([]byte, []byte, error) {
305+
func (c *Client) ExecRawWithExtensions(ctx context.Context, query string, variables map[string]any, options ...Option) ([]byte, []byte, error) {
296306
optionsOutput, err := constructOptions(options)
297307
if err != nil {
298308
return nil, nil, err
@@ -305,7 +315,7 @@ func (c *Client) ExecRawWithExtensions(ctx context.Context, query string, variab
305315
return data, ext, nil
306316
}
307317

308-
func (c *Client) processResponse(v interface{}, data []byte, resp *http.Response, respBuf io.Reader, errs Errors) error {
318+
func (c *Client) processResponse(v any, data []byte, extensions any, rawExtensions []byte, resp *http.Response, respBuf io.Reader, errs Errors) error {
309319
if len(data) > 0 {
310320
err := jsonutil.UnmarshalGraphQL(data, v)
311321
if err != nil {
@@ -317,6 +327,14 @@ func (c *Client) processResponse(v interface{}, data []byte, resp *http.Response
317327
}
318328
}
319329

330+
if len(rawExtensions) > 0 && extensions != nil {
331+
err := json.Unmarshal(rawExtensions, extensions)
332+
if err != nil {
333+
we := newError(ErrGraphQLExtensionsDecode, err)
334+
errs = append(errs, we)
335+
}
336+
}
337+
320338
if len(errs) > 0 {
321339
return errs
322340
}
@@ -352,13 +370,13 @@ func (c *Client) WithDebug(debug bool) *Client {
352370
type Errors []Error
353371

354372
type Error struct {
355-
Message string `json:"message"`
356-
Extensions map[string]interface{} `json:"extensions"`
373+
Message string `json:"message"`
374+
Extensions map[string]any `json:"extensions"`
357375
Locations []struct {
358376
Line int `json:"line"`
359377
Column int `json:"column"`
360378
} `json:"locations"`
361-
Path []interface{} `json:"path"`
379+
Path []any `json:"path"`
362380
err error
363381
}
364382

@@ -390,22 +408,22 @@ func (e Errors) Unwrap() []error {
390408
return errs
391409
}
392410

393-
func (e Error) getInternalExtension() map[string]interface{} {
411+
func (e Error) getInternalExtension() map[string]any {
394412
if e.Extensions == nil {
395-
return make(map[string]interface{})
413+
return make(map[string]any)
396414
}
397415

398416
if ex, ok := e.Extensions["internal"]; ok {
399-
return ex.(map[string]interface{})
417+
return ex.(map[string]any)
400418
}
401419

402-
return make(map[string]interface{})
420+
return make(map[string]any)
403421
}
404422

405423
func newError(code string, err error) Error {
406424
return Error{
407425
Message: err.Error(),
408-
Extensions: map[string]interface{}{
426+
Extensions: map[string]any{
409427
"code": code,
410428
},
411429
err: err,
@@ -435,31 +453,35 @@ func (e Error) withRequest(req *http.Request, bodyReader io.Reader) Error {
435453
if err != nil {
436454
internal["error"] = err
437455
} else {
438-
internal["request"] = map[string]interface{}{
456+
internal["request"] = map[string]any{
439457
"headers": req.Header,
440458
"body": string(bodyBytes),
441459
}
442460
}
443461

444462
if e.Extensions == nil {
445-
e.Extensions = make(map[string]interface{})
463+
e.Extensions = make(map[string]any)
446464
}
447465
e.Extensions["internal"] = internal
448466
return e
449467
}
450468

451469
func (e Error) withResponse(res *http.Response, bodyReader io.Reader) Error {
452470
internal := e.getInternalExtension()
453-
bodyBytes, err := io.ReadAll(bodyReader)
454-
if err != nil {
455-
internal["error"] = err
456-
} else {
457-
internal["response"] = map[string]interface{}{
458-
"headers": res.Header,
459-
"body": string(bodyBytes),
460-
}
471+
472+
response := map[string]any{
473+
"headers": res.Header,
461474
}
462475

476+
if bodyReader != nil {
477+
bodyBytes, err := io.ReadAll(bodyReader)
478+
if err != nil {
479+
internal["error"] = err
480+
} else {
481+
response["body"] = string(bodyBytes)
482+
}
483+
}
484+
internal["response"] = response
463485
e.Extensions["internal"] = internal
464486
return e
465487
}
@@ -470,7 +492,7 @@ func (e Error) withResponse(res *http.Response, bodyReader io.Reader) Error {
470492
// The implementation is created on top of the JSON tokenizer available
471493
// in "encoding/json".Decoder.
472494
// This function is re-exported from the internal package
473-
func UnmarshalGraphQL(data []byte, v interface{}) error {
495+
func UnmarshalGraphQL(data []byte, v any) error {
474496
return jsonutil.UnmarshalGraphQL(data, v)
475497
}
476498

@@ -481,9 +503,10 @@ const (
481503
mutationOperation
482504
// subscriptionOperation // Unused.
483505

484-
ErrRequestError = "request_error"
485-
ErrJsonEncode = "json_encode_error"
486-
ErrJsonDecode = "json_decode_error"
487-
ErrGraphQLEncode = "graphql_encode_error"
488-
ErrGraphQLDecode = "graphql_decode_error"
506+
ErrRequestError = "request_error"
507+
ErrJsonEncode = "json_encode_error"
508+
ErrJsonDecode = "json_decode_error"
509+
ErrGraphQLEncode = "graphql_encode_error"
510+
ErrGraphQLDecode = "graphql_decode_error"
511+
ErrGraphQLExtensionsDecode = "graphql_extensions_decode_error"
489512
)

graphql_test.go

Lines changed: 52 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -470,6 +470,56 @@ func TestClient_Exec_QueryRaw(t *testing.T) {
470470
}
471471
}
472472

473+
func TestClient_BindExtensions(t *testing.T) {
474+
mux := http.NewServeMux()
475+
mux.HandleFunc("/graphql", func(w http.ResponseWriter, req *http.Request) {
476+
body := mustRead(req.Body)
477+
if got, want := body, `{"query":"{user{id,name}}"}`+"\n"; got != want {
478+
t.Errorf("got body: %v, want %v", got, want)
479+
}
480+
w.Header().Set("Content-Type", "application/json")
481+
mustWrite(w, `{"data": {"user": {"name": "Gopher"}}, "extensions": {"id": 1, "domain": "users"}}`)
482+
})
483+
client := graphql.NewClient("/graphql", &http.Client{Transport: localRoundTripper{handler: mux}})
484+
485+
var q struct {
486+
User struct {
487+
ID string `graphql:"id"`
488+
Name string `graphql:"name"`
489+
}
490+
}
491+
492+
var ext struct {
493+
ID int `json:"id"`
494+
Domain string `json:"domain"`
495+
}
496+
497+
err := client.Query(context.Background(), &q, map[string]interface{}{})
498+
if err != nil {
499+
t.Fatal(err)
500+
}
501+
502+
if got, want := q.User.Name, "Gopher"; got != want {
503+
t.Fatalf("got q.User.Name: %q, want: %q", got, want)
504+
}
505+
506+
err = client.Query(context.Background(), &q, map[string]interface{}{}, graphql.BindExtensions(&ext))
507+
if err != nil {
508+
t.Fatal(err)
509+
}
510+
511+
if got, want := q.User.Name, "Gopher"; got != want {
512+
t.Fatalf("got q.User.Name: %q, want: %q", got, want)
513+
}
514+
515+
if got, want := ext.ID, 1; got != want {
516+
t.Errorf("got ext.ID: %q, want: %q", got, want)
517+
}
518+
if got, want := ext.Domain, "users"; got != want {
519+
t.Errorf("got ext.Domain: %q, want: %q", got, want)
520+
}
521+
}
522+
473523
// Test exec pre-built query, return raw json string and map
474524
// with extensions
475525
func TestClient_Exec_QueryRawWithExtensions(t *testing.T) {
@@ -485,8 +535,8 @@ func TestClient_Exec_QueryRawWithExtensions(t *testing.T) {
485535
client := graphql.NewClient("/graphql", &http.Client{Transport: localRoundTripper{handler: mux}})
486536

487537
var ext struct {
488-
ID int `graphql:"id"`
489-
Domain string `graphql:"domain"`
538+
ID int `json:"id"`
539+
Domain string `json:"domain"`
490540
}
491541

492542
_, extensions, err := client.ExecRawWithExtensions(context.Background(), "{user{id,name}}", map[string]interface{}{})

0 commit comments

Comments
 (0)