diff --git a/README.md b/README.md index 6ba5878..2647628 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,4 @@ -go-graphql-client -======= +# go-graphql-client [![Unit tests](https://github.com/hasura/go-graphql-client/actions/workflows/test.yml/badge.svg)](https://github.com/hasura/go-graphql-client/actions/workflows/test.yml) @@ -14,41 +13,42 @@ For more information, see package [`github.com/shurcooL/githubv4`](https://githu **Note**: Before v0.8.0, `QueryRaw`, `MutateRaw` and `Subscribe` methods return `*json.RawMessage`. This output type is redundant to be decoded. From v0.8.0, the output type is changed to `[]byte`. - [go-graphql-client](#go-graphql-client) - - [Installation](#installation) - - [Usage](#usage) - - [Authentication](#authentication) - - [Simple Query](#simple-query) - - [Arguments and Variables](#arguments-and-variables) - - [Custom scalar tag](#custom-scalar-tag) - - [Skip GraphQL field](#skip-graphql-field) - - [Inline Fragments](#inline-fragments) - - [Specify GraphQL type name](#specify-graphql-type-name) - - [Mutations](#mutations) - - [Mutations Without Fields](#mutations-without-fields) - - [Subscription](#subscription) - - [Usage](#usage-1) - - [Subscribe](#subscribe) - - [Stop the subscription](#stop-the-subscription) - - [Authentication](#authentication-1) - - [Options](#options) - - [Subscription Protocols](#subscription-protocols) - - [Handle connection error](#handle-connection-error) - - [Events](#events) - - [Custom HTTP Client](#custom-http-client) - - [Custom WebSocket client](#custom-websocket-client) - - [Options](#options-1) - - [Execute pre-built query](#execute-pre-built-query) - - [With operation name (deprecated)](#with-operation-name-deprecated) - - [Raw bytes response](#raw-bytes-response) - - [Multiple mutations with ordered map](#multiple-mutations-with-ordered-map) - - [Debugging and Unit test](#debugging-and-unit-test) - - [Directories](#directories) - - [References](#references) - - [License](#license) + - [Installation](#installation) + - [Usage](#usage) + - [Authentication](#authentication) + - [Simple Query](#simple-query) + - [Arguments and Variables](#arguments-and-variables) + - [Custom scalar tag](#custom-scalar-tag) + - [Skip GraphQL field](#skip-graphql-field) + - [Inline Fragments](#inline-fragments) + - [Specify GraphQL type name](#specify-graphql-type-name) + - [Mutations](#mutations) + - [Mutations Without Fields](#mutations-without-fields) + - [Subscription](#subscription) + - [Usage](#usage-1) + - [Subscribe](#subscribe) + - [Stop the subscription](#stop-the-subscription) + - [Authentication](#authentication-1) + - [Options](#options) + - [Subscription Protocols](#subscription-protocols) + - [Handle connection error](#handle-connection-error) + - [Events](#events) + - [Custom HTTP Client](#custom-http-client) + - [Custom WebSocket client](#custom-websocket-client) + - [Options](#options-1) + - [Execute pre-built query](#execute-pre-built-query) + - [With operation name (deprecated)](#with-operation-name-deprecated) + - [Raw bytes response](#raw-bytes-response) + - [Multiple mutations with ordered map](#multiple-mutations-with-ordered-map) + - [Debugging and Unit test](#debugging-and-unit-test) + - [Directories](#directories) + - [References](#references) + - [License](#license) ## Installation -`go-graphql-client` requires Go version 1.20 or later. For older Go versions: +`go-graphql-client` requires Go version 1.20 or later. For older Go versions: + - **>= 1.16 < 1.20**: downgrade the library to version v0.9.x - **< 1.16**: downgrade the library version below v0.7.1. @@ -189,6 +189,7 @@ if err != nil { ``` Variables get encoded as normal json. So if you supply a struct for a variable and want to rename fields, you can do this like that: + ```Go type Dimensions struct { Width int `json:"ship_width"`, @@ -213,6 +214,7 @@ variables := map[string]interface{}{ err := client.Mutate(context.TODO(), &mutation, variables) ``` + which will set `ship_dimensions` to an object with the properties `ship_width` and `ship_height`. ### Custom scalar tag @@ -580,7 +582,7 @@ client := graphql.NewSubscriptionClient(serverEndpoint). "Authorization": []string{"Bearer random-secret"}, }, }) -``` +``` #### Options @@ -608,6 +610,7 @@ client. #### Subscription Protocols The subscription client supports 2 protocols: + - [subscriptions-transport-ws](https://github.com/apollographql/subscriptions-transport-ws/blob/master/PROTOCOL.md) (default) - [graphql-ws](https://github.com/enisdenjo/graphql-ws/blob/master/PROTOCOL.md) @@ -818,6 +821,29 @@ if err != nil { err = json.Unmarshal(raw, &res) ``` +### Get extensions from response + +The response map may also contain an entry with key `extensions`. To decode this field you need to bind a struct or map pointer. The client will optionally unmarshal the field using json decoder. + +```go +var q struct { + User struct { + ID string `graphql:"id"` + Name string `graphql:"name"` + } +} + +var ext struct { + ID int `json:"id"` + Domain string `json:"domain"` +} + +err := client.Query(context.Background(), &q, map[string]interface{}{}, graphql.BindExtensions(&ext)) +if err != nil { + t.Fatal(err) +} +``` + Additionally, if you need information about the extensions returned in the response use `ExecRawWithExtensions`. This function returns a map with extensions as the second variable. ```Go @@ -902,37 +928,38 @@ Enable debug mode with the `WithDebug` function. If the request is failed, the r ```json { - "errors": [ - { - "message":"Field 'user' is missing required arguments: login", - "extensions": { - "internal": { - "request": { - "body":"{\"query\":\"{user{name}}\"}", - "headers": { - "Content-Type": ["application/json"] - } - }, - "response": { - "body":"{\"errors\": [{\"message\": \"Field 'user' is missing required arguments: login\",\"locations\": [{\"line\": 7,\"column\": 3}]}]}", - "headers": { - "Content-Type": ["application/json"] - } - } - } - }, - "locations": [ - { - "line":7, - "column":3 - } - ] - } - ] + "errors": [ + { + "message": "Field 'user' is missing required arguments: login", + "extensions": { + "internal": { + "request": { + "body": "{\"query\":\"{user{name}}\"}", + "headers": { + "Content-Type": ["application/json"] + } + }, + "response": { + "body": "{\"errors\": [{\"message\": \"Field 'user' is missing required arguments: login\",\"locations\": [{\"line\": 7,\"column\": 3}]}]}", + "headers": { + "Content-Type": ["application/json"] + } + } + } + }, + "locations": [ + { + "line": 7, + "column": 3 + } + ] + } + ] } ``` For debugging queries, you can use `Construct*` functions to see what the generated query looks like: + ```go // ConstructQuery build GraphQL query string from struct and variables func ConstructQuery(v interface{}, variables map[string]interface{}, options ...Option) (string, error) @@ -950,23 +977,20 @@ func UnmarshalGraphQL(data []byte, v interface{}) error Because the GraphQL query string is generated in runtime using reflection, it isn't really safe. To assure the GraphQL query is expected, it's necessary to write some unit test for query construction. -Directories ------------ +## Directories | Path | Synopsis | -|----------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------| +| -------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------- | | [example/graphqldev](https://godoc.org/github.com/shurcooL/graphql/example/graphqldev) | graphqldev is a test program currently being used for developing graphql package. | | [ident](https://godoc.org/github.com/shurcooL/graphql/ident) | Package ident provides functions for parsing and converting identifier names between various naming convention. | | [internal/jsonutil](https://godoc.org/github.com/shurcooL/graphql/internal/jsonutil) | Package jsonutil provides a function for decoding JSON into a GraphQL query data structure. | -References ----------- +## References + - https://github.com/shurcooL/graphql - https://github.com/apollographql/subscriptions-transport-ws/blob/master/PROTOCOL.md - https://github.com/nhooyr/websocket +## License -License -------- - -- [MIT License](LICENSE) +- [MIT License](LICENSE) diff --git a/graphql.go b/graphql.go index cda5bde..31bad9d 100644 --- a/graphql.go +++ b/graphql.go @@ -47,28 +47,28 @@ func NewClient(url string, httpClient Doer) *Client { // Query executes a single GraphQL query request, // with a query derived from q, populating the response into it. // q should be a pointer to struct that corresponds to the GraphQL schema. -func (c *Client) Query(ctx context.Context, q interface{}, variables map[string]interface{}, options ...Option) error { +func (c *Client) Query(ctx context.Context, q any, variables map[string]any, options ...Option) error { return c.do(ctx, queryOperation, q, variables, options...) } // NamedQuery executes a single GraphQL query request, with operation name // // Deprecated: this is the shortcut of Query method, with NewOperationName option -func (c *Client) NamedQuery(ctx context.Context, name string, q interface{}, variables map[string]interface{}, options ...Option) error { +func (c *Client) NamedQuery(ctx context.Context, name string, q any, variables map[string]any, options ...Option) error { return c.do(ctx, queryOperation, q, variables, append(options, OperationName(name))...) } // Mutate executes a single GraphQL mutation request, // with a mutation derived from m, populating the response into it. // m should be a pointer to struct that corresponds to the GraphQL schema. -func (c *Client) Mutate(ctx context.Context, m interface{}, variables map[string]interface{}, options ...Option) error { +func (c *Client) Mutate(ctx context.Context, m any, variables map[string]any, options ...Option) error { return c.do(ctx, mutationOperation, m, variables, options...) } // NamedMutate executes a single GraphQL mutation request, with operation name // // Deprecated: this is the shortcut of Mutate method, with NewOperationName option -func (c *Client) NamedMutate(ctx context.Context, name string, m interface{}, variables map[string]interface{}, options ...Option) error { +func (c *Client) NamedMutate(ctx context.Context, name string, m any, variables map[string]any, options ...Option) error { return c.do(ctx, mutationOperation, m, variables, append(options, OperationName(name))...) } @@ -76,13 +76,13 @@ func (c *Client) NamedMutate(ctx context.Context, name string, m interface{}, va // with a query derived from q, populating the response into it. // q should be a pointer to struct that corresponds to the GraphQL schema. // return raw bytes message. -func (c *Client) QueryRaw(ctx context.Context, q interface{}, variables map[string]interface{}, options ...Option) ([]byte, error) { +func (c *Client) QueryRaw(ctx context.Context, q any, variables map[string]any, options ...Option) ([]byte, error) { return c.doRaw(ctx, queryOperation, q, variables, options...) } // NamedQueryRaw executes a single GraphQL query request, with operation name // return raw bytes message. -func (c *Client) NamedQueryRaw(ctx context.Context, name string, q interface{}, variables map[string]interface{}, options ...Option) ([]byte, error) { +func (c *Client) NamedQueryRaw(ctx context.Context, name string, q any, variables map[string]any, options ...Option) ([]byte, error) { return c.doRaw(ctx, queryOperation, q, variables, append(options, OperationName(name))...) } @@ -90,18 +90,18 @@ func (c *Client) NamedQueryRaw(ctx context.Context, name string, q interface{}, // with a mutation derived from m, populating the response into it. // m should be a pointer to struct that corresponds to the GraphQL schema. // return raw bytes message. -func (c *Client) MutateRaw(ctx context.Context, m interface{}, variables map[string]interface{}, options ...Option) ([]byte, error) { +func (c *Client) MutateRaw(ctx context.Context, m any, variables map[string]any, options ...Option) ([]byte, error) { return c.doRaw(ctx, mutationOperation, m, variables, options...) } // NamedMutateRaw executes a single GraphQL mutation request, with operation name // return raw bytes message. -func (c *Client) NamedMutateRaw(ctx context.Context, name string, m interface{}, variables map[string]interface{}, options ...Option) ([]byte, error) { +func (c *Client) NamedMutateRaw(ctx context.Context, name string, m any, variables map[string]any, options ...Option) ([]byte, error) { return c.doRaw(ctx, mutationOperation, m, variables, append(options, OperationName(name))...) } -// buildAndRequest the common method that builds and send graphql request -func (c *Client) buildAndRequest(ctx context.Context, op operationType, v interface{}, variables map[string]interface{}, options ...Option) ([]byte, *http.Response, io.Reader, Errors) { +// buildQueryAndOptions the common method to build query and options +func (c *Client) buildQueryAndOptions(op operationType, v any, variables map[string]any, options ...Option) (string, *constructOptionsOutput, error) { var query string var err error var optionOutput *constructOptionsOutput @@ -110,18 +110,18 @@ func (c *Client) buildAndRequest(ctx context.Context, op operationType, v interf query, optionOutput, err = constructQuery(v, variables, options...) case mutationOperation: query, optionOutput, err = constructMutation(v, variables, options...) + default: + err = fmt.Errorf("invalid operation type: %v", op) } if err != nil { - return nil, nil, nil, Errors{newError(ErrGraphQLEncode, err)} + return "", nil, Errors{newError(ErrGraphQLEncode, err)} } - - data, _, resp, respBuf, errs := c.request(ctx, query, variables, optionOutput) - return data, resp, respBuf, errs + return query, optionOutput, nil } // Request the common method that send graphql request -func (c *Client) request(ctx context.Context, query string, variables map[string]interface{}, options *constructOptionsOutput) ([]byte, []byte, *http.Response, io.Reader, Errors) { +func (c *Client) request(ctx context.Context, query string, variables map[string]any, options *constructOptionsOutput) ([]byte, []byte, *http.Response, io.Reader, Errors) { in := GraphQLRequestPayload{ Query: query, Variables: variables, @@ -248,35 +248,45 @@ func (c *Client) request(ctx context.Context, query string, variables map[string // do executes a single GraphQL operation. // return raw message and error -func (c *Client) doRaw(ctx context.Context, op operationType, v interface{}, variables map[string]interface{}, options ...Option) ([]byte, error) { - data, _, _, err := c.buildAndRequest(ctx, op, v, variables, options...) - if len(err) > 0 { - return data, err +func (c *Client) doRaw(ctx context.Context, op operationType, v any, variables map[string]any, options ...Option) ([]byte, error) { + query, optionsOutput, err := c.buildQueryAndOptions(op, v, variables, options...) + if err != nil { + return nil, err + } + data, _, _, _, errs := c.request(ctx, query, variables, optionsOutput) + if len(errs) > 0 { + return data, errs } + return data, nil } // do executes a single GraphQL operation and unmarshal json. -func (c *Client) do(ctx context.Context, op operationType, v interface{}, variables map[string]interface{}, options ...Option) error { - data, resp, respBuf, errs := c.buildAndRequest(ctx, op, v, variables, options...) - return c.processResponse(v, data, resp, respBuf, errs) +func (c *Client) do(ctx context.Context, op operationType, v any, variables map[string]any, options ...Option) error { + query, optionsOutput, err := c.buildQueryAndOptions(op, v, variables, options...) + if err != nil { + return err + } + data, extData, resp, respBuf, errs := c.request(ctx, query, variables, optionsOutput) + + return c.processResponse(v, data, optionsOutput.extensions, extData, resp, respBuf, errs) } // Executes a pre-built query and unmarshals the response into v. Unlike the Query method you have to specify in the query the // 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. -func (c *Client) Exec(ctx context.Context, query string, v interface{}, variables map[string]interface{}, options ...Option) error { +func (c *Client) Exec(ctx context.Context, query string, v any, variables map[string]any, options ...Option) error { optionsOutput, err := constructOptions(options) if err != nil { return err } - data, _, resp, respBuf, errs := c.request(ctx, query, variables, optionsOutput) - return c.processResponse(v, data, resp, respBuf, errs) + data, extData, resp, respBuf, errs := c.request(ctx, query, variables, optionsOutput) + return c.processResponse(v, data, optionsOutput.extensions, extData, resp, respBuf, errs) } // Executes a pre-built query and returns the raw json message. Unlike the 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 is useful if you need to build the query dynamically. -func (c *Client) ExecRaw(ctx context.Context, query string, variables map[string]interface{}, options ...Option) ([]byte, error) { +func (c *Client) ExecRaw(ctx context.Context, query string, variables map[string]any, options ...Option) ([]byte, error) { optionsOutput, err := constructOptions(options) if err != nil { return nil, err @@ -289,10 +299,10 @@ func (c *Client) ExecRaw(ctx context.Context, query string, variables map[string return data, nil } -// Executes a pre-built query and returns the raw json message and a map with extensions (values also as raw json objects). Unlike the +// 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 // 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 // is useful if you need to build the query dynamically. -func (c *Client) ExecRawWithExtensions(ctx context.Context, query string, variables map[string]interface{}, options ...Option) ([]byte, []byte, error) { +func (c *Client) ExecRawWithExtensions(ctx context.Context, query string, variables map[string]any, options ...Option) ([]byte, []byte, error) { optionsOutput, err := constructOptions(options) if err != nil { return nil, nil, err @@ -305,7 +315,7 @@ func (c *Client) ExecRawWithExtensions(ctx context.Context, query string, variab return data, ext, nil } -func (c *Client) processResponse(v interface{}, data []byte, resp *http.Response, respBuf io.Reader, errs Errors) error { +func (c *Client) processResponse(v any, data []byte, extensions any, rawExtensions []byte, resp *http.Response, respBuf io.Reader, errs Errors) error { if len(data) > 0 { err := jsonutil.UnmarshalGraphQL(data, v) if err != nil { @@ -317,6 +327,14 @@ func (c *Client) processResponse(v interface{}, data []byte, resp *http.Response } } + if len(rawExtensions) > 0 && extensions != nil { + err := json.Unmarshal(rawExtensions, extensions) + if err != nil { + we := newError(ErrGraphQLExtensionsDecode, err) + errs = append(errs, we) + } + } + if len(errs) > 0 { return errs } @@ -352,13 +370,13 @@ func (c *Client) WithDebug(debug bool) *Client { type Errors []Error type Error struct { - Message string `json:"message"` - Extensions map[string]interface{} `json:"extensions"` + Message string `json:"message"` + Extensions map[string]any `json:"extensions"` Locations []struct { Line int `json:"line"` Column int `json:"column"` } `json:"locations"` - Path []interface{} `json:"path"` + Path []any `json:"path"` err error } @@ -390,22 +408,22 @@ func (e Errors) Unwrap() []error { return errs } -func (e Error) getInternalExtension() map[string]interface{} { +func (e Error) getInternalExtension() map[string]any { if e.Extensions == nil { - return make(map[string]interface{}) + return make(map[string]any) } if ex, ok := e.Extensions["internal"]; ok { - return ex.(map[string]interface{}) + return ex.(map[string]any) } - return make(map[string]interface{}) + return make(map[string]any) } func newError(code string, err error) Error { return Error{ Message: err.Error(), - Extensions: map[string]interface{}{ + Extensions: map[string]any{ "code": code, }, err: err, @@ -435,14 +453,14 @@ func (e Error) withRequest(req *http.Request, bodyReader io.Reader) Error { if err != nil { internal["error"] = err } else { - internal["request"] = map[string]interface{}{ + internal["request"] = map[string]any{ "headers": req.Header, "body": string(bodyBytes), } } if e.Extensions == nil { - e.Extensions = make(map[string]interface{}) + e.Extensions = make(map[string]any) } e.Extensions["internal"] = internal return e @@ -450,16 +468,20 @@ func (e Error) withRequest(req *http.Request, bodyReader io.Reader) Error { func (e Error) withResponse(res *http.Response, bodyReader io.Reader) Error { internal := e.getInternalExtension() - bodyBytes, err := io.ReadAll(bodyReader) - if err != nil { - internal["error"] = err - } else { - internal["response"] = map[string]interface{}{ - "headers": res.Header, - "body": string(bodyBytes), - } + + response := map[string]any{ + "headers": res.Header, } + if bodyReader != nil { + bodyBytes, err := io.ReadAll(bodyReader) + if err != nil { + internal["error"] = err + } else { + response["body"] = string(bodyBytes) + } + } + internal["response"] = response e.Extensions["internal"] = internal return e } @@ -470,7 +492,7 @@ func (e Error) withResponse(res *http.Response, bodyReader io.Reader) Error { // The implementation is created on top of the JSON tokenizer available // in "encoding/json".Decoder. // This function is re-exported from the internal package -func UnmarshalGraphQL(data []byte, v interface{}) error { +func UnmarshalGraphQL(data []byte, v any) error { return jsonutil.UnmarshalGraphQL(data, v) } @@ -481,9 +503,10 @@ const ( mutationOperation // subscriptionOperation // Unused. - ErrRequestError = "request_error" - ErrJsonEncode = "json_encode_error" - ErrJsonDecode = "json_decode_error" - ErrGraphQLEncode = "graphql_encode_error" - ErrGraphQLDecode = "graphql_decode_error" + ErrRequestError = "request_error" + ErrJsonEncode = "json_encode_error" + ErrJsonDecode = "json_decode_error" + ErrGraphQLEncode = "graphql_encode_error" + ErrGraphQLDecode = "graphql_decode_error" + ErrGraphQLExtensionsDecode = "graphql_extensions_decode_error" ) diff --git a/graphql_test.go b/graphql_test.go index 10394a0..031fbc2 100644 --- a/graphql_test.go +++ b/graphql_test.go @@ -470,6 +470,56 @@ func TestClient_Exec_QueryRaw(t *testing.T) { } } +func TestClient_BindExtensions(t *testing.T) { + mux := http.NewServeMux() + mux.HandleFunc("/graphql", func(w http.ResponseWriter, req *http.Request) { + body := mustRead(req.Body) + if got, want := body, `{"query":"{user{id,name}}"}`+"\n"; got != want { + t.Errorf("got body: %v, want %v", got, want) + } + w.Header().Set("Content-Type", "application/json") + mustWrite(w, `{"data": {"user": {"name": "Gopher"}}, "extensions": {"id": 1, "domain": "users"}}`) + }) + client := graphql.NewClient("/graphql", &http.Client{Transport: localRoundTripper{handler: mux}}) + + var q struct { + User struct { + ID string `graphql:"id"` + Name string `graphql:"name"` + } + } + + var ext struct { + ID int `json:"id"` + Domain string `json:"domain"` + } + + err := client.Query(context.Background(), &q, map[string]interface{}{}) + if err != nil { + t.Fatal(err) + } + + if got, want := q.User.Name, "Gopher"; got != want { + t.Fatalf("got q.User.Name: %q, want: %q", got, want) + } + + err = client.Query(context.Background(), &q, map[string]interface{}{}, graphql.BindExtensions(&ext)) + if err != nil { + t.Fatal(err) + } + + if got, want := q.User.Name, "Gopher"; got != want { + t.Fatalf("got q.User.Name: %q, want: %q", got, want) + } + + if got, want := ext.ID, 1; got != want { + t.Errorf("got ext.ID: %q, want: %q", got, want) + } + if got, want := ext.Domain, "users"; got != want { + t.Errorf("got ext.Domain: %q, want: %q", got, want) + } +} + // Test exec pre-built query, return raw json string and map // with extensions func TestClient_Exec_QueryRawWithExtensions(t *testing.T) { @@ -485,8 +535,8 @@ func TestClient_Exec_QueryRawWithExtensions(t *testing.T) { client := graphql.NewClient("/graphql", &http.Client{Transport: localRoundTripper{handler: mux}}) var ext struct { - ID int `graphql:"id"` - Domain string `graphql:"domain"` + ID int `json:"id"` + Domain string `json:"domain"` } _, extensions, err := client.ExecRawWithExtensions(context.Background(), "{user{id,name}}", map[string]interface{}{}) diff --git a/option.go b/option.go index 88ac865..b89d7f9 100644 --- a/option.go +++ b/option.go @@ -4,8 +4,6 @@ package graphql type OptionType string const ( - // optionTypeOperationName is private because it's option is built-in and unique - optionTypeOperationName OptionType = "operation_name" OptionTypeOperationDirective OptionType = "operation_directive" ) @@ -15,8 +13,6 @@ type Option interface { // Type returns the supported type of the renderer // available types: operation_name and operation_directive Type() OptionType - // String returns the query component string - String() string } // operationNameOption represents the operation name render component @@ -25,7 +21,7 @@ type operationNameOption struct { } func (ono operationNameOption) Type() OptionType { - return optionTypeOperationName + return "operation_name" } func (ono operationNameOption) String() string { @@ -36,3 +32,17 @@ func (ono operationNameOption) String() string { func OperationName(name string) Option { return operationNameOption{name} } + +// bind the struct pointer to decode extensions from response +type bindExtensionsOption struct { + value any +} + +func (ono bindExtensionsOption) Type() OptionType { + return "bind_extensions" +} + +// BindExtensions bind the struct pointer to decode extensions from json response +func BindExtensions(value any) Option { + return bindExtensionsOption{value: value} +} diff --git a/query.go b/query.go index dfd6edc..0954cce 100644 --- a/query.go +++ b/query.go @@ -16,6 +16,7 @@ import ( type constructOptionsOutput struct { operationName string operationDirectives []string + extensions any } func (coo constructOptionsOutput) OperationDirectivesString() string { @@ -30,13 +31,21 @@ func constructOptions(options []Option) (*constructOptionsOutput, error) { output := &constructOptionsOutput{} for _, option := range options { - switch option.Type() { - case optionTypeOperationName: - output.operationName = option.String() - case OptionTypeOperationDirective: - output.operationDirectives = append(output.operationDirectives, option.String()) + switch opt := option.(type) { + case operationNameOption: + output.operationName = opt.name + case bindExtensionsOption: + output.extensions = opt.value default: - return nil, fmt.Errorf("invalid query option type: %s", option.Type()) + if opt.Type() != OptionTypeOperationDirective { + return nil, fmt.Errorf("invalid query option type: %s", option.Type()) + } + if d, ok := option.(fmt.Stringer); ok { + output.operationDirectives = append(output.operationDirectives, d.String()) + } else { + return nil, fmt.Errorf("please implement the fmt.Stringer interface for %s option", OptionTypeOperationDirective) + } + } } @@ -234,7 +243,7 @@ func writeQuery(w io.Writer, t reflect.Type, v reflect.Value, inline bool) error } case reflect.Struct: // If the type implements json.Unmarshaler, it's a scalar. Don't expand it. - if reflect.PtrTo(t).Implements(jsonUnmarshaler) { + if reflect.PointerTo(t).Implements(jsonUnmarshaler) { return nil } if t.AssignableTo(idType) { diff --git a/query_test.go b/query_test.go index e73de3a..6263772 100644 --- a/query_test.go +++ b/query_test.go @@ -342,13 +342,15 @@ func TestConstructQuery(t *testing.T) { want: `{viewer{login,databaseId}}`, }, } - for _, tc := range tests { - got, err := ConstructQuery(tc.inV, tc.inVariables, tc.options...) - if err != nil { - t.Error(err) - } else if got != tc.want { - t.Errorf("\ngot: %q\nwant: %q\n", got, tc.want) - } + for i, tc := range tests { + t.Run(fmt.Sprintf("%d", i), func(t *testing.T) { + got, err := ConstructQuery(tc.inV, tc.inVariables, tc.options...) + if err != nil { + t.Error(err) + } else if got != tc.want { + t.Errorf("\ngot: %q\nwant: %q\n", got, tc.want) + } + }) } }