Add support for inline fragments (#65)

## Summary:
In this commit I add support for inline fragments
(`... on MyType { fields }`) to genqlient.  This will make interfaces a
lot more useful!  In future commits I'll add named fragments, for which
we'll generate slightly different types, as discussed in DESIGN.md.

In general, implementing the flattening approach described in DESIGN.md
was... surprisingly easy.  All we have to do is recurse on applicable
fragments when generating our selection-set.  The refactor to
selection-set handling this encouraged was, I think, quite beneficial.
It did reveal two tricky pre-existing issues.

One issue is that GraphQL allows for duplicate selections, as long as
they match.  (In practice, this is only useful in the context of
fragments, although GraphQL allows it even without.) I decided to handle
the simple case (duplicate leaf fields; we just deduplicate) but leave
to the future the complex cases where we need to merge different
sub-selections (now #64).  For now we just forbid that; we can see how
much it comes up.

The other issue is that we are generating type-names incorrectly for
interface types; I had intended to do `MyInterfaceMyFieldMyType` for
shared fields and `MyImplMyFieldMyType` for non-shared ones, but instead
I did `MyFieldMyType`, which is inconsistent already and can result in
conflicts in the presence of fragments.  I'm going to fix this in a
separate commit, though, because it's going to require some refactoring
and is irrelevant to the main logic of this commit; I left some TODOs in
the tests related to this.

Issue: https://github.com/Khan/genqlient/issues/8

## Test plan:
make check


Author: benjaminjkraft

Reviewers: dnerdy, aberkan, MiguelCastillo

Required Reviewers: 

Approved by: dnerdy

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

Pull request URL: https://github.com/Khan/genqlient/pull/65
This commit is contained in:
Ben Kraft
2021-08-27 18:06:30 -07:00
committed by GitHub
parent e99dced757
commit e6b1984d44
19 changed files with 2270 additions and 66 deletions
+372 -6
View File
@@ -43,20 +43,31 @@ type DirectiveRoot struct {
type ComplexityRoot struct {
Animal struct {
Hair func(childComplexity int) int
ID func(childComplexity int) int
Name func(childComplexity int) int
Owner func(childComplexity int) int
Species func(childComplexity int) int
}
BeingsHair struct {
HasHair func(childComplexity int) int
}
Hair struct {
Color func(childComplexity int) int
}
Query struct {
Being func(childComplexity int, id string) int
Beings func(childComplexity int, ids []string) int
Me func(childComplexity int) int
User func(childComplexity int, id string) int
Being func(childComplexity int, id string) int
Beings func(childComplexity int, ids []string) int
LotteryWinner func(childComplexity int, number int) int
Me func(childComplexity int) int
User func(childComplexity int, id string) int
}
User struct {
Hair func(childComplexity int) int
ID func(childComplexity int) int
LuckyNumber func(childComplexity int) int
Name func(childComplexity int) int
@@ -68,6 +79,7 @@ type QueryResolver interface {
User(ctx context.Context, id string) (*User, error)
Being(ctx context.Context, id string) (Being, error)
Beings(ctx context.Context, ids []string) ([]Being, error)
LotteryWinner(ctx context.Context, number int) (Lucky, error)
}
type executableSchema struct {
@@ -85,6 +97,13 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
_ = ec
switch typeName + "." + field {
case "Animal.hair":
if e.complexity.Animal.Hair == nil {
break
}
return e.complexity.Animal.Hair(childComplexity), true
case "Animal.id":
if e.complexity.Animal.ID == nil {
break
@@ -113,6 +132,20 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
return e.complexity.Animal.Species(childComplexity), true
case "BeingsHair.hasHair":
if e.complexity.BeingsHair.HasHair == nil {
break
}
return e.complexity.BeingsHair.HasHair(childComplexity), true
case "Hair.color":
if e.complexity.Hair.Color == nil {
break
}
return e.complexity.Hair.Color(childComplexity), true
case "Query.being":
if e.complexity.Query.Being == nil {
break
@@ -137,6 +170,18 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
return e.complexity.Query.Beings(childComplexity, args["ids"].([]string)), true
case "Query.lotteryWinner":
if e.complexity.Query.LotteryWinner == nil {
break
}
args, err := ec.field_Query_lotteryWinner_args(context.TODO(), rawArgs)
if err != nil {
return 0, false
}
return e.complexity.Query.LotteryWinner(childComplexity, args["number"].(int)), true
case "Query.me":
if e.complexity.Query.Me == nil {
break
@@ -156,6 +201,13 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
return e.complexity.Query.User(childComplexity, args["id"].(string)), true
case "User.hair":
if e.complexity.User.Hair == nil {
break
}
return e.complexity.User.Hair(childComplexity), true
case "User.id":
if e.complexity.User.ID == nil {
break
@@ -232,21 +284,28 @@ var sources = []*ast.Source{
user(id: ID!): User
being(id: ID!): Being
beings(ids: [ID!]!): [Being]!
lotteryWinner(number: Int!): Lucky
}
type User implements Being {
type User implements Being & Lucky {
id: ID!
name: String!
luckyNumber: Int
hair: Hair
}
type Hair { color: String } # silly name to confuse the name-generator
type Animal implements Being {
id: ID!
name: String!
species: Species!
owner: Being
hair: BeingsHair
}
type BeingsHair { hasHair: Boolean! } # silly name to confuse the name-generator
enum Species {
DOG
COELACANTH
@@ -256,6 +315,10 @@ interface Being {
id: ID!
name: String!
}
interface Lucky {
luckyNumber: Int
}
`, BuiltIn: false},
}
var parsedSchema = gqlparser.MustLoadSchema(sources...)
@@ -309,6 +372,21 @@ func (ec *executionContext) field_Query_beings_args(ctx context.Context, rawArgs
return args, nil
}
func (ec *executionContext) field_Query_lotteryWinner_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) {
var err error
args := map[string]interface{}{}
var arg0 int
if tmp, ok := rawArgs["number"]; ok {
ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("number"))
arg0, err = ec.unmarshalNInt2int(ctx, tmp)
if err != nil {
return nil, err
}
}
args["number"] = arg0
return args, nil
}
func (ec *executionContext) field_Query_user_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) {
var err error
args := map[string]interface{}{}
@@ -499,6 +577,105 @@ func (ec *executionContext) _Animal_owner(ctx context.Context, field graphql.Col
return ec.marshalOBeing2githubᚗcomᚋKhanᚋgenqlientᚋinternalᚋintegrationᚋserverᚐBeing(ctx, field.Selections, res)
}
func (ec *executionContext) _Animal_hair(ctx context.Context, field graphql.CollectedField, obj *Animal) (ret graphql.Marshaler) {
defer func() {
if r := recover(); r != nil {
ec.Error(ctx, ec.Recover(ctx, r))
ret = graphql.Null
}
}()
fc := &graphql.FieldContext{
Object: "Animal",
Field: field,
Args: nil,
IsMethod: false,
IsResolver: false,
}
ctx = graphql.WithFieldContext(ctx, fc)
resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
ctx = rctx // use context from middleware stack in children
return obj.Hair, nil
})
if err != nil {
ec.Error(ctx, err)
return graphql.Null
}
if resTmp == nil {
return graphql.Null
}
res := resTmp.(*BeingsHair)
fc.Result = res
return ec.marshalOBeingsHair2ᚖgithubᚗcomᚋKhanᚋgenqlientᚋinternalᚋintegrationᚋserverᚐBeingsHair(ctx, field.Selections, res)
}
func (ec *executionContext) _BeingsHair_hasHair(ctx context.Context, field graphql.CollectedField, obj *BeingsHair) (ret graphql.Marshaler) {
defer func() {
if r := recover(); r != nil {
ec.Error(ctx, ec.Recover(ctx, r))
ret = graphql.Null
}
}()
fc := &graphql.FieldContext{
Object: "BeingsHair",
Field: field,
Args: nil,
IsMethod: false,
IsResolver: false,
}
ctx = graphql.WithFieldContext(ctx, fc)
resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
ctx = rctx // use context from middleware stack in children
return obj.HasHair, nil
})
if err != nil {
ec.Error(ctx, err)
return graphql.Null
}
if resTmp == nil {
if !graphql.HasFieldError(ctx, fc) {
ec.Errorf(ctx, "must not be null")
}
return graphql.Null
}
res := resTmp.(bool)
fc.Result = res
return ec.marshalNBoolean2bool(ctx, field.Selections, res)
}
func (ec *executionContext) _Hair_color(ctx context.Context, field graphql.CollectedField, obj *Hair) (ret graphql.Marshaler) {
defer func() {
if r := recover(); r != nil {
ec.Error(ctx, ec.Recover(ctx, r))
ret = graphql.Null
}
}()
fc := &graphql.FieldContext{
Object: "Hair",
Field: field,
Args: nil,
IsMethod: false,
IsResolver: false,
}
ctx = graphql.WithFieldContext(ctx, fc)
resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
ctx = rctx // use context from middleware stack in children
return obj.Color, nil
})
if err != nil {
ec.Error(ctx, err)
return graphql.Null
}
if resTmp == nil {
return graphql.Null
}
res := resTmp.(*string)
fc.Result = res
return ec.marshalOString2ᚖstring(ctx, field.Selections, res)
}
func (ec *executionContext) _Query_me(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) {
defer func() {
if r := recover(); r != nil {
@@ -651,6 +828,45 @@ func (ec *executionContext) _Query_beings(ctx context.Context, field graphql.Col
return ec.marshalNBeing2ᚕgithubᚗcomᚋKhanᚋgenqlientᚋinternalᚋintegrationᚋserverᚐBeing(ctx, field.Selections, res)
}
func (ec *executionContext) _Query_lotteryWinner(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) {
defer func() {
if r := recover(); r != nil {
ec.Error(ctx, ec.Recover(ctx, r))
ret = graphql.Null
}
}()
fc := &graphql.FieldContext{
Object: "Query",
Field: field,
Args: nil,
IsMethod: true,
IsResolver: true,
}
ctx = graphql.WithFieldContext(ctx, fc)
rawArgs := field.ArgumentMap(ec.Variables)
args, err := ec.field_Query_lotteryWinner_args(ctx, rawArgs)
if err != nil {
ec.Error(ctx, err)
return graphql.Null
}
fc.Args = args
resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
ctx = rctx // use context from middleware stack in children
return ec.resolvers.Query().LotteryWinner(rctx, args["number"].(int))
})
if err != nil {
ec.Error(ctx, err)
return graphql.Null
}
if resTmp == nil {
return graphql.Null
}
res := resTmp.(Lucky)
fc.Result = res
return ec.marshalOLucky2githubᚗcomᚋKhanᚋgenqlientᚋinternalᚋintegrationᚋserverᚐLucky(ctx, field.Selections, res)
}
func (ec *executionContext) _Query___type(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) {
defer func() {
if r := recover(); r != nil {
@@ -824,6 +1040,38 @@ func (ec *executionContext) _User_luckyNumber(ctx context.Context, field graphql
return ec.marshalOInt2ᚖint(ctx, field.Selections, res)
}
func (ec *executionContext) _User_hair(ctx context.Context, field graphql.CollectedField, obj *User) (ret graphql.Marshaler) {
defer func() {
if r := recover(); r != nil {
ec.Error(ctx, ec.Recover(ctx, r))
ret = graphql.Null
}
}()
fc := &graphql.FieldContext{
Object: "User",
Field: field,
Args: nil,
IsMethod: false,
IsResolver: false,
}
ctx = graphql.WithFieldContext(ctx, fc)
resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
ctx = rctx // use context from middleware stack in children
return obj.Hair, nil
})
if err != nil {
ec.Error(ctx, err)
return graphql.Null
}
if resTmp == nil {
return graphql.Null
}
res := resTmp.(*Hair)
fc.Result = res
return ec.marshalOHair2ᚖgithubᚗcomᚋKhanᚋgenqlientᚋinternalᚋintegrationᚋserverᚐHair(ctx, field.Selections, res)
}
func (ec *executionContext) ___Directive_name(ctx context.Context, field graphql.CollectedField, obj *introspection.Directive) (ret graphql.Marshaler) {
defer func() {
if r := recover(); r != nil {
@@ -1938,6 +2186,22 @@ func (ec *executionContext) _Being(ctx context.Context, sel ast.SelectionSet, ob
}
}
func (ec *executionContext) _Lucky(ctx context.Context, sel ast.SelectionSet, obj Lucky) graphql.Marshaler {
switch obj := (obj).(type) {
case nil:
return graphql.Null
case User:
return ec._User(ctx, sel, &obj)
case *User:
if obj == nil {
return graphql.Null
}
return ec._User(ctx, sel, obj)
default:
panic(fmt.Errorf("unexpected type %T", obj))
}
}
// endregion ************************** interface.gotpl ***************************
// region **************************** object.gotpl ****************************
@@ -1970,6 +2234,59 @@ func (ec *executionContext) _Animal(ctx context.Context, sel ast.SelectionSet, o
}
case "owner":
out.Values[i] = ec._Animal_owner(ctx, field, obj)
case "hair":
out.Values[i] = ec._Animal_hair(ctx, field, obj)
default:
panic("unknown field " + strconv.Quote(field.Name))
}
}
out.Dispatch()
if invalids > 0 {
return graphql.Null
}
return out
}
var beingsHairImplementors = []string{"BeingsHair"}
func (ec *executionContext) _BeingsHair(ctx context.Context, sel ast.SelectionSet, obj *BeingsHair) graphql.Marshaler {
fields := graphql.CollectFields(ec.OperationContext, sel, beingsHairImplementors)
out := graphql.NewFieldSet(fields)
var invalids uint32
for i, field := range fields {
switch field.Name {
case "__typename":
out.Values[i] = graphql.MarshalString("BeingsHair")
case "hasHair":
out.Values[i] = ec._BeingsHair_hasHair(ctx, field, obj)
if out.Values[i] == graphql.Null {
invalids++
}
default:
panic("unknown field " + strconv.Quote(field.Name))
}
}
out.Dispatch()
if invalids > 0 {
return graphql.Null
}
return out
}
var hairImplementors = []string{"Hair"}
func (ec *executionContext) _Hair(ctx context.Context, sel ast.SelectionSet, obj *Hair) graphql.Marshaler {
fields := graphql.CollectFields(ec.OperationContext, sel, hairImplementors)
out := graphql.NewFieldSet(fields)
var invalids uint32
for i, field := range fields {
switch field.Name {
case "__typename":
out.Values[i] = graphql.MarshalString("Hair")
case "color":
out.Values[i] = ec._Hair_color(ctx, field, obj)
default:
panic("unknown field " + strconv.Quote(field.Name))
}
@@ -2043,6 +2360,17 @@ func (ec *executionContext) _Query(ctx context.Context, sel ast.SelectionSet) gr
}
return res
})
case "lotteryWinner":
field := field
out.Concurrently(i, func() (res graphql.Marshaler) {
defer func() {
if r := recover(); r != nil {
ec.Error(ctx, ec.Recover(ctx, r))
}
}()
res = ec._Query_lotteryWinner(ctx, field)
return res
})
case "__type":
out.Values[i] = ec._Query___type(ctx, field)
case "__schema":
@@ -2058,7 +2386,7 @@ func (ec *executionContext) _Query(ctx context.Context, sel ast.SelectionSet) gr
return out
}
var userImplementors = []string{"User", "Being"}
var userImplementors = []string{"User", "Being", "Lucky"}
func (ec *executionContext) _User(ctx context.Context, sel ast.SelectionSet, obj *User) graphql.Marshaler {
fields := graphql.CollectFields(ec.OperationContext, sel, userImplementors)
@@ -2081,6 +2409,8 @@ func (ec *executionContext) _User(ctx context.Context, sel ast.SelectionSet, obj
}
case "luckyNumber":
out.Values[i] = ec._User_luckyNumber(ctx, field, obj)
case "hair":
out.Values[i] = ec._User_hair(ctx, field, obj)
default:
panic("unknown field " + strconv.Quote(field.Name))
}
@@ -2434,6 +2764,21 @@ func (ec *executionContext) marshalNID2ᚕstringᚄ(ctx context.Context, sel ast
return ret
}
func (ec *executionContext) unmarshalNInt2int(ctx context.Context, v interface{}) (int, error) {
res, err := graphql.UnmarshalInt(v)
return res, graphql.ErrorOnPath(ctx, err)
}
func (ec *executionContext) marshalNInt2int(ctx context.Context, sel ast.SelectionSet, v int) graphql.Marshaler {
res := graphql.MarshalInt(v)
if res == graphql.Null {
if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) {
ec.Errorf(ctx, "must not be null")
}
}
return res
}
func (ec *executionContext) unmarshalNSpecies2githubᚗcomᚋKhanᚋgenqlientᚋinternalᚋintegrationᚋserverᚐSpecies(ctx context.Context, v interface{}) (Species, error) {
var res Species
err := res.UnmarshalGQL(v)
@@ -2695,6 +3040,13 @@ func (ec *executionContext) marshalOBeing2githubᚗcomᚋKhanᚋgenqlientᚋinte
return ec._Being(ctx, sel, v)
}
func (ec *executionContext) marshalOBeingsHair2ᚖgithubᚗcomᚋKhanᚋgenqlientᚋinternalᚋintegrationᚋserverᚐBeingsHair(ctx context.Context, sel ast.SelectionSet, v *BeingsHair) graphql.Marshaler {
if v == nil {
return graphql.Null
}
return ec._BeingsHair(ctx, sel, v)
}
func (ec *executionContext) unmarshalOBoolean2bool(ctx context.Context, v interface{}) (bool, error) {
res, err := graphql.UnmarshalBoolean(v)
return res, graphql.ErrorOnPath(ctx, err)
@@ -2719,6 +3071,13 @@ func (ec *executionContext) marshalOBoolean2ᚖbool(ctx context.Context, sel ast
return graphql.MarshalBoolean(*v)
}
func (ec *executionContext) marshalOHair2ᚖgithubᚗcomᚋKhanᚋgenqlientᚋinternalᚋintegrationᚋserverᚐHair(ctx context.Context, sel ast.SelectionSet, v *Hair) graphql.Marshaler {
if v == nil {
return graphql.Null
}
return ec._Hair(ctx, sel, v)
}
func (ec *executionContext) unmarshalOInt2ᚖint(ctx context.Context, v interface{}) (*int, error) {
if v == nil {
return nil, nil
@@ -2734,6 +3093,13 @@ func (ec *executionContext) marshalOInt2ᚖint(ctx context.Context, sel ast.Sele
return graphql.MarshalInt(*v)
}
func (ec *executionContext) marshalOLucky2githubᚗcomᚋKhanᚋgenqlientᚋinternalᚋintegrationᚋserverᚐLucky(ctx context.Context, sel ast.SelectionSet, v Lucky) graphql.Marshaler {
if v == nil {
return graphql.Null
}
return ec._Lucky(ctx, sel, v)
}
func (ec *executionContext) unmarshalOString2string(ctx context.Context, v interface{}) (string, error) {
res, err := graphql.UnmarshalString(v)
return res, graphql.ErrorOnPath(ctx, err)
+19 -4
View File
@@ -12,22 +12,37 @@ type Being interface {
IsBeing()
}
type Lucky interface {
IsLucky()
}
type Animal struct {
ID string `json:"id"`
Name string `json:"name"`
Species Species `json:"species"`
Owner Being `json:"owner"`
ID string `json:"id"`
Name string `json:"name"`
Species Species `json:"species"`
Owner Being `json:"owner"`
Hair *BeingsHair `json:"hair"`
}
func (Animal) IsBeing() {}
type BeingsHair struct {
HasHair bool `json:"hasHair"`
}
type Hair struct {
Color *string `json:"color"`
}
type User struct {
ID string `json:"id"`
Name string `json:"name"`
LuckyNumber *int `json:"luckyNumber"`
Hair *Hair `json:"hair"`
}
func (User) IsBeing() {}
func (User) IsLucky() {}
type Species string
+24 -5
View File
@@ -8,16 +8,26 @@ import (
"github.com/99designs/gqlgen/graphql/handler/transport"
)
func intptr(v int) *int { return &v }
func strptr(v string) *string { return &v }
func intptr(v int) *int { return &v }
var users = []*User{
{ID: "1", Name: "Yours Truly", LuckyNumber: intptr(17)},
{ID: "2", Name: "Raven", LuckyNumber: intptr(-1)},
{
ID: "1", Name: "Yours Truly", LuckyNumber: intptr(17),
Hair: &Hair{Color: strptr("Black")},
},
{ID: "2", Name: "Raven", LuckyNumber: intptr(-1), Hair: nil},
}
var animals = []*Animal{
{ID: "3", Name: "Fido", Species: SpeciesDog, Owner: userByID("0")},
{ID: "4", Name: "Old One", Species: SpeciesCoelacanth, Owner: nil},
{
ID: "3", Name: "Fido", Species: SpeciesDog, Owner: userByID("1"),
Hair: &BeingsHair{HasHair: true},
},
{
ID: "4", Name: "Old One", Species: SpeciesCoelacanth, Owner: nil,
Hair: &BeingsHair{HasHair: false},
},
}
func userByID(id string) *User {
@@ -63,6 +73,15 @@ func (r *queryResolver) Beings(ctx context.Context, ids []string) ([]Being, erro
return ret, nil
}
func (r *queryResolver) LotteryWinner(ctx context.Context, number int) (Lucky, error) {
for _, user := range users {
if user.LuckyNumber != nil && *user.LuckyNumber == number {
return user, nil
}
}
return nil, nil
}
func RunServer() *httptest.Server {
gqlgenServer := handler.New(NewExecutableSchema(Config{Resolvers: &resolver{}}))
gqlgenServer.AddTransport(transport.POST{})