Add support for "flattening" fragment-spreads (#121)

## Summary:
One common use of fragment spreads is as the entirety of a field's
selection, e.g.
```graphql
query MyQuery {
  myField {
    ...MyFragment
  }
}
```
In this case, by default, genqlient generates a wrapper type
`MyQueryMyFieldMyType`, which just embeds `MyFragment`.  This makes
sense if you later want to add more fields in addition to the fragment
spread.  But if you don't -- and you did the fragment because you want
to share types, it's an extra layer of indirection.  (Which becomes
especially onerous if `myField` has list type (`[MyType!]`), such that
it's not just an extra attribute-access to get to `MyFragment`.)

The new option `# @genqlient(flatten: true)` simplifies this situation:
if applied to `myField` is skips the wrapper type;
`MyQueryResponse.MyField` will simply have type `MyFragment` (or
`[]MyFragment`, or whatever).  This should hopefully make the `typename`
option, which has more limitations, less necessary.

Note that in #30 the initial idea was to support this for fields as
well.  This would require significant additional complexity in the
JSON-(un)marshaling code, and has proven less necessary, so I
implemented this option only for fragment-spreads for now.  With that
restriction, it was shockingly simple; we have to hook into a bunch of
different places, but they're all quite simple, since the structure of
the Go types still matches the structure in GraphQL.

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

## Test plan:
make check


Author: benjaminjkraft

Reviewers: csilvers, dnerdy, aberkan, jvoll, mahtabsabet, MiguelCastillo, StevenACoffman

Required Reviewers: 

Approved By: csilvers, dnerdy

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

Pull Request URL: https://github.com/Khan/genqlient/pull/121
This commit is contained in:
Ben Kraft
2021-09-29 17:52:06 -07:00
committed by GitHub
parent f4c981031e
commit c6d087c29b
21 changed files with 1246 additions and 9 deletions
+546
View File
@@ -255,6 +255,177 @@ func (v *AnimalFieldsOwnerUser) __premarshalJSON() (*__premarshalAnimalFieldsOwn
return &retval, nil
}
// FriendsFields includes the GraphQL fields of User requested by the fragment FriendsFields.
type FriendsFields struct {
Id string `json:"id"`
Name string `json:"name"`
}
// InnerBeingFields includes the GraphQL fields of Being requested by the fragment InnerBeingFields.
//
// InnerBeingFields is implemented by the following types:
// InnerBeingFieldsUser
// InnerBeingFieldsAnimal
type InnerBeingFields interface {
implementsGraphQLInterfaceInnerBeingFields()
// GetId returns the interface-field "id" from its implementation.
GetId() string
// GetName returns the interface-field "name" from its implementation.
GetName() string
}
func (v *InnerBeingFieldsUser) implementsGraphQLInterfaceInnerBeingFields() {}
// GetId is a part of, and documented with, the interface InnerBeingFields.
func (v *InnerBeingFieldsUser) GetId() string { return v.Id }
// GetName is a part of, and documented with, the interface InnerBeingFields.
func (v *InnerBeingFieldsUser) GetName() string { return v.Name }
func (v *InnerBeingFieldsAnimal) implementsGraphQLInterfaceInnerBeingFields() {}
// GetId is a part of, and documented with, the interface InnerBeingFields.
func (v *InnerBeingFieldsAnimal) GetId() string { return v.Id }
// GetName is a part of, and documented with, the interface InnerBeingFields.
func (v *InnerBeingFieldsAnimal) GetName() string { return v.Name }
func __unmarshalInnerBeingFields(b []byte, v *InnerBeingFields) error {
if string(b) == "null" {
return nil
}
var tn struct {
TypeName string `json:"__typename"`
}
err := json.Unmarshal(b, &tn)
if err != nil {
return err
}
switch tn.TypeName {
case "User":
*v = new(InnerBeingFieldsUser)
return json.Unmarshal(b, *v)
case "Animal":
*v = new(InnerBeingFieldsAnimal)
return json.Unmarshal(b, *v)
case "":
return fmt.Errorf(
"Response was missing Being.__typename")
default:
return fmt.Errorf(
`Unexpected concrete type for InnerBeingFields: "%v"`, tn.TypeName)
}
}
func __marshalInnerBeingFields(v *InnerBeingFields) ([]byte, error) {
var typename string
switch v := (*v).(type) {
case *InnerBeingFieldsUser:
typename = "User"
result := struct {
TypeName string `json:"__typename"`
*InnerBeingFieldsUser
}{typename, v}
return json.Marshal(result)
case *InnerBeingFieldsAnimal:
typename = "Animal"
result := struct {
TypeName string `json:"__typename"`
*InnerBeingFieldsAnimal
}{typename, v}
return json.Marshal(result)
case nil:
return []byte("null"), nil
default:
return nil, fmt.Errorf(
`Unexpected concrete type for InnerBeingFields: "%T"`, v)
}
}
// InnerBeingFields includes the GraphQL fields of Animal requested by the fragment InnerBeingFields.
type InnerBeingFieldsAnimal struct {
Id string `json:"id"`
Name string `json:"name"`
}
// InnerBeingFields includes the GraphQL fields of User requested by the fragment InnerBeingFields.
type InnerBeingFieldsUser struct {
Id string `json:"id"`
Name string `json:"name"`
Friends []FriendsFields `json:"friends"`
}
// InnerLuckyFields includes the GraphQL fields of Lucky requested by the fragment InnerLuckyFields.
//
// InnerLuckyFields is implemented by the following types:
// InnerLuckyFieldsUser
type InnerLuckyFields interface {
implementsGraphQLInterfaceInnerLuckyFields()
// GetLuckyNumber returns the interface-field "luckyNumber" from its implementation.
GetLuckyNumber() int
}
func (v *InnerLuckyFieldsUser) implementsGraphQLInterfaceInnerLuckyFields() {}
// GetLuckyNumber is a part of, and documented with, the interface InnerLuckyFields.
func (v *InnerLuckyFieldsUser) GetLuckyNumber() int { return v.LuckyNumber }
func __unmarshalInnerLuckyFields(b []byte, v *InnerLuckyFields) error {
if string(b) == "null" {
return nil
}
var tn struct {
TypeName string `json:"__typename"`
}
err := json.Unmarshal(b, &tn)
if err != nil {
return err
}
switch tn.TypeName {
case "User":
*v = new(InnerLuckyFieldsUser)
return json.Unmarshal(b, *v)
case "":
return fmt.Errorf(
"Response was missing Lucky.__typename")
default:
return fmt.Errorf(
`Unexpected concrete type for InnerLuckyFields: "%v"`, tn.TypeName)
}
}
func __marshalInnerLuckyFields(v *InnerLuckyFields) ([]byte, error) {
var typename string
switch v := (*v).(type) {
case *InnerLuckyFieldsUser:
typename = "User"
result := struct {
TypeName string `json:"__typename"`
*InnerLuckyFieldsUser
}{typename, v}
return json.Marshal(result)
case nil:
return []byte("null"), nil
default:
return nil, fmt.Errorf(
`Unexpected concrete type for InnerLuckyFields: "%T"`, v)
}
}
// InnerLuckyFields includes the GraphQL fields of User requested by the fragment InnerLuckyFields.
type InnerLuckyFieldsUser struct {
LuckyNumber int `json:"luckyNumber"`
}
// LuckyFields includes the GraphQL fields of Lucky requested by the fragment LuckyFields.
//
// LuckyFields is implemented by the following types:
@@ -387,6 +558,313 @@ type MoreUserFieldsHair struct {
Color string `json:"color"`
}
// QueryFragment includes the GraphQL fields of Query requested by the fragment QueryFragment.
type QueryFragment struct {
Beings []QueryFragmentBeingsBeing `json:"-"`
}
func (v *QueryFragment) UnmarshalJSON(b []byte) error {
if string(b) == "null" {
return nil
}
var firstPass struct {
*QueryFragment
Beings []json.RawMessage `json:"beings"`
graphql.NoUnmarshalJSON
}
firstPass.QueryFragment = v
err := json.Unmarshal(b, &firstPass)
if err != nil {
return err
}
{
dst := &v.Beings
src := firstPass.Beings
*dst = make(
[]QueryFragmentBeingsBeing,
len(src))
for i, src := range src {
dst := &(*dst)[i]
if len(src) != 0 && string(src) != "null" {
err = __unmarshalQueryFragmentBeingsBeing(
src, dst)
if err != nil {
return fmt.Errorf(
"Unable to unmarshal QueryFragment.Beings: %w", err)
}
}
}
}
return nil
}
type __premarshalQueryFragment struct {
Beings []json.RawMessage `json:"beings"`
}
func (v *QueryFragment) MarshalJSON() ([]byte, error) {
premarshaled, err := v.__premarshalJSON()
if err != nil {
return nil, err
}
return json.Marshal(premarshaled)
}
func (v *QueryFragment) __premarshalJSON() (*__premarshalQueryFragment, error) {
var retval __premarshalQueryFragment
{
dst := &retval.Beings
src := v.Beings
*dst = make(
[]json.RawMessage,
len(src))
for i, src := range src {
dst := &(*dst)[i]
var err error
*dst, err = __marshalQueryFragmentBeingsBeing(
&src)
if err != nil {
return nil, fmt.Errorf(
"Unable to marshal QueryFragment.Beings: %w", err)
}
}
}
return &retval, nil
}
// QueryFragmentBeingsAnimal includes the requested fields of the GraphQL type Animal.
type QueryFragmentBeingsAnimal struct {
Typename string `json:"__typename"`
Id string `json:"id"`
Owner InnerBeingFields `json:"-"`
}
func (v *QueryFragmentBeingsAnimal) UnmarshalJSON(b []byte) error {
if string(b) == "null" {
return nil
}
var firstPass struct {
*QueryFragmentBeingsAnimal
Owner json.RawMessage `json:"owner"`
graphql.NoUnmarshalJSON
}
firstPass.QueryFragmentBeingsAnimal = v
err := json.Unmarshal(b, &firstPass)
if err != nil {
return err
}
{
dst := &v.Owner
src := firstPass.Owner
if len(src) != 0 && string(src) != "null" {
err = __unmarshalInnerBeingFields(
src, dst)
if err != nil {
return fmt.Errorf(
"Unable to unmarshal QueryFragmentBeingsAnimal.Owner: %w", err)
}
}
}
return nil
}
type __premarshalQueryFragmentBeingsAnimal struct {
Typename string `json:"__typename"`
Id string `json:"id"`
Owner json.RawMessage `json:"owner"`
}
func (v *QueryFragmentBeingsAnimal) MarshalJSON() ([]byte, error) {
premarshaled, err := v.__premarshalJSON()
if err != nil {
return nil, err
}
return json.Marshal(premarshaled)
}
func (v *QueryFragmentBeingsAnimal) __premarshalJSON() (*__premarshalQueryFragmentBeingsAnimal, error) {
var retval __premarshalQueryFragmentBeingsAnimal
retval.Typename = v.Typename
retval.Id = v.Id
{
dst := &retval.Owner
src := v.Owner
var err error
*dst, err = __marshalInnerBeingFields(
&src)
if err != nil {
return nil, fmt.Errorf(
"Unable to marshal QueryFragmentBeingsAnimal.Owner: %w", err)
}
}
return &retval, nil
}
// QueryFragmentBeingsBeing includes the requested fields of the GraphQL interface Being.
//
// QueryFragmentBeingsBeing is implemented by the following types:
// QueryFragmentBeingsUser
// QueryFragmentBeingsAnimal
type QueryFragmentBeingsBeing interface {
implementsGraphQLInterfaceQueryFragmentBeingsBeing()
// 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 *QueryFragmentBeingsUser) implementsGraphQLInterfaceQueryFragmentBeingsBeing() {}
// GetTypename is a part of, and documented with, the interface QueryFragmentBeingsBeing.
func (v *QueryFragmentBeingsUser) GetTypename() string { return v.Typename }
// GetId is a part of, and documented with, the interface QueryFragmentBeingsBeing.
func (v *QueryFragmentBeingsUser) GetId() string { return v.Id }
func (v *QueryFragmentBeingsAnimal) implementsGraphQLInterfaceQueryFragmentBeingsBeing() {}
// GetTypename is a part of, and documented with, the interface QueryFragmentBeingsBeing.
func (v *QueryFragmentBeingsAnimal) GetTypename() string { return v.Typename }
// GetId is a part of, and documented with, the interface QueryFragmentBeingsBeing.
func (v *QueryFragmentBeingsAnimal) GetId() string { return v.Id }
func __unmarshalQueryFragmentBeingsBeing(b []byte, v *QueryFragmentBeingsBeing) error {
if string(b) == "null" {
return nil
}
var tn struct {
TypeName string `json:"__typename"`
}
err := json.Unmarshal(b, &tn)
if err != nil {
return err
}
switch tn.TypeName {
case "User":
*v = new(QueryFragmentBeingsUser)
return json.Unmarshal(b, *v)
case "Animal":
*v = new(QueryFragmentBeingsAnimal)
return json.Unmarshal(b, *v)
case "":
return fmt.Errorf(
"Response was missing Being.__typename")
default:
return fmt.Errorf(
`Unexpected concrete type for QueryFragmentBeingsBeing: "%v"`, tn.TypeName)
}
}
func __marshalQueryFragmentBeingsBeing(v *QueryFragmentBeingsBeing) ([]byte, error) {
var typename string
switch v := (*v).(type) {
case *QueryFragmentBeingsUser:
typename = "User"
premarshaled, err := v.__premarshalJSON()
if err != nil {
return nil, err
}
result := struct {
TypeName string `json:"__typename"`
*__premarshalQueryFragmentBeingsUser
}{typename, premarshaled}
return json.Marshal(result)
case *QueryFragmentBeingsAnimal:
typename = "Animal"
premarshaled, err := v.__premarshalJSON()
if err != nil {
return nil, err
}
result := struct {
TypeName string `json:"__typename"`
*__premarshalQueryFragmentBeingsAnimal
}{typename, premarshaled}
return json.Marshal(result)
case nil:
return []byte("null"), nil
default:
return nil, fmt.Errorf(
`Unexpected concrete type for QueryFragmentBeingsBeing: "%T"`, v)
}
}
// QueryFragmentBeingsUser includes the requested fields of the GraphQL type User.
type QueryFragmentBeingsUser struct {
Typename string `json:"__typename"`
Id string `json:"id"`
InnerLuckyFieldsUser `json:"-"`
}
func (v *QueryFragmentBeingsUser) UnmarshalJSON(b []byte) error {
if string(b) == "null" {
return nil
}
var firstPass struct {
*QueryFragmentBeingsUser
graphql.NoUnmarshalJSON
}
firstPass.QueryFragmentBeingsUser = v
err := json.Unmarshal(b, &firstPass)
if err != nil {
return err
}
err = json.Unmarshal(
b, &v.InnerLuckyFieldsUser)
if err != nil {
return err
}
return nil
}
type __premarshalQueryFragmentBeingsUser struct {
Typename string `json:"__typename"`
Id string `json:"id"`
LuckyNumber int `json:"luckyNumber"`
}
func (v *QueryFragmentBeingsUser) MarshalJSON() ([]byte, error) {
premarshaled, err := v.__premarshalJSON()
if err != nil {
return nil, err
}
return json.Marshal(premarshaled)
}
func (v *QueryFragmentBeingsUser) __premarshalJSON() (*__premarshalQueryFragmentBeingsUser, error) {
var retval __premarshalQueryFragmentBeingsUser
retval.Typename = v.Typename
retval.Id = v.Id
retval.LuckyNumber = v.InnerLuckyFieldsUser.LuckyNumber
return &retval, nil
}
type Species string
const (
@@ -679,6 +1157,11 @@ func (v *__queryWithCustomMarshalSliceInput) __premarshalJSON() (*__premarshal__
return &retval, nil
}
// __queryWithFlattenInput is used internally by genqlient
type __queryWithFlattenInput struct {
Ids []string `json:"ids"`
}
// __queryWithFragmentsInput is used internally by genqlient
type __queryWithFragmentsInput struct {
Ids []string `json:"ids"`
@@ -2678,3 +3161,66 @@ fragment MoreUserFields on User {
)
return &retval, err
}
func queryWithFlatten(
ctx context.Context,
client graphql.Client,
ids []string,
) (*QueryFragment, error) {
__input := __queryWithFlattenInput{
Ids: ids,
}
var err error
var retval QueryFragment
err = client.MakeRequest(
ctx,
"queryWithFlatten",
`
query queryWithFlatten ($ids: [ID!]!) {
... QueryFragment
}
fragment QueryFragment on Query {
beings(ids: $ids) {
__typename
id
... FlattenedUserFields
... on Animal {
owner {
__typename
... BeingFields
}
}
}
}
fragment FlattenedUserFields on User {
... FlattenedLuckyFields
}
fragment BeingFields on Being {
... InnerBeingFields
}
fragment FlattenedLuckyFields on Lucky {
... InnerLuckyFields
}
fragment InnerBeingFields on Being {
id
name
... on User {
friends {
... FriendsFields
}
}
}
fragment InnerLuckyFields on Lucky {
luckyNumber
}
fragment FriendsFields on User {
id
name
}
`,
&retval,
&__input,
)
return &retval, err
}
+110
View File
@@ -554,6 +554,116 @@ func TestNamedFragments(t *testing.T) {
assert.Nil(t, resp.Beings[2])
}
func TestFlatten(t *testing.T) {
_ = `# @genqlient
# @genqlient(flatten: true)
fragment BeingFields on Being {
...InnerBeingFields
}
fragment InnerBeingFields on Being {
id
name
... on User {
# @genqlient(flatten: true)
friends {
...FriendsFields
}
}
}
fragment FriendsFields on User {
id
name
}
# @genqlient(flatten: true)
fragment FlattenedUserFields on User {
...FlattenedLuckyFields
}
# @genqlient(flatten: true)
fragment FlattenedLuckyFields on Lucky {
...InnerLuckyFields
}
fragment InnerLuckyFields on Lucky {
luckyNumber
}
fragment QueryFragment on Query {
beings(ids: $ids) {
__typename id
...FlattenedUserFields
... on Animal {
# @genqlient(flatten: true)
owner {
...BeingFields
}
}
}
}
# @genqlient(flatten: true)
query queryWithFlatten(
$ids: [ID!]!,
) {
...QueryFragment
}`
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])
}
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
+1
View File
@@ -18,6 +18,7 @@ type User implements Being & Lucky {
luckyNumber: Int
hair: Hair
birthdate: Date
friends: [User!]!
}
type Hair { color: String } # silly name to confuse the name-generator
@@ -72,6 +72,7 @@ type ComplexityRoot struct {
User struct {
Birthdate func(childComplexity int) int
Friends func(childComplexity int) int
Hair func(childComplexity int) int
ID func(childComplexity int) int
LuckyNumber func(childComplexity int) int
@@ -260,6 +261,13 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
return e.complexity.User.Birthdate(childComplexity), true
case "User.friends":
if e.complexity.User.Friends == nil {
break
}
return e.complexity.User.Friends(childComplexity), true
case "User.hair":
if e.complexity.User.Hair == nil {
break
@@ -358,6 +366,7 @@ type User implements Being & Lucky {
luckyNumber: Int
hair: Hair
birthdate: Date
friends: [User!]!
}
type Hair { color: String } # silly name to confuse the name-generator
@@ -1379,6 +1388,41 @@ func (ec *executionContext) _User_birthdate(ctx context.Context, field graphql.C
return ec.marshalODate2ᚖstring(ctx, field.Selections, res)
}
func (ec *executionContext) _User_friends(ctx context.Context, field graphql.CollectedField, obj *User) (ret graphql.Marshaler) {
defer func() {
if r := recover(); r != nil {
ec.Error(ctx, ec.Recover(ctx, r))
ret = graphql.Null
}
}()
fc := &graphql.FieldContext{
Object: "User",
Field: field,
Args: nil,
IsMethod: false,
IsResolver: false,
}
ctx = graphql.WithFieldContext(ctx, fc)
resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
ctx = rctx // use context from middleware stack in children
return obj.Friends, nil
})
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) ___Directive_name(ctx context.Context, field graphql.CollectedField, obj *introspection.Directive) (ret graphql.Marshaler) {
defer func() {
if r := recover(); r != nil {
@@ -2770,6 +2814,11 @@ func (ec *executionContext) _User(ctx context.Context, sel ast.SelectionSet, obj
out.Values[i] = ec._User_hair(ctx, field, obj)
case "birthdate":
out.Values[i] = ec._User_birthdate(ctx, field, obj)
case "friends":
out.Values[i] = ec._User_friends(ctx, field, obj)
if out.Values[i] == graphql.Null {
invalids++
}
default:
panic("unknown field " + strconv.Quote(field.Name))
}
@@ -40,6 +40,7 @@ type User struct {
LuckyNumber *int `json:"luckyNumber"`
Hair *Hair `json:"hair"`
Birthdate *string `json:"birthdate"`
Friends []*User `json:"friends"`
}
func (User) IsBeing() {}
+5
View File
@@ -21,6 +21,11 @@ var users = []*User{
{ID: "2", Name: "Raven", LuckyNumber: intptr(-1), Hair: nil},
}
func init() {
users[0].Friends = []*User{users[1]} // (obviously a lie, but)
users[1].Friends = users // try to crash the system
}
var animals = []*Animal{
{
ID: "3", Name: "Fido", Species: SpeciesDog, Owner: userByID("1"),