Add support for concrete-typed named fragments (#75)

## Summary:
In previous commits I added support to genqlient for interfaces and
inline fragments.  This means the only query structures that remain are
named fragments and their spreads, e.g.
```
fragment MyFragment on MyType { myField }
query MyQuery { getMyType { ...MyFragment } }
```
Other than mere completionism, these are potentially useful for code
sharing: you can spread the same fragment multiple places; and then
genqlient can notice that and generate the same type for each.  (They
can even be shared between different queries in the same package.)

In this commit I add support for named fragments of concrete
(object/struct, not interface) type, spread into either concrete or
abstract scope.  For genqlient's purposes, these are a new "root"
type-name, just like each operation, and are then embedded into the
appropriate struct.  (Using embeds allows their fields to be referenced
as fields of the containing type, if convenient.  Further design
considerations are discussed in DESIGN.md.)

This requires new code in two main places (plus miscellaneous glue),
both nontrivial but neither particularly complex:
- We need to actually traverse both structures and generate the types
  (in `convert.go`).
- We need to decide which fragments from this package to send to the
  server, both for good hyigene and because GraphQL requires we send
  only ones this query uses (in `generate.go`).
- We need a little new wiring for options -- because fragments can be
  shared between queries they get their own toplevel options, rather
  than inheriting the query's options.

Finally, this required slightly subtler changes to how we do
unmarshaling (in `types.go` and `unmarshal.go.tmpl`).  Basically,
because embedded fields' methods, including `UnmarshalJSON`, get
promoted to the parent type, and because the JSON library ignores their
fields when shadowed by those of the parent type, we need a little bit
of special logic in each such parent type to do its own unmarshal and
then delegate to each embed.  This is similar (and much simpler) to
what we did for interfaces, although it required some changes to the
"method-hiding" trick (used for both).  It's only really necessary in
certain specific cases (namely when an embedded type has an
`UnmarshalJSON` method or a field with the same name as the embedder),
but it's easier to just generate it always.  This is all described in
more detail inline.

This does not support fragments of abstract type, which have their own
complexities.  I'll address those, which are now the only remaining
piece of #8, in a future commit.

Issue: https://github.com/Khan/genqlient/issues/8

## Test plan:
make check


Author: benjaminjkraft

Reviewers: dnerdy, benjaminjkraft, aberkan, MiguelCastillo

Required Reviewers: 

Approved By: dnerdy

Checks:  Lint,  Test (1.17),  Test (1.16),  Test (1.15),  Test (1.14),  Test (1.13),  Test (1.17),  Test (1.16),  Test (1.15),  Test (1.14),  Test (1.13),  Lint,  Test (1.17),  Test (1.16),  Test (1.15),  Test (1.14),  Test (1.13),  Lint

Pull Request URL: https://github.com/Khan/genqlient/pull/75
This commit is contained in:
Ben Kraft
2021-09-09 09:39:30 -07:00
committed by GitHub
parent 1c061b153a
commit f99c10d6fd
22 changed files with 1857 additions and 123 deletions
+403 -20
View File
@@ -10,6 +10,152 @@ import (
"github.com/Khan/genqlient/graphql"
)
// AnimalFields includes the GraphQL fields of Animal requested by the fragment AnimalFields.
type AnimalFields struct {
Id string `json:"id"`
Hair AnimalFieldsHairBeingsHair `json:"hair"`
Owner AnimalFieldsOwnerBeing `json:"-"`
}
func (v *AnimalFields) UnmarshalJSON(b []byte) error {
var firstPass struct {
*AnimalFields
Owner json.RawMessage `json:"owner"`
graphql.NoUnmarshalJSON
}
firstPass.AnimalFields = v
err := json.Unmarshal(b, &firstPass)
if err != nil {
return err
}
{
target := &v.Owner
raw := firstPass.Owner
err = __unmarshalAnimalFieldsOwnerBeing(
target, raw)
if err != nil {
return fmt.Errorf(
"Unable to unmarshal AnimalFields.Owner: %w", err)
}
}
return nil
}
// AnimalFieldsHairBeingsHair includes the requested fields of the GraphQL type BeingsHair.
type AnimalFieldsHairBeingsHair struct {
HasHair bool `json:"hasHair"`
}
// AnimalFieldsOwnerAnimal includes the requested fields of the GraphQL type Animal.
type AnimalFieldsOwnerAnimal struct {
Typename string `json:"__typename"`
Id string `json:"id"`
}
// AnimalFieldsOwnerBeing includes the requested fields of the GraphQL interface Being.
//
// AnimalFieldsOwnerBeing is implemented by the following types:
// AnimalFieldsOwnerUser
// AnimalFieldsOwnerAnimal
//
// The GraphQL type's documentation follows.
//
//
type AnimalFieldsOwnerBeing interface {
implementsGraphQLInterfaceAnimalFieldsOwnerBeing()
// GetTypename returns the receiver's concrete GraphQL type-name (see interface doc for possible values).
GetTypename() string
// GetId returns the interface-field "id" from its implementation.
GetId() string
}
func (v *AnimalFieldsOwnerUser) implementsGraphQLInterfaceAnimalFieldsOwnerBeing() {}
// GetTypename is a part of, and documented with, the interface AnimalFieldsOwnerBeing.
func (v *AnimalFieldsOwnerUser) GetTypename() string { return v.Typename }
// GetId is a part of, and documented with, the interface AnimalFieldsOwnerBeing.
func (v *AnimalFieldsOwnerUser) GetId() string { return v.Id }
func (v *AnimalFieldsOwnerAnimal) implementsGraphQLInterfaceAnimalFieldsOwnerBeing() {}
// GetTypename is a part of, and documented with, the interface AnimalFieldsOwnerBeing.
func (v *AnimalFieldsOwnerAnimal) GetTypename() string { return v.Typename }
// GetId is a part of, and documented with, the interface AnimalFieldsOwnerBeing.
func (v *AnimalFieldsOwnerAnimal) GetId() string { return v.Id }
func __unmarshalAnimalFieldsOwnerBeing(v *AnimalFieldsOwnerBeing, m json.RawMessage) error {
if string(m) == "null" {
return nil
}
var tn struct {
TypeName string `json:"__typename"`
}
err := json.Unmarshal(m, &tn)
if err != nil {
return err
}
switch tn.TypeName {
case "User":
*v = new(AnimalFieldsOwnerUser)
return json.Unmarshal(m, *v)
case "Animal":
*v = new(AnimalFieldsOwnerAnimal)
return json.Unmarshal(m, *v)
case "":
return fmt.Errorf(
"Response was missing Being.__typename")
default:
return fmt.Errorf(
`Unexpected concrete type for AnimalFieldsOwnerBeing: "%v"`, tn.TypeName)
}
}
// AnimalFieldsOwnerUser includes the requested fields of the GraphQL type User.
type AnimalFieldsOwnerUser struct {
Typename string `json:"__typename"`
Id string `json:"id"`
UserFields `json:"-"`
}
func (v *AnimalFieldsOwnerUser) UnmarshalJSON(b []byte) error {
var firstPass struct {
*AnimalFieldsOwnerUser
graphql.NoUnmarshalJSON
}
firstPass.AnimalFieldsOwnerUser = v
err := json.Unmarshal(b, &firstPass)
if err != nil {
return err
}
err = json.Unmarshal(b, &v.UserFields)
if err != nil {
return err
}
return nil
}
// MoreUserFields includes the GraphQL fields of User requested by the fragment MoreUserFields.
type MoreUserFields struct {
Id string `json:"id"`
Hair MoreUserFieldsHair `json:"hair"`
}
// MoreUserFieldsHair includes the requested fields of the GraphQL type Hair.
type MoreUserFieldsHair struct {
Color string `json:"color"`
}
type Species string
const (
@@ -17,6 +163,33 @@ const (
SpeciesCoelacanth Species = "COELACANTH"
)
// UserFields includes the GraphQL fields of User requested by the fragment UserFields.
type UserFields struct {
Id string `json:"id"`
LuckyNumber int `json:"luckyNumber"`
MoreUserFields `json:"-"`
}
func (v *UserFields) UnmarshalJSON(b []byte) error {
var firstPass struct {
*UserFields
graphql.NoUnmarshalJSON
}
firstPass.UserFields = v
err := json.Unmarshal(b, &firstPass)
if err != nil {
return err
}
err = json.Unmarshal(b, &v.MoreUserFields)
if err != nil {
return err
}
return nil
}
// queryWithFragmentsBeingsAnimal includes the requested fields of the GraphQL type Animal.
type queryWithFragmentsBeingsAnimal struct {
Typename string `json:"__typename"`
@@ -29,13 +202,12 @@ type queryWithFragmentsBeingsAnimal struct {
func (v *queryWithFragmentsBeingsAnimal) UnmarshalJSON(b []byte) error {
type queryWithFragmentsBeingsAnimalWrapper queryWithFragmentsBeingsAnimal
var firstPass struct {
*queryWithFragmentsBeingsAnimalWrapper
*queryWithFragmentsBeingsAnimal
Owner json.RawMessage `json:"owner"`
graphql.NoUnmarshalJSON
}
firstPass.queryWithFragmentsBeingsAnimalWrapper = (*queryWithFragmentsBeingsAnimalWrapper)(v)
firstPass.queryWithFragmentsBeingsAnimal = v
err := json.Unmarshal(b, &firstPass)
if err != nil {
@@ -52,6 +224,7 @@ func (v *queryWithFragmentsBeingsAnimal) UnmarshalJSON(b []byte) error {
"Unable to unmarshal queryWithFragmentsBeingsAnimal.Owner: %w", err)
}
}
return nil
}
@@ -238,13 +411,12 @@ type queryWithFragmentsResponse struct {
func (v *queryWithFragmentsResponse) UnmarshalJSON(b []byte) error {
type queryWithFragmentsResponseWrapper queryWithFragmentsResponse
var firstPass struct {
*queryWithFragmentsResponseWrapper
*queryWithFragmentsResponse
Beings []json.RawMessage `json:"beings"`
graphql.NoUnmarshalJSON
}
firstPass.queryWithFragmentsResponseWrapper = (*queryWithFragmentsResponseWrapper)(v)
firstPass.queryWithFragmentsResponse = v
err := json.Unmarshal(b, &firstPass)
if err != nil {
@@ -267,6 +439,7 @@ func (v *queryWithFragmentsResponse) UnmarshalJSON(b []byte) error {
}
}
}
return nil
}
@@ -363,13 +536,12 @@ type queryWithInterfaceListFieldResponse struct {
func (v *queryWithInterfaceListFieldResponse) UnmarshalJSON(b []byte) error {
type queryWithInterfaceListFieldResponseWrapper queryWithInterfaceListFieldResponse
var firstPass struct {
*queryWithInterfaceListFieldResponseWrapper
*queryWithInterfaceListFieldResponse
Beings []json.RawMessage `json:"beings"`
graphql.NoUnmarshalJSON
}
firstPass.queryWithInterfaceListFieldResponseWrapper = (*queryWithInterfaceListFieldResponseWrapper)(v)
firstPass.queryWithInterfaceListFieldResponse = v
err := json.Unmarshal(b, &firstPass)
if err != nil {
@@ -392,6 +564,7 @@ func (v *queryWithInterfaceListFieldResponse) UnmarshalJSON(b []byte) error {
}
}
}
return nil
}
@@ -488,13 +661,12 @@ type queryWithInterfaceListPointerFieldResponse struct {
func (v *queryWithInterfaceListPointerFieldResponse) UnmarshalJSON(b []byte) error {
type queryWithInterfaceListPointerFieldResponseWrapper queryWithInterfaceListPointerFieldResponse
var firstPass struct {
*queryWithInterfaceListPointerFieldResponseWrapper
*queryWithInterfaceListPointerFieldResponse
Beings []json.RawMessage `json:"beings"`
graphql.NoUnmarshalJSON
}
firstPass.queryWithInterfaceListPointerFieldResponseWrapper = (*queryWithInterfaceListPointerFieldResponseWrapper)(v)
firstPass.queryWithInterfaceListPointerFieldResponse = v
err := json.Unmarshal(b, &firstPass)
if err != nil {
@@ -518,6 +690,7 @@ func (v *queryWithInterfaceListPointerFieldResponse) UnmarshalJSON(b []byte) err
}
}
}
return nil
}
@@ -621,13 +794,12 @@ type queryWithInterfaceNoFragmentsResponse struct {
func (v *queryWithInterfaceNoFragmentsResponse) UnmarshalJSON(b []byte) error {
type queryWithInterfaceNoFragmentsResponseWrapper queryWithInterfaceNoFragmentsResponse
var firstPass struct {
*queryWithInterfaceNoFragmentsResponseWrapper
*queryWithInterfaceNoFragmentsResponse
Being json.RawMessage `json:"being"`
graphql.NoUnmarshalJSON
}
firstPass.queryWithInterfaceNoFragmentsResponseWrapper = (*queryWithInterfaceNoFragmentsResponseWrapper)(v)
firstPass.queryWithInterfaceNoFragmentsResponse = v
err := json.Unmarshal(b, &firstPass)
if err != nil {
@@ -644,6 +816,164 @@ func (v *queryWithInterfaceNoFragmentsResponse) UnmarshalJSON(b []byte) error {
"Unable to unmarshal queryWithInterfaceNoFragmentsResponse.Being: %w", err)
}
}
return nil
}
// queryWithNamedFragmentsBeingsAnimal includes the requested fields of the GraphQL type Animal.
type queryWithNamedFragmentsBeingsAnimal struct {
Typename string `json:"__typename"`
Id string `json:"id"`
AnimalFields `json:"-"`
}
func (v *queryWithNamedFragmentsBeingsAnimal) UnmarshalJSON(b []byte) error {
var firstPass struct {
*queryWithNamedFragmentsBeingsAnimal
graphql.NoUnmarshalJSON
}
firstPass.queryWithNamedFragmentsBeingsAnimal = v
err := json.Unmarshal(b, &firstPass)
if err != nil {
return err
}
err = json.Unmarshal(b, &v.AnimalFields)
if err != nil {
return err
}
return nil
}
// queryWithNamedFragmentsBeingsBeing includes the requested fields of the GraphQL interface Being.
//
// queryWithNamedFragmentsBeingsBeing is implemented by the following types:
// queryWithNamedFragmentsBeingsUser
// queryWithNamedFragmentsBeingsAnimal
//
// The GraphQL type's documentation follows.
//
//
type queryWithNamedFragmentsBeingsBeing interface {
implementsGraphQLInterfacequeryWithNamedFragmentsBeingsBeing()
// GetTypename returns the receiver's concrete GraphQL type-name (see interface doc for possible values).
GetTypename() string
// GetId returns the interface-field "id" from its implementation.
GetId() string
}
func (v *queryWithNamedFragmentsBeingsUser) implementsGraphQLInterfacequeryWithNamedFragmentsBeingsBeing() {
}
// GetTypename is a part of, and documented with, the interface queryWithNamedFragmentsBeingsBeing.
func (v *queryWithNamedFragmentsBeingsUser) GetTypename() string { return v.Typename }
// GetId is a part of, and documented with, the interface queryWithNamedFragmentsBeingsBeing.
func (v *queryWithNamedFragmentsBeingsUser) GetId() string { return v.Id }
func (v *queryWithNamedFragmentsBeingsAnimal) implementsGraphQLInterfacequeryWithNamedFragmentsBeingsBeing() {
}
// GetTypename is a part of, and documented with, the interface queryWithNamedFragmentsBeingsBeing.
func (v *queryWithNamedFragmentsBeingsAnimal) GetTypename() string { return v.Typename }
// GetId is a part of, and documented with, the interface queryWithNamedFragmentsBeingsBeing.
func (v *queryWithNamedFragmentsBeingsAnimal) GetId() string { return v.Id }
func __unmarshalqueryWithNamedFragmentsBeingsBeing(v *queryWithNamedFragmentsBeingsBeing, m json.RawMessage) error {
if string(m) == "null" {
return nil
}
var tn struct {
TypeName string `json:"__typename"`
}
err := json.Unmarshal(m, &tn)
if err != nil {
return err
}
switch tn.TypeName {
case "User":
*v = new(queryWithNamedFragmentsBeingsUser)
return json.Unmarshal(m, *v)
case "Animal":
*v = new(queryWithNamedFragmentsBeingsAnimal)
return json.Unmarshal(m, *v)
case "":
return fmt.Errorf(
"Response was missing Being.__typename")
default:
return fmt.Errorf(
`Unexpected concrete type for queryWithNamedFragmentsBeingsBeing: "%v"`, tn.TypeName)
}
}
// queryWithNamedFragmentsBeingsUser includes the requested fields of the GraphQL type User.
type queryWithNamedFragmentsBeingsUser struct {
Typename string `json:"__typename"`
Id string `json:"id"`
UserFields `json:"-"`
}
func (v *queryWithNamedFragmentsBeingsUser) UnmarshalJSON(b []byte) error {
var firstPass struct {
*queryWithNamedFragmentsBeingsUser
graphql.NoUnmarshalJSON
}
firstPass.queryWithNamedFragmentsBeingsUser = v
err := json.Unmarshal(b, &firstPass)
if err != nil {
return err
}
err = json.Unmarshal(b, &v.UserFields)
if err != nil {
return err
}
return nil
}
// queryWithNamedFragmentsResponse is returned by queryWithNamedFragments on success.
type queryWithNamedFragmentsResponse struct {
Beings []queryWithNamedFragmentsBeingsBeing `json:"-"`
}
func (v *queryWithNamedFragmentsResponse) UnmarshalJSON(b []byte) error {
var firstPass struct {
*queryWithNamedFragmentsResponse
Beings []json.RawMessage `json:"beings"`
graphql.NoUnmarshalJSON
}
firstPass.queryWithNamedFragmentsResponse = v
err := json.Unmarshal(b, &firstPass)
if err != nil {
return err
}
{
target := &v.Beings
raw := firstPass.Beings
*target = make(
[]queryWithNamedFragmentsBeingsBeing,
len(raw))
for i, raw := range raw {
target := &(*target)[i]
err = __unmarshalqueryWithNamedFragmentsBeingsBeing(
target, raw)
if err != nil {
return fmt.Errorf(
"Unable to unmarshal queryWithNamedFragmentsResponse.Beings: %w", err)
}
}
}
return nil
}
@@ -877,3 +1207,56 @@ query queryWithFragments ($ids: [ID!]!) {
)
return &retval, err
}
func queryWithNamedFragments(
ctx context.Context,
client graphql.Client,
ids []string,
) (*queryWithNamedFragmentsResponse, error) {
variables := map[string]interface{}{
"ids": ids,
}
var err error
var retval queryWithNamedFragmentsResponse
err = client.MakeRequest(
ctx,
"queryWithNamedFragments",
`
query queryWithNamedFragments ($ids: [ID!]!) {
beings(ids: $ids) {
__typename
id
... AnimalFields
... UserFields
}
}
fragment AnimalFields on Animal {
id
hair {
hasHair
}
owner {
__typename
id
... UserFields
}
}
fragment UserFields on User {
id
luckyNumber
... MoreUserFields
}
fragment MoreUserFields on User {
id
hair {
color
}
}
`,
&retval,
variables,
)
return &retval, err
}
+88
View File
@@ -288,6 +288,94 @@ func TestFragments(t *testing.T) {
assert.Nil(t, resp.Beings[2])
}
func TestNamedFragments(t *testing.T) {
_ = `# @genqlient
fragment AnimalFields on Animal {
id
hair { hasHair }
owner { id ...UserFields }
}
fragment MoreUserFields on User {
id
hair { color }
}
fragment UserFields on User {
id luckyNumber
...MoreUserFields
}
query queryWithNamedFragments($ids: [ID!]!) {
beings(ids: $ids) {
__typename id
...AnimalFields
...UserFields
}
}`
ctx := context.Background()
server := server.RunServer()
defer server.Close()
client := graphql.NewClient(server.URL, http.DefaultClient)
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)
// 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)
// 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)
// on UserFields:
assert.Equal(t, 17, owner.LuckyNumber)
assert.Equal(t, "Black", owner.Hair.Color)
assert.Nil(t, resp.Beings[2])
}
func TestGeneratedCode(t *testing.T) {
// TODO(benkraft): Check that gqlgen is up to date too. In practice that's
// less likely to be a problem, since it should only change if you update