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
+6
View File
@@ -0,0 +1,6 @@
query FlattenField {
# @genqlient(flatten: true)
t {
f
}
}
+2
View File
@@ -0,0 +1,2 @@
type Query { t: T }
type T { f: String }
@@ -0,0 +1,7 @@
fragment F on T { f }
query FlattenImplementation {
# @genqlient(flatten: true)
i {
...F
}
}
@@ -0,0 +1,3 @@
type Query { i: I }
interface I { f: String }
type T implements I { f: String }
+42
View File
@@ -0,0 +1,42 @@
# @genqlient(flatten: true)
fragment QueryFragment on Query {
...InnerQueryFragment
}
fragment InnerQueryFragment on Query {
# @genqlient(flatten: true)
randomVideo {
...VideoFields
}
# @genqlient(flatten: true)
randomItem {
...ContentFields
}
# @genqlient(flatten: true)
otherVideo: randomVideo {
...ContentFields
}
}
fragment VideoFields on Video {
id
parent {
# @genqlient(flatten: true)
videoChildren {
...ChildVideoFields
}
}
}
fragment ChildVideoFields on Video {
id name
}
fragment ContentFields on Content {
name url
}
# @genqlient(flatten: true)
query ComplexNamedFragments {
...QueryFragment
}
+2
View File
@@ -131,6 +131,7 @@ type Topic implements Content {
parent: Topic
url: String!
children: [Content!]!
videoChildren: [Video!]!
schoolGrade: String
}
@@ -162,6 +163,7 @@ type Query {
root: Topic!
randomItem: Content!
randomLeaf: LeafContent!
randomVideo: Video!
convert(dt: DateTime!, tz: String): DateTime!
maybeConvert(dt: DateTime, tz: String): DateTime
getJunk: Junk
@@ -0,0 +1,297 @@
package test
// Code generated by github.com/Khan/genqlient, DO NOT EDIT.
import (
"encoding/json"
"fmt"
"github.com/Khan/genqlient/graphql"
"github.com/Khan/genqlient/internal/testutil"
)
// ChildVideoFields includes the GraphQL fields of Video requested by the fragment ChildVideoFields.
type ChildVideoFields struct {
// ID is documented in the Content interface.
Id testutil.ID `json:"id"`
Name string `json:"name"`
}
// ContentFields includes the GraphQL fields of Content requested by the fragment ContentFields.
// The GraphQL type's documentation follows.
//
// Content is implemented by various types like Article, Video, and Topic.
//
// ContentFields is implemented by the following types:
// ContentFieldsArticle
// ContentFieldsVideo
// ContentFieldsTopic
type ContentFields interface {
implementsGraphQLInterfaceContentFields()
// GetName returns the interface-field "name" from its implementation.
GetName() string
// GetUrl returns the interface-field "url" from its implementation.
GetUrl() string
}
func (v *ContentFieldsArticle) implementsGraphQLInterfaceContentFields() {}
// GetName is a part of, and documented with, the interface ContentFields.
func (v *ContentFieldsArticle) GetName() string { return v.Name }
// GetUrl is a part of, and documented with, the interface ContentFields.
func (v *ContentFieldsArticle) GetUrl() string { return v.Url }
func (v *ContentFieldsVideo) implementsGraphQLInterfaceContentFields() {}
// GetName is a part of, and documented with, the interface ContentFields.
func (v *ContentFieldsVideo) GetName() string { return v.Name }
// GetUrl is a part of, and documented with, the interface ContentFields.
func (v *ContentFieldsVideo) GetUrl() string { return v.Url }
func (v *ContentFieldsTopic) implementsGraphQLInterfaceContentFields() {}
// GetName is a part of, and documented with, the interface ContentFields.
func (v *ContentFieldsTopic) GetName() string { return v.Name }
// GetUrl is a part of, and documented with, the interface ContentFields.
func (v *ContentFieldsTopic) GetUrl() string { return v.Url }
func __unmarshalContentFields(b []byte, v *ContentFields) 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 "Article":
*v = new(ContentFieldsArticle)
return json.Unmarshal(b, *v)
case "Video":
*v = new(ContentFieldsVideo)
return json.Unmarshal(b, *v)
case "Topic":
*v = new(ContentFieldsTopic)
return json.Unmarshal(b, *v)
case "":
return fmt.Errorf(
"Response was missing Content.__typename")
default:
return fmt.Errorf(
`Unexpected concrete type for ContentFields: "%v"`, tn.TypeName)
}
}
func __marshalContentFields(v *ContentFields) ([]byte, error) {
var typename string
switch v := (*v).(type) {
case *ContentFieldsArticle:
typename = "Article"
result := struct {
TypeName string `json:"__typename"`
*ContentFieldsArticle
}{typename, v}
return json.Marshal(result)
case *ContentFieldsVideo:
typename = "Video"
result := struct {
TypeName string `json:"__typename"`
*ContentFieldsVideo
}{typename, v}
return json.Marshal(result)
case *ContentFieldsTopic:
typename = "Topic"
result := struct {
TypeName string `json:"__typename"`
*ContentFieldsTopic
}{typename, v}
return json.Marshal(result)
case nil:
return []byte("null"), nil
default:
return nil, fmt.Errorf(
`Unexpected concrete type for ContentFields: "%T"`, v)
}
}
// ContentFields includes the GraphQL fields of Article requested by the fragment ContentFields.
// The GraphQL type's documentation follows.
//
// Content is implemented by various types like Article, Video, and Topic.
type ContentFieldsArticle struct {
Name string `json:"name"`
Url string `json:"url"`
}
// ContentFields includes the GraphQL fields of Topic requested by the fragment ContentFields.
// The GraphQL type's documentation follows.
//
// Content is implemented by various types like Article, Video, and Topic.
type ContentFieldsTopic struct {
Name string `json:"name"`
Url string `json:"url"`
}
// ContentFields includes the GraphQL fields of Video requested by the fragment ContentFields.
// The GraphQL type's documentation follows.
//
// Content is implemented by various types like Article, Video, and Topic.
type ContentFieldsVideo struct {
Name string `json:"name"`
Url string `json:"url"`
}
// InnerQueryFragment includes the GraphQL fields of Query requested by the fragment InnerQueryFragment.
// The GraphQL type's documentation follows.
//
// Query's description is probably ignored by almost all callers.
type InnerQueryFragment struct {
RandomVideo VideoFields `json:"randomVideo"`
RandomItem ContentFields `json:"-"`
OtherVideo ContentFieldsVideo `json:"otherVideo"`
}
func (v *InnerQueryFragment) UnmarshalJSON(b []byte) error {
if string(b) == "null" {
return nil
}
var firstPass struct {
*InnerQueryFragment
RandomItem json.RawMessage `json:"randomItem"`
graphql.NoUnmarshalJSON
}
firstPass.InnerQueryFragment = v
err := json.Unmarshal(b, &firstPass)
if err != nil {
return err
}
{
dst := &v.RandomItem
src := firstPass.RandomItem
if len(src) != 0 && string(src) != "null" {
err = __unmarshalContentFields(
src, dst)
if err != nil {
return fmt.Errorf(
"Unable to unmarshal InnerQueryFragment.RandomItem: %w", err)
}
}
}
return nil
}
type __premarshalInnerQueryFragment struct {
RandomVideo VideoFields `json:"randomVideo"`
RandomItem json.RawMessage `json:"randomItem"`
OtherVideo ContentFieldsVideo `json:"otherVideo"`
}
func (v *InnerQueryFragment) MarshalJSON() ([]byte, error) {
premarshaled, err := v.__premarshalJSON()
if err != nil {
return nil, err
}
return json.Marshal(premarshaled)
}
func (v *InnerQueryFragment) __premarshalJSON() (*__premarshalInnerQueryFragment, error) {
var retval __premarshalInnerQueryFragment
retval.RandomVideo = v.RandomVideo
{
dst := &retval.RandomItem
src := v.RandomItem
var err error
*dst, err = __marshalContentFields(
&src)
if err != nil {
return nil, fmt.Errorf(
"Unable to marshal InnerQueryFragment.RandomItem: %w", err)
}
}
retval.OtherVideo = v.OtherVideo
return &retval, nil
}
// VideoFields includes the GraphQL fields of Video requested by the fragment VideoFields.
type VideoFields struct {
// ID is documented in the Content interface.
Id testutil.ID `json:"id"`
Parent VideoFieldsParentTopic `json:"parent"`
}
// VideoFieldsParentTopic includes the requested fields of the GraphQL type Topic.
type VideoFieldsParentTopic struct {
VideoChildren []ChildVideoFields `json:"videoChildren"`
}
func ComplexNamedFragments(
client graphql.Client,
) (*InnerQueryFragment, error) {
var err error
var retval InnerQueryFragment
err = client.MakeRequest(
nil,
"ComplexNamedFragments",
`
query ComplexNamedFragments {
... QueryFragment
}
fragment QueryFragment on Query {
... InnerQueryFragment
}
fragment InnerQueryFragment on Query {
randomVideo {
... VideoFields
}
randomItem {
__typename
... ContentFields
}
otherVideo: randomVideo {
... ContentFields
}
}
fragment VideoFields on Video {
id
parent {
videoChildren {
... ChildVideoFields
}
}
}
fragment ContentFields on Content {
name
url
}
fragment ChildVideoFields on Video {
id
name
}
`,
&retval,
nil,
)
return &retval, err
}
@@ -0,0 +1,9 @@
{
"operations": [
{
"operationName": "ComplexNamedFragments",
"query": "\nquery ComplexNamedFragments {\n\t... QueryFragment\n}\nfragment QueryFragment on Query {\n\t... InnerQueryFragment\n}\nfragment InnerQueryFragment on Query {\n\trandomVideo {\n\t\t... VideoFields\n\t}\n\trandomItem {\n\t\t__typename\n\t\t... ContentFields\n\t}\n\totherVideo: randomVideo {\n\t\t... ContentFields\n\t}\n}\nfragment VideoFields on Video {\n\tid\n\tparent {\n\t\tvideoChildren {\n\t\t\t... ChildVideoFields\n\t\t}\n\t}\n}\nfragment ContentFields on Content {\n\tname\n\turl\n}\nfragment ChildVideoFields on Video {\n\tid\n\tname\n}\n",
"sourceLocation": "testdata/queries/Flatten.graphql"
}
]
}
@@ -0,0 +1 @@
testdata/errors/FlattenField.graphql:3: flatten is not yet supported for fields (only fragment spreads)
@@ -0,0 +1 @@
testdata/errors/FlattenImplementation.graphql:4: flatten is not allowed for fields with fragment-spreads unless the field-type implements the fragment-type; field-type I does not implement fragment-type T