Skip to content

Commit 5211442

Browse files
Add a new option to treat an interface like an object (#97)
## Summary: The basic idea here is if you only request interface fields (no fragments) you may not care about the concrete type, and so we could just generate a struct as if it were an object. I don't think it's a good idea to do that by default, because then if you later add a fragment all your code totally changes, but it's quite reasonable as an option! Most of the code involved is just wiring and validation; the core implementation is literally just: treat it like an object. Issue: #85 ## Test plan: make check Author: benjaminjkraft Reviewers: csilvers, StevenACoffman, benjaminjkraft, aberkan, dnerdy, jvoll, mahtabsabet, MiguelCastillo Required Reviewers: Approved By: StevenACoffman 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: #97
1 parent 463cffd commit 5211442

14 files changed

+477
-9
lines changed

docs/FAQ.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -183,6 +183,28 @@ if novel, ok := resp.Favorite.(*GetBooksFavoriteNovel); ok {
183183

184184
The interface-type's GoDoc will include a list of its implementations, for your convenience.
185185

186+
If you only want to request shared fields of the interface (i.e. no fragments), this may seem like a lot of ceremony. If you prefer, you can instead add `# @genqlient(struct: true)` to the field, and genqlient will just generate a struct, like it does for GraphQL object types. For example, given:
187+
188+
```graphql
189+
query GetBooks {
190+
# @genqlient(struct: true)
191+
favorite {
192+
title
193+
}
194+
}
195+
```
196+
197+
genqlient will generate just:
198+
199+
```go
200+
type GetBooksFavoriteBook struct {
201+
Title string
202+
}
203+
```
204+
205+
Keep in mind that if you later want to add fragments to your selection, you won't be able to use `struct` anymore; when you remove it you may need to update your code to replace `.Title` with `.GetTitle()` and so on.
206+
207+
186208
### … documentation on the output types?
187209

188210
For any GraphQL types or fields with documentation in the GraphQL schema, genqlient automatically includes that documentation in the generated code's GoDoc. To add additional information to genqlient entrypoints, you can put comments in the GraphQL source:

docs/genqlient_directive.graphql

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,22 @@ directive genqlient(
5454
# zero value and null (for nullable fields).
5555
pointer: Boolean
5656

57+
# If set, this field will use a struct type in Go, even if it's an interface.
58+
#
59+
# This is useful when you have a query like
60+
# query MyQuery {
61+
# myInterface { myField }
62+
# }
63+
# where you are requesting only shared fields of an interface. By default,
64+
# genqlient still generates an interface type, for consistency. But this
65+
# isn't necessary: a struct would do just fine since there are no
66+
# type-specific fields. Setting `struct: true` tells genqlient to do that.
67+
#
68+
# Note that this is only allowed when there are no fragments in play, such
69+
# that all fields are on the interface type. Note that if you later add a
70+
# fragment, you'll have to remove this option, and the types will change.
71+
struct: Boolean
72+
5773
# If set, this argument or field will use the given Go type instead of a
5874
# genqlient-generated type.
5975
#

generate/convert.go

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -175,7 +175,14 @@ func (g *generator) convertDefinition(
175175
GraphQLName: def.Name,
176176
}
177177

178-
switch def.Kind {
178+
// The struct option basically means "treat this as if it were an object".
179+
// (It only applies if valid; this is important if you said the whole
180+
// query should have `struct: true`.)
181+
kind := def.Kind
182+
if options.GetStruct() && validateStructOption(def, selectionSet, pos) == nil {
183+
kind = ast.Object
184+
}
185+
switch kind {
179186
case ast.Object:
180187
name := makeTypeName(namePrefix, def.Name)
181188

@@ -463,7 +470,7 @@ func (g *generator) convertInlineFragment(
463470
containingTypedef *ast.Definition,
464471
queryOptions *genqlientDirective,
465472
) ([]*goStructField, error) {
466-
// You might think fragmentTypedef would be a fragment.ObjectDefinition, but
473+
// You might think fragmentTypedef is just fragment.ObjectDefinition, but
467474
// actually that's the type into which the fragment is spread.
468475
fragmentTypedef := g.schema.Types[fragment.TypeCondition]
469476
if !fragmentMatches(containingTypedef, fragmentTypedef) {

generate/generate.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -209,6 +209,8 @@ func (g *generator) preprocessQueryDocument(doc *ast.QueryDocument) {
209209
// needed). Note this does mean abstract-typed fragments spread into
210210
// object-typed scope will *not* have access to `__typename`, but they
211211
// indeed don't need it, since we do know the type in that context.
212+
// TODO(benkraft): We should omit __typename if you asked for
213+
// `# @genqlient(struct: true)`.
212214
observers.OnField(func(_ *validator.Walker, field *ast.Field) {
213215
// We are interested in a field from the query like
214216
// field { subField ... }

generate/genqlient_directive.go

Lines changed: 59 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,13 @@ type genqlientDirective struct {
1414
pos *ast.Position
1515
Omitempty *bool
1616
Pointer *bool
17+
Struct *bool
1718
Bind string
1819
}
1920

2021
func (dir *genqlientDirective) GetOmitempty() bool { return dir.Omitempty != nil && *dir.Omitempty }
2122
func (dir *genqlientDirective) GetPointer() bool { return dir.Pointer != nil && *dir.Pointer }
23+
func (dir *genqlientDirective) GetStruct() bool { return dir.Struct != nil && *dir.Struct }
2224

2325
func setBool(dst **bool, v *ast.Value) error {
2426
ei, err := v.Value(nil) // no vars allowed
@@ -44,15 +46,15 @@ func setString(dst *string, v *ast.Value) error {
4446
return errorf(v.Position, "expected string, got non-string value %T(%v)", ei, ei)
4547
}
4648

47-
func fromGraphQL(dir *ast.Directive) (*genqlientDirective, error) {
49+
func fromGraphQL(dir *ast.Directive, pos *ast.Position) (*genqlientDirective, error) {
4850
if dir.Name != "genqlient" {
4951
// Actually we just won't get here; we only get here if the line starts
5052
// with "# @genqlient", unless there's some sort of bug.
51-
return nil, errorf(dir.Position, "the only valid comment-directive is @genqlient, got %v", dir.Name)
53+
return nil, errorf(pos, "the only valid comment-directive is @genqlient, got %v", dir.Name)
5254
}
5355

5456
var retval genqlientDirective
55-
retval.pos = dir.Position
57+
retval.pos = pos
5658

5759
var err error
5860
for _, arg := range dir.Arguments {
@@ -62,10 +64,12 @@ func fromGraphQL(dir *ast.Directive) (*genqlientDirective, error) {
6264
err = setBool(&retval.Omitempty, arg.Value)
6365
case "pointer":
6466
err = setBool(&retval.Pointer, arg.Value)
67+
case "struct":
68+
err = setBool(&retval.Struct, arg.Value)
6569
case "bind":
6670
err = setString(&retval.Bind, arg.Value)
6771
default:
68-
return nil, errorf(arg.Position, "unknown argument %v for @genqlient", arg.Name)
72+
return nil, errorf(pos, "unknown argument %v for @genqlient", arg.Name)
6973
}
7074
if err != nil {
7175
return nil, err
@@ -74,7 +78,7 @@ func fromGraphQL(dir *ast.Directive) (*genqlientDirective, error) {
7478
return &retval, nil
7579
}
7680

77-
func (dir *genqlientDirective) validate(node interface{}) error {
81+
func (dir *genqlientDirective) validate(node interface{}, schema *ast.Schema) error {
7882
switch node := node.(type) {
7983
case *ast.OperationDefinition:
8084
if dir.Bind != "" {
@@ -90,24 +94,69 @@ func (dir *genqlientDirective) validate(node interface{}) error {
9094
return errorf(dir.pos, "bind is not implemented for named fragments")
9195
}
9296

97+
if dir.Struct != nil {
98+
return errorf(dir.pos, "struct is only applicable to fields")
99+
}
100+
93101
// Like operations, anything else will just apply to the entire
94102
// fragment.
95103
return nil
96104
case *ast.VariableDefinition:
97105
if dir.Omitempty != nil && node.Type.NonNull {
98106
return errorf(dir.pos, "omitempty may only be used on optional arguments")
99107
}
108+
109+
if dir.Struct != nil {
110+
return errorf(dir.pos, "struct is only applicable to fields")
111+
}
112+
100113
return nil
101114
case *ast.Field:
102115
if dir.Omitempty != nil {
103116
return errorf(dir.pos, "omitempty is not applicable to fields")
104117
}
118+
119+
if dir.Struct != nil {
120+
typ := schema.Types[node.Definition.Type.Name()]
121+
if err := validateStructOption(typ, node.SelectionSet, dir.pos); err != nil {
122+
return err
123+
}
124+
}
125+
105126
return nil
106127
default:
107128
return errorf(dir.pos, "invalid @genqlient directive location: %T", node)
108129
}
109130
}
110131

132+
func validateStructOption(
133+
typ *ast.Definition,
134+
selectionSet ast.SelectionSet,
135+
pos *ast.Position,
136+
) error {
137+
if typ.Kind != ast.Interface && typ.Kind != ast.Union {
138+
return errorf(pos, "struct is only applicable to interface-typed fields")
139+
}
140+
141+
// Make sure that all the requested fields apply to the interface itself
142+
// (not just certain implementations).
143+
for _, selection := range selectionSet {
144+
switch selection.(type) {
145+
case *ast.Field:
146+
// fields are fine.
147+
case *ast.InlineFragment, *ast.FragmentSpread:
148+
// Fragments aren't allowed. In principle we could allow them under
149+
// the condition that the fragment applies to the whole interface
150+
// (not just one implementation; and so on recursively), and for
151+
// fragment spreads additionally that the fragment has the same
152+
// option applied to it, but it seems more trouble than it's worth
153+
// right now.
154+
return errorf(pos, "struct is not allowed for types with fragments")
155+
}
156+
}
157+
return nil
158+
}
159+
111160
func (dir *genqlientDirective) merge(other *genqlientDirective) *genqlientDirective {
112161
retval := *dir
113162
if other.Omitempty != nil {
@@ -116,6 +165,9 @@ func (dir *genqlientDirective) merge(other *genqlientDirective) *genqlientDirect
116165
if other.Pointer != nil {
117166
retval.Pointer = other.Pointer
118167
}
168+
if other.Struct != nil {
169+
retval.Struct = other.Struct
170+
}
119171
if other.Bind != "" {
120172
retval.Bind = other.Bind
121173
}
@@ -141,11 +193,11 @@ func (g *generator) parsePrecedingComment(
141193
if err != nil {
142194
return "", nil, err
143195
}
144-
genqlientDirective, err := fromGraphQL(graphQLDirective)
196+
genqlientDirective, err := fromGraphQL(graphQLDirective, pos)
145197
if err != nil {
146198
return "", nil, err
147199
}
148-
err = genqlientDirective.validate(node)
200+
err = genqlientDirective.validate(node, g.schema)
149201
if err != nil {
150202
return "", nil, err
151203
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
query StructOptionOnObject {
2+
# @genqlient(struct: true)
3+
myObject {
4+
f
5+
}
6+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
type Query {
2+
myObject: MyObject
3+
}
4+
5+
type MyObject {
6+
f: String!
7+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
query StructOptionOnObject {
2+
# @genqlient(struct: true)
3+
myInterface {
4+
f
5+
... on MyObject {
6+
g
7+
}
8+
}
9+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
type Query {
2+
myInterface: MyInterface
3+
}
4+
5+
interface MyInterface {
6+
f: String!
7+
}
8+
9+
type MyObject implements MyInterface {
10+
f: String!
11+
g: String!
12+
}
13+
14+
type OtherObject implements MyInterface {
15+
f: String!
16+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
fragment VideoFields on Video { duration }
2+
3+
# @genqlient(struct: true)
4+
query StructOption {
5+
root {
6+
id
7+
children {
8+
id
9+
parent {
10+
id
11+
children {
12+
id
13+
}
14+
# (it won't apply to this)
15+
interfaceChildren: children {
16+
id
17+
...VideoFields
18+
}
19+
}
20+
}
21+
}
22+
# (nor this)
23+
user { roles }
24+
}

0 commit comments

Comments
 (0)