diff --git a/generate/convert.go b/generate/convert.go index b8ad019..7512193 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,72 @@ func (g *generator) convertInlineFragment( containingTypedef, queryOptions) } +// convertFragmentSpread converts a single GraphQL fragment-spread +// (`...MyFragment`) into a Go struct-field. 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 c54f29c..2011fc3 100644 --- a/generate/generate.go +++ b/generate/generate.go @@ -35,6 +35,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). @@ -67,7 +72,11 @@ type argument struct { Options *GenqlientDirective } -func newGenerator(config *Config, schema *ast.Schema) (*generator, error) { +func newGenerator( + config *Config, + schema *ast.Schema, + fragments ast.FragmentDefinitionList, +) (*generator, error) { g := generator{ Config: config, typeMap: map[string]goType{}, @@ -75,6 +84,11 @@ func newGenerator(config *Config, schema *ast.Schema) (*generator, error) { 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 } _, err := g.addRef("github.com/Khan/genqlient/graphql.Client") @@ -146,6 +160,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 @@ -158,9 +204,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 ... } @@ -214,6 +262,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") @@ -221,7 +274,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) @@ -304,19 +357,13 @@ 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, err := 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, err := newGenerator(config, schema, document.Fragments) if err != nil { return nil, err } - 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 5abf8b4..99f68cf 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 0000000..e837fc6 --- /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 0000000..01d3370 --- /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 7ba7137..2e06b35 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 0000000..bc4bf69 --- /dev/null +++ b/generate/testdata/snapshots/TestGenerate-ComplexNamedFragments.graphql-ComplexNamedFragments.graphql.go @@ -0,0 +1,666 @@ +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 err 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 0000000..369d66a --- /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 f42f68e..810b1e6 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 db66082..1c8e4ef 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 e38e2b0..82ecba7 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 aeff4ca..d23d766 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 3afb18f..8fb67d3 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 0000000..86078cf --- /dev/null +++ b/generate/testdata/snapshots/TestGenerate-SimpleNamedFragment.graphql-SimpleNamedFragment.graphql.go @@ -0,0 +1,334 @@ +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 err 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 0000000..edfc888 --- /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 a31e28c..14f7e4b 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 f978385..4ffbd10 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. // - // 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 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. + // + // For embedded fields (from fragments), mostly the JSON library would just + // do what we want, but there are two problems. First, if the embedded + // type 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 d91a7f6..fa99cf3 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/go.sum b/go.sum index 72df9cc..f7c511b 100644 --- a/go.sum +++ b/go.sum @@ -10,6 +10,7 @@ github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0 h1:jfIu9sQUG6Ig github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0/go.mod h1:t2tdKJDJF9BV14lnkjHmOQgcvEKgtqs5a1N3LNdJhGE= github.com/bradleyjkemp/cupaloy/v2 v2.6.0 h1:knToPYa2xtfg42U3I6punFEjaGFKWQRXJwj0JTv4mTs= github.com/bradleyjkemp/cupaloy/v2 v2.6.0/go.mod h1:bm7JXdkRd4BHJk9HpwqAI8BoAY1lps46Enkdqw6aRX0= +github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d h1:U+s90UTSYgptZMwQh2aRr3LuazLJIa+Pg3Kc1ylSYVY= github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= @@ -39,6 +40,7 @@ github.com/mitchellh/mapstructure v0.0.0-20180203102830-a4e142e9c047 h1:zCoDWFD5 github.com/mitchellh/mapstructure v0.0.0-20180203102830-a4e142e9c047/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/opentracing/basictracer-go v1.0.0/go.mod h1:QfBfYuafItcjQuMwinw9GhYKwFXS9KnPs5lxoYwgW74= github.com/opentracing/opentracing-go v1.0.2/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= +github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= diff --git a/graphql/util.go b/graphql/util.go new file mode 100644 index 0000000..b5b4564 --- /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 bbc3b73..eb234ec 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 } @@ -877,3 +1207,56 @@ 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 err error + + 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 a201c12..0cfd723 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