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
@@ -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"),