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
+37
View File
@@ -115,6 +115,15 @@ func (g *generator) convertOperation(
return nil, err
}
// It's not common to use a fragment-spread for the whole query, but you
// can if you want two queries to return the same type!
if queryOptions.GetFlatten() {
i, err := validateFlattenOption(baseType, operation.SelectionSet, operation.Position)
if err == nil {
return fields[i].GoType, nil
}
}
goType := &goStructType{
GoName: name,
descriptionInfo: descriptionInfo{
@@ -340,6 +349,17 @@ func (g *generator) convertDefinition(
if err != nil {
return nil, err
}
if options.GetFlatten() {
// As with struct, flatten only applies if valid, important if you
// applied it to the whole query.
// TODO(benkraft): This is a slightly fragile way to do this;
// figure out a good way to do it before/while constructing the
// fields, rather than after.
i, err := validateFlattenOption(def, selectionSet, pos)
if err == nil {
return fields[i].GoType, nil
}
}
goType := &goStructType{
GoName: name,
@@ -406,6 +426,14 @@ func (g *generator) convertDefinition(
if err != nil {
return nil, err
}
// Flatten can only flatten if there is only one field (plus perhaps
// __typename), and it's shared.
if options.GetFlatten() {
i, err := validateFlattenOption(def, selectionSet, pos)
if err == nil {
return sharedFields[i].GoType, nil
}
}
implementationTypes := g.schema.GetPossibleTypes(def)
goType := &goInterfaceType{
@@ -705,6 +733,15 @@ func (g *generator) convertNamedFragment(fragment *ast.FragmentDefinition) (goTy
if err != nil {
return nil, err
}
if directive.GetFlatten() {
// Flatten on a fragment-definition is a bit weird -- it makes one
// fragment effectively an alias for another -- but no reason we can't
// allow it.
i, err := validateFlattenOption(typ, fragment.SelectionSet, fragment.Position)
if err == nil {
return fields[i].GoType, nil
}
}
switch typ.Kind {
case ast.Object: