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
+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:
+73 -4
View File
@@ -15,6 +15,7 @@ type genqlientDirective struct {
Omitempty *bool
Pointer *bool
Struct *bool
Flatten *bool
Bind string
TypeName string
}
@@ -28,6 +29,7 @@ func newGenqlientDirective(pos *ast.Position) *genqlientDirective {
func (dir *genqlientDirective) GetOmitempty() bool { return dir.Omitempty != nil && *dir.Omitempty }
func (dir *genqlientDirective) GetPointer() bool { return dir.Pointer != nil && *dir.Pointer }
func (dir *genqlientDirective) GetStruct() bool { return dir.Struct != nil && *dir.Struct }
func (dir *genqlientDirective) GetFlatten() bool { return dir.Flatten != nil && *dir.Flatten }
func setBool(optionName string, dst **bool, v *ast.Value, pos *ast.Position) error {
if *dst != nil {
@@ -85,6 +87,8 @@ func (dir *genqlientDirective) add(graphQLDirective *ast.Directive, pos *ast.Pos
err = setBool("pointer", &dir.Pointer, arg.Value, pos)
case "struct":
err = setBool("struct", &dir.Struct, arg.Value, pos)
case "flatten":
err = setBool("flatten", &dir.Flatten, arg.Value, pos)
case "bind":
err = setString("bind", &dir.Bind, arg.Value, pos)
case "typename":
@@ -116,7 +120,7 @@ func (dir *genqlientDirective) validate(node interface{}, schema *ast.Schema) er
}
if dir.Struct != nil {
return errorf(dir.pos, "struct is only applicable to fields")
return errorf(dir.pos, "struct is only applicable to fields, not frragment-definitions")
}
// Like operations, anything else will just apply to the entire
@@ -128,22 +132,32 @@ func (dir *genqlientDirective) validate(node interface{}, schema *ast.Schema) er
}
if dir.Struct != nil {
return errorf(dir.pos, "struct is only applicable to fields")
return errorf(dir.pos, "struct is only applicable to fields, not variable-definitions")
}
if dir.Flatten != nil {
return errorf(dir.pos, "flatten is only applicable to fields, not variable-definitions")
}
return nil
case *ast.Field:
if dir.Omitempty != nil {
return errorf(dir.pos, "omitempty is not applicable to fields")
return errorf(dir.pos, "omitempty is not applicable to variables, not fields")
}
typ := schema.Types[node.Definition.Type.Name()]
if dir.Struct != nil {
typ := schema.Types[node.Definition.Type.Name()]
if err := validateStructOption(typ, node.SelectionSet, dir.pos); err != nil {
return err
}
}
if dir.Flatten != nil {
if _, err := validateFlattenOption(typ, node.SelectionSet, dir.pos); err != nil {
return err
}
}
return nil
default:
return errorf(dir.pos, "invalid @genqlient directive location: %T", node)
@@ -178,6 +192,58 @@ func validateStructOption(
return nil
}
func validateFlattenOption(
typ *ast.Definition,
selectionSet ast.SelectionSet,
pos *ast.Position,
) (index int, err error) {
index = -1
if len(selectionSet) == 0 {
return -1, errorf(pos, "flatten is not allowed for leaf fields")
}
for i, selection := range selectionSet {
switch selection := selection.(type) {
case *ast.Field:
// If the field is auto-added __typename, ignore it for flattening
// purposes.
if selection.Name == "__typename" && selection.Position == nil {
continue
}
// Type-wise, it's no harder to implement flatten for fields, but
// it requires new logic in UnmarshalJSON. We can add that if it
// proves useful relative to its complexity.
return -1, errorf(pos, "flatten is not yet supported for fields (only fragment spreads)")
case *ast.InlineFragment:
// Inline fragments aren't allowed. In principle there's nothing
// stopping us from allowing them (under the same type-match
// conditions as fragment spreads), but there's little value to it.
return -1, errorf(pos, "flatten is not allowed for selections with inline fragments")
case *ast.FragmentSpread:
if index != -1 {
return -1, errorf(pos, "flatten is not allowed for fields with multiple selections")
} else if !fragmentMatches(typ, selection.Definition.Definition) {
// We don't let you flatten
// field { # type: FieldType
// ...Fragment # type: FragmentType
// }
// unless FragmentType implements FieldType, because otherwise
// what do we do if we get back a type that doesn't implement
// FragmentType?
return -1, errorf(pos,
"flatten is not allowed for fields with fragment-spreads "+
"unless the field-type implements the fragment-type; "+
"field-type %s does not implement fragment-type %s",
typ.Name, selection.Definition.Definition.Name)
}
index = i
}
}
return index, nil
}
// merge joins the directive applied to this node (the argument) and the one
// applied to the entire operation (the receiver) and returns a new
// directive-object representing the options to apply to this node (where in
@@ -193,6 +259,9 @@ func (dir *genqlientDirective) merge(other *genqlientDirective) *genqlientDirect
if other.Struct != nil {
retval.Struct = other.Struct
}
if other.Flatten != nil {
retval.Flatten = other.Flatten
}
if other.Bind != "" {
retval.Bind = other.Bind
}
+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
+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"),