diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index a86884e2..6e0bd6bf 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -29,6 +29,7 @@ When releasing a new version: - genqlient can now run as a portable binary (i.e. without a local checkout of the repository or `go run`). - You can now enable `use_extensions` in the configuration file, to receive extensions returned by the GraphQL API server. Generated functions will return extensions as `map[string]interface{}`, if enabled. +- You can now use `graphql.NewClientUsingGet` to create a client that uses query parameters to pass the query to the GraphQL API server. ### Bug fixes: diff --git a/docs/FAQ.md b/docs/FAQ.md index 496fcb2a..4793b4f3 100644 --- a/docs/FAQ.md +++ b/docs/FAQ.md @@ -8,6 +8,34 @@ This document describes common questions about genqlient, and provides an index There's a [doc for that](INTRODUCTION.md)! +## … use GET requests instead of POST requests? + +You can use `graphql.NewClientUsingGet` to create a client that will use query parameters to create the request. For example: +```go +ctx := context.Background() +client := graphql.NewClientUsingGet("https://api.github.com/graphql", http.DefaultClient) +resp, err := getUser(ctx, client, "benjaminjkraft") +fmt.Println(resp.User.Name, err) +``` + +This request will be sent via an HTTP GET request, with the query, operation name and variables encoded in the URL. + +For example, if the query is defined as: + +```graphql +query getUser($login: String!) { + user(login: $login) { + name + } +} +``` + +The URL requested will be: + +`https://api.github.com/graphql?operationName%3DgetUser%26query%3D%0Aquery%20getUser(%24login%3A%20String!)%20%7B%0A%20%20user(login%3A%20%24login)%20%7B%0A%20%20%20%20name%0A%20%20%7D%0A%7D%0A%26variables%3D%7B%22login%22%3A%22benjaminjkraft%22%7D` + +The client does not support mutations, and will return an error if passed a request that attempts one. + ### … use an API that requires authentication? When you call `graphql.NewClient`, pass in an HTTP client that adds whatever authentication headers you need (typically by wrapping the client's `Transport`). For example: diff --git a/graphql/client.go b/graphql/client.go index 47154702..600f2438 100644 --- a/graphql/client.go +++ b/graphql/client.go @@ -4,9 +4,12 @@ import ( "bytes" "context" "encoding/json" + "errors" "fmt" "io" "net/http" + "net/url" + "strings" "github.com/vektah/gqlparser/v2/gqlerror" ) @@ -53,10 +56,30 @@ type client struct { // The typical method of adding authentication headers is to wrap the client's // Transport to add those headers. See example/caller.go for an example. func NewClient(endpoint string, httpClient Doer) Client { + return newClient(endpoint, httpClient, http.MethodPost) +} + +// NewClientUsingGet returns a Client which makes requests to the given endpoint, +// suitable for most users. +// +// The client makes GET requests to the given GraphQL endpoint using a GET query, +// with the query, operation name and variables encoded as URL parameters. +// It will use the given http client, or http.DefaultClient if a nil client is passed. +// +// The client does not support mutations, and will return an error if passed a request +// that attempts one. +// +// The typical method of adding authentication headers is to wrap the client's +// Transport to add those headers. See example/caller.go for an example. +func NewClientUsingGet(endpoint string, httpClient Doer) Client { + return newClient(endpoint, httpClient, http.MethodGet) +} + +func newClient(endpoint string, httpClient Doer, method string) Client { if httpClient == nil || httpClient == (*http.Client)(nil) { httpClient = http.DefaultClient } - return &client{httpClient, endpoint, http.MethodPost} + return &client{httpClient, endpoint, method} } // Doer encapsulates the methods from *http.Client needed by Client. @@ -100,15 +123,14 @@ type Response struct { } func (c *client) MakeRequest(ctx context.Context, req *Request, resp *Response) error { - body, err := json.Marshal(req) - if err != nil { - return err + var httpReq *http.Request + var err error + if c.method == http.MethodGet { + httpReq, err = c.createGetRequest(req) + } else { + httpReq, err = c.createPostRequest(req) } - httpReq, err := http.NewRequest( - c.method, - c.endpoint, - bytes.NewReader(body)) if err != nil { return err } @@ -142,3 +164,66 @@ func (c *client) MakeRequest(ctx context.Context, req *Request, resp *Response) } return nil } + +func (c *client) createPostRequest(req *Request) (*http.Request, error) { + body, err := json.Marshal(req) + if err != nil { + return nil, err + } + + httpReq, err := http.NewRequest( + c.method, + c.endpoint, + bytes.NewReader(body)) + if err != nil { + return nil, err + } + + return httpReq, nil +} + +func (c *client) createGetRequest(req *Request) (*http.Request, error) { + parsedURL, err := url.Parse(c.endpoint) + if err != nil { + return nil, err + } + + queryParams := parsedURL.Query() + queryUpdated := false + + if req.Query != "" { + if strings.HasPrefix(strings.TrimSpace(req.Query), "mutation") { + return nil, errors.New("client does not support mutations") + } + queryParams.Set("query", req.Query) + queryUpdated = true + } + + if req.OpName != "" { + queryParams.Set("operationName", req.OpName) + queryUpdated = true + } + + if req.Variables != nil { + variables, variablesErr := json.Marshal(req.Variables) + if variablesErr != nil { + return nil, variablesErr + } + queryParams.Set("variables", string(variables)) + queryUpdated = true + } + + if queryUpdated { + parsedURL.RawQuery = queryParams.Encode() + } + + httpReq, err := http.NewRequest( + c.method, + parsedURL.String(), + http.NoBody) + if err != nil { + return nil, err + } + + return httpReq, nil +} diff --git a/internal/integration/generated.go b/internal/integration/generated.go index 8ebe975d..77d42681 100644 --- a/internal/integration/generated.go +++ b/internal/integration/generated.go @@ -598,6 +598,13 @@ type MoreUserFieldsHair struct { // GetColor returns MoreUserFieldsHair.Color, and is useful for accessing the field via an interface. func (v *MoreUserFieldsHair) GetColor() string { return v.Color } +type NewUser struct { + Name string `json:"name"` +} + +// GetName returns NewUser.Name, and is useful for accessing the field via an interface. +func (v *NewUser) GetName() string { return v.Name } + // QueryFragment includes the GraphQL fields of Query requested by the fragment QueryFragment. type QueryFragment struct { Beings []QueryFragmentBeingsBeing `json:"-"` @@ -991,6 +998,14 @@ func (v *UserFields) __premarshalJSON() (*__premarshalUserFields, error) { return &retval, nil } +// __createUserInput is used internally by genqlient +type __createUserInput struct { + User NewUser `json:"user"` +} + +// GetUser returns __createUserInput.User, and is useful for accessing the field via an interface. +func (v *__createUserInput) GetUser() NewUser { return v.User } + // __queryWithCustomMarshalInput is used internally by genqlient type __queryWithCustomMarshalInput struct { Date time.Time `json:"-"` @@ -1290,6 +1305,26 @@ type __queryWithVariablesInput struct { // GetId returns __queryWithVariablesInput.Id, and is useful for accessing the field via an interface. func (v *__queryWithVariablesInput) GetId() string { return v.Id } +// createUserCreateUser includes the requested fields of the GraphQL type User. +type createUserCreateUser struct { + Id string `json:"id"` + Name string `json:"name"` +} + +// GetId returns createUserCreateUser.Id, and is useful for accessing the field via an interface. +func (v *createUserCreateUser) GetId() string { return v.Id } + +// GetName returns createUserCreateUser.Name, and is useful for accessing the field via an interface. +func (v *createUserCreateUser) GetName() string { return v.Name } + +// createUserResponse is returned by createUser on success. +type createUserResponse struct { + CreateUser createUserCreateUser `json:"createUser"` +} + +// GetCreateUser returns createUserResponse.CreateUser, and is useful for accessing the field via an interface. +func (v *createUserResponse) GetCreateUser() createUserCreateUser { return v.CreateUser } + // failingQueryMeUser includes the requested fields of the GraphQL type User. type failingQueryMeUser struct { Id string `json:"id"` @@ -3043,6 +3078,39 @@ type simpleQueryResponse struct { // GetMe returns simpleQueryResponse.Me, and is useful for accessing the field via an interface. func (v *simpleQueryResponse) GetMe() simpleQueryMeUser { return v.Me } +func createUser( + ctx context.Context, + client graphql.Client, + user NewUser, +) (*createUserResponse, map[string]interface{}, error) { + req := &graphql.Request{ + OpName: "createUser", + Query: ` +mutation createUser ($user: NewUser!) { + createUser(input: $user) { + id + name + } +} +`, + Variables: &__createUserInput{ + User: user, + }, + } + var err error + + var data createUserResponse + resp := &graphql.Response{Data: &data} + + err = client.MakeRequest( + ctx, + req, + resp, + ) + + return &data, resp.Extensions, err +} + func failingQuery( ctx context.Context, client graphql.Client, diff --git a/internal/integration/integration_test.go b/internal/integration/integration_test.go index e41ced5e..0e056fab 100644 --- a/internal/integration/integration_test.go +++ b/internal/integration/integration_test.go @@ -25,14 +25,35 @@ func TestSimpleQuery(t *testing.T) { ctx := context.Background() server := server.RunServer() defer server.Close() - client := newRoundtripClient(t, server.URL) + clients := newRoundtripClients(t, server.URL) - resp, _, err := simpleQuery(ctx, client) + for _, client := range clients { + resp, _, err := simpleQuery(ctx, client) + require.NoError(t, err) + + assert.Equal(t, "1", resp.Me.Id) + assert.Equal(t, "Yours Truly", resp.Me.Name) + assert.Equal(t, 17, resp.Me.LuckyNumber) + } +} + +func TestMutation(t *testing.T) { + _ = `# @genqlient + mutation createUser($user: NewUser!) { createUser(input: $user) { id name } }` + + ctx := context.Background() + server := server.RunServer() + defer server.Close() + postClient := newRoundtripClient(t, server.URL) + getClient := newRoundtripGetClient(t, server.URL) + + resp, _, err := createUser(ctx, postClient, NewUser{Name: "Jack"}) require.NoError(t, err) + assert.Equal(t, "5", resp.CreateUser.Id) + assert.Equal(t, "Jack", resp.CreateUser.Name) - assert.Equal(t, "1", resp.Me.Id) - assert.Equal(t, "Yours Truly", resp.Me.Name) - assert.Equal(t, 17, resp.Me.LuckyNumber) + _, _, err = createUser(ctx, getClient, NewUser{Name: "Jill"}) + require.Errorf(t, err, "client does not support mutations") } func TestServerError(t *testing.T) { @@ -42,30 +63,34 @@ func TestServerError(t *testing.T) { ctx := context.Background() server := server.RunServer() defer server.Close() - client := newRoundtripClient(t, server.URL) - - resp, _, err := failingQuery(ctx, client) - // As long as we get some response back, we should still return a full - // response -- and indeed in this case it should even have another field - // (which didn't err) set. - assert.Error(t, err) - assert.NotNil(t, resp) - assert.Equal(t, "1", resp.Me.Id) + clients := newRoundtripClients(t, server.URL) + + for _, client := range clients { + resp, _, err := failingQuery(ctx, client) + // As long as we get some response back, we should still return a full + // response -- and indeed in this case it should even have another field + // (which didn't err) set. + assert.Error(t, err) + assert.NotNil(t, resp) + assert.Equal(t, "1", resp.Me.Id) + } } func TestNetworkError(t *testing.T) { ctx := context.Background() - client := newRoundtripClient(t, "https://nothing.invalid/graphql") - - resp, _, err := failingQuery(ctx, client) - // As we guarantee in the README, even on network error you always get a - // non-nil response; this is so you can write e.g. - // resp, err := failingQuery(ctx) - // return resp.Me.Id, err - // without a bunch of extra ceremony. - assert.Error(t, err) - assert.NotNil(t, resp) - assert.Equal(t, new(failingQueryResponse), resp) + clients := newRoundtripClients(t, "https://nothing.invalid/graphql") + + for _, client := range clients { + resp, _, err := failingQuery(ctx, client) + // As we guarantee in the README, even on network error you always get a + // non-nil response; this is so you can write e.g. + // resp, err := failingQuery(ctx) + // return resp.Me.Id, err + // without a bunch of extra ceremony. + assert.Error(t, err) + assert.NotNil(t, resp) + assert.Equal(t, new(failingQueryResponse), resp) + } } func TestVariables(t *testing.T) { @@ -80,19 +105,24 @@ func TestVariables(t *testing.T) { // this right in Go (without adding `pointer: true` just for this purpose), // and unmarshal(marshal(resp)) == resp should still hold, so we don't // worry about it. - client := graphql.NewClient(server.URL, http.DefaultClient) + clients := []graphql.Client{ + graphql.NewClient(server.URL, http.DefaultClient), + graphql.NewClientUsingGet(server.URL, http.DefaultClient), + } - resp, _, err := queryWithVariables(ctx, client, "2") - require.NoError(t, err) + for _, client := range clients { + resp, _, err := queryWithVariables(ctx, client, "2") + require.NoError(t, err) - assert.Equal(t, "2", resp.User.Id) - assert.Equal(t, "Raven", resp.User.Name) - assert.Equal(t, -1, resp.User.LuckyNumber) + assert.Equal(t, "2", resp.User.Id) + assert.Equal(t, "Raven", resp.User.Name) + assert.Equal(t, -1, resp.User.LuckyNumber) - resp, _, err = queryWithVariables(ctx, client, "374892379482379") - require.NoError(t, err) + resp, _, err = queryWithVariables(ctx, client, "374892379482379") + require.NoError(t, err) - assert.Zero(t, resp.User) + assert.Zero(t, resp.User) + } } func TestExtensions(t *testing.T) { @@ -102,12 +132,14 @@ func TestExtensions(t *testing.T) { ctx := context.Background() server := server.RunServer() defer server.Close() - client := newRoundtripClient(t, server.URL) + clients := newRoundtripClients(t, server.URL) - _, extensions, err := simpleQueryExt(ctx, client) - require.NoError(t, err) - assert.NotNil(t, extensions) - assert.Equal(t, extensions["foobar"], "test") + for _, client := range clients { + _, extensions, err := simpleQueryExt(ctx, client) + require.NoError(t, err) + assert.NotNil(t, extensions) + assert.Equal(t, extensions["foobar"], "test") + } } func TestOmitempty(t *testing.T) { @@ -119,22 +151,24 @@ func TestOmitempty(t *testing.T) { ctx := context.Background() server := server.RunServer() defer server.Close() - client := newRoundtripClient(t, server.URL) + clients := newRoundtripClients(t, server.URL) - resp, _, err := queryWithOmitempty(ctx, client, "2") - require.NoError(t, err) + for _, client := range clients { + resp, _, err := queryWithOmitempty(ctx, client, "2") + require.NoError(t, err) - assert.Equal(t, "2", resp.User.Id) - assert.Equal(t, "Raven", resp.User.Name) - assert.Equal(t, -1, resp.User.LuckyNumber) + assert.Equal(t, "2", resp.User.Id) + assert.Equal(t, "Raven", resp.User.Name) + assert.Equal(t, -1, resp.User.LuckyNumber) - // should return default user, not the user with ID "" - resp, _, err = queryWithOmitempty(ctx, client, "") - require.NoError(t, err) + // should return default user, not the user with ID "" + resp, _, err = queryWithOmitempty(ctx, client, "") + require.NoError(t, err) - assert.Equal(t, "1", resp.User.Id) - assert.Equal(t, "Yours Truly", resp.User.Name) - assert.Equal(t, 17, resp.User.LuckyNumber) + assert.Equal(t, "1", resp.User.Id) + assert.Equal(t, "Yours Truly", resp.User.Name) + assert.Equal(t, 17, resp.User.LuckyNumber) + } } func TestCustomMarshal(t *testing.T) { @@ -146,24 +180,26 @@ func TestCustomMarshal(t *testing.T) { ctx := context.Background() server := server.RunServer() defer server.Close() - client := newRoundtripClient(t, server.URL) - - resp, _, err := queryWithCustomMarshal(ctx, client, - time.Date(2025, time.January, 1, 12, 34, 56, 789, time.UTC)) - require.NoError(t, err) - - assert.Len(t, resp.UsersBornOn, 1) - user := resp.UsersBornOn[0] - assert.Equal(t, "1", user.Id) - assert.Equal(t, "Yours Truly", user.Name) - assert.Equal(t, - time.Date(2025, time.January, 1, 0, 0, 0, 0, time.UTC), - user.Birthdate) - - resp, _, err = queryWithCustomMarshal(ctx, client, - time.Date(2021, time.January, 1, 12, 34, 56, 789, time.UTC)) - require.NoError(t, err) - assert.Len(t, resp.UsersBornOn, 0) + clients := newRoundtripClients(t, server.URL) + + for _, client := range clients { + resp, _, err := queryWithCustomMarshal(ctx, client, + time.Date(2025, time.January, 1, 12, 34, 56, 789, time.UTC)) + require.NoError(t, err) + + assert.Len(t, resp.UsersBornOn, 1) + user := resp.UsersBornOn[0] + assert.Equal(t, "1", user.Id) + assert.Equal(t, "Yours Truly", user.Name) + assert.Equal(t, + time.Date(2025, time.January, 1, 0, 0, 0, 0, time.UTC), + user.Birthdate) + + resp, _, err = queryWithCustomMarshal(ctx, client, + time.Date(2021, time.January, 1, 12, 34, 56, 789, time.UTC)) + require.NoError(t, err) + assert.Len(t, resp.UsersBornOn, 0) + } } func TestCustomMarshalSlice(t *testing.T) { @@ -175,24 +211,26 @@ func TestCustomMarshalSlice(t *testing.T) { ctx := context.Background() server := server.RunServer() defer server.Close() - client := newRoundtripClient(t, server.URL) - - resp, _, err := queryWithCustomMarshalSlice(ctx, client, - []time.Time{time.Date(2025, time.January, 1, 12, 34, 56, 789, time.UTC)}) - require.NoError(t, err) - - assert.Len(t, resp.UsersBornOnDates, 1) - user := resp.UsersBornOnDates[0] - assert.Equal(t, "1", user.Id) - assert.Equal(t, "Yours Truly", user.Name) - assert.Equal(t, - time.Date(2025, time.January, 1, 0, 0, 0, 0, time.UTC), - user.Birthdate) - - resp, _, err = queryWithCustomMarshalSlice(ctx, client, - []time.Time{time.Date(2021, time.January, 1, 12, 34, 56, 789, time.UTC)}) - require.NoError(t, err) - assert.Len(t, resp.UsersBornOnDates, 0) + clients := newRoundtripClients(t, server.URL) + + for _, client := range clients { + resp, _, err := queryWithCustomMarshalSlice(ctx, client, + []time.Time{time.Date(2025, time.January, 1, 12, 34, 56, 789, time.UTC)}) + require.NoError(t, err) + + assert.Len(t, resp.UsersBornOnDates, 1) + user := resp.UsersBornOnDates[0] + assert.Equal(t, "1", user.Id) + assert.Equal(t, "Yours Truly", user.Name) + assert.Equal(t, + time.Date(2025, time.January, 1, 0, 0, 0, 0, time.UTC), + user.Birthdate) + + resp, _, err = queryWithCustomMarshalSlice(ctx, client, + []time.Time{time.Date(2021, time.January, 1, 12, 34, 56, 789, time.UTC)}) + require.NoError(t, err) + assert.Len(t, resp.UsersBornOnDates, 0) + } } func TestCustomMarshalOptional(t *testing.T) { @@ -209,28 +247,30 @@ func TestCustomMarshalOptional(t *testing.T) { ctx := context.Background() server := server.RunServer() defer server.Close() - client := newRoundtripClient(t, server.URL) - - date := time.Date(2025, time.January, 1, 12, 34, 56, 789, time.UTC) - resp, _, err := queryWithCustomMarshalOptional(ctx, client, &date, nil) - require.NoError(t, err) - - assert.Len(t, resp.UserSearch, 1) - user := resp.UserSearch[0] - assert.Equal(t, "1", user.Id) - assert.Equal(t, "Yours Truly", user.Name) - assert.Equal(t, - time.Date(2025, time.January, 1, 0, 0, 0, 0, time.UTC), - user.Birthdate) - - id := "2" - resp, _, err = queryWithCustomMarshalOptional(ctx, client, nil, &id) - require.NoError(t, err) - assert.Len(t, resp.UserSearch, 1) - user = resp.UserSearch[0] - assert.Equal(t, "2", user.Id) - assert.Equal(t, "Raven", user.Name) - assert.Zero(t, user.Birthdate) + clients := newRoundtripClients(t, server.URL) + + for _, client := range clients { + date := time.Date(2025, time.January, 1, 12, 34, 56, 789, time.UTC) + resp, _, err := queryWithCustomMarshalOptional(ctx, client, &date, nil) + require.NoError(t, err) + + assert.Len(t, resp.UserSearch, 1) + user := resp.UserSearch[0] + assert.Equal(t, "1", user.Id) + assert.Equal(t, "Yours Truly", user.Name) + assert.Equal(t, + time.Date(2025, time.January, 1, 0, 0, 0, 0, time.UTC), + user.Birthdate) + + id := "2" + resp, _, err = queryWithCustomMarshalOptional(ctx, client, nil, &id) + require.NoError(t, err) + assert.Len(t, resp.UserSearch, 1) + user = resp.UserSearch[0] + assert.Equal(t, "2", user.Id) + assert.Equal(t, "Raven", user.Name) + assert.Zero(t, user.Birthdate) + } } func TestInterfaceNoFragments(t *testing.T) { @@ -243,58 +283,60 @@ func TestInterfaceNoFragments(t *testing.T) { ctx := context.Background() server := server.RunServer() defer server.Close() - client := newRoundtripClient(t, server.URL) + clients := newRoundtripClients(t, server.URL) - resp, _, err := queryWithInterfaceNoFragments(ctx, client, "1") - require.NoError(t, err) + for _, client := range clients { + resp, _, err := queryWithInterfaceNoFragments(ctx, client, "1") + require.NoError(t, err) - // We should get the following response: - // me: User{Id: 1, Name: "Yours Truly"}, - // being: User{Id: 1, Name: "Yours Truly"}, + // We should get the following response: + // me: User{Id: 1, Name: "Yours Truly"}, + // being: User{Id: 1, Name: "Yours Truly"}, - assert.Equal(t, "1", resp.Me.Id) - assert.Equal(t, "Yours Truly", resp.Me.Name) + assert.Equal(t, "1", resp.Me.Id) + assert.Equal(t, "Yours Truly", resp.Me.Name) - // Check fields both via interface and via type-assertion: - assert.Equal(t, "User", resp.Being.GetTypename()) - assert.Equal(t, "1", resp.Being.GetId()) - assert.Equal(t, "Yours Truly", resp.Being.GetName()) + // Check fields both via interface and via type-assertion: + assert.Equal(t, "User", resp.Being.GetTypename()) + assert.Equal(t, "1", resp.Being.GetId()) + assert.Equal(t, "Yours Truly", resp.Being.GetName()) - user, ok := resp.Being.(*queryWithInterfaceNoFragmentsBeingUser) - require.Truef(t, ok, "got %T, not User", resp.Being) - assert.Equal(t, "1", user.Id) - assert.Equal(t, "Yours Truly", user.Name) + user, ok := resp.Being.(*queryWithInterfaceNoFragmentsBeingUser) + require.Truef(t, ok, "got %T, not User", resp.Being) + assert.Equal(t, "1", user.Id) + assert.Equal(t, "Yours Truly", user.Name) - resp, _, err = queryWithInterfaceNoFragments(ctx, client, "3") - require.NoError(t, err) + resp, _, err = queryWithInterfaceNoFragments(ctx, client, "3") + require.NoError(t, err) - // We should get the following response: - // me: User{Id: 1, Name: "Yours Truly"}, - // being: Animal{Id: 3, Name: "Fido"}, + // We should get the following response: + // me: User{Id: 1, Name: "Yours Truly"}, + // being: Animal{Id: 3, Name: "Fido"}, - assert.Equal(t, "1", resp.Me.Id) - assert.Equal(t, "Yours Truly", resp.Me.Name) + assert.Equal(t, "1", resp.Me.Id) + assert.Equal(t, "Yours Truly", resp.Me.Name) - assert.Equal(t, "Animal", resp.Being.GetTypename()) - assert.Equal(t, "3", resp.Being.GetId()) - assert.Equal(t, "Fido", resp.Being.GetName()) + assert.Equal(t, "Animal", resp.Being.GetTypename()) + assert.Equal(t, "3", resp.Being.GetId()) + assert.Equal(t, "Fido", resp.Being.GetName()) - animal, ok := resp.Being.(*queryWithInterfaceNoFragmentsBeingAnimal) - require.Truef(t, ok, "got %T, not Animal", resp.Being) - assert.Equal(t, "3", animal.Id) - assert.Equal(t, "Fido", animal.Name) + animal, ok := resp.Being.(*queryWithInterfaceNoFragmentsBeingAnimal) + require.Truef(t, ok, "got %T, not Animal", resp.Being) + assert.Equal(t, "3", animal.Id) + assert.Equal(t, "Fido", animal.Name) - resp, _, err = queryWithInterfaceNoFragments(ctx, client, "4757233945723") - require.NoError(t, err) + resp, _, err = queryWithInterfaceNoFragments(ctx, client, "4757233945723") + require.NoError(t, err) - // We should get the following response: - // me: User{Id: 1, Name: "Yours Truly"}, - // being: null + // We should get the following response: + // me: User{Id: 1, Name: "Yours Truly"}, + // being: null - assert.Equal(t, "1", resp.Me.Id) - assert.Equal(t, "Yours Truly", resp.Me.Name) + assert.Equal(t, "1", resp.Me.Id) + assert.Equal(t, "Yours Truly", resp.Me.Name) - assert.Nil(t, resp.Being) + assert.Nil(t, resp.Being) + } } func TestInterfaceListField(t *testing.T) { @@ -306,39 +348,41 @@ func TestInterfaceListField(t *testing.T) { ctx := context.Background() server := server.RunServer() defer server.Close() - client := newRoundtripClient(t, server.URL) + clients := newRoundtripClients(t, server.URL) - resp, _, err := queryWithInterfaceListField(ctx, client, - []string{"1", "3", "12847394823"}) - require.NoError(t, err) + for _, client := range clients { + resp, _, err := queryWithInterfaceListField(ctx, client, + []string{"1", "3", "12847394823"}) + require.NoError(t, err) - require.Len(t, resp.Beings, 3) + require.Len(t, resp.Beings, 3) - // We should get the following three beings: - // User{Id: 1, Name: "Yours Truly"}, - // Animal{Id: 3, Name: "Fido"}, - // null + // We should get the following three beings: + // User{Id: 1, Name: "Yours Truly"}, + // Animal{Id: 3, Name: "Fido"}, + // null - // Check fields both via interface and via type-assertion: - assert.Equal(t, "User", resp.Beings[0].GetTypename()) - assert.Equal(t, "1", resp.Beings[0].GetId()) - assert.Equal(t, "Yours Truly", resp.Beings[0].GetName()) + // Check fields both via interface and via type-assertion: + assert.Equal(t, "User", resp.Beings[0].GetTypename()) + assert.Equal(t, "1", resp.Beings[0].GetId()) + assert.Equal(t, "Yours Truly", resp.Beings[0].GetName()) - user, ok := resp.Beings[0].(*queryWithInterfaceListFieldBeingsUser) - require.Truef(t, ok, "got %T, not User", resp.Beings[0]) - assert.Equal(t, "1", user.Id) - assert.Equal(t, "Yours Truly", user.Name) + user, ok := resp.Beings[0].(*queryWithInterfaceListFieldBeingsUser) + require.Truef(t, ok, "got %T, not User", resp.Beings[0]) + assert.Equal(t, "1", user.Id) + assert.Equal(t, "Yours Truly", user.Name) - assert.Equal(t, "Animal", resp.Beings[1].GetTypename()) - assert.Equal(t, "3", resp.Beings[1].GetId()) - assert.Equal(t, "Fido", resp.Beings[1].GetName()) + assert.Equal(t, "Animal", resp.Beings[1].GetTypename()) + assert.Equal(t, "3", resp.Beings[1].GetId()) + assert.Equal(t, "Fido", resp.Beings[1].GetName()) - animal, ok := resp.Beings[1].(*queryWithInterfaceListFieldBeingsAnimal) - require.Truef(t, ok, "got %T, not Animal", resp.Beings[1]) - assert.Equal(t, "3", animal.Id) - assert.Equal(t, "Fido", animal.Name) + animal, ok := resp.Beings[1].(*queryWithInterfaceListFieldBeingsAnimal) + require.Truef(t, ok, "got %T, not Animal", resp.Beings[1]) + assert.Equal(t, "3", animal.Id) + assert.Equal(t, "Fido", animal.Name) - assert.Nil(t, resp.Beings[2]) + assert.Nil(t, resp.Beings[2]) + } } func TestInterfaceListPointerField(t *testing.T) { @@ -353,34 +397,36 @@ func TestInterfaceListPointerField(t *testing.T) { ctx := context.Background() server := server.RunServer() defer server.Close() - client := newRoundtripClient(t, server.URL) + clients := newRoundtripClients(t, server.URL) - resp, _, err := queryWithInterfaceListPointerField(ctx, client, - []string{"1", "3", "12847394823"}) - require.NoError(t, err) + for _, client := range clients { + resp, _, err := queryWithInterfaceListPointerField(ctx, client, + []string{"1", "3", "12847394823"}) + require.NoError(t, err) - require.Len(t, resp.Beings, 3) + require.Len(t, resp.Beings, 3) - // Check fields both via interface and via type-assertion: - assert.Equal(t, "User", (*resp.Beings[0]).GetTypename()) - assert.Equal(t, "1", (*resp.Beings[0]).GetId()) - assert.Equal(t, "Yours Truly", (*resp.Beings[0]).GetName()) + // Check fields both via interface and via type-assertion: + assert.Equal(t, "User", (*resp.Beings[0]).GetTypename()) + assert.Equal(t, "1", (*resp.Beings[0]).GetId()) + assert.Equal(t, "Yours Truly", (*resp.Beings[0]).GetName()) - user, ok := (*resp.Beings[0]).(*queryWithInterfaceListPointerFieldBeingsUser) - require.Truef(t, ok, "got %T, not User", *resp.Beings[0]) - assert.Equal(t, "1", user.Id) - assert.Equal(t, "Yours Truly", user.Name) + user, ok := (*resp.Beings[0]).(*queryWithInterfaceListPointerFieldBeingsUser) + require.Truef(t, ok, "got %T, not User", *resp.Beings[0]) + assert.Equal(t, "1", user.Id) + assert.Equal(t, "Yours Truly", user.Name) - assert.Equal(t, "Animal", (*resp.Beings[1]).GetTypename()) - assert.Equal(t, "3", (*resp.Beings[1]).GetId()) - assert.Equal(t, "Fido", (*resp.Beings[1]).GetName()) + assert.Equal(t, "Animal", (*resp.Beings[1]).GetTypename()) + assert.Equal(t, "3", (*resp.Beings[1]).GetId()) + assert.Equal(t, "Fido", (*resp.Beings[1]).GetName()) - animal, ok := (*resp.Beings[1]).(*queryWithInterfaceListPointerFieldBeingsAnimal) - require.Truef(t, ok, "got %T, not Animal", resp.Beings[1]) - assert.Equal(t, "3", animal.Id) - assert.Equal(t, "Fido", animal.Name) + animal, ok := (*resp.Beings[1]).(*queryWithInterfaceListPointerFieldBeingsAnimal) + require.Truef(t, ok, "got %T, not Animal", resp.Beings[1]) + assert.Equal(t, "3", animal.Id) + assert.Equal(t, "Fido", animal.Name) - assert.Nil(t, resp.Beings[2]) + assert.Nil(t, resp.Beings[2]) + } } func TestFragments(t *testing.T) { @@ -407,62 +453,64 @@ func TestFragments(t *testing.T) { ctx := context.Background() server := server.RunServer() defer server.Close() - client := newRoundtripClient(t, server.URL) - - resp, _, err := queryWithFragments(ctx, client, []string{"1", "3", "12847394823"}) - require.NoError(t, err) - - require.Len(t, resp.Beings, 3) - - // We should get the following three beings: - // User{Id: 1, Name: "Yours Truly"}, - // Animal{Id: 3, Name: "Fido"}, - // null - - // Check fields both via interface and via type-assertion when possible - // User has, in total, the fields: __typename id name luckyNumber. - assert.Equal(t, "User", resp.Beings[0].GetTypename()) - assert.Equal(t, "1", resp.Beings[0].GetId()) - assert.Equal(t, "Yours Truly", resp.Beings[0].GetName()) - // (hair and luckyNumber we need to cast for) - - user, ok := resp.Beings[0].(*queryWithFragmentsBeingsUser) - require.Truef(t, ok, "got %T, not User", resp.Beings[0]) - assert.Equal(t, "1", user.Id) - assert.Equal(t, "Yours Truly", user.Name) - assert.Equal(t, "Black", user.Hair.Color) - assert.Equal(t, 17, user.LuckyNumber) - - // Animal has, in total, the fields: - // __typename - // id - // species - // owner { - // id - // name - // ... on User { luckyNumber } - // } - assert.Equal(t, "Animal", resp.Beings[1].GetTypename()) - assert.Equal(t, "3", resp.Beings[1].GetId()) - // (hair, species, and owner.* we have to cast for) - - animal, ok := resp.Beings[1].(*queryWithFragmentsBeingsAnimal) - require.Truef(t, ok, "got %T, not Animal", resp.Beings[1]) - assert.Equal(t, "3", animal.Id) - assert.Equal(t, SpeciesDog, animal.Species) - assert.True(t, animal.Hair.HasHair) - - assert.Equal(t, "1", animal.Owner.GetId()) - assert.Equal(t, "Yours Truly", animal.Owner.GetName()) - // (luckyNumber we have to cast for, again) - - owner, ok := animal.Owner.(*queryWithFragmentsBeingsAnimalOwnerUser) - require.Truef(t, ok, "got %T, not User", animal.Owner) - assert.Equal(t, "1", owner.Id) - assert.Equal(t, "Yours Truly", owner.Name) - assert.Equal(t, 17, owner.LuckyNumber) - - assert.Nil(t, resp.Beings[2]) + clients := newRoundtripClients(t, server.URL) + + for _, client := range clients { + resp, _, err := queryWithFragments(ctx, client, []string{"1", "3", "12847394823"}) + require.NoError(t, err) + + require.Len(t, resp.Beings, 3) + + // We should get the following three beings: + // User{Id: 1, Name: "Yours Truly"}, + // Animal{Id: 3, Name: "Fido"}, + // null + + // Check fields both via interface and via type-assertion when possible + // User has, in total, the fields: __typename id name luckyNumber. + assert.Equal(t, "User", resp.Beings[0].GetTypename()) + assert.Equal(t, "1", resp.Beings[0].GetId()) + assert.Equal(t, "Yours Truly", resp.Beings[0].GetName()) + // (hair and luckyNumber we need to cast for) + + user, ok := resp.Beings[0].(*queryWithFragmentsBeingsUser) + require.Truef(t, ok, "got %T, not User", resp.Beings[0]) + assert.Equal(t, "1", user.Id) + assert.Equal(t, "Yours Truly", user.Name) + assert.Equal(t, "Black", user.Hair.Color) + assert.Equal(t, 17, user.LuckyNumber) + + // Animal has, in total, the fields: + // __typename + // id + // species + // owner { + // id + // name + // ... on User { luckyNumber } + // } + assert.Equal(t, "Animal", resp.Beings[1].GetTypename()) + assert.Equal(t, "3", resp.Beings[1].GetId()) + // (hair, species, and owner.* we have to cast for) + + animal, ok := resp.Beings[1].(*queryWithFragmentsBeingsAnimal) + require.Truef(t, ok, "got %T, not Animal", resp.Beings[1]) + assert.Equal(t, "3", animal.Id) + assert.Equal(t, SpeciesDog, animal.Species) + assert.True(t, animal.Hair.HasHair) + + assert.Equal(t, "1", animal.Owner.GetId()) + assert.Equal(t, "Yours Truly", animal.Owner.GetName()) + // (luckyNumber we have to cast for, again) + + owner, ok := animal.Owner.(*queryWithFragmentsBeingsAnimalOwnerUser) + require.Truef(t, ok, "got %T, not User", animal.Owner) + assert.Equal(t, "1", owner.Id) + assert.Equal(t, "Yours Truly", owner.Name) + assert.Equal(t, 17, owner.LuckyNumber) + + assert.Nil(t, resp.Beings[2]) + } } func TestNamedFragments(t *testing.T) { @@ -500,73 +548,75 @@ func TestNamedFragments(t *testing.T) { ctx := context.Background() server := server.RunServer() defer server.Close() - client := newRoundtripClient(t, server.URL) - - resp, _, err := queryWithNamedFragments(ctx, client, []string{"1", "3", "12847394823"}) - require.NoError(t, err) - - require.Len(t, resp.Beings, 3) - - // We should get the following three beings: - // User{Id: 1, Name: "Yours Truly"}, - // Animal{Id: 3, Name: "Fido"}, - // null - - // Check fields both via interface and via type-assertion when possible - // User has, in total, the fields: __typename id luckyNumber. - assert.Equal(t, "User", resp.Beings[0].GetTypename()) - assert.Equal(t, "1", resp.Beings[0].GetId()) - // (luckyNumber, hair we need to cast for) - - user, ok := resp.Beings[0].(*queryWithNamedFragmentsBeingsUser) - require.Truef(t, ok, "got %T, not User", resp.Beings[0]) - assert.Equal(t, "1", user.Id) - assert.Equal(t, "1", user.UserFields.Id) - assert.Equal(t, "1", user.UserFields.MoreUserFields.Id) - assert.Equal(t, "1", user.UserFields.LuckyFieldsUser.MoreUserFields.Id) - // on UserFields, but we should be able to access directly via embedding: - assert.Equal(t, 17, user.LuckyNumber) - assert.Equal(t, "Black", user.Hair.Color) - assert.Equal(t, "Black", user.UserFields.MoreUserFields.Hair.Color) - assert.Equal(t, "Black", user.UserFields.LuckyFieldsUser.MoreUserFields.Hair.Color) - - // Animal has, in total, the fields: - // __typename - // id - // hair { hasHair } - // owner { id luckyNumber } - assert.Equal(t, "Animal", resp.Beings[1].GetTypename()) - assert.Equal(t, "3", resp.Beings[1].GetId()) - // (hair.* and owner.* we have to cast for) - - animal, ok := resp.Beings[1].(*queryWithNamedFragmentsBeingsAnimal) - require.Truef(t, ok, "got %T, not Animal", resp.Beings[1]) - // Check that we filled in *both* ID fields: - assert.Equal(t, "3", animal.Id) - assert.Equal(t, "3", animal.AnimalFields.Id) - // on AnimalFields: - assert.True(t, animal.Hair.HasHair) - assert.Equal(t, "1", animal.Owner.GetId()) - // (luckyNumber we have to cast for, again) - - owner, ok := animal.Owner.(*AnimalFieldsOwnerUser) - require.Truef(t, ok, "got %T, not User", animal.Owner) - // Check that we filled in *both* ID fields: - assert.Equal(t, "1", owner.Id) - assert.Equal(t, "1", owner.UserFields.Id) - assert.Equal(t, "1", owner.UserFields.MoreUserFields.Id) - assert.Equal(t, "1", owner.UserFields.LuckyFieldsUser.MoreUserFields.Id) - // on UserFields: - assert.Equal(t, 17, owner.LuckyNumber) - assert.Equal(t, "Black", owner.UserFields.MoreUserFields.Hair.Color) - assert.Equal(t, "Black", owner.UserFields.LuckyFieldsUser.MoreUserFields.Hair.Color) - - // Lucky-based fields we can also get by casting to the fragment-interface. - luckyOwner, ok := animal.Owner.(LuckyFields) - require.Truef(t, ok, "got %T, not Lucky", animal.Owner) - assert.Equal(t, 17, luckyOwner.GetLuckyNumber()) - - assert.Nil(t, resp.Beings[2]) + clients := newRoundtripClients(t, server.URL) + + for _, client := range clients { + resp, _, err := queryWithNamedFragments(ctx, client, []string{"1", "3", "12847394823"}) + require.NoError(t, err) + + require.Len(t, resp.Beings, 3) + + // We should get the following three beings: + // User{Id: 1, Name: "Yours Truly"}, + // Animal{Id: 3, Name: "Fido"}, + // null + + // Check fields both via interface and via type-assertion when possible + // User has, in total, the fields: __typename id luckyNumber. + assert.Equal(t, "User", resp.Beings[0].GetTypename()) + assert.Equal(t, "1", resp.Beings[0].GetId()) + // (luckyNumber, hair we need to cast for) + + user, ok := resp.Beings[0].(*queryWithNamedFragmentsBeingsUser) + require.Truef(t, ok, "got %T, not User", resp.Beings[0]) + assert.Equal(t, "1", user.Id) + assert.Equal(t, "1", user.UserFields.Id) + assert.Equal(t, "1", user.UserFields.MoreUserFields.Id) + assert.Equal(t, "1", user.UserFields.LuckyFieldsUser.MoreUserFields.Id) + // on UserFields, but we should be able to access directly via embedding: + assert.Equal(t, 17, user.LuckyNumber) + assert.Equal(t, "Black", user.Hair.Color) + assert.Equal(t, "Black", user.UserFields.MoreUserFields.Hair.Color) + assert.Equal(t, "Black", user.UserFields.LuckyFieldsUser.MoreUserFields.Hair.Color) + + // Animal has, in total, the fields: + // __typename + // id + // hair { hasHair } + // owner { id luckyNumber } + assert.Equal(t, "Animal", resp.Beings[1].GetTypename()) + assert.Equal(t, "3", resp.Beings[1].GetId()) + // (hair.* and owner.* we have to cast for) + + animal, ok := resp.Beings[1].(*queryWithNamedFragmentsBeingsAnimal) + require.Truef(t, ok, "got %T, not Animal", resp.Beings[1]) + // Check that we filled in *both* ID fields: + assert.Equal(t, "3", animal.Id) + assert.Equal(t, "3", animal.AnimalFields.Id) + // on AnimalFields: + assert.True(t, animal.Hair.HasHair) + assert.Equal(t, "1", animal.Owner.GetId()) + // (luckyNumber we have to cast for, again) + + owner, ok := animal.Owner.(*AnimalFieldsOwnerUser) + require.Truef(t, ok, "got %T, not User", animal.Owner) + // Check that we filled in *both* ID fields: + assert.Equal(t, "1", owner.Id) + assert.Equal(t, "1", owner.UserFields.Id) + assert.Equal(t, "1", owner.UserFields.MoreUserFields.Id) + assert.Equal(t, "1", owner.UserFields.LuckyFieldsUser.MoreUserFields.Id) + // on UserFields: + assert.Equal(t, 17, owner.LuckyNumber) + assert.Equal(t, "Black", owner.UserFields.MoreUserFields.Hair.Color) + assert.Equal(t, "Black", owner.UserFields.LuckyFieldsUser.MoreUserFields.Hair.Color) + + // Lucky-based fields we can also get by casting to the fragment-interface. + luckyOwner, ok := animal.Owner.(LuckyFields) + require.Truef(t, ok, "got %T, not Lucky", animal.Owner) + assert.Equal(t, 17, luckyOwner.GetLuckyNumber()) + + assert.Nil(t, resp.Beings[2]) + } } func TestFlatten(t *testing.T) { @@ -629,54 +679,56 @@ func TestFlatten(t *testing.T) { ctx := context.Background() server := server.RunServer() defer server.Close() - client := newRoundtripClient(t, server.URL) - - resp, _, err := queryWithFlatten(ctx, client, []string{"1", "3", "12847394823"}) - require.NoError(t, err) - - require.Len(t, resp.Beings, 3) - - // We should get the following three beings: - // User{Id: 1, Name: "Yours Truly"}, - // Animal{Id: 3, Name: "Fido"}, - // null - - // Check fields both via interface and via type-assertion when possible - // User has, in total, the fields: __typename id luckyNumber. - assert.Equal(t, "User", resp.Beings[0].GetTypename()) - assert.Equal(t, "1", resp.Beings[0].GetId()) - // (luckyNumber we need to cast for) - - user, ok := resp.Beings[0].(*QueryFragmentBeingsUser) - require.Truef(t, ok, "got %T, not User", resp.Beings[0]) - assert.Equal(t, "1", user.Id) - assert.Equal(t, 17, user.InnerLuckyFieldsUser.LuckyNumber) - - // Animal has, in total, the fields: - // __typename - // id - // owner { id name ... on User { friends { id name } } } - assert.Equal(t, "Animal", resp.Beings[1].GetTypename()) - assert.Equal(t, "3", resp.Beings[1].GetId()) - // (owner.* we have to cast for) - - animal, ok := resp.Beings[1].(*QueryFragmentBeingsAnimal) - require.Truef(t, ok, "got %T, not Animal", resp.Beings[1]) - assert.Equal(t, "3", animal.Id) - // on AnimalFields: - assert.Equal(t, "1", animal.Owner.GetId()) - assert.Equal(t, "Yours Truly", animal.Owner.GetName()) - // (friends.* we have to cast for, again) - - owner, ok := animal.Owner.(*InnerBeingFieldsUser) - require.Truef(t, ok, "got %T, not User", animal.Owner) - assert.Equal(t, "1", owner.Id) - assert.Equal(t, "Yours Truly", owner.Name) - assert.Len(t, owner.Friends, 1) - assert.Equal(t, "2", owner.Friends[0].Id) - assert.Equal(t, "Raven", owner.Friends[0].Name) - - assert.Nil(t, resp.Beings[2]) + clients := newRoundtripClients(t, server.URL) + + for _, client := range clients { + resp, _, err := queryWithFlatten(ctx, client, []string{"1", "3", "12847394823"}) + require.NoError(t, err) + + require.Len(t, resp.Beings, 3) + + // We should get the following three beings: + // User{Id: 1, Name: "Yours Truly"}, + // Animal{Id: 3, Name: "Fido"}, + // null + + // Check fields both via interface and via type-assertion when possible + // User has, in total, the fields: __typename id luckyNumber. + assert.Equal(t, "User", resp.Beings[0].GetTypename()) + assert.Equal(t, "1", resp.Beings[0].GetId()) + // (luckyNumber we need to cast for) + + user, ok := resp.Beings[0].(*QueryFragmentBeingsUser) + require.Truef(t, ok, "got %T, not User", resp.Beings[0]) + assert.Equal(t, "1", user.Id) + assert.Equal(t, 17, user.InnerLuckyFieldsUser.LuckyNumber) + + // Animal has, in total, the fields: + // __typename + // id + // owner { id name ... on User { friends { id name } } } + assert.Equal(t, "Animal", resp.Beings[1].GetTypename()) + assert.Equal(t, "3", resp.Beings[1].GetId()) + // (owner.* we have to cast for) + + animal, ok := resp.Beings[1].(*QueryFragmentBeingsAnimal) + require.Truef(t, ok, "got %T, not Animal", resp.Beings[1]) + assert.Equal(t, "3", animal.Id) + // on AnimalFields: + assert.Equal(t, "1", animal.Owner.GetId()) + assert.Equal(t, "Yours Truly", animal.Owner.GetName()) + // (friends.* we have to cast for, again) + + owner, ok := animal.Owner.(*InnerBeingFieldsUser) + require.Truef(t, ok, "got %T, not User", animal.Owner) + assert.Equal(t, "1", owner.Id) + assert.Equal(t, "Yours Truly", owner.Name) + assert.Len(t, owner.Friends, 1) + assert.Equal(t, "2", owner.Friends[0].Id) + assert.Equal(t, "Raven", owner.Friends[0].Name) + + assert.Nil(t, resp.Beings[2]) + } } func TestGeneratedCode(t *testing.T) { diff --git a/internal/integration/roundtrip.go b/internal/integration/roundtrip.go index 4653b18e..a7b0c8da 100644 --- a/internal/integration/roundtrip.go +++ b/internal/integration/roundtrip.go @@ -101,10 +101,25 @@ func (c *roundtripClient) MakeRequest(ctx context.Context, req *graphql.Request, return nil } +func newRoundtripClients(t *testing.T, endpoint string) []graphql.Client { + return []graphql.Client{newRoundtripClient(t, endpoint), newRoundtripGetClient(t, endpoint)} +} + func newRoundtripClient(t *testing.T, endpoint string) graphql.Client { transport := &lastResponseTransport{wrapped: http.DefaultTransport} + httpClient := &http.Client{Transport: transport} + return &roundtripClient{ + wrapped: graphql.NewClient(endpoint, httpClient), + transport: transport, + t: t, + } +} + +func newRoundtripGetClient(t *testing.T, endpoint string) graphql.Client { + transport := &lastResponseTransport{wrapped: http.DefaultTransport} + httpClient := &http.Client{Transport: transport} return &roundtripClient{ - wrapped: graphql.NewClient(endpoint, &http.Client{Transport: transport}), + wrapped: graphql.NewClientUsingGet(endpoint, httpClient), transport: transport, t: t, } diff --git a/internal/integration/schema.graphql b/internal/integration/schema.graphql index 6d918621..7857a4a7 100644 --- a/internal/integration/schema.graphql +++ b/internal/integration/schema.graphql @@ -12,6 +12,10 @@ type Query { fail: Boolean } +type Mutation { + createUser(input: NewUser!): User! +} + type User implements Being & Lucky { id: ID! name: String! @@ -21,6 +25,10 @@ type User implements Being & Lucky { friends: [User!]! } +input NewUser { + name: String! +} + type Hair { color: String } # silly name to confuse the name-generator type Animal implements Being { diff --git a/internal/integration/server/gqlgen_exec.go b/internal/integration/server/gqlgen_exec.go index d0e4fd99..b73090f6 100644 --- a/internal/integration/server/gqlgen_exec.go +++ b/internal/integration/server/gqlgen_exec.go @@ -35,6 +35,7 @@ type Config struct { } type ResolverRoot interface { + Mutation() MutationResolver Query() QueryResolver } @@ -58,6 +59,10 @@ type ComplexityRoot struct { Color func(childComplexity int) int } + Mutation struct { + CreateUser func(childComplexity int, input NewUser) int + } + Query struct { Being func(childComplexity int, id string) int Beings func(childComplexity int, ids []string) int @@ -80,6 +85,9 @@ type ComplexityRoot struct { } } +type MutationResolver interface { + CreateUser(ctx context.Context, input NewUser) (*User, error) +} type QueryResolver interface { Me(ctx context.Context) (*User, error) User(ctx context.Context, id *string) (*User, error) @@ -156,6 +164,18 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.Hair.Color(childComplexity), true + case "Mutation.createUser": + if e.complexity.Mutation.CreateUser == nil { + break + } + + args, err := ec.field_Mutation_createUser_args(context.TODO(), rawArgs) + if err != nil { + return 0, false + } + + return e.complexity.Mutation.CreateUser(childComplexity, args["input"].(NewUser)), true + case "Query.being": if e.complexity.Query.Being == nil { break @@ -316,6 +336,20 @@ func (e *executableSchema) Exec(ctx context.Context) graphql.ResponseHandler { var buf bytes.Buffer data.MarshalGQL(&buf) + return &graphql.Response{ + Data: buf.Bytes(), + } + } + case ast.Mutation: + return func(ctx context.Context) *graphql.Response { + if !first { + return nil + } + first = false + data := ec._Mutation(ctx, rc.Operation.SelectionSet) + var buf bytes.Buffer + data.MarshalGQL(&buf) + return &graphql.Response{ Data: buf.Bytes(), } @@ -360,6 +394,10 @@ type Query { fail: Boolean } +type Mutation { + createUser(input: NewUser!): User! +} + type User implements Being & Lucky { id: ID! name: String! @@ -369,6 +407,10 @@ type User implements Being & Lucky { friends: [User!]! } +input NewUser { + name: String! +} + type Hair { color: String } # silly name to confuse the name-generator type Animal implements Being { @@ -402,6 +444,21 @@ var parsedSchema = gqlparser.MustLoadSchema(sources...) // region ***************************** args.gotpl ***************************** +func (ec *executionContext) field_Mutation_createUser_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) { + var err error + args := map[string]interface{}{} + var arg0 NewUser + if tmp, ok := rawArgs["input"]; ok { + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("input")) + arg0, err = ec.unmarshalNNewUser2githubᚗcomᚋKhanᚋgenqlientᚋinternalᚋintegrationᚋserverᚐNewUser(ctx, tmp) + if err != nil { + return nil, err + } + } + args["input"] = arg0 + return args, nil +} + func (ec *executionContext) field_Query___type_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) { var err error args := map[string]interface{}{} @@ -805,6 +862,48 @@ func (ec *executionContext) _Hair_color(ctx context.Context, field graphql.Colle return ec.marshalOString2ᚖstring(ctx, field.Selections, res) } +func (ec *executionContext) _Mutation_createUser(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + fc := &graphql.FieldContext{ + Object: "Mutation", + Field: field, + Args: nil, + IsMethod: true, + IsResolver: true, + } + + ctx = graphql.WithFieldContext(ctx, fc) + rawArgs := field.ArgumentMap(ec.Variables) + args, err := ec.field_Mutation_createUser_args(ctx, rawArgs) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + fc.Args = args + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return ec.resolvers.Mutation().CreateUser(rctx, args["input"].(NewUser)) + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(*User) + fc.Result = res + return ec.marshalNUser2ᚖgithubᚗcomᚋKhanᚋgenqlientᚋinternalᚋintegrationᚋserverᚐUser(ctx, field.Selections, res) +} + func (ec *executionContext) _Query_me(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { defer func() { if r := recover(); r != nil { @@ -2609,6 +2708,29 @@ func (ec *executionContext) ___Type_specifiedByURL(ctx context.Context, field gr // region **************************** input.gotpl ***************************** +func (ec *executionContext) unmarshalInputNewUser(ctx context.Context, obj interface{}) (NewUser, error) { + var it NewUser + asMap := map[string]interface{}{} + for k, v := range obj.(map[string]interface{}) { + asMap[k] = v + } + + for k, v := range asMap { + switch k { + case "name": + var err error + + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("name")) + it.Name, err = ec.unmarshalNString2string(ctx, v) + if err != nil { + return it, err + } + } + } + + return it, nil +} + // endregion **************************** input.gotpl ***************************** // region ************************** interface.gotpl *************************** @@ -2780,6 +2902,46 @@ func (ec *executionContext) _Hair(ctx context.Context, sel ast.SelectionSet, obj return out } +var mutationImplementors = []string{"Mutation"} + +func (ec *executionContext) _Mutation(ctx context.Context, sel ast.SelectionSet) graphql.Marshaler { + fields := graphql.CollectFields(ec.OperationContext, sel, mutationImplementors) + ctx = graphql.WithFieldContext(ctx, &graphql.FieldContext{ + Object: "Mutation", + }) + + out := graphql.NewFieldSet(fields) + var invalids uint32 + for i, field := range fields { + innerCtx := graphql.WithRootFieldContext(ctx, &graphql.RootFieldContext{ + Object: field.Name, + Field: field, + }) + + switch field.Name { + case "__typename": + out.Values[i] = graphql.MarshalString("Mutation") + case "createUser": + innerFunc := func(ctx context.Context) (res graphql.Marshaler) { + return ec._Mutation_createUser(ctx, field) + } + + out.Values[i] = ec.OperationContext.RootResolverMiddleware(innerCtx, innerFunc) + + if out.Values[i] == graphql.Null { + invalids++ + } + default: + panic("unknown field " + strconv.Quote(field.Name)) + } + } + out.Dispatch() + if invalids > 0 { + return graphql.Null + } + return out +} + var queryImplementors = []string{"Query"} func (ec *executionContext) _Query(ctx context.Context, sel ast.SelectionSet) graphql.Marshaler { @@ -3670,6 +3832,11 @@ func (ec *executionContext) marshalNInt2int(ctx context.Context, sel ast.Selecti return res } +func (ec *executionContext) unmarshalNNewUser2githubᚗcomᚋKhanᚋgenqlientᚋinternalᚋintegrationᚋserverᚐNewUser(ctx context.Context, v interface{}) (NewUser, error) { + res, err := ec.unmarshalInputNewUser(ctx, v) + return res, graphql.ErrorOnPath(ctx, err) +} + func (ec *executionContext) unmarshalNSpecies2githubᚗcomᚋKhanᚋgenqlientᚋinternalᚋintegrationᚋserverᚐSpecies(ctx context.Context, v interface{}) (Species, error) { var res Species err := res.UnmarshalGQL(v) @@ -3695,6 +3862,10 @@ func (ec *executionContext) marshalNString2string(ctx context.Context, sel ast.S return res } +func (ec *executionContext) marshalNUser2githubᚗcomᚋKhanᚋgenqlientᚋinternalᚋintegrationᚋserverᚐUser(ctx context.Context, sel ast.SelectionSet, v User) graphql.Marshaler { + return ec._User(ctx, sel, &v) +} + func (ec *executionContext) marshalNUser2ᚕᚖgithubᚗcomᚋKhanᚋgenqlientᚋinternalᚋintegrationᚋserverᚐUserᚄ(ctx context.Context, sel ast.SelectionSet, v []*User) graphql.Marshaler { ret := make(graphql.Array, len(v)) var wg sync.WaitGroup diff --git a/internal/integration/server/gqlgen_models.go b/internal/integration/server/gqlgen_models.go index 73ae3924..0a8ac66e 100644 --- a/internal/integration/server/gqlgen_models.go +++ b/internal/integration/server/gqlgen_models.go @@ -34,6 +34,10 @@ type Hair struct { Color *string `json:"color"` } +type NewUser struct { + Name string `json:"name"` +} + type User struct { ID string `json:"id"` Name string `json:"name"` diff --git a/internal/integration/server/server.go b/internal/integration/server/server.go index e94adb4e..00dbb308 100644 --- a/internal/integration/server/server.go +++ b/internal/integration/server/server.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "net/http/httptest" + "strconv" "github.com/99designs/gqlgen/graphql" "github.com/99designs/gqlgen/graphql/handler" @@ -73,6 +74,24 @@ func beingByID(id string) Being { return nil } +func getNewID() string { + maxID := 0 + for _, user := range users { + intID, _ := strconv.Atoi(user.ID) + if intID > maxID { + maxID = intID + } + } + for _, animal := range animals { + intID, _ := strconv.Atoi(animal.ID) + if intID > maxID { + maxID = intID + } + } + newID := maxID + 1 + return strconv.Itoa(newID) +} + func (r *queryResolver) Me(ctx context.Context) (*User, error) { return userByID("1"), nil } @@ -129,9 +148,16 @@ func (r *queryResolver) Fail(ctx context.Context) (*bool, error) { return &f, fmt.Errorf("oh no") } +func (m mutationResolver) CreateUser(ctx context.Context, input NewUser) (*User, error) { + newUser := User{ID: getNewID(), Name: input.Name, Friends: []*User{}} + users = append(users, &newUser) + return &newUser, nil +} + func RunServer() *httptest.Server { gqlgenServer := handler.New(NewExecutableSchema(Config{Resolvers: &resolver{}})) gqlgenServer.AddTransport(transport.POST{}) + gqlgenServer.AddTransport(transport.GET{}) gqlgenServer.AroundResponses(func(ctx context.Context, next graphql.ResponseHandler) *graphql.Response { graphql.RegisterExtension(ctx, "foobar", "test") return next(ctx) @@ -140,10 +166,15 @@ func RunServer() *httptest.Server { } type ( - resolver struct{} - queryResolver struct{} + resolver struct{} + queryResolver struct{} + mutationResolver struct{} ) +func (r *resolver) Mutation() MutationResolver { + return &mutationResolver{} +} + func (r *resolver) Query() QueryResolver { return &queryResolver{} } //go:generate go run github.com/99designs/gqlgen