From 0e6477387d048e3d1a4903165fa6f418028ba9d4 Mon Sep 17 00:00:00 2001 From: Ben Kraft Date: Mon, 30 Aug 2021 17:34:31 -0700 Subject: [PATCH] Add support for concrete-typed named fragments In previous commits I added support to genqlient for interfaces and inline fragments. This means the only query structures that remain are named fragments and their spreads, e.g. ``` fragment MyFragment on MyType { myField } query MyQuery { getMyType { ...MyFragment } } ``` Other than mere completionism, these are potentially useful for code sharing: you can spread the same fragment multiple places; and then genqlient can notice that and generate the same type for each. (They can even be shared between different queries in the same package.) In this commit I add support for named fragments of concrete (object/struct, not interface) type, spread into either concrete or abstract scope. For genqlient's purposes, these are a new "root" type-name, just like each operation, and are then embedded into the appropriate struct. (Using embeds allows their fields to be referenced as fields of the containing type, if convenient. Further design considerations are discussed in DESIGN.md.) This requires new code in two main places (plus miscellaneous glue), both nontrivial but neither particularly complex: - We need to actually traverse both structures and generate the types (in `convert.go`). - We need to decide which fragments from this package to send to the server, both for good hyigene and because GraphQL requires we send only ones this query uses (in `generate.go`). - We need a little new wiring for options -- because fragments can be shared between queries they get their own toplevel options, rather than inheriting the query's options. Finally, this required slightly subtler changes to how we do unmarshaling (in `types.go` and `unmarshal.go.tmpl`). Basically, because embedded fields' methods, including `UnmarshalJSON`, get promoted to the parent type, and because the JSON library ignores their fields when shadowed by those of the parent type, we need a little bit of special logic in each such parent type to do its own unmarshal and then delegate to each embed. This is similar (and much simpler) to what we did for interfaces, although it required some changes to the "method-hiding" trick (used for both). It's only really necessary in certain specific cases (namely when an embedded type has an `UnmarshalJSON` method or a field with the same name as the embedder), but it's easier to just generate it always. This is all described in more detail inline. This does not support fragments of abstract type, which have their own complexities. I'll address those, which are now the only remaining piece of #8, in a future commit. Issue: https://github.com/Khan/genqlient/issues/8 Test plan: make check Reviewers: marksandstrom, miguel, adam --- generate/convert.go | 78 +- generate/generate.go | 76 +- generate/genqlient_directive.go | 11 +- .../queries/ComplexNamedFragments.graphql | 39 + .../queries/SimpleNamedFragment.graphql | 13 + ....graphql-ComplexInlineFragments.graphql.go | 27 +- ...s.graphql-ComplexNamedFragments.graphql.go | 664 ++++++++++++++++++ ...graphql-ComplexNamedFragments.graphql.json | 9 + ...ield.graphql-InterfaceListField.graphql.go | 16 +- ...nterfaceListOfListsOfListsField.graphql.go | 9 +- ...esting.graphql-InterfaceNesting.graphql.go | 16 +- ...ts.graphql-InterfaceNoFragments.graphql.go | 10 +- ...nt.graphql-SimpleInlineFragment.graphql.go | 8 +- ...ent.graphql-SimpleNamedFragment.graphql.go | 332 +++++++++ ...t.graphql-SimpleNamedFragment.graphql.json | 9 + ...gments.graphql-UnionNoFragments.graphql.go | 8 +- generate/types.go | 67 +- generate/unmarshal.go.tmpl | 52 +- graphql/util.go | 19 + internal/integration/generated.go | 421 ++++++++++- internal/integration/integration_test.go | 88 +++ 21 files changed, 1850 insertions(+), 122 deletions(-) create mode 100644 generate/testdata/queries/ComplexNamedFragments.graphql create mode 100644 generate/testdata/queries/SimpleNamedFragment.graphql create mode 100644 generate/testdata/snapshots/TestGenerate-ComplexNamedFragments.graphql-ComplexNamedFragments.graphql.go create mode 100644 generate/testdata/snapshots/TestGenerate-ComplexNamedFragments.graphql-ComplexNamedFragments.graphql.json create mode 100644 generate/testdata/snapshots/TestGenerate-SimpleNamedFragment.graphql-SimpleNamedFragment.graphql.go create mode 100644 generate/testdata/snapshots/TestGenerate-SimpleNamedFragment.graphql-SimpleNamedFragment.graphql.json create mode 100644 graphql/util.go diff --git a/generate/convert.go b/generate/convert.go index b8ad0198..e02245e6 100644 --- a/generate/convert.go +++ b/generate/convert.go @@ -330,7 +330,12 @@ func (g *generator) convertSelectionSet( } fields = append(fields, field) case *ast.FragmentSpread: - return nil, errorf(selection.Position, "not implemented: %T", selection) + maybeField, err := g.convertFragmentSpread(selection, containingTypedef) + if err != nil { + return nil, err + } else if maybeField != nil { + fields = append(fields, maybeField) + } case *ast.InlineFragment: // (Note this will return nil, nil if the fragment doesn't apply to // this type.) @@ -354,7 +359,9 @@ func (g *generator) convertSelectionSet( // GraphQL (and, effectively, JSON) requires that all fields with the // same alias (JSON-name) must be the same (i.e. refer to the same // field), so that's how we deduplicate. - if fieldNames[field.JSONName] { + // It's fine to have duplicate embeds (i.e. via named fragments), even + // ones with complicated overlaps, since they are separate types to us. + if field.JSONName != "" && fieldNames[field.JSONName] { // GraphQL (and, effectively, JSON) forbids you from having two // fields with the same alias (JSON-name) that refer to different // GraphQL fields. But it does allow you to have the same field @@ -444,6 +451,73 @@ func (g *generator) convertInlineFragment( containingTypedef, queryOptions) } +// convertFragmentSpread converts a single GraphQL fragment-spread +// (`...MyFragment`) into a Go struct-field. It assumes that +// convertNamedFragment has already been called on the fragment-definition. If +// the fragment does not apply to this type, returns nil. +// +// containingTypedef is as described in convertInlineFragment, above. +func (g *generator) convertFragmentSpread( + fragmentSpread *ast.FragmentSpread, + containingTypedef *ast.Definition, +) (*goStructField, error) { + if !fragmentMatches(containingTypedef, fragmentSpread.Definition.Definition) { + return nil, nil + } + + typ, ok := g.typeMap[fragmentSpread.Name] + if !ok { + // If we haven't yet, convert the fragment itself. Note that fragments + // aren't allowed to have cycles, so this won't recurse forever. + var err error + typ, err = g.convertNamedFragment(fragmentSpread.Definition) + if err != nil { + return nil, err + } + } + + return &goStructField{GoName: "" /* i.e. embedded */, GoType: typ}, nil +} + +// convertNamedFragment converts a single GraphQL named fragment-definition +// (`fragment MyFragment on MyType { ... }`) into a Go struct. +func (g *generator) convertNamedFragment(fragment *ast.FragmentDefinition) (goType, error) { + typ := g.schema.Types[fragment.TypeCondition] + if !g.Config.AllowBrokenFeatures && + (typ.Kind == ast.Interface || typ.Kind == ast.Union) { + return nil, errorf(fragment.Position, "not implemented: abstract-typed fragments") + } + + description, directive, err := g.parsePrecedingComment(fragment, fragment.Position) + if err != nil { + return nil, err + } + + // If the user included a comment, use that. Else make up something + // generic; there's not much to say though. + if description == "" { + description = fmt.Sprintf( + "%v includes the GraphQL fields of %v requested by the fragment %v.", + fragment.Name, fragment.TypeCondition, fragment.Name) + } + + fields, err := g.convertSelectionSet( + newPrefixList(fragment.Name), fragment.SelectionSet, typ, directive) + if err != nil { + return nil, err + } + + goType := &goStructType{ + GoName: fragment.Name, + Description: description, + GraphQLName: fragment.TypeCondition, + Fields: fields, + Incomplete: false, + } + g.typeMap[fragment.Name] = goType + return goType, nil +} + // convertField converts a single GraphQL operation-field into a Go // struct-field (and its type). // diff --git a/generate/generate.go b/generate/generate.go index befd9511..258777b3 100644 --- a/generate/generate.go +++ b/generate/generate.go @@ -34,6 +34,11 @@ type generator struct { templateCache map[string]*template.Template // Schema we are generating code against schema *ast.Schema + // Named fragments (map by name), so we can look them up from spreads. + // TODO(benkraft): In theory we shouldn't need this, we can just use + // ast.FragmentSpread.Definition, but for some reason it doesn't seem to be + // set consistently, even post-validation. + fragments map[string]*ast.FragmentDefinition } // JSON tags in operation are for ExportOperations (see Config for details). @@ -66,7 +71,11 @@ type argument struct { Options *GenqlientDirective } -func newGenerator(config *Config, schema *ast.Schema) *generator { +func newGenerator( + config *Config, + schema *ast.Schema, + fragments ast.FragmentDefinitionList, +) *generator { g := generator{ Config: config, typeMap: map[string]goType{}, @@ -74,6 +83,11 @@ func newGenerator(config *Config, schema *ast.Schema) *generator { usedAliases: map[string]bool{}, templateCache: map[string]*template.Template{}, schema: schema, + fragments: make(map[string]*ast.FragmentDefinition, len(fragments)), + } + + for _, fragment := range fragments { + g.fragments[fragment.Name] = fragment } if g.Config.ClientGetter == "" { @@ -139,6 +153,38 @@ func (g *generator) getArgument( }, nil } +// usedFragmentNames returns the named-fragments used by (i.e. spread into) +// this operation. +func (g *generator) usedFragments(op *ast.OperationDefinition) ast.FragmentDefinitionList { + var retval, queue ast.FragmentDefinitionList + seen := map[string]bool{} + + var observers validator.Events + // Fragment-spreads are easy to find; just ask for them! + observers.OnFragmentSpread(func(_ *validator.Walker, fragmentSpread *ast.FragmentSpread) { + if seen[fragmentSpread.Name] { + return + } + def := g.fragments[fragmentSpread.Name] + seen[fragmentSpread.Name] = true + retval = append(retval, def) + queue = append(queue, def) + }) + + doc := ast.QueryDocument{Operations: ast.OperationList{op}} + validator.Walk(g.schema, &doc, &observers) + // Well, easy-ish: we also have to look recursively. + // Note GraphQL guarantees there are no cycles among fragments: + // https://spec.graphql.org/draft/#sec-Fragment-spreads-must-not-form-cycles + for len(queue) > 0 { + doc = ast.QueryDocument{Fragments: ast.FragmentDefinitionList{queue[0]}} + validator.Walk(g.schema, &doc, &observers) // traversal is the same + queue = queue[1:] + } + + return retval +} + // Preprocess each query to make any changes that genqlient needs. // // At present, the only change is that we add __typename, if not already @@ -151,9 +197,11 @@ func (g *generator) preprocessQueryDocument(doc *ast.QueryDocument) { // __typename field. There are four places we might find a selection-set: // at the toplevel of a query, on a field, or in an inline or named // fragment. The toplevel of a query must be an object type, so we don't - // need to consider that. - // TODO(benkraft): Once we support fragments, figure out whether we need to - // traverse inline/named fragments here too. + // need to consider that. And fragments must (if used at all) be spread + // into some parent selection-set, so we'll add __typename there (if + // needed). Note this does mean abstract-typed fragments spread into + // object-typed scope will *not* have access to `__typename`, but they + // indeed don't need it, since we do know the type in that context. observers.OnField(func(_ *validator.Walker, field *ast.Field) { // We are interested in a field from the query like // field { subField ... } @@ -207,6 +255,11 @@ func (g *generator) preprocessQueryDocument(doc *ast.QueryDocument) { validator.Walk(g.schema, doc, &observers) } +// addOperation adds to g.Operations the information needed to generate a +// genqlient entrypoint function for the given operation. It also adds to +// g.typeMap any types referenced by the operation, except for types belonging +// to named fragments, which are added separately by Generate via +// convertFragment. func (g *generator) addOperation(op *ast.OperationDefinition) error { if op.Name == "" { return errorf(op.Position, "operations must have operation-names") @@ -214,7 +267,7 @@ func (g *generator) addOperation(op *ast.OperationDefinition) error { queryDoc := &ast.QueryDocument{ Operations: ast.OperationList{op}, - // TODO: handle fragments + Fragments: g.usedFragments(op), } g.preprocessQueryDocument(queryDoc) @@ -297,15 +350,10 @@ func Generate(config *Config) (map[string][]byte, error) { strings.Join(config.Operations, ", ")) } - if len(document.Fragments) > 0 && !config.AllowBrokenFeatures { - return nil, errorf(document.Fragments[0].Position, - "genqlient does not yet support fragments") - } - - // Step 2: For each operation, convert it into data structures representing - // Go types (defined in types.go). The bulk of this logic is in - // convert.go. - g := newGenerator(config, schema) + // Step 2: For each operation and fragment, convert it into data structures + // representing Go types (defined in types.go). The bulk of this logic is + // in convert.go. + g := newGenerator(config, schema, document.Fragments) for _, op := range document.Operations { if err = g.addOperation(op); err != nil { return nil, err diff --git a/generate/genqlient_directive.go b/generate/genqlient_directive.go index 5abf8b41..99f68cfb 100644 --- a/generate/genqlient_directive.go +++ b/generate/genqlient_directive.go @@ -153,6 +153,15 @@ func (dir *GenqlientDirective) validate(node interface{}) error { // Anything else is valid on the entire operation; it will just apply // to whatever it is relevant to. return nil + case *ast.FragmentDefinition: + if dir.Bind != "" { + // TODO(benkraft): Implement this if people find it useful. + return errorf(dir.pos, "bind is not implemented for named fragments") + } + + // Like operations, anything else will just apply to the entire + // fragment. + return nil case *ast.VariableDefinition: if dir.Omitempty != nil && node.Type.NonNull { return errorf(dir.pos, "omitempty may only be used on optional arguments") @@ -164,7 +173,7 @@ func (dir *GenqlientDirective) validate(node interface{}) error { } return nil default: - return errorf(dir.pos, "invalid directive location: %T", node) + return errorf(dir.pos, "invalid @genqlient directive location: %T", node) } } diff --git a/generate/testdata/queries/ComplexNamedFragments.graphql b/generate/testdata/queries/ComplexNamedFragments.graphql new file mode 100644 index 00000000..e837fc6d --- /dev/null +++ b/generate/testdata/queries/ComplexNamedFragments.graphql @@ -0,0 +1,39 @@ +fragment QueryFragment on Query { + ...InnerQueryFragment +} + +fragment InnerQueryFragment on Query { + randomItem { + id name + ...VideoFields + } + randomLeaf { + ...VideoFields + ...MoreVideoFields + } + otherLeaf: randomLeaf { + ... on Video { + ...MoreVideoFields + } + } +} + +fragment VideoFields on Video { + id name url duration thumbnail { id } +} + +# @genqlient(pointer: true) +fragment MoreVideoFields on Video { + id + parent { + name url + # @genqlient(pointer: false) + children { + ...VideoFields + } + } +} + +query ComplexNamedFragments { + ... on Query { ...QueryFragment } +} diff --git a/generate/testdata/queries/SimpleNamedFragment.graphql b/generate/testdata/queries/SimpleNamedFragment.graphql new file mode 100644 index 00000000..01d3370d --- /dev/null +++ b/generate/testdata/queries/SimpleNamedFragment.graphql @@ -0,0 +1,13 @@ +fragment VideoFields on Video { + id name url duration thumbnail { id } +} + +query SimpleNamedFragment { + randomItem { + id name + ...VideoFields + } + randomLeaf { + ...VideoFields + } +} diff --git a/generate/testdata/snapshots/TestGenerate-ComplexInlineFragments.graphql-ComplexInlineFragments.graphql.go b/generate/testdata/snapshots/TestGenerate-ComplexInlineFragments.graphql-ComplexInlineFragments.graphql.go index 6aa166bd..f2813500 100644 --- a/generate/testdata/snapshots/TestGenerate-ComplexInlineFragments.graphql-ComplexInlineFragments.graphql.go +++ b/generate/testdata/snapshots/TestGenerate-ComplexInlineFragments.graphql-ComplexInlineFragments.graphql.go @@ -184,13 +184,12 @@ type ComplexInlineFragmentsNestedStuffTopic struct { func (v *ComplexInlineFragmentsNestedStuffTopic) UnmarshalJSON(b []byte) error { - type ComplexInlineFragmentsNestedStuffTopicWrapper ComplexInlineFragmentsNestedStuffTopic - var firstPass struct { - *ComplexInlineFragmentsNestedStuffTopicWrapper + *ComplexInlineFragmentsNestedStuffTopic Children []json.RawMessage `json:"children"` + graphql.NoUnmarshalJSON } - firstPass.ComplexInlineFragmentsNestedStuffTopicWrapper = (*ComplexInlineFragmentsNestedStuffTopicWrapper)(v) + firstPass.ComplexInlineFragmentsNestedStuffTopic = v err := json.Unmarshal(b, &firstPass) if err != nil { @@ -213,6 +212,7 @@ func (v *ComplexInlineFragmentsNestedStuffTopic) UnmarshalJSON(b []byte) error { } } } + return nil } @@ -232,13 +232,12 @@ type ComplexInlineFragmentsNestedStuffTopicChildrenArticleParentContentParentTop func (v *ComplexInlineFragmentsNestedStuffTopicChildrenArticleParentContentParentTopic) UnmarshalJSON(b []byte) error { - type ComplexInlineFragmentsNestedStuffTopicChildrenArticleParentContentParentTopicWrapper ComplexInlineFragmentsNestedStuffTopicChildrenArticleParentContentParentTopic - var firstPass struct { - *ComplexInlineFragmentsNestedStuffTopicChildrenArticleParentContentParentTopicWrapper + *ComplexInlineFragmentsNestedStuffTopicChildrenArticleParentContentParentTopic Children []json.RawMessage `json:"children"` + graphql.NoUnmarshalJSON } - firstPass.ComplexInlineFragmentsNestedStuffTopicChildrenArticleParentContentParentTopicWrapper = (*ComplexInlineFragmentsNestedStuffTopicChildrenArticleParentContentParentTopicWrapper)(v) + firstPass.ComplexInlineFragmentsNestedStuffTopicChildrenArticleParentContentParentTopic = v err := json.Unmarshal(b, &firstPass) if err != nil { @@ -261,6 +260,7 @@ func (v *ComplexInlineFragmentsNestedStuffTopicChildrenArticleParentContentParen } } } + return nil } @@ -797,16 +797,15 @@ type ComplexInlineFragmentsResponse struct { func (v *ComplexInlineFragmentsResponse) UnmarshalJSON(b []byte) error { - type ComplexInlineFragmentsResponseWrapper ComplexInlineFragmentsResponse - var firstPass struct { - *ComplexInlineFragmentsResponseWrapper + *ComplexInlineFragmentsResponse RandomItem json.RawMessage `json:"randomItem"` RepeatedStuff json.RawMessage `json:"repeatedStuff"` ConflictingStuff json.RawMessage `json:"conflictingStuff"` NestedStuff json.RawMessage `json:"nestedStuff"` + graphql.NoUnmarshalJSON } - firstPass.ComplexInlineFragmentsResponseWrapper = (*ComplexInlineFragmentsResponseWrapper)(v) + firstPass.ComplexInlineFragmentsResponse = v err := json.Unmarshal(b, &firstPass) if err != nil { @@ -823,6 +822,7 @@ func (v *ComplexInlineFragmentsResponse) UnmarshalJSON(b []byte) error { "Unable to unmarshal ComplexInlineFragmentsResponse.RandomItem: %w", err) } } + { target := &v.RepeatedStuff raw := firstPass.RepeatedStuff @@ -833,6 +833,7 @@ func (v *ComplexInlineFragmentsResponse) UnmarshalJSON(b []byte) error { "Unable to unmarshal ComplexInlineFragmentsResponse.RepeatedStuff: %w", err) } } + { target := &v.ConflictingStuff raw := firstPass.ConflictingStuff @@ -843,6 +844,7 @@ func (v *ComplexInlineFragmentsResponse) UnmarshalJSON(b []byte) error { "Unable to unmarshal ComplexInlineFragmentsResponse.ConflictingStuff: %w", err) } } + { target := &v.NestedStuff raw := firstPass.NestedStuff @@ -853,6 +855,7 @@ func (v *ComplexInlineFragmentsResponse) UnmarshalJSON(b []byte) error { "Unable to unmarshal ComplexInlineFragmentsResponse.NestedStuff: %w", err) } } + return nil } diff --git a/generate/testdata/snapshots/TestGenerate-ComplexNamedFragments.graphql-ComplexNamedFragments.graphql.go b/generate/testdata/snapshots/TestGenerate-ComplexNamedFragments.graphql-ComplexNamedFragments.graphql.go new file mode 100644 index 00000000..1a2e8cc6 --- /dev/null +++ b/generate/testdata/snapshots/TestGenerate-ComplexNamedFragments.graphql-ComplexNamedFragments.graphql.go @@ -0,0 +1,664 @@ +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" +) + +// ComplexNamedFragmentsResponse is returned by ComplexNamedFragments on success. +type ComplexNamedFragmentsResponse struct { + QueryFragment `json:"-"` +} + +func (v *ComplexNamedFragmentsResponse) UnmarshalJSON(b []byte) error { + + var firstPass struct { + *ComplexNamedFragmentsResponse + graphql.NoUnmarshalJSON + } + firstPass.ComplexNamedFragmentsResponse = v + + err := json.Unmarshal(b, &firstPass) + if err != nil { + return err + } + + err = json.Unmarshal(b, &v.QueryFragment) + if err != nil { + return err + } + return nil +} + +// InnerQueryFragment includes the GraphQL fields of Query requested by the fragment InnerQueryFragment. +type InnerQueryFragment struct { + RandomItem InnerQueryFragmentRandomItemContent `json:"-"` + RandomLeaf InnerQueryFragmentRandomLeafLeafContent `json:"-"` + OtherLeaf InnerQueryFragmentOtherLeafLeafContent `json:"-"` +} + +func (v *InnerQueryFragment) UnmarshalJSON(b []byte) error { + + var firstPass struct { + *InnerQueryFragment + RandomItem json.RawMessage `json:"randomItem"` + RandomLeaf json.RawMessage `json:"randomLeaf"` + OtherLeaf json.RawMessage `json:"otherLeaf"` + graphql.NoUnmarshalJSON + } + firstPass.InnerQueryFragment = v + + err := json.Unmarshal(b, &firstPass) + if err != nil { + return err + } + + { + target := &v.RandomItem + raw := firstPass.RandomItem + err = __unmarshalInnerQueryFragmentRandomItemContent( + target, raw) + if err != nil { + return fmt.Errorf( + "Unable to unmarshal InnerQueryFragment.RandomItem: %w", err) + } + } + + { + target := &v.RandomLeaf + raw := firstPass.RandomLeaf + err = __unmarshalInnerQueryFragmentRandomLeafLeafContent( + target, raw) + if err != nil { + return fmt.Errorf( + "Unable to unmarshal InnerQueryFragment.RandomLeaf: %w", err) + } + } + + { + target := &v.OtherLeaf + raw := firstPass.OtherLeaf + err = __unmarshalInnerQueryFragmentOtherLeafLeafContent( + target, raw) + if err != nil { + return fmt.Errorf( + "Unable to unmarshal InnerQueryFragment.OtherLeaf: %w", err) + } + } + + return nil +} + +// InnerQueryFragmentOtherLeafArticle includes the requested fields of the GraphQL type Article. +type InnerQueryFragmentOtherLeafArticle struct { + Typename string `json:"__typename"` +} + +// InnerQueryFragmentOtherLeafLeafContent includes the requested fields of the GraphQL interface LeafContent. +// +// InnerQueryFragmentOtherLeafLeafContent is implemented by the following types: +// InnerQueryFragmentOtherLeafArticle +// InnerQueryFragmentOtherLeafVideo +// +// The GraphQL type's documentation follows. +// +// LeafContent represents content items that can't have child-nodes. +type InnerQueryFragmentOtherLeafLeafContent interface { + implementsGraphQLInterfaceInnerQueryFragmentOtherLeafLeafContent() + // GetTypename returns the receiver's concrete GraphQL type-name (see interface doc for possible values). + GetTypename() string +} + +func (v *InnerQueryFragmentOtherLeafArticle) implementsGraphQLInterfaceInnerQueryFragmentOtherLeafLeafContent() { +} + +// GetTypename is a part of, and documented with, the interface InnerQueryFragmentOtherLeafLeafContent. +func (v *InnerQueryFragmentOtherLeafArticle) GetTypename() string { return v.Typename } + +func (v *InnerQueryFragmentOtherLeafVideo) implementsGraphQLInterfaceInnerQueryFragmentOtherLeafLeafContent() { +} + +// GetTypename is a part of, and documented with, the interface InnerQueryFragmentOtherLeafLeafContent. +func (v *InnerQueryFragmentOtherLeafVideo) GetTypename() string { return v.Typename } + +func __unmarshalInnerQueryFragmentOtherLeafLeafContent(v *InnerQueryFragmentOtherLeafLeafContent, m json.RawMessage) error { + if string(m) == "null" { + return nil + } + + var tn struct { + TypeName string `json:"__typename"` + } + err := json.Unmarshal(m, &tn) + if err != nil { + return err + } + + switch tn.TypeName { + case "Article": + *v = new(InnerQueryFragmentOtherLeafArticle) + return json.Unmarshal(m, *v) + case "Video": + *v = new(InnerQueryFragmentOtherLeafVideo) + return json.Unmarshal(m, *v) + case "": + return fmt.Errorf( + "Response was missing LeafContent.__typename") + default: + return fmt.Errorf( + `Unexpected concrete type for InnerQueryFragmentOtherLeafLeafContent: "%v"`, tn.TypeName) + } +} + +// InnerQueryFragmentOtherLeafVideo includes the requested fields of the GraphQL type Video. +type InnerQueryFragmentOtherLeafVideo struct { + Typename string `json:"__typename"` + MoreVideoFields `json:"-"` +} + +func (v *InnerQueryFragmentOtherLeafVideo) UnmarshalJSON(b []byte) error { + + var firstPass struct { + *InnerQueryFragmentOtherLeafVideo + graphql.NoUnmarshalJSON + } + firstPass.InnerQueryFragmentOtherLeafVideo = v + + err := json.Unmarshal(b, &firstPass) + if err != nil { + return err + } + + err = json.Unmarshal(b, &v.MoreVideoFields) + if err != nil { + return err + } + return nil +} + +// InnerQueryFragmentRandomItemArticle includes the requested fields of the GraphQL type Article. +type InnerQueryFragmentRandomItemArticle struct { + Typename string `json:"__typename"` + // ID is the identifier of the content. + Id testutil.ID `json:"id"` + Name string `json:"name"` +} + +// InnerQueryFragmentRandomItemContent includes the requested fields of the GraphQL interface Content. +// +// InnerQueryFragmentRandomItemContent is implemented by the following types: +// InnerQueryFragmentRandomItemArticle +// InnerQueryFragmentRandomItemVideo +// InnerQueryFragmentRandomItemTopic +// +// The GraphQL type's documentation follows. +// +// Content is implemented by various types like Article, Video, and Topic. +type InnerQueryFragmentRandomItemContent interface { + implementsGraphQLInterfaceInnerQueryFragmentRandomItemContent() + // 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. + // The GraphQL interface field's documentation follows. + // + // ID is the identifier of the content. + GetId() testutil.ID + // GetName returns the interface-field "name" from its implementation. + GetName() string +} + +func (v *InnerQueryFragmentRandomItemArticle) implementsGraphQLInterfaceInnerQueryFragmentRandomItemContent() { +} + +// GetTypename is a part of, and documented with, the interface InnerQueryFragmentRandomItemContent. +func (v *InnerQueryFragmentRandomItemArticle) GetTypename() string { return v.Typename } + +// GetId is a part of, and documented with, the interface InnerQueryFragmentRandomItemContent. +func (v *InnerQueryFragmentRandomItemArticle) GetId() testutil.ID { return v.Id } + +// GetName is a part of, and documented with, the interface InnerQueryFragmentRandomItemContent. +func (v *InnerQueryFragmentRandomItemArticle) GetName() string { return v.Name } + +func (v *InnerQueryFragmentRandomItemVideo) implementsGraphQLInterfaceInnerQueryFragmentRandomItemContent() { +} + +// GetTypename is a part of, and documented with, the interface InnerQueryFragmentRandomItemContent. +func (v *InnerQueryFragmentRandomItemVideo) GetTypename() string { return v.Typename } + +// GetId is a part of, and documented with, the interface InnerQueryFragmentRandomItemContent. +func (v *InnerQueryFragmentRandomItemVideo) GetId() testutil.ID { return v.Id } + +// GetName is a part of, and documented with, the interface InnerQueryFragmentRandomItemContent. +func (v *InnerQueryFragmentRandomItemVideo) GetName() string { return v.Name } + +func (v *InnerQueryFragmentRandomItemTopic) implementsGraphQLInterfaceInnerQueryFragmentRandomItemContent() { +} + +// GetTypename is a part of, and documented with, the interface InnerQueryFragmentRandomItemContent. +func (v *InnerQueryFragmentRandomItemTopic) GetTypename() string { return v.Typename } + +// GetId is a part of, and documented with, the interface InnerQueryFragmentRandomItemContent. +func (v *InnerQueryFragmentRandomItemTopic) GetId() testutil.ID { return v.Id } + +// GetName is a part of, and documented with, the interface InnerQueryFragmentRandomItemContent. +func (v *InnerQueryFragmentRandomItemTopic) GetName() string { return v.Name } + +func __unmarshalInnerQueryFragmentRandomItemContent(v *InnerQueryFragmentRandomItemContent, m json.RawMessage) error { + if string(m) == "null" { + return nil + } + + var tn struct { + TypeName string `json:"__typename"` + } + err := json.Unmarshal(m, &tn) + if err != nil { + return err + } + + switch tn.TypeName { + case "Article": + *v = new(InnerQueryFragmentRandomItemArticle) + return json.Unmarshal(m, *v) + case "Video": + *v = new(InnerQueryFragmentRandomItemVideo) + return json.Unmarshal(m, *v) + case "Topic": + *v = new(InnerQueryFragmentRandomItemTopic) + return json.Unmarshal(m, *v) + case "": + return fmt.Errorf( + "Response was missing Content.__typename") + default: + return fmt.Errorf( + `Unexpected concrete type for InnerQueryFragmentRandomItemContent: "%v"`, tn.TypeName) + } +} + +// InnerQueryFragmentRandomItemTopic includes the requested fields of the GraphQL type Topic. +type InnerQueryFragmentRandomItemTopic struct { + Typename string `json:"__typename"` + // ID is the identifier of the content. + Id testutil.ID `json:"id"` + Name string `json:"name"` +} + +// InnerQueryFragmentRandomItemVideo includes the requested fields of the GraphQL type Video. +type InnerQueryFragmentRandomItemVideo struct { + Typename string `json:"__typename"` + // ID is the identifier of the content. + Id testutil.ID `json:"id"` + Name string `json:"name"` + VideoFields `json:"-"` +} + +func (v *InnerQueryFragmentRandomItemVideo) UnmarshalJSON(b []byte) error { + + var firstPass struct { + *InnerQueryFragmentRandomItemVideo + graphql.NoUnmarshalJSON + } + firstPass.InnerQueryFragmentRandomItemVideo = v + + err := json.Unmarshal(b, &firstPass) + if err != nil { + return err + } + + err = json.Unmarshal(b, &v.VideoFields) + if err != nil { + return err + } + return nil +} + +// InnerQueryFragmentRandomLeafArticle includes the requested fields of the GraphQL type Article. +type InnerQueryFragmentRandomLeafArticle struct { + Typename string `json:"__typename"` +} + +// InnerQueryFragmentRandomLeafLeafContent includes the requested fields of the GraphQL interface LeafContent. +// +// InnerQueryFragmentRandomLeafLeafContent is implemented by the following types: +// InnerQueryFragmentRandomLeafArticle +// InnerQueryFragmentRandomLeafVideo +// +// The GraphQL type's documentation follows. +// +// LeafContent represents content items that can't have child-nodes. +type InnerQueryFragmentRandomLeafLeafContent interface { + implementsGraphQLInterfaceInnerQueryFragmentRandomLeafLeafContent() + // GetTypename returns the receiver's concrete GraphQL type-name (see interface doc for possible values). + GetTypename() string +} + +func (v *InnerQueryFragmentRandomLeafArticle) implementsGraphQLInterfaceInnerQueryFragmentRandomLeafLeafContent() { +} + +// GetTypename is a part of, and documented with, the interface InnerQueryFragmentRandomLeafLeafContent. +func (v *InnerQueryFragmentRandomLeafArticle) GetTypename() string { return v.Typename } + +func (v *InnerQueryFragmentRandomLeafVideo) implementsGraphQLInterfaceInnerQueryFragmentRandomLeafLeafContent() { +} + +// GetTypename is a part of, and documented with, the interface InnerQueryFragmentRandomLeafLeafContent. +func (v *InnerQueryFragmentRandomLeafVideo) GetTypename() string { return v.Typename } + +func __unmarshalInnerQueryFragmentRandomLeafLeafContent(v *InnerQueryFragmentRandomLeafLeafContent, m json.RawMessage) error { + if string(m) == "null" { + return nil + } + + var tn struct { + TypeName string `json:"__typename"` + } + err := json.Unmarshal(m, &tn) + if err != nil { + return err + } + + switch tn.TypeName { + case "Article": + *v = new(InnerQueryFragmentRandomLeafArticle) + return json.Unmarshal(m, *v) + case "Video": + *v = new(InnerQueryFragmentRandomLeafVideo) + return json.Unmarshal(m, *v) + case "": + return fmt.Errorf( + "Response was missing LeafContent.__typename") + default: + return fmt.Errorf( + `Unexpected concrete type for InnerQueryFragmentRandomLeafLeafContent: "%v"`, tn.TypeName) + } +} + +// InnerQueryFragmentRandomLeafVideo includes the requested fields of the GraphQL type Video. +type InnerQueryFragmentRandomLeafVideo struct { + Typename string `json:"__typename"` + VideoFields `json:"-"` + MoreVideoFields `json:"-"` +} + +func (v *InnerQueryFragmentRandomLeafVideo) UnmarshalJSON(b []byte) error { + + var firstPass struct { + *InnerQueryFragmentRandomLeafVideo + graphql.NoUnmarshalJSON + } + firstPass.InnerQueryFragmentRandomLeafVideo = v + + err := json.Unmarshal(b, &firstPass) + if err != nil { + return err + } + + err = json.Unmarshal(b, &v.VideoFields) + if err != nil { + return err + } + + err = json.Unmarshal(b, &v.MoreVideoFields) + if err != nil { + return err + } + return nil +} + +// MoreVideoFields includes the GraphQL fields of Video requested by the fragment MoreVideoFields. +type MoreVideoFields struct { + // ID is documented in the Content interface. + Id *testutil.ID `json:"id"` + Parent *MoreVideoFieldsParentTopic `json:"parent"` +} + +// MoreVideoFieldsParentTopic includes the requested fields of the GraphQL type Topic. +type MoreVideoFieldsParentTopic struct { + Name *string `json:"name"` + Url *string `json:"url"` + Children []MoreVideoFieldsParentTopicChildrenContent `json:"-"` +} + +func (v *MoreVideoFieldsParentTopic) UnmarshalJSON(b []byte) error { + + var firstPass struct { + *MoreVideoFieldsParentTopic + Children []json.RawMessage `json:"children"` + graphql.NoUnmarshalJSON + } + firstPass.MoreVideoFieldsParentTopic = v + + err := json.Unmarshal(b, &firstPass) + if err != nil { + return err + } + + { + target := &v.Children + raw := firstPass.Children + *target = make( + []MoreVideoFieldsParentTopicChildrenContent, + len(raw)) + for i, raw := range raw { + target := &(*target)[i] + err = __unmarshalMoreVideoFieldsParentTopicChildrenContent( + target, raw) + if err != nil { + return fmt.Errorf( + "Unable to unmarshal MoreVideoFieldsParentTopic.Children: %w", err) + } + } + } + + return nil +} + +// MoreVideoFieldsParentTopicChildrenArticle includes the requested fields of the GraphQL type Article. +type MoreVideoFieldsParentTopicChildrenArticle struct { + Typename *string `json:"__typename"` +} + +// MoreVideoFieldsParentTopicChildrenContent includes the requested fields of the GraphQL interface Content. +// +// MoreVideoFieldsParentTopicChildrenContent is implemented by the following types: +// MoreVideoFieldsParentTopicChildrenArticle +// MoreVideoFieldsParentTopicChildrenVideo +// MoreVideoFieldsParentTopicChildrenTopic +// +// The GraphQL type's documentation follows. +// +// Content is implemented by various types like Article, Video, and Topic. +type MoreVideoFieldsParentTopicChildrenContent interface { + implementsGraphQLInterfaceMoreVideoFieldsParentTopicChildrenContent() + // GetTypename returns the receiver's concrete GraphQL type-name (see interface doc for possible values). + GetTypename() *string +} + +func (v *MoreVideoFieldsParentTopicChildrenArticle) implementsGraphQLInterfaceMoreVideoFieldsParentTopicChildrenContent() { +} + +// GetTypename is a part of, and documented with, the interface MoreVideoFieldsParentTopicChildrenContent. +func (v *MoreVideoFieldsParentTopicChildrenArticle) GetTypename() *string { return v.Typename } + +func (v *MoreVideoFieldsParentTopicChildrenVideo) implementsGraphQLInterfaceMoreVideoFieldsParentTopicChildrenContent() { +} + +// GetTypename is a part of, and documented with, the interface MoreVideoFieldsParentTopicChildrenContent. +func (v *MoreVideoFieldsParentTopicChildrenVideo) GetTypename() *string { return v.Typename } + +func (v *MoreVideoFieldsParentTopicChildrenTopic) implementsGraphQLInterfaceMoreVideoFieldsParentTopicChildrenContent() { +} + +// GetTypename is a part of, and documented with, the interface MoreVideoFieldsParentTopicChildrenContent. +func (v *MoreVideoFieldsParentTopicChildrenTopic) GetTypename() *string { return v.Typename } + +func __unmarshalMoreVideoFieldsParentTopicChildrenContent(v *MoreVideoFieldsParentTopicChildrenContent, m json.RawMessage) error { + if string(m) == "null" { + return nil + } + + var tn struct { + TypeName string `json:"__typename"` + } + err := json.Unmarshal(m, &tn) + if err != nil { + return err + } + + switch tn.TypeName { + case "Article": + *v = new(MoreVideoFieldsParentTopicChildrenArticle) + return json.Unmarshal(m, *v) + case "Video": + *v = new(MoreVideoFieldsParentTopicChildrenVideo) + return json.Unmarshal(m, *v) + case "Topic": + *v = new(MoreVideoFieldsParentTopicChildrenTopic) + return json.Unmarshal(m, *v) + case "": + return fmt.Errorf( + "Response was missing Content.__typename") + default: + return fmt.Errorf( + `Unexpected concrete type for MoreVideoFieldsParentTopicChildrenContent: "%v"`, tn.TypeName) + } +} + +// MoreVideoFieldsParentTopicChildrenTopic includes the requested fields of the GraphQL type Topic. +type MoreVideoFieldsParentTopicChildrenTopic struct { + Typename *string `json:"__typename"` +} + +// MoreVideoFieldsParentTopicChildrenVideo includes the requested fields of the GraphQL type Video. +type MoreVideoFieldsParentTopicChildrenVideo struct { + Typename *string `json:"__typename"` + VideoFields `json:"-"` +} + +func (v *MoreVideoFieldsParentTopicChildrenVideo) UnmarshalJSON(b []byte) error { + + var firstPass struct { + *MoreVideoFieldsParentTopicChildrenVideo + graphql.NoUnmarshalJSON + } + firstPass.MoreVideoFieldsParentTopicChildrenVideo = v + + err := json.Unmarshal(b, &firstPass) + if err != nil { + return err + } + + err = json.Unmarshal(b, &v.VideoFields) + if err != nil { + return err + } + return nil +} + +// QueryFragment includes the GraphQL fields of Query requested by the fragment QueryFragment. +type QueryFragment struct { + InnerQueryFragment `json:"-"` +} + +func (v *QueryFragment) UnmarshalJSON(b []byte) error { + + var firstPass struct { + *QueryFragment + graphql.NoUnmarshalJSON + } + firstPass.QueryFragment = v + + err := json.Unmarshal(b, &firstPass) + if err != nil { + return err + } + + err = json.Unmarshal(b, &v.InnerQueryFragment) + if err != nil { + return err + } + return 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"` + Name string `json:"name"` + Url string `json:"url"` + Duration int `json:"duration"` + Thumbnail VideoFieldsThumbnail `json:"thumbnail"` +} + +// VideoFieldsThumbnail includes the requested fields of the GraphQL type Thumbnail. +type VideoFieldsThumbnail struct { + Id testutil.ID `json:"id"` +} + +func ComplexNamedFragments( + client graphql.Client, +) (*ComplexNamedFragmentsResponse, error) { + var retval ComplexNamedFragmentsResponse + err := client.MakeRequest( + nil, + "ComplexNamedFragments", + ` +query ComplexNamedFragments { + ... on Query { + ... QueryFragment + } +} +fragment QueryFragment on Query { + ... InnerQueryFragment +} +fragment InnerQueryFragment on Query { + randomItem { + __typename + id + name + ... VideoFields + } + randomLeaf { + __typename + ... VideoFields + ... MoreVideoFields + } + otherLeaf: randomLeaf { + __typename + ... on Video { + ... MoreVideoFields + } + } +} +fragment VideoFields on Video { + id + name + url + duration + thumbnail { + id + } +} +fragment MoreVideoFields on Video { + id + parent { + name + url + children { + __typename + ... VideoFields + } + } +} +`, + &retval, + nil, + ) + return &retval, err +} + diff --git a/generate/testdata/snapshots/TestGenerate-ComplexNamedFragments.graphql-ComplexNamedFragments.graphql.json b/generate/testdata/snapshots/TestGenerate-ComplexNamedFragments.graphql-ComplexNamedFragments.graphql.json new file mode 100644 index 00000000..369d66ad --- /dev/null +++ b/generate/testdata/snapshots/TestGenerate-ComplexNamedFragments.graphql-ComplexNamedFragments.graphql.json @@ -0,0 +1,9 @@ +{ + "operations": [ + { + "operationName": "ComplexNamedFragments", + "query": "\nquery ComplexNamedFragments {\n\t... on Query {\n\t\t... QueryFragment\n\t}\n}\nfragment QueryFragment on Query {\n\t... InnerQueryFragment\n}\nfragment InnerQueryFragment on Query {\n\trandomItem {\n\t\t__typename\n\t\tid\n\t\tname\n\t\t... VideoFields\n\t}\n\trandomLeaf {\n\t\t__typename\n\t\t... VideoFields\n\t\t... MoreVideoFields\n\t}\n\totherLeaf: randomLeaf {\n\t\t__typename\n\t\t... on Video {\n\t\t\t... MoreVideoFields\n\t\t}\n\t}\n}\nfragment VideoFields on Video {\n\tid\n\tname\n\turl\n\tduration\n\tthumbnail {\n\t\tid\n\t}\n}\nfragment MoreVideoFields on Video {\n\tid\n\tparent {\n\t\tname\n\t\turl\n\t\tchildren {\n\t\t\t__typename\n\t\t\t... VideoFields\n\t\t}\n\t}\n}\n", + "sourceLocation": "testdata/queries/ComplexNamedFragments.graphql" + } + ] +} diff --git a/generate/testdata/snapshots/TestGenerate-InterfaceListField.graphql-InterfaceListField.graphql.go b/generate/testdata/snapshots/TestGenerate-InterfaceListField.graphql-InterfaceListField.graphql.go index c5619bd8..096a3440 100644 --- a/generate/testdata/snapshots/TestGenerate-InterfaceListField.graphql-InterfaceListField.graphql.go +++ b/generate/testdata/snapshots/TestGenerate-InterfaceListField.graphql-InterfaceListField.graphql.go @@ -26,13 +26,12 @@ type InterfaceListFieldRootTopic struct { func (v *InterfaceListFieldRootTopic) UnmarshalJSON(b []byte) error { - type InterfaceListFieldRootTopicWrapper InterfaceListFieldRootTopic - var firstPass struct { - *InterfaceListFieldRootTopicWrapper + *InterfaceListFieldRootTopic Children []json.RawMessage `json:"children"` + graphql.NoUnmarshalJSON } - firstPass.InterfaceListFieldRootTopicWrapper = (*InterfaceListFieldRootTopicWrapper)(v) + firstPass.InterfaceListFieldRootTopic = v err := json.Unmarshal(b, &firstPass) if err != nil { @@ -55,6 +54,7 @@ func (v *InterfaceListFieldRootTopic) UnmarshalJSON(b []byte) error { } } } + return nil } @@ -183,13 +183,12 @@ type InterfaceListFieldWithPointerTopic struct { func (v *InterfaceListFieldWithPointerTopic) UnmarshalJSON(b []byte) error { - type InterfaceListFieldWithPointerTopicWrapper InterfaceListFieldWithPointerTopic - var firstPass struct { - *InterfaceListFieldWithPointerTopicWrapper + *InterfaceListFieldWithPointerTopic Children []json.RawMessage `json:"children"` + graphql.NoUnmarshalJSON } - firstPass.InterfaceListFieldWithPointerTopicWrapper = (*InterfaceListFieldWithPointerTopicWrapper)(v) + firstPass.InterfaceListFieldWithPointerTopic = v err := json.Unmarshal(b, &firstPass) if err != nil { @@ -212,6 +211,7 @@ func (v *InterfaceListFieldWithPointerTopic) UnmarshalJSON(b []byte) error { } } } + return nil } diff --git a/generate/testdata/snapshots/TestGenerate-InterfaceListOfListsOfListsField.graphql-InterfaceListOfListsOfListsField.graphql.go b/generate/testdata/snapshots/TestGenerate-InterfaceListOfListsOfListsField.graphql-InterfaceListOfListsOfListsField.graphql.go index ffcc2371..c9f931c2 100644 --- a/generate/testdata/snapshots/TestGenerate-InterfaceListOfListsOfListsField.graphql-InterfaceListOfListsOfListsField.graphql.go +++ b/generate/testdata/snapshots/TestGenerate-InterfaceListOfListsOfListsField.graphql-InterfaceListOfListsOfListsField.graphql.go @@ -151,14 +151,13 @@ type InterfaceListOfListOfListsFieldResponse struct { func (v *InterfaceListOfListOfListsFieldResponse) UnmarshalJSON(b []byte) error { - type InterfaceListOfListOfListsFieldResponseWrapper InterfaceListOfListOfListsFieldResponse - var firstPass struct { - *InterfaceListOfListOfListsFieldResponseWrapper + *InterfaceListOfListOfListsFieldResponse ListOfListsOfListsOfContent [][][]json.RawMessage `json:"listOfListsOfListsOfContent"` WithPointer [][][]json.RawMessage `json:"withPointer"` + graphql.NoUnmarshalJSON } - firstPass.InterfaceListOfListOfListsFieldResponseWrapper = (*InterfaceListOfListOfListsFieldResponseWrapper)(v) + firstPass.InterfaceListOfListOfListsFieldResponse = v err := json.Unmarshal(b, &firstPass) if err != nil { @@ -193,6 +192,7 @@ func (v *InterfaceListOfListOfListsFieldResponse) UnmarshalJSON(b []byte) error } } } + { target := &v.WithPointer raw := firstPass.WithPointer @@ -222,6 +222,7 @@ func (v *InterfaceListOfListOfListsFieldResponse) UnmarshalJSON(b []byte) error } } } + return nil } diff --git a/generate/testdata/snapshots/TestGenerate-InterfaceNesting.graphql-InterfaceNesting.graphql.go b/generate/testdata/snapshots/TestGenerate-InterfaceNesting.graphql-InterfaceNesting.graphql.go index e615f03e..c503dd09 100644 --- a/generate/testdata/snapshots/TestGenerate-InterfaceNesting.graphql-InterfaceNesting.graphql.go +++ b/generate/testdata/snapshots/TestGenerate-InterfaceNesting.graphql-InterfaceNesting.graphql.go @@ -24,13 +24,12 @@ type InterfaceNestingRootTopic struct { func (v *InterfaceNestingRootTopic) UnmarshalJSON(b []byte) error { - type InterfaceNestingRootTopicWrapper InterfaceNestingRootTopic - var firstPass struct { - *InterfaceNestingRootTopicWrapper + *InterfaceNestingRootTopic Children []json.RawMessage `json:"children"` + graphql.NoUnmarshalJSON } - firstPass.InterfaceNestingRootTopicWrapper = (*InterfaceNestingRootTopicWrapper)(v) + firstPass.InterfaceNestingRootTopic = v err := json.Unmarshal(b, &firstPass) if err != nil { @@ -53,6 +52,7 @@ func (v *InterfaceNestingRootTopic) UnmarshalJSON(b []byte) error { } } } + return nil } @@ -170,13 +170,12 @@ type InterfaceNestingRootTopicChildrenContentParentTopic struct { func (v *InterfaceNestingRootTopicChildrenContentParentTopic) UnmarshalJSON(b []byte) error { - type InterfaceNestingRootTopicChildrenContentParentTopicWrapper InterfaceNestingRootTopicChildrenContentParentTopic - var firstPass struct { - *InterfaceNestingRootTopicChildrenContentParentTopicWrapper + *InterfaceNestingRootTopicChildrenContentParentTopic Children []json.RawMessage `json:"children"` + graphql.NoUnmarshalJSON } - firstPass.InterfaceNestingRootTopicChildrenContentParentTopicWrapper = (*InterfaceNestingRootTopicChildrenContentParentTopicWrapper)(v) + firstPass.InterfaceNestingRootTopicChildrenContentParentTopic = v err := json.Unmarshal(b, &firstPass) if err != nil { @@ -199,6 +198,7 @@ func (v *InterfaceNestingRootTopicChildrenContentParentTopic) UnmarshalJSON(b [] } } } + return nil } diff --git a/generate/testdata/snapshots/TestGenerate-InterfaceNoFragments.graphql-InterfaceNoFragments.graphql.go b/generate/testdata/snapshots/TestGenerate-InterfaceNoFragments.graphql-InterfaceNoFragments.graphql.go index 10b0aa16..bd426879 100644 --- a/generate/testdata/snapshots/TestGenerate-InterfaceNoFragments.graphql-InterfaceNoFragments.graphql.go +++ b/generate/testdata/snapshots/TestGenerate-InterfaceNoFragments.graphql-InterfaceNoFragments.graphql.go @@ -256,15 +256,14 @@ type InterfaceNoFragmentsQueryResponse struct { func (v *InterfaceNoFragmentsQueryResponse) UnmarshalJSON(b []byte) error { - type InterfaceNoFragmentsQueryResponseWrapper InterfaceNoFragmentsQueryResponse - var firstPass struct { - *InterfaceNoFragmentsQueryResponseWrapper + *InterfaceNoFragmentsQueryResponse RandomItem json.RawMessage `json:"randomItem"` RandomItemWithTypeName json.RawMessage `json:"randomItemWithTypeName"` WithPointer json.RawMessage `json:"withPointer"` + graphql.NoUnmarshalJSON } - firstPass.InterfaceNoFragmentsQueryResponseWrapper = (*InterfaceNoFragmentsQueryResponseWrapper)(v) + firstPass.InterfaceNoFragmentsQueryResponse = v err := json.Unmarshal(b, &firstPass) if err != nil { @@ -281,6 +280,7 @@ func (v *InterfaceNoFragmentsQueryResponse) UnmarshalJSON(b []byte) error { "Unable to unmarshal InterfaceNoFragmentsQueryResponse.RandomItem: %w", err) } } + { target := &v.RandomItemWithTypeName raw := firstPass.RandomItemWithTypeName @@ -291,6 +291,7 @@ func (v *InterfaceNoFragmentsQueryResponse) UnmarshalJSON(b []byte) error { "Unable to unmarshal InterfaceNoFragmentsQueryResponse.RandomItemWithTypeName: %w", err) } } + { target := &v.WithPointer raw := firstPass.WithPointer @@ -302,6 +303,7 @@ func (v *InterfaceNoFragmentsQueryResponse) UnmarshalJSON(b []byte) error { "Unable to unmarshal InterfaceNoFragmentsQueryResponse.WithPointer: %w", err) } } + return nil } diff --git a/generate/testdata/snapshots/TestGenerate-SimpleInlineFragment.graphql-SimpleInlineFragment.graphql.go b/generate/testdata/snapshots/TestGenerate-SimpleInlineFragment.graphql-SimpleInlineFragment.graphql.go index f9a93995..f8891167 100644 --- a/generate/testdata/snapshots/TestGenerate-SimpleInlineFragment.graphql-SimpleInlineFragment.graphql.go +++ b/generate/testdata/snapshots/TestGenerate-SimpleInlineFragment.graphql-SimpleInlineFragment.graphql.go @@ -134,13 +134,12 @@ type SimpleInlineFragmentResponse struct { func (v *SimpleInlineFragmentResponse) UnmarshalJSON(b []byte) error { - type SimpleInlineFragmentResponseWrapper SimpleInlineFragmentResponse - var firstPass struct { - *SimpleInlineFragmentResponseWrapper + *SimpleInlineFragmentResponse RandomItem json.RawMessage `json:"randomItem"` + graphql.NoUnmarshalJSON } - firstPass.SimpleInlineFragmentResponseWrapper = (*SimpleInlineFragmentResponseWrapper)(v) + firstPass.SimpleInlineFragmentResponse = v err := json.Unmarshal(b, &firstPass) if err != nil { @@ -157,6 +156,7 @@ func (v *SimpleInlineFragmentResponse) UnmarshalJSON(b []byte) error { "Unable to unmarshal SimpleInlineFragmentResponse.RandomItem: %w", err) } } + return nil } diff --git a/generate/testdata/snapshots/TestGenerate-SimpleNamedFragment.graphql-SimpleNamedFragment.graphql.go b/generate/testdata/snapshots/TestGenerate-SimpleNamedFragment.graphql-SimpleNamedFragment.graphql.go new file mode 100644 index 00000000..ea362919 --- /dev/null +++ b/generate/testdata/snapshots/TestGenerate-SimpleNamedFragment.graphql-SimpleNamedFragment.graphql.go @@ -0,0 +1,332 @@ +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" +) + +// SimpleNamedFragmentRandomItemArticle includes the requested fields of the GraphQL type Article. +type SimpleNamedFragmentRandomItemArticle struct { + Typename string `json:"__typename"` + // ID is the identifier of the content. + Id testutil.ID `json:"id"` + Name string `json:"name"` +} + +// SimpleNamedFragmentRandomItemContent includes the requested fields of the GraphQL interface Content. +// +// SimpleNamedFragmentRandomItemContent is implemented by the following types: +// SimpleNamedFragmentRandomItemArticle +// SimpleNamedFragmentRandomItemVideo +// SimpleNamedFragmentRandomItemTopic +// +// The GraphQL type's documentation follows. +// +// Content is implemented by various types like Article, Video, and Topic. +type SimpleNamedFragmentRandomItemContent interface { + implementsGraphQLInterfaceSimpleNamedFragmentRandomItemContent() + // 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. + // The GraphQL interface field's documentation follows. + // + // ID is the identifier of the content. + GetId() testutil.ID + // GetName returns the interface-field "name" from its implementation. + GetName() string +} + +func (v *SimpleNamedFragmentRandomItemArticle) implementsGraphQLInterfaceSimpleNamedFragmentRandomItemContent() { +} + +// GetTypename is a part of, and documented with, the interface SimpleNamedFragmentRandomItemContent. +func (v *SimpleNamedFragmentRandomItemArticle) GetTypename() string { return v.Typename } + +// GetId is a part of, and documented with, the interface SimpleNamedFragmentRandomItemContent. +func (v *SimpleNamedFragmentRandomItemArticle) GetId() testutil.ID { return v.Id } + +// GetName is a part of, and documented with, the interface SimpleNamedFragmentRandomItemContent. +func (v *SimpleNamedFragmentRandomItemArticle) GetName() string { return v.Name } + +func (v *SimpleNamedFragmentRandomItemVideo) implementsGraphQLInterfaceSimpleNamedFragmentRandomItemContent() { +} + +// GetTypename is a part of, and documented with, the interface SimpleNamedFragmentRandomItemContent. +func (v *SimpleNamedFragmentRandomItemVideo) GetTypename() string { return v.Typename } + +// GetId is a part of, and documented with, the interface SimpleNamedFragmentRandomItemContent. +func (v *SimpleNamedFragmentRandomItemVideo) GetId() testutil.ID { return v.Id } + +// GetName is a part of, and documented with, the interface SimpleNamedFragmentRandomItemContent. +func (v *SimpleNamedFragmentRandomItemVideo) GetName() string { return v.Name } + +func (v *SimpleNamedFragmentRandomItemTopic) implementsGraphQLInterfaceSimpleNamedFragmentRandomItemContent() { +} + +// GetTypename is a part of, and documented with, the interface SimpleNamedFragmentRandomItemContent. +func (v *SimpleNamedFragmentRandomItemTopic) GetTypename() string { return v.Typename } + +// GetId is a part of, and documented with, the interface SimpleNamedFragmentRandomItemContent. +func (v *SimpleNamedFragmentRandomItemTopic) GetId() testutil.ID { return v.Id } + +// GetName is a part of, and documented with, the interface SimpleNamedFragmentRandomItemContent. +func (v *SimpleNamedFragmentRandomItemTopic) GetName() string { return v.Name } + +func __unmarshalSimpleNamedFragmentRandomItemContent(v *SimpleNamedFragmentRandomItemContent, m json.RawMessage) error { + if string(m) == "null" { + return nil + } + + var tn struct { + TypeName string `json:"__typename"` + } + err := json.Unmarshal(m, &tn) + if err != nil { + return err + } + + switch tn.TypeName { + case "Article": + *v = new(SimpleNamedFragmentRandomItemArticle) + return json.Unmarshal(m, *v) + case "Video": + *v = new(SimpleNamedFragmentRandomItemVideo) + return json.Unmarshal(m, *v) + case "Topic": + *v = new(SimpleNamedFragmentRandomItemTopic) + return json.Unmarshal(m, *v) + case "": + return fmt.Errorf( + "Response was missing Content.__typename") + default: + return fmt.Errorf( + `Unexpected concrete type for SimpleNamedFragmentRandomItemContent: "%v"`, tn.TypeName) + } +} + +// SimpleNamedFragmentRandomItemTopic includes the requested fields of the GraphQL type Topic. +type SimpleNamedFragmentRandomItemTopic struct { + Typename string `json:"__typename"` + // ID is the identifier of the content. + Id testutil.ID `json:"id"` + Name string `json:"name"` +} + +// SimpleNamedFragmentRandomItemVideo includes the requested fields of the GraphQL type Video. +type SimpleNamedFragmentRandomItemVideo struct { + Typename string `json:"__typename"` + // ID is the identifier of the content. + Id testutil.ID `json:"id"` + Name string `json:"name"` + VideoFields `json:"-"` +} + +func (v *SimpleNamedFragmentRandomItemVideo) UnmarshalJSON(b []byte) error { + + var firstPass struct { + *SimpleNamedFragmentRandomItemVideo + graphql.NoUnmarshalJSON + } + firstPass.SimpleNamedFragmentRandomItemVideo = v + + err := json.Unmarshal(b, &firstPass) + if err != nil { + return err + } + + err = json.Unmarshal(b, &v.VideoFields) + if err != nil { + return err + } + return nil +} + +// SimpleNamedFragmentRandomLeafArticle includes the requested fields of the GraphQL type Article. +type SimpleNamedFragmentRandomLeafArticle struct { + Typename string `json:"__typename"` +} + +// SimpleNamedFragmentRandomLeafLeafContent includes the requested fields of the GraphQL interface LeafContent. +// +// SimpleNamedFragmentRandomLeafLeafContent is implemented by the following types: +// SimpleNamedFragmentRandomLeafArticle +// SimpleNamedFragmentRandomLeafVideo +// +// The GraphQL type's documentation follows. +// +// LeafContent represents content items that can't have child-nodes. +type SimpleNamedFragmentRandomLeafLeafContent interface { + implementsGraphQLInterfaceSimpleNamedFragmentRandomLeafLeafContent() + // GetTypename returns the receiver's concrete GraphQL type-name (see interface doc for possible values). + GetTypename() string +} + +func (v *SimpleNamedFragmentRandomLeafArticle) implementsGraphQLInterfaceSimpleNamedFragmentRandomLeafLeafContent() { +} + +// GetTypename is a part of, and documented with, the interface SimpleNamedFragmentRandomLeafLeafContent. +func (v *SimpleNamedFragmentRandomLeafArticle) GetTypename() string { return v.Typename } + +func (v *SimpleNamedFragmentRandomLeafVideo) implementsGraphQLInterfaceSimpleNamedFragmentRandomLeafLeafContent() { +} + +// GetTypename is a part of, and documented with, the interface SimpleNamedFragmentRandomLeafLeafContent. +func (v *SimpleNamedFragmentRandomLeafVideo) GetTypename() string { return v.Typename } + +func __unmarshalSimpleNamedFragmentRandomLeafLeafContent(v *SimpleNamedFragmentRandomLeafLeafContent, m json.RawMessage) error { + if string(m) == "null" { + return nil + } + + var tn struct { + TypeName string `json:"__typename"` + } + err := json.Unmarshal(m, &tn) + if err != nil { + return err + } + + switch tn.TypeName { + case "Article": + *v = new(SimpleNamedFragmentRandomLeafArticle) + return json.Unmarshal(m, *v) + case "Video": + *v = new(SimpleNamedFragmentRandomLeafVideo) + return json.Unmarshal(m, *v) + case "": + return fmt.Errorf( + "Response was missing LeafContent.__typename") + default: + return fmt.Errorf( + `Unexpected concrete type for SimpleNamedFragmentRandomLeafLeafContent: "%v"`, tn.TypeName) + } +} + +// SimpleNamedFragmentRandomLeafVideo includes the requested fields of the GraphQL type Video. +type SimpleNamedFragmentRandomLeafVideo struct { + Typename string `json:"__typename"` + VideoFields `json:"-"` +} + +func (v *SimpleNamedFragmentRandomLeafVideo) UnmarshalJSON(b []byte) error { + + var firstPass struct { + *SimpleNamedFragmentRandomLeafVideo + graphql.NoUnmarshalJSON + } + firstPass.SimpleNamedFragmentRandomLeafVideo = v + + err := json.Unmarshal(b, &firstPass) + if err != nil { + return err + } + + err = json.Unmarshal(b, &v.VideoFields) + if err != nil { + return err + } + return nil +} + +// SimpleNamedFragmentResponse is returned by SimpleNamedFragment on success. +type SimpleNamedFragmentResponse struct { + RandomItem SimpleNamedFragmentRandomItemContent `json:"-"` + RandomLeaf SimpleNamedFragmentRandomLeafLeafContent `json:"-"` +} + +func (v *SimpleNamedFragmentResponse) UnmarshalJSON(b []byte) error { + + var firstPass struct { + *SimpleNamedFragmentResponse + RandomItem json.RawMessage `json:"randomItem"` + RandomLeaf json.RawMessage `json:"randomLeaf"` + graphql.NoUnmarshalJSON + } + firstPass.SimpleNamedFragmentResponse = v + + err := json.Unmarshal(b, &firstPass) + if err != nil { + return err + } + + { + target := &v.RandomItem + raw := firstPass.RandomItem + err = __unmarshalSimpleNamedFragmentRandomItemContent( + target, raw) + if err != nil { + return fmt.Errorf( + "Unable to unmarshal SimpleNamedFragmentResponse.RandomItem: %w", err) + } + } + + { + target := &v.RandomLeaf + raw := firstPass.RandomLeaf + err = __unmarshalSimpleNamedFragmentRandomLeafLeafContent( + target, raw) + if err != nil { + return fmt.Errorf( + "Unable to unmarshal SimpleNamedFragmentResponse.RandomLeaf: %w", err) + } + } + + return 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"` + Name string `json:"name"` + Url string `json:"url"` + Duration int `json:"duration"` + Thumbnail VideoFieldsThumbnail `json:"thumbnail"` +} + +// VideoFieldsThumbnail includes the requested fields of the GraphQL type Thumbnail. +type VideoFieldsThumbnail struct { + Id testutil.ID `json:"id"` +} + +func SimpleNamedFragment( + client graphql.Client, +) (*SimpleNamedFragmentResponse, error) { + var retval SimpleNamedFragmentResponse + err := client.MakeRequest( + nil, + "SimpleNamedFragment", + ` +query SimpleNamedFragment { + randomItem { + __typename + id + name + ... VideoFields + } + randomLeaf { + __typename + ... VideoFields + } +} +fragment VideoFields on Video { + id + name + url + duration + thumbnail { + id + } +} +`, + &retval, + nil, + ) + return &retval, err +} + diff --git a/generate/testdata/snapshots/TestGenerate-SimpleNamedFragment.graphql-SimpleNamedFragment.graphql.json b/generate/testdata/snapshots/TestGenerate-SimpleNamedFragment.graphql-SimpleNamedFragment.graphql.json new file mode 100644 index 00000000..edfc8884 --- /dev/null +++ b/generate/testdata/snapshots/TestGenerate-SimpleNamedFragment.graphql-SimpleNamedFragment.graphql.json @@ -0,0 +1,9 @@ +{ + "operations": [ + { + "operationName": "SimpleNamedFragment", + "query": "\nquery SimpleNamedFragment {\n\trandomItem {\n\t\t__typename\n\t\tid\n\t\tname\n\t\t... VideoFields\n\t}\n\trandomLeaf {\n\t\t__typename\n\t\t... VideoFields\n\t}\n}\nfragment VideoFields on Video {\n\tid\n\tname\n\turl\n\tduration\n\tthumbnail {\n\t\tid\n\t}\n}\n", + "sourceLocation": "testdata/queries/SimpleNamedFragment.graphql" + } + ] +} diff --git a/generate/testdata/snapshots/TestGenerate-UnionNoFragments.graphql-UnionNoFragments.graphql.go b/generate/testdata/snapshots/TestGenerate-UnionNoFragments.graphql-UnionNoFragments.graphql.go index da7013d4..ab489b90 100644 --- a/generate/testdata/snapshots/TestGenerate-UnionNoFragments.graphql-UnionNoFragments.graphql.go +++ b/generate/testdata/snapshots/TestGenerate-UnionNoFragments.graphql-UnionNoFragments.graphql.go @@ -82,13 +82,12 @@ type UnionNoFragmentsQueryResponse struct { func (v *UnionNoFragmentsQueryResponse) UnmarshalJSON(b []byte) error { - type UnionNoFragmentsQueryResponseWrapper UnionNoFragmentsQueryResponse - var firstPass struct { - *UnionNoFragmentsQueryResponseWrapper + *UnionNoFragmentsQueryResponse RandomLeaf json.RawMessage `json:"randomLeaf"` + graphql.NoUnmarshalJSON } - firstPass.UnionNoFragmentsQueryResponseWrapper = (*UnionNoFragmentsQueryResponseWrapper)(v) + firstPass.UnionNoFragmentsQueryResponse = v err := json.Unmarshal(b, &firstPass) if err != nil { @@ -105,6 +104,7 @@ func (v *UnionNoFragmentsQueryResponse) UnmarshalJSON(b []byte) error { "Unable to unmarshal UnionNoFragmentsQueryResponse.RandomLeaf: %w", err) } } + return nil } diff --git a/generate/types.go b/generate/types.go index f9783857..fe04da1f 100644 --- a/generate/types.go +++ b/generate/types.go @@ -119,11 +119,19 @@ type goStructField struct { Description string } -func isAbstract(typ goType) bool { - _, ok := typ.Unwrap().(*goInterfaceType) +// IsAbstract returns true if this field is of abstract type (i.e. GraphQL +// union or interface; equivalently, represented by an interface in Go). +func (field *goStructField) IsAbstract() bool { + _, ok := field.GoType.Unwrap().(*goInterfaceType) return ok } +// IsEmbedded returns true if this field is embedded (a.k.a. anonymous), which +// is in practice true if it corresponds to a named fragment spread in GraphQL. +func (field *goStructField) IsEmbedded() bool { + return field.GoName == "" +} + func (typ *goStructType) WriteDefinition(w io.Writer, g *generator) error { description := typ.Description if typ.Incomplete { @@ -144,28 +152,47 @@ func (typ *goStructType) WriteDefinition(w io.Writer, g *generator) error { } writeDescription(w, description) + needUnmarshaler := false fmt.Fprintf(w, "type %s struct {\n", typ.GoName) for _, field := range typ.Fields { writeDescription(w, field.Description) jsonName := field.JSONName - if isAbstract(field.GoType) { - // abstract types are handled in our UnmarshalJSON + if field.IsAbstract() { + // abstract types are handled in our UnmarshalJSON (see below) + needUnmarshaler = true jsonName = "-" } - fmt.Fprintf(w, "\t%s %s `json:\"%s\"`\n", - field.GoName, field.GoType.Reference(), jsonName) + if field.IsEmbedded() { + // embedded fields also need UnmarshalJSON handling (see below) + needUnmarshaler = true + fmt.Fprintf(w, "\t%s `json:\"-\"`\n", field.GoType.Unwrap().Reference()) + } else { + fmt.Fprintf(w, "\t%s %s `json:\"%s\"`\n", + field.GoName, field.GoType.Reference(), jsonName) + } } fmt.Fprintf(w, "}\n") - // Now, if needed, write the unmarshaler. + // Now, if needed, write the unmarshaler. We need one if we have any + // interface-typed fields, or any embedded fields. + // + // For interface-typed fields, ideally we'd write an UnmarshalJSON method + // on the field, but you can't add a method to an interface. So we write a + // per-interface-type helper, but we have to call it (with a little + // boilerplate) everywhere the type is referenced. // - // Specifically, in order to unmarshal interface values, we need to add an - // UnmarshalJSON method to each type which has an interface-typed *field* - // (not the interface type itself -- we can't add methods to that). - // But we put most of the logic in a per-interface-type helper function, - // written along with the interface type; the UnmarshalJSON method is just - // the boilerplate. - if len(typ.AbstractFields()) == 0 { + // For embedded fields (from fragments), mostly the JSON library would just + // do what we want, but there are two problems. First, if the embedded + // ype has its own UnmarshalJSON, naively that would be promoted to + // become our UnmarshalJSON, which is no good. But we don't want to just + // hide that method and inline its fields, either; we need to call its + // UnmarshalJSON (on the same object we unmarshal into this struct). + // Second, if the embedded type duplicates any fields of the embedding type + // -- maybe both the fragment and the selection into which it's spread + // select the same field, or several fragments select the same field -- the + // JSON library will only fill one of those (the least-nested one); we want + // to fill them all. + if !needUnmarshaler { return nil } @@ -181,18 +208,6 @@ func (typ *goStructType) WriteDefinition(w io.Writer, g *generator) error { func (typ *goStructType) Reference() string { return typ.GoName } -// AbstractFields returns all the fields which are abstract types (i.e. GraphQL -// unions and interfaces; equivalently, types represented by interfaces in Go). -func (typ *goStructType) AbstractFields() []*goStructField { - var ret []*goStructField - for _, field := range typ.Fields { - if isAbstract(field.GoType) { - ret = append(ret, field) - } - } - return ret -} - // goInterfaceType represents a Go interface type, used to represent a GraphQL // interface or union type. type goInterfaceType struct { diff --git a/generate/unmarshal.go.tmpl b/generate/unmarshal.go.tmpl index d91a7f67..fa99cf39 100644 --- a/generate/unmarshal.go.tmpl +++ b/generate/unmarshal.go.tmpl @@ -2,36 +2,49 @@ UnmarshalJSON from the function it follows) */}} func (v *{{.GoName}}) UnmarshalJSON(b []byte) error { - {{/* We want to specially handle the abstract fields (.AbstractFields), - but unmarshal everything else normally. To handle abstract fields, + {{/* We want to specially handle the abstract or embedded fields, but + unmarshal everything else normally. To handle abstract fields, first we unmarshal them into a json.RawMessage, and then handle those - further, below. For the rest, we just want to call json.Unmarshal. + further, below. Embedded fields we just unmarshal directly into the + embedded value. For the rest, we just want to call json.Unmarshal. But if we do that naively on a value of type `.Type`, it will call this function again, and recurse infinitely. So we make a wrapper - type -- with a different name, thus different methods, but the same - fields, and unmarshal into that. For more on why this is so - difficult, see - https://github.com/benjaminjkraft/notes/blob/master/go-json-interfaces.md + type which embeds both this type and NoUmnarshalJSON, which prevents + either's UnmarshalJSON method from being promoted. For more on why + this is so difficult, see + https://github.com/benjaminjkraft/notes/blob/master/go-json-interfaces.md. + (Note there are a few different ways "hide" the method, but this one + seems to be the best option that works if this type has embedded types + with UnmarshalJSON methods.) TODO(benkraft)): Ensure `{{.Type}}Wrapper` won't collide with any other type we need. (For the most part it being locally-scoped saves us; it's not clear if this can be a problem in practice.) */}} - type {{.GoName}}Wrapper {{.GoName}} + {{/* TODO(benkraft): Omit/simplify the first pass if all fields are + embedded/abstract. */ -}} var firstPass struct{ - *{{.GoName}}Wrapper - {{range .AbstractFields -}} + *{{.GoName}} + {{range .Fields -}} + {{if .IsAbstract -}} {{.GoName}} {{repeat .GoType.SliceDepth "[]"}}{{ref "encoding/json.RawMessage"}} `json:"{{.JSONName}}"` - {{end}} + {{end -}} + {{end -}} + {{/* TODO(benkraft): In principle you might have a field-name that + conflicts with this one; avoid that. */ -}} + {{ref "github.com/Khan/genqlient/graphql.NoUnmarshalJSON"}} } - firstPass.{{.GoName}}Wrapper = (*{{.GoName}}Wrapper)(v) + firstPass.{{.GoName}} = v err := {{ref "encoding/json.Unmarshal"}}(b, &firstPass) if err != nil { return err } - {{/* Now, for each field, call out to the unmarshal-helper. + {{/* Now, handle the fields needing special handling. */}} + {{range $field := .Fields -}} + {{if $field.IsAbstract -}} + {{/* First, for abstract fields, call the unmarshal-helper. This gets a little complicated because we may have a slice field. So what we do is basically, for each field of type `[][]...[]MyType`: @@ -65,7 +78,6 @@ func (v *{{.GoName}}) UnmarshalJSON(b []byte) error { each field we are handling, which would otherwise conflict. (We could instead suffix the names, but that makes things much harder to read.) */}} - {{range $field := .AbstractFields -}} { target := &v.{{$field.GoName}} raw := firstPass.{{$field.GoName}} @@ -92,7 +104,17 @@ func (v *{{.GoName}}) UnmarshalJSON(b []byte) error { } {{end -}} } - {{end -}} + {{end -}}{{/* end if .IsAbstract */}} + {{if $field.IsEmbedded -}} + {{/* Embedded fields are easier: we just unmarshal the same input into + them. (They're also easier because they can't be lists, since they + arise from GraphQL fragment spreads.) */}} + err = json.Unmarshal(b, &v.{{$field.GoType.Unwrap.Reference}}) + if err != nil { + return err + } + {{end}}{{/* end if .IsEmbedded */ -}} + {{end}}{{/* end range .Fields */ -}} return nil } diff --git a/graphql/util.go b/graphql/util.go new file mode 100644 index 00000000..b5b4564a --- /dev/null +++ b/graphql/util.go @@ -0,0 +1,19 @@ +package graphql + +// Utility types used by the generated code. In general, these are *not* +// intended for end-users. + +// NoUnmarshalJSON is intended for the use of genqlient's generated code only. +// +// It is used to prevent a struct type from inheriting its embed's +// UnmarshalJSON method: given a type +// type T struct { E; NoUnmarshalJSON } +// where E has an UnmarshalJSON method, T will not inherit it, per the Go +// selector rules: https://golang.org/ref/spec#Selectors. +type NoUnmarshalJSON struct{} + +// UnmarshalJSON should never be called; it exists only to prevent a sibling +// UnmarshalJSON method from being promoted. +func (NoUnmarshalJSON) UnmarshalJSON(b []byte) error { + panic("NoUnmarshalJSON.UnmarshalJSON should never be called!") +} diff --git a/internal/integration/generated.go b/internal/integration/generated.go index a80fe006..bfc24136 100644 --- a/internal/integration/generated.go +++ b/internal/integration/generated.go @@ -10,6 +10,152 @@ import ( "github.com/Khan/genqlient/graphql" ) +// AnimalFields includes the GraphQL fields of Animal requested by the fragment AnimalFields. +type AnimalFields struct { + Id string `json:"id"` + Hair AnimalFieldsHairBeingsHair `json:"hair"` + Owner AnimalFieldsOwnerBeing `json:"-"` +} + +func (v *AnimalFields) UnmarshalJSON(b []byte) error { + + var firstPass struct { + *AnimalFields + Owner json.RawMessage `json:"owner"` + graphql.NoUnmarshalJSON + } + firstPass.AnimalFields = v + + err := json.Unmarshal(b, &firstPass) + if err != nil { + return err + } + + { + target := &v.Owner + raw := firstPass.Owner + err = __unmarshalAnimalFieldsOwnerBeing( + target, raw) + if err != nil { + return fmt.Errorf( + "Unable to unmarshal AnimalFields.Owner: %w", err) + } + } + + return nil +} + +// AnimalFieldsHairBeingsHair includes the requested fields of the GraphQL type BeingsHair. +type AnimalFieldsHairBeingsHair struct { + HasHair bool `json:"hasHair"` +} + +// AnimalFieldsOwnerAnimal includes the requested fields of the GraphQL type Animal. +type AnimalFieldsOwnerAnimal struct { + Typename string `json:"__typename"` + Id string `json:"id"` +} + +// AnimalFieldsOwnerBeing includes the requested fields of the GraphQL interface Being. +// +// AnimalFieldsOwnerBeing is implemented by the following types: +// AnimalFieldsOwnerUser +// AnimalFieldsOwnerAnimal +// +// The GraphQL type's documentation follows. +// +// +type AnimalFieldsOwnerBeing interface { + implementsGraphQLInterfaceAnimalFieldsOwnerBeing() + // 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 *AnimalFieldsOwnerUser) implementsGraphQLInterfaceAnimalFieldsOwnerBeing() {} + +// GetTypename is a part of, and documented with, the interface AnimalFieldsOwnerBeing. +func (v *AnimalFieldsOwnerUser) GetTypename() string { return v.Typename } + +// GetId is a part of, and documented with, the interface AnimalFieldsOwnerBeing. +func (v *AnimalFieldsOwnerUser) GetId() string { return v.Id } + +func (v *AnimalFieldsOwnerAnimal) implementsGraphQLInterfaceAnimalFieldsOwnerBeing() {} + +// GetTypename is a part of, and documented with, the interface AnimalFieldsOwnerBeing. +func (v *AnimalFieldsOwnerAnimal) GetTypename() string { return v.Typename } + +// GetId is a part of, and documented with, the interface AnimalFieldsOwnerBeing. +func (v *AnimalFieldsOwnerAnimal) GetId() string { return v.Id } + +func __unmarshalAnimalFieldsOwnerBeing(v *AnimalFieldsOwnerBeing, m json.RawMessage) error { + if string(m) == "null" { + return nil + } + + var tn struct { + TypeName string `json:"__typename"` + } + err := json.Unmarshal(m, &tn) + if err != nil { + return err + } + + switch tn.TypeName { + case "User": + *v = new(AnimalFieldsOwnerUser) + return json.Unmarshal(m, *v) + case "Animal": + *v = new(AnimalFieldsOwnerAnimal) + return json.Unmarshal(m, *v) + case "": + return fmt.Errorf( + "Response was missing Being.__typename") + default: + return fmt.Errorf( + `Unexpected concrete type for AnimalFieldsOwnerBeing: "%v"`, tn.TypeName) + } +} + +// AnimalFieldsOwnerUser includes the requested fields of the GraphQL type User. +type AnimalFieldsOwnerUser struct { + Typename string `json:"__typename"` + Id string `json:"id"` + UserFields `json:"-"` +} + +func (v *AnimalFieldsOwnerUser) UnmarshalJSON(b []byte) error { + + var firstPass struct { + *AnimalFieldsOwnerUser + graphql.NoUnmarshalJSON + } + firstPass.AnimalFieldsOwnerUser = v + + err := json.Unmarshal(b, &firstPass) + if err != nil { + return err + } + + err = json.Unmarshal(b, &v.UserFields) + if err != nil { + return err + } + return nil +} + +// MoreUserFields includes the GraphQL fields of User requested by the fragment MoreUserFields. +type MoreUserFields struct { + Id string `json:"id"` + Hair MoreUserFieldsHair `json:"hair"` +} + +// MoreUserFieldsHair includes the requested fields of the GraphQL type Hair. +type MoreUserFieldsHair struct { + Color string `json:"color"` +} + type Species string const ( @@ -17,6 +163,33 @@ const ( SpeciesCoelacanth Species = "COELACANTH" ) +// UserFields includes the GraphQL fields of User requested by the fragment UserFields. +type UserFields struct { + Id string `json:"id"` + LuckyNumber int `json:"luckyNumber"` + MoreUserFields `json:"-"` +} + +func (v *UserFields) UnmarshalJSON(b []byte) error { + + var firstPass struct { + *UserFields + graphql.NoUnmarshalJSON + } + firstPass.UserFields = v + + err := json.Unmarshal(b, &firstPass) + if err != nil { + return err + } + + err = json.Unmarshal(b, &v.MoreUserFields) + if err != nil { + return err + } + return nil +} + // queryWithFragmentsBeingsAnimal includes the requested fields of the GraphQL type Animal. type queryWithFragmentsBeingsAnimal struct { Typename string `json:"__typename"` @@ -29,13 +202,12 @@ type queryWithFragmentsBeingsAnimal struct { func (v *queryWithFragmentsBeingsAnimal) UnmarshalJSON(b []byte) error { - type queryWithFragmentsBeingsAnimalWrapper queryWithFragmentsBeingsAnimal - var firstPass struct { - *queryWithFragmentsBeingsAnimalWrapper + *queryWithFragmentsBeingsAnimal Owner json.RawMessage `json:"owner"` + graphql.NoUnmarshalJSON } - firstPass.queryWithFragmentsBeingsAnimalWrapper = (*queryWithFragmentsBeingsAnimalWrapper)(v) + firstPass.queryWithFragmentsBeingsAnimal = v err := json.Unmarshal(b, &firstPass) if err != nil { @@ -52,6 +224,7 @@ func (v *queryWithFragmentsBeingsAnimal) UnmarshalJSON(b []byte) error { "Unable to unmarshal queryWithFragmentsBeingsAnimal.Owner: %w", err) } } + return nil } @@ -238,13 +411,12 @@ type queryWithFragmentsResponse struct { func (v *queryWithFragmentsResponse) UnmarshalJSON(b []byte) error { - type queryWithFragmentsResponseWrapper queryWithFragmentsResponse - var firstPass struct { - *queryWithFragmentsResponseWrapper + *queryWithFragmentsResponse Beings []json.RawMessage `json:"beings"` + graphql.NoUnmarshalJSON } - firstPass.queryWithFragmentsResponseWrapper = (*queryWithFragmentsResponseWrapper)(v) + firstPass.queryWithFragmentsResponse = v err := json.Unmarshal(b, &firstPass) if err != nil { @@ -267,6 +439,7 @@ func (v *queryWithFragmentsResponse) UnmarshalJSON(b []byte) error { } } } + return nil } @@ -363,13 +536,12 @@ type queryWithInterfaceListFieldResponse struct { func (v *queryWithInterfaceListFieldResponse) UnmarshalJSON(b []byte) error { - type queryWithInterfaceListFieldResponseWrapper queryWithInterfaceListFieldResponse - var firstPass struct { - *queryWithInterfaceListFieldResponseWrapper + *queryWithInterfaceListFieldResponse Beings []json.RawMessage `json:"beings"` + graphql.NoUnmarshalJSON } - firstPass.queryWithInterfaceListFieldResponseWrapper = (*queryWithInterfaceListFieldResponseWrapper)(v) + firstPass.queryWithInterfaceListFieldResponse = v err := json.Unmarshal(b, &firstPass) if err != nil { @@ -392,6 +564,7 @@ func (v *queryWithInterfaceListFieldResponse) UnmarshalJSON(b []byte) error { } } } + return nil } @@ -488,13 +661,12 @@ type queryWithInterfaceListPointerFieldResponse struct { func (v *queryWithInterfaceListPointerFieldResponse) UnmarshalJSON(b []byte) error { - type queryWithInterfaceListPointerFieldResponseWrapper queryWithInterfaceListPointerFieldResponse - var firstPass struct { - *queryWithInterfaceListPointerFieldResponseWrapper + *queryWithInterfaceListPointerFieldResponse Beings []json.RawMessage `json:"beings"` + graphql.NoUnmarshalJSON } - firstPass.queryWithInterfaceListPointerFieldResponseWrapper = (*queryWithInterfaceListPointerFieldResponseWrapper)(v) + firstPass.queryWithInterfaceListPointerFieldResponse = v err := json.Unmarshal(b, &firstPass) if err != nil { @@ -518,6 +690,7 @@ func (v *queryWithInterfaceListPointerFieldResponse) UnmarshalJSON(b []byte) err } } } + return nil } @@ -621,13 +794,12 @@ type queryWithInterfaceNoFragmentsResponse struct { func (v *queryWithInterfaceNoFragmentsResponse) UnmarshalJSON(b []byte) error { - type queryWithInterfaceNoFragmentsResponseWrapper queryWithInterfaceNoFragmentsResponse - var firstPass struct { - *queryWithInterfaceNoFragmentsResponseWrapper + *queryWithInterfaceNoFragmentsResponse Being json.RawMessage `json:"being"` + graphql.NoUnmarshalJSON } - firstPass.queryWithInterfaceNoFragmentsResponseWrapper = (*queryWithInterfaceNoFragmentsResponseWrapper)(v) + firstPass.queryWithInterfaceNoFragmentsResponse = v err := json.Unmarshal(b, &firstPass) if err != nil { @@ -644,6 +816,164 @@ func (v *queryWithInterfaceNoFragmentsResponse) UnmarshalJSON(b []byte) error { "Unable to unmarshal queryWithInterfaceNoFragmentsResponse.Being: %w", err) } } + + return nil +} + +// queryWithNamedFragmentsBeingsAnimal includes the requested fields of the GraphQL type Animal. +type queryWithNamedFragmentsBeingsAnimal struct { + Typename string `json:"__typename"` + Id string `json:"id"` + AnimalFields `json:"-"` +} + +func (v *queryWithNamedFragmentsBeingsAnimal) UnmarshalJSON(b []byte) error { + + var firstPass struct { + *queryWithNamedFragmentsBeingsAnimal + graphql.NoUnmarshalJSON + } + firstPass.queryWithNamedFragmentsBeingsAnimal = v + + err := json.Unmarshal(b, &firstPass) + if err != nil { + return err + } + + err = json.Unmarshal(b, &v.AnimalFields) + if err != nil { + return err + } + return nil +} + +// queryWithNamedFragmentsBeingsBeing includes the requested fields of the GraphQL interface Being. +// +// queryWithNamedFragmentsBeingsBeing is implemented by the following types: +// queryWithNamedFragmentsBeingsUser +// queryWithNamedFragmentsBeingsAnimal +// +// The GraphQL type's documentation follows. +// +// +type queryWithNamedFragmentsBeingsBeing interface { + implementsGraphQLInterfacequeryWithNamedFragmentsBeingsBeing() + // 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 *queryWithNamedFragmentsBeingsUser) implementsGraphQLInterfacequeryWithNamedFragmentsBeingsBeing() { +} + +// GetTypename is a part of, and documented with, the interface queryWithNamedFragmentsBeingsBeing. +func (v *queryWithNamedFragmentsBeingsUser) GetTypename() string { return v.Typename } + +// GetId is a part of, and documented with, the interface queryWithNamedFragmentsBeingsBeing. +func (v *queryWithNamedFragmentsBeingsUser) GetId() string { return v.Id } + +func (v *queryWithNamedFragmentsBeingsAnimal) implementsGraphQLInterfacequeryWithNamedFragmentsBeingsBeing() { +} + +// GetTypename is a part of, and documented with, the interface queryWithNamedFragmentsBeingsBeing. +func (v *queryWithNamedFragmentsBeingsAnimal) GetTypename() string { return v.Typename } + +// GetId is a part of, and documented with, the interface queryWithNamedFragmentsBeingsBeing. +func (v *queryWithNamedFragmentsBeingsAnimal) GetId() string { return v.Id } + +func __unmarshalqueryWithNamedFragmentsBeingsBeing(v *queryWithNamedFragmentsBeingsBeing, m json.RawMessage) error { + if string(m) == "null" { + return nil + } + + var tn struct { + TypeName string `json:"__typename"` + } + err := json.Unmarshal(m, &tn) + if err != nil { + return err + } + + switch tn.TypeName { + case "User": + *v = new(queryWithNamedFragmentsBeingsUser) + return json.Unmarshal(m, *v) + case "Animal": + *v = new(queryWithNamedFragmentsBeingsAnimal) + return json.Unmarshal(m, *v) + case "": + return fmt.Errorf( + "Response was missing Being.__typename") + default: + return fmt.Errorf( + `Unexpected concrete type for queryWithNamedFragmentsBeingsBeing: "%v"`, tn.TypeName) + } +} + +// queryWithNamedFragmentsBeingsUser includes the requested fields of the GraphQL type User. +type queryWithNamedFragmentsBeingsUser struct { + Typename string `json:"__typename"` + Id string `json:"id"` + UserFields `json:"-"` +} + +func (v *queryWithNamedFragmentsBeingsUser) UnmarshalJSON(b []byte) error { + + var firstPass struct { + *queryWithNamedFragmentsBeingsUser + graphql.NoUnmarshalJSON + } + firstPass.queryWithNamedFragmentsBeingsUser = v + + err := json.Unmarshal(b, &firstPass) + if err != nil { + return err + } + + err = json.Unmarshal(b, &v.UserFields) + if err != nil { + return err + } + return nil +} + +// queryWithNamedFragmentsResponse is returned by queryWithNamedFragments on success. +type queryWithNamedFragmentsResponse struct { + Beings []queryWithNamedFragmentsBeingsBeing `json:"-"` +} + +func (v *queryWithNamedFragmentsResponse) UnmarshalJSON(b []byte) error { + + var firstPass struct { + *queryWithNamedFragmentsResponse + Beings []json.RawMessage `json:"beings"` + graphql.NoUnmarshalJSON + } + firstPass.queryWithNamedFragmentsResponse = v + + err := json.Unmarshal(b, &firstPass) + if err != nil { + return err + } + + { + target := &v.Beings + raw := firstPass.Beings + *target = make( + []queryWithNamedFragmentsBeingsBeing, + len(raw)) + for i, raw := range raw { + target := &(*target)[i] + err = __unmarshalqueryWithNamedFragmentsBeingsBeing( + target, raw) + if err != nil { + return fmt.Errorf( + "Unable to unmarshal queryWithNamedFragmentsResponse.Beings: %w", err) + } + } + } + return nil } @@ -865,3 +1195,54 @@ query queryWithFragments ($ids: [ID!]!) { ) return &retval, err } + +func queryWithNamedFragments( + ctx context.Context, + client graphql.Client, + ids []string, +) (*queryWithNamedFragmentsResponse, error) { + variables := map[string]interface{}{ + "ids": ids, + } + + var retval queryWithNamedFragmentsResponse + err := client.MakeRequest( + ctx, + "queryWithNamedFragments", + ` +query queryWithNamedFragments ($ids: [ID!]!) { + beings(ids: $ids) { + __typename + id + ... AnimalFields + ... UserFields + } +} +fragment AnimalFields on Animal { + id + hair { + hasHair + } + owner { + __typename + id + ... UserFields + } +} +fragment UserFields on User { + id + luckyNumber + ... MoreUserFields +} +fragment MoreUserFields on User { + id + hair { + color + } +} +`, + &retval, + variables, + ) + return &retval, err +} diff --git a/internal/integration/integration_test.go b/internal/integration/integration_test.go index a201c124..0cfd7233 100644 --- a/internal/integration/integration_test.go +++ b/internal/integration/integration_test.go @@ -288,6 +288,94 @@ func TestFragments(t *testing.T) { assert.Nil(t, resp.Beings[2]) } +func TestNamedFragments(t *testing.T) { + _ = `# @genqlient + fragment AnimalFields on Animal { + id + hair { hasHair } + owner { id ...UserFields } + } + + fragment MoreUserFields on User { + id + hair { color } + } + + fragment UserFields on User { + id luckyNumber + ...MoreUserFields + } + + query queryWithNamedFragments($ids: [ID!]!) { + beings(ids: $ids) { + __typename id + ...AnimalFields + ...UserFields + } + }` + + ctx := context.Background() + server := server.RunServer() + defer server.Close() + client := graphql.NewClient(server.URL, http.DefaultClient) + + resp, err := queryWithNamedFragments(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, hair we need to cast for) + + user, ok := resp.Beings[0].(*queryWithNamedFragmentsBeingsUser) + require.Truef(t, ok, "got %T, not User", resp.Beings[0]) + assert.Equal(t, "1", user.Id) + assert.Equal(t, "1", user.UserFields.Id) + assert.Equal(t, "1", user.UserFields.MoreUserFields.Id) + // on UserFields, but we should be able to access directly via embedding: + assert.Equal(t, 17, user.LuckyNumber) + assert.Equal(t, "Black", user.Hair.Color) + + // Animal has, in total, the fields: + // __typename + // id + // hair { hasHair } + // owner { id luckyNumber } + assert.Equal(t, "Animal", resp.Beings[1].GetTypename()) + assert.Equal(t, "3", resp.Beings[1].GetId()) + // (hair.* and owner.* we have to cast for) + + animal, ok := resp.Beings[1].(*queryWithNamedFragmentsBeingsAnimal) + require.Truef(t, ok, "got %T, not Animal", resp.Beings[1]) + // Check that we filled in *both* ID fields: + assert.Equal(t, "3", animal.Id) + assert.Equal(t, "3", animal.AnimalFields.Id) + // on AnimalFields: + assert.True(t, animal.Hair.HasHair) + assert.Equal(t, "1", animal.Owner.GetId()) + // (luckyNumber we have to cast for, again) + + owner, ok := animal.Owner.(*AnimalFieldsOwnerUser) + require.Truef(t, ok, "got %T, not User", animal.Owner) + // Check that we filled in *both* ID fields: + assert.Equal(t, "1", owner.Id) + assert.Equal(t, "1", owner.UserFields.Id) + assert.Equal(t, "1", owner.UserFields.MoreUserFields.Id) + // on UserFields: + assert.Equal(t, 17, owner.LuckyNumber) + assert.Equal(t, "Black", owner.Hair.Color) + + 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