diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index a8451245..725e3b75 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -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: diff --git a/docs/FAQ.md b/docs/FAQ.md index 7d24a0a0..9bd58f14 100644 --- a/docs/FAQ.md +++ b/docs/FAQ.md @@ -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? diff --git a/docs/genqlient_directive.graphql b/docs/genqlient_directive.graphql index d00a8fe9..b3d7f186 100644 --- a/docs/genqlient_directive.graphql +++ b/docs/genqlient_directive.graphql @@ -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 diff --git a/generate/convert.go b/generate/convert.go index 889d18e4..8f9ceb8c 100644 --- a/generate/convert.go +++ b/generate/convert.go @@ -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: diff --git a/generate/genqlient_directive.go b/generate/genqlient_directive.go index 15ed1c8b..903c83fa 100644 --- a/generate/genqlient_directive.go +++ b/generate/genqlient_directive.go @@ -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 } diff --git a/generate/testdata/errors/FlattenField.graphql b/generate/testdata/errors/FlattenField.graphql new file mode 100644 index 00000000..8d32f10b --- /dev/null +++ b/generate/testdata/errors/FlattenField.graphql @@ -0,0 +1,6 @@ +query FlattenField { + # @genqlient(flatten: true) + t { + f + } +} diff --git a/generate/testdata/errors/FlattenField.schema.graphql b/generate/testdata/errors/FlattenField.schema.graphql new file mode 100644 index 00000000..0977d984 --- /dev/null +++ b/generate/testdata/errors/FlattenField.schema.graphql @@ -0,0 +1,2 @@ +type Query { t: T } +type T { f: String } diff --git a/generate/testdata/errors/FlattenImplementation.graphql b/generate/testdata/errors/FlattenImplementation.graphql new file mode 100644 index 00000000..901bc533 --- /dev/null +++ b/generate/testdata/errors/FlattenImplementation.graphql @@ -0,0 +1,7 @@ +fragment F on T { f } +query FlattenImplementation { + # @genqlient(flatten: true) + i { + ...F + } +} diff --git a/generate/testdata/errors/FlattenImplementation.schema.graphql b/generate/testdata/errors/FlattenImplementation.schema.graphql new file mode 100644 index 00000000..e5371a6e --- /dev/null +++ b/generate/testdata/errors/FlattenImplementation.schema.graphql @@ -0,0 +1,3 @@ +type Query { i: I } +interface I { f: String } +type T implements I { f: String } diff --git a/generate/testdata/queries/Flatten.graphql b/generate/testdata/queries/Flatten.graphql new file mode 100644 index 00000000..b929aee6 --- /dev/null +++ b/generate/testdata/queries/Flatten.graphql @@ -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 +} diff --git a/generate/testdata/queries/schema.graphql b/generate/testdata/queries/schema.graphql index d0e69082..4e951bba 100644 --- a/generate/testdata/queries/schema.graphql +++ b/generate/testdata/queries/schema.graphql @@ -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 diff --git a/generate/testdata/snapshots/TestGenerate-Flatten.graphql-Flatten.graphql.go b/generate/testdata/snapshots/TestGenerate-Flatten.graphql-Flatten.graphql.go new file mode 100644 index 00000000..523b5d63 --- /dev/null +++ b/generate/testdata/snapshots/TestGenerate-Flatten.graphql-Flatten.graphql.go @@ -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 +} + diff --git a/generate/testdata/snapshots/TestGenerate-Flatten.graphql-Flatten.graphql.json b/generate/testdata/snapshots/TestGenerate-Flatten.graphql-Flatten.graphql.json new file mode 100644 index 00000000..1daf0193 --- /dev/null +++ b/generate/testdata/snapshots/TestGenerate-Flatten.graphql-Flatten.graphql.json @@ -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" + } + ] +} diff --git a/generate/testdata/snapshots/TestGenerateErrors-FlattenField-graphql b/generate/testdata/snapshots/TestGenerateErrors-FlattenField-graphql new file mode 100644 index 00000000..d1606afa --- /dev/null +++ b/generate/testdata/snapshots/TestGenerateErrors-FlattenField-graphql @@ -0,0 +1 @@ +testdata/errors/FlattenField.graphql:3: flatten is not yet supported for fields (only fragment spreads) diff --git a/generate/testdata/snapshots/TestGenerateErrors-FlattenImplementation-graphql b/generate/testdata/snapshots/TestGenerateErrors-FlattenImplementation-graphql new file mode 100644 index 00000000..599292b7 --- /dev/null +++ b/generate/testdata/snapshots/TestGenerateErrors-FlattenImplementation-graphql @@ -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 diff --git a/internal/integration/generated.go b/internal/integration/generated.go index f2f4dc66..f6674c3c 100644 --- a/internal/integration/generated.go +++ b/internal/integration/generated.go @@ -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 +} diff --git a/internal/integration/integration_test.go b/internal/integration/integration_test.go index b0fd5283..55162ad8 100644 --- a/internal/integration/integration_test.go +++ b/internal/integration/integration_test.go @@ -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 diff --git a/internal/integration/schema.graphql b/internal/integration/schema.graphql index a9e581c1..6d918621 100644 --- a/internal/integration/schema.graphql +++ b/internal/integration/schema.graphql @@ -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 diff --git a/internal/integration/server/gqlgen_exec.go b/internal/integration/server/gqlgen_exec.go index 6a4cf241..6b7e7759 100644 --- a/internal/integration/server/gqlgen_exec.go +++ b/internal/integration/server/gqlgen_exec.go @@ -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)) } diff --git a/internal/integration/server/gqlgen_models.go b/internal/integration/server/gqlgen_models.go index b74a7097..73ae3924 100644 --- a/internal/integration/server/gqlgen_models.go +++ b/internal/integration/server/gqlgen_models.go @@ -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() {} diff --git a/internal/integration/server/server.go b/internal/integration/server/server.go index d0412b09..69883565 100644 --- a/internal/integration/server/server.go +++ b/internal/integration/server/server.go @@ -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"),