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
+1
View File
@@ -25,6 +25,7 @@ When releasing a new version:
### New features:
- genqlient's types are now safe to JSON-marshal, which can be useful for putting them in a cache, for example. See the [docs](FAQ.md#-let-me-json-marshal-my-response-objects) for details.
- The new `flatten` option in the `# @genqlient` directive allows for a simpler form of type-sharing using fragment spreads. See the [docs](FAQ.md#-shared-types-between-different-parts-of-the-query) for details.
### Bug fixes:
+18 -4
View File
@@ -260,9 +260,9 @@ type GetMonopolyPlayersGameWinnerUser struct {
// (others similarly)
```
But maybe you wanted to be able to pass all those users to a shared function (defined in your code), say `FormatUser(user ???) string`. That's no good; you need to put three different types as the `???`. genqlient has two ways to deal with this.
But maybe you wanted to be able to pass all those users to a shared function (defined in your code), say `FormatUser(user ???) string`. That's no good; you need to put three different types as the `???`. genqlient has several ways to deal with this.
One option -- the GraphQL Way, perhaps -- is to use fragments. You'd write your query like:
**Fragments:** One option -- the GraphQL Way, perhaps -- is to use fragments. You'd write your query like:
```graphql
fragment MonopolyUser on User {
@@ -319,7 +319,21 @@ query GetMonopolyPlayers {
and you can even spread the fragment into interface types. It also avoids having to list the fields several times.
Alternately, if you always want exactly the same fields, you can use the simpler but more restrictive genqlient option `typename`:
**Fragments, flattened:** The Go field for `winner`, in the first query above, has type `GetMonopolyPlayersGameWinnerUser` which just wraps `MonopolyUser`. If we don't want to add any other fields, that's unnecessary! Instead, we could do
```
query GetMonopolyPlayers {
game {
# @genqlient(flatten: true)
winner {
...MonopolyUser
}
# (etc.)
}
}
```
and genqlient will skip the indirection and give the field `Winner` type `MonopolyUser` directly. This is often much more convenient if you put all the fields in the fragment, like the first query did. See the [options documentation](genqlient_directive.graphql) for more details.
**Type names:** Finally, if you always want exactly the same fields, you can use the simpler but more restrictive genqlient option `typename`:
```graphql
query GetMonopolyPlayers {
@@ -351,7 +365,7 @@ type User struct {
In this case, genqlient will validate that each type given the name `User` has the exact same fields; see the [full documentation](genqlient_directive.graphql) for details.
Note that it's also possible to use the `bindings` option (see [`genqlient.yaml` documentation](genqlient.yaml)) for a similar purpose, but this is not recommended as it typically requires more work for less gain.
**Bindings:** It's also possible to use the `bindings` option (see [`genqlient.yaml` documentation](genqlient.yaml)) for a similar purpose, but this is not recommended as it typically requires more work for less gain.
### … documentation on the output types?
+35 -1
View File
@@ -82,6 +82,40 @@ directive genqlient(
# fragment, you'll have to remove this option, and the types will change.
struct: Boolean
# If set, this field's selection must contain a single fragment-spread; we'll
# use the type of that fragment-spread as the type of the field.
#
# For example, given a query like
# query MyQuery {
# myField {
# ...MyFragment
# }
# }
# by default genqlient will generate these types:
# type MyQueryResponse struct {
# MyField MyQueryMyFieldMyType
# }
# type MyQueryMyFieldMyType struct {
# MyFragment
# }
# If we instead do:
# query MyQuery {
# # @genqlient(flatten: true)
# myField {
# ...MyFragment
# }
# }
# genqlient will simplify things:
# type MyQueryResponse struct {
# MyField MyFragment
# }
#
# This is only applicable to fields whose selection is a single
# fragment-spread, such that the field-type implements the fragment-type
# (i.e. we can't do this if MyFragment is on one implementation of the type
# of MyField; what if we got back the other type?).
flatten: Boolean
# If set, this argument or field will use the given Go type instead of a
# genqlient-generated type.
#
@@ -128,7 +162,7 @@ directive genqlient(
# if your type-name conflicts with an autogenerated one (again, unless they
# request the exact same fields). They must even have the fields in the
# same order. Fragments are often easier to use (see the discussion of
# code-sharing in FAQ.md).
# code-sharing in FAQ.md, and the "flatten" option above).
#
# Note that unlike most directives, if applied to the entire operation,
# typename affects the overall response type, rather than being propagated