Add support for specifying type-names, and conflict-detection (#94)

## Summary:
In this commit I add two related features to genqlient:
conflict-detection to avoid generating two distinct types with the same
name, and an option to specify the type-name genqlient should use for
some type.

The conflict-detection was pretty simple once I realized I had already
written all the code to do it in #70.  There was a bunch of wiring,
since we now need to keep track of the GraphQL type/selection-set that
each type corresponds to, but it was pretty straightforward.  This
allows us to:
- detect and reject if you have really sneaky type-names (there are some
  examples documented in `names.go`)
- more clearly crash if genqlient accidentally generates two conflicting
  types, and
- avoid stack-overflow when handing recursive (input) types (although
  sadly the poor support for options on input types (#14) makes them
  difficult to use in many cases; you really need to be able to set
  `pointer: true`)

And with that all set up, the type-naming was also easy!  (It doesn't
have to get into the core of the type-generator, just plug in where we
choose names.  The desire for conflict detection was the main reason I
hadn't set it up already.)  Note that the existing limitation of #70 that
the fields have to be in exactly the same order remains (and is now
documented as #93); it's not deeply hard to fix but it's surprisingly
much work.

Issue: https://github.com/Khan/genqlient/issues/60
Issue: https://github.com/Khan/genqlient/issues/12

## Test plan:
make check


Author: benjaminjkraft

Reviewers: StevenACoffman, jvoll, benjaminjkraft, aberkan, csilvers, dnerdy, mahtabsabet, MiguelCastillo

Required Reviewers: 

Approved By: StevenACoffman, jvoll

Checks:  Test (1.17),  Test (1.16),  Test (1.15),  Test (1.14),  Lint,  Test (1.17),  Test (1.16),  Test (1.15),  Test (1.14),  Lint

Pull Request URL: https://github.com/Khan/genqlient/pull/94
This commit is contained in:
Ben Kraft
2021-09-15 18:06:43 -07:00
committed by GitHub
parent 5211442843
commit fcae8dd1d7
20 changed files with 713 additions and 47 deletions
+10
View File
@@ -0,0 +1,10 @@
package errors
_ = `# @genqlient
query ConflictingTypeNames {
# @genqlient(typename: "T")
f { g }
# @genqlient(typename: "T")
otherF: f { g h }
}
`
+6
View File
@@ -0,0 +1,6 @@
query ConflictingTypeNames {
# @genqlient(typename: "T")
f { g }
# @genqlient(typename: "T")
otherF: f { g h }
}
@@ -0,0 +1,8 @@
type Query {
f: T
}
type T {
g: String!
h: String!
}
+6
View File
@@ -0,0 +1,6 @@
query Recursion($input: RecursiveInput!) {
recur(input: $input) {
# (sadly, or happily, GraphQL doesn't let us recur infinitely here)
rec { rec { rec { id } } }
}
}
+10
View File
@@ -0,0 +1,10 @@
# @genqlient(typename: "Resp")
query TypeNames {
# @genqlient(typename: "User")
user { id name }
# @genqlient(typename: "Item")
randomItem { id name }
# (ok to reuse the name as long as they match)
# @genqlient(typename: "User")
users { id name }
}
+11 -1
View File
@@ -131,6 +131,15 @@ type Topic implements Content {
schoolGrade: String
}
input RecursiveInput {
rec: [RecursiveInput]
}
type Recursive {
id: ID!
rec: Recursive
}
"""Query's description is probably ignored by almost all callers."""
type Query {
"""user looks up a user by some stuff.
@@ -140,7 +149,7 @@ type Query {
"""
user(query: UserQueryInput): User
users(query: [UserQueryInput]): User
users(query: [UserQueryInput]): [User]
"""usersWithRole looks a user up by role."""
usersWithRole(role: Role!): [User!]!
@@ -153,6 +162,7 @@ type Query {
getComplexJunk: ComplexJunk
listOfListsOfLists: [[[String!]!]!]!
listOfListsOfListsOfContent: [[[Content!]!]!]!
recur(input: RecursiveInput!): Recursive
}
type Mutation {
@@ -15,10 +15,10 @@ type OmitEmptyQueryResponse struct {
//
// See UserQueryInput for what stuff is supported.
// If query is null, returns the current user.
User OmitEmptyQueryUser `json:"user"`
Users OmitEmptyQueryUsersUser `json:"users"`
MaybeConvert time.Time `json:"maybeConvert"`
Convert2 time.Time `json:"convert2"`
User OmitEmptyQueryUser `json:"user"`
Users []OmitEmptyQueryUsersUser `json:"users"`
MaybeConvert time.Time `json:"maybeConvert"`
Convert2 time.Time `json:"convert2"`
}
// OmitEmptyQueryUser includes the requested fields of the GraphQL type User.
@@ -0,0 +1,71 @@
package test
// Code generated by github.com/Khan/genqlient, DO NOT EDIT.
import (
"github.com/Khan/genqlient/graphql"
"github.com/Khan/genqlient/internal/testutil"
)
// RecursionRecurRecursive includes the requested fields of the GraphQL type Recursive.
type RecursionRecurRecursive struct {
Rec RecursionRecurRecursiveRecRecursive `json:"rec"`
}
// RecursionRecurRecursiveRecRecursive includes the requested fields of the GraphQL type Recursive.
type RecursionRecurRecursiveRecRecursive struct {
Rec RecursionRecurRecursiveRecRecursiveRecRecursive `json:"rec"`
}
// RecursionRecurRecursiveRecRecursiveRecRecursive includes the requested fields of the GraphQL type Recursive.
type RecursionRecurRecursiveRecRecursiveRecRecursive struct {
Rec RecursionRecurRecursiveRecRecursiveRecRecursiveRecRecursive `json:"rec"`
}
// RecursionRecurRecursiveRecRecursiveRecRecursiveRecRecursive includes the requested fields of the GraphQL type Recursive.
type RecursionRecurRecursiveRecRecursiveRecRecursiveRecRecursive struct {
Id testutil.ID `json:"id"`
}
// RecursionResponse is returned by Recursion on success.
type RecursionResponse struct {
Recur RecursionRecurRecursive `json:"recur"`
}
type RecursiveInput struct {
Rec []RecursiveInput `json:"rec"`
}
func Recursion(
client graphql.Client,
input RecursiveInput,
) (*RecursionResponse, error) {
variables := map[string]interface{}{
"input": input,
}
var err error
var retval RecursionResponse
err = client.MakeRequest(
nil,
"Recursion",
`
query Recursion ($input: RecursiveInput!) {
recur(input: $input) {
rec {
rec {
rec {
id
}
}
}
}
}
`,
&retval,
variables,
)
return &retval, err
}
@@ -0,0 +1,9 @@
{
"operations": [
{
"operationName": "Recursion",
"query": "\nquery Recursion ($input: RecursiveInput!) {\n\trecur(input: $input) {\n\t\trec {\n\t\t\trec {\n\t\t\t\trec {\n\t\t\t\t\tid\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n}\n",
"sourceLocation": "testdata/queries/Recursion.graphql"
}
]
}
@@ -0,0 +1,205 @@
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"
)
// Item includes the requested fields of the GraphQL interface Content.
//
// Item is implemented by the following types:
// ItemArticle
// ItemVideo
// ItemTopic
// The GraphQL type's documentation follows.
//
// Content is implemented by various types like Article, Video, and Topic.
type Item interface {
implementsGraphQLInterfaceItem()
// 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 *ItemArticle) implementsGraphQLInterfaceItem() {}
// GetTypename is a part of, and documented with, the interface Item.
func (v *ItemArticle) GetTypename() string { return v.Typename }
// GetId is a part of, and documented with, the interface Item.
func (v *ItemArticle) GetId() testutil.ID { return v.Id }
// GetName is a part of, and documented with, the interface Item.
func (v *ItemArticle) GetName() string { return v.Name }
func (v *ItemVideo) implementsGraphQLInterfaceItem() {}
// GetTypename is a part of, and documented with, the interface Item.
func (v *ItemVideo) GetTypename() string { return v.Typename }
// GetId is a part of, and documented with, the interface Item.
func (v *ItemVideo) GetId() testutil.ID { return v.Id }
// GetName is a part of, and documented with, the interface Item.
func (v *ItemVideo) GetName() string { return v.Name }
func (v *ItemTopic) implementsGraphQLInterfaceItem() {}
// GetTypename is a part of, and documented with, the interface Item.
func (v *ItemTopic) GetTypename() string { return v.Typename }
// GetId is a part of, and documented with, the interface Item.
func (v *ItemTopic) GetId() testutil.ID { return v.Id }
// GetName is a part of, and documented with, the interface Item.
func (v *ItemTopic) GetName() string { return v.Name }
func __unmarshalItem(v *Item, 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(ItemArticle)
return json.Unmarshal(m, *v)
case "Video":
*v = new(ItemVideo)
return json.Unmarshal(m, *v)
case "Topic":
*v = new(ItemTopic)
return json.Unmarshal(m, *v)
case "":
return fmt.Errorf(
"Response was missing Content.__typename")
default:
return fmt.Errorf(
`Unexpected concrete type for Item: "%v"`, tn.TypeName)
}
}
// ItemArticle includes the requested fields of the GraphQL type Article.
type ItemArticle struct {
Typename string `json:"__typename"`
// ID is the identifier of the content.
Id testutil.ID `json:"id"`
Name string `json:"name"`
}
// ItemTopic includes the requested fields of the GraphQL type Topic.
type ItemTopic struct {
Typename string `json:"__typename"`
// ID is the identifier of the content.
Id testutil.ID `json:"id"`
Name string `json:"name"`
}
// ItemVideo includes the requested fields of the GraphQL type Video.
type ItemVideo struct {
Typename string `json:"__typename"`
// ID is the identifier of the content.
Id testutil.ID `json:"id"`
Name string `json:"name"`
}
// Resp is returned by TypeNames on success.
type Resp struct {
// user looks up a user by some stuff.
//
// See UserQueryInput for what stuff is supported.
// If query is null, returns the current user.
User User `json:"user"`
RandomItem Item `json:"-"`
Users []User `json:"users"`
}
func (v *Resp) UnmarshalJSON(b []byte) error {
var firstPass struct {
*Resp
RandomItem json.RawMessage `json:"randomItem"`
graphql.NoUnmarshalJSON
}
firstPass.Resp = v
err := json.Unmarshal(b, &firstPass)
if err != nil {
return err
}
{
target := &v.RandomItem
raw := firstPass.RandomItem
err = __unmarshalItem(
target, raw)
if err != nil {
return fmt.Errorf(
"Unable to unmarshal Resp.RandomItem: %w", err)
}
}
return nil
}
// User includes the requested fields of the GraphQL type User.
// The GraphQL type's documentation follows.
//
// A User is a user!
type User struct {
// id is the user's ID.
//
// It is stable, unique, and opaque, like all good IDs.
Id testutil.ID `json:"id"`
Name string `json:"name"`
}
func TypeNames(
client graphql.Client,
) (*Resp, error) {
var err error
var retval Resp
err = client.MakeRequest(
nil,
"TypeNames",
`
query TypeNames {
user {
id
name
}
randomItem {
__typename
id
name
}
users {
id
name
}
}
`,
&retval,
nil,
)
return &retval, err
}
@@ -0,0 +1,9 @@
{
"operations": [
{
"operationName": "TypeNames",
"query": "\nquery TypeNames {\n\tuser {\n\t\tid\n\t\tname\n\t}\n\trandomItem {\n\t\t__typename\n\t\tid\n\t\tname\n\t}\n\tusers {\n\t\tid\n\t\tname\n\t}\n}\n",
"sourceLocation": "testdata/queries/TypeNames.graphql"
}
]
}
@@ -0,0 +1 @@
invalid Go file testdata/errors/ConflictingTypeNames.go: testdata/errors/ConflictingTypeNames.go:3:1: expected declaration, found _
@@ -0,0 +1 @@
testdata/errors/ConflictingTypeNames.schema.graphql:2: conflicting definition for T; this can indicate either a genqlient internal error, a conflict between user-specified type-names, or some very tricksy GraphQL field/type names: expected 2 fields, got 1