Skip to content

Commit 6e2b1a5

Browse files
Fix broken behavior for several options on the same node (#105)
## Summary: Previously, we actually allowed you to put several genqlient directives on the same node, but the semantics were undocumented (and somewhat confusing, when it comes to `typename`). In order to support directives on input options, we're actually going to be encouraging this usage (see notes in #14), so it's time to fix it. To avoid confusion, I just had conflicting directives be an error, rather than defining which one "wins". The same applies to specifying the same option several times in one directive. I also fixed two small bugs: - `typename` on an operation would incorrectly cascade down to all input types in a query (causing conflicts). - directive parse errors had useless positions, now they're correct ## Test plan: make check Author: benjaminjkraft Reviewers: StevenACoffman, dnerdy, aberkan, jvoll, mahtabsabet, MiguelCastillo Required Reviewers: Approved By: StevenACoffman, dnerdy 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: #105
1 parent 8de55d3 commit 6e2b1a5

13 files changed

+293
-39
lines changed

docs/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ When releasing a new version:
2828
### New features:
2929

3030
- The new `bindings.marshaler` and `bindings.unmarshaler` options in `genqlient.yaml` allow binding to a type without using its standard JSON serialization; see the [documentation](genqlient.yaml) for details.
31+
- Multiple genqlient directives may now be applied to the same node, as long as they don't conflict; see the [directive documentation](genqlient_directive.graphql) for details.
3132

3233
### Bug fixes:
3334

docs/genqlient_directive.graphql

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,15 +21,25 @@
2121
# # @genqlient(n: "c")
2222
# query MyQuery(arg1: String,
2323
# # @genqlient(n: "d")
24-
# arg2: String, arg3: String,
24+
# arg2: String, arg3: MyInput,
2525
# arg4: String,
2626
# ) {
2727
# # @genqlient(n: "e")
2828
# field1, field2
29-
# field3
29+
# # @genqlient(n: "f")
30+
# field3 {
31+
# field4
32+
# }
3033
# }
3134
# the directive "a" is ignored, "b" and "c" apply to all relevant nodes in the
32-
# query, "d" applies to arg2 and arg3, and "e" applies to field1 and field2.
35+
# query, "d" applies to arg2 and arg3, "e" applies to field1 and field2, and
36+
# "f" applies to field3.
37+
#
38+
# Except as noted below, directives on nodes take precedence over ones on the
39+
# entire query (so "d", "e", and "f" take precedence over "b" and "c"), and
40+
# multiple directives on the same node ("b" and "c") must not conflict. Note
41+
# that directives on nodes do *not* apply to their "children", so "d" does not
42+
# apply to the fields of MyInput, and "f" does not apply to field4.
3343
directive genqlient(
3444

3545
# If set, this argument will be omitted if it has an empty value, defined
@@ -125,7 +135,9 @@ directive genqlient(
125135
# down to all child fields (which would cause conflicts).
126136
typename: String
127137

128-
) on
138+
# Multiple genqlient directives are allowed in the same location, as long as
139+
# they don't have conflicting options.
140+
) repeatable on
129141
# genqlient directives can go almost anywhere, although some options are only
130142
# applicable in certain locations as described above.
131143
| QUERY

generate/convert.go

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -290,7 +290,7 @@ func (g *generator) convertDefinition(
290290
if options.TypeName != "" {
291291
// If the user specified a name, use it!
292292
name = options.TypeName
293-
if namePrefix.head == name && namePrefix.tail == nil {
293+
if namePrefix != nil && namePrefix.head == name && namePrefix.tail == nil {
294294
// Special case: if this name is also the only component of the
295295
// name-prefix, append the type-name anyway. This happens when you
296296
// assign a type name to an interface type, and we are generating
@@ -367,10 +367,12 @@ func (g *generator) convertDefinition(
367367

368368
for i, field := range def.Fields {
369369
goName := upperFirst(field.Name)
370-
// Several of the arguments don't really make sense here
370+
// There are no field-specific options for inputs (yet, see #14),
371+
// but we still need to merge with an empty directive to clear out
372+
// any query-options that shouldn't apply here (namely "typename").
373+
fieldOptions := queryOptions.merge(newGenqlientDirective(pos))
374+
// Several of the arguments don't really make sense here:
371375
// (note field.Type is necessarily a scalar, input, or enum)
372-
// - no field-specific options can apply, because this is
373-
// a field in the type, not in the query (see also #14).
374376
// - namePrefix is ignored for input types and enums (see
375377
// names.go) and for scalars (they use client-specified
376378
// names)
@@ -381,7 +383,7 @@ func (g *generator) convertDefinition(
381383
// will be ignored? We know field.Type is a scalar, enum, or input
382384
// type. But plumbing that is a bit tricky in practice.
383385
fieldGoType, err := g.convertType(
384-
namePrefix, field.Type, nil, queryOptions, queryOptions)
386+
namePrefix, field.Type, nil, fieldOptions, queryOptions)
385387
if err != nil {
386388
return nil, err
387389
}

generate/genqlient_directive.go

Lines changed: 57 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -19,66 +19,84 @@ type genqlientDirective struct {
1919
TypeName string
2020
}
2121

22+
func newGenqlientDirective(pos *ast.Position) *genqlientDirective {
23+
return &genqlientDirective{
24+
pos: pos,
25+
}
26+
}
27+
2228
func (dir *genqlientDirective) GetOmitempty() bool { return dir.Omitempty != nil && *dir.Omitempty }
2329
func (dir *genqlientDirective) GetPointer() bool { return dir.Pointer != nil && *dir.Pointer }
2430
func (dir *genqlientDirective) GetStruct() bool { return dir.Struct != nil && *dir.Struct }
2531

26-
func setBool(dst **bool, v *ast.Value) error {
32+
func setBool(optionName string, dst **bool, v *ast.Value, pos *ast.Position) error {
33+
if *dst != nil {
34+
return errorf(pos, "conflicting values for %v", optionName)
35+
}
2736
ei, err := v.Value(nil) // no vars allowed
2837
if err != nil {
29-
return errorf(v.Position, "invalid boolean value %v: %v", v, err)
38+
return errorf(pos, "invalid boolean value %v: %v", v, err)
3039
}
3140
if b, ok := ei.(bool); ok {
3241
*dst = &b
3342
return nil
3443
}
35-
return errorf(v.Position, "expected boolean, got non-boolean value %T(%v)", ei, ei)
44+
return errorf(pos, "expected boolean, got non-boolean value %T(%v)", ei, ei)
3645
}
3746

38-
func setString(dst *string, v *ast.Value) error {
47+
func setString(optionName string, dst *string, v *ast.Value, pos *ast.Position) error {
48+
if *dst != "" {
49+
return errorf(pos, "conflicting values for %v", optionName)
50+
}
3951
ei, err := v.Value(nil) // no vars allowed
4052
if err != nil {
41-
return errorf(v.Position, "invalid string value %v: %v", v, err)
53+
return errorf(pos, "invalid string value %v: %v", v, err)
4254
}
4355
if b, ok := ei.(string); ok {
4456
*dst = b
4557
return nil
4658
}
47-
return errorf(v.Position, "expected string, got non-string value %T(%v)", ei, ei)
59+
return errorf(pos, "expected string, got non-string value %T(%v)", ei, ei)
4860
}
4961

50-
func fromGraphQL(dir *ast.Directive, pos *ast.Position) (*genqlientDirective, error) {
51-
if dir.Name != "genqlient" {
62+
// add adds to this genqlientDirective struct the settings from then given
63+
// GraphQL directive.
64+
//
65+
// If there are multiple genqlient directives are applied to the same node,
66+
// e.g.
67+
// # @genqlient(...)
68+
// # @genqlient(...)
69+
// add will be called several times. In this case, conflicts between the
70+
// options are an error.
71+
func (dir *genqlientDirective) add(graphQLDirective *ast.Directive, pos *ast.Position) error {
72+
if graphQLDirective.Name != "genqlient" {
5273
// Actually we just won't get here; we only get here if the line starts
5374
// with "# @genqlient", unless there's some sort of bug.
54-
return nil, errorf(pos, "the only valid comment-directive is @genqlient, got %v", dir.Name)
75+
return errorf(pos, "the only valid comment-directive is @genqlient, got %v", graphQLDirective.Name)
5576
}
5677

57-
var retval genqlientDirective
58-
retval.pos = pos
59-
6078
var err error
61-
for _, arg := range dir.Arguments {
79+
for _, arg := range graphQLDirective.Arguments {
6280
switch arg.Name {
63-
// TODO: reflect and struct tags?
81+
// TODO(benkraft): Use reflect and struct tags?
6482
case "omitempty":
65-
err = setBool(&retval.Omitempty, arg.Value)
83+
err = setBool("omitempty", &dir.Omitempty, arg.Value, pos)
6684
case "pointer":
67-
err = setBool(&retval.Pointer, arg.Value)
85+
err = setBool("pointer", &dir.Pointer, arg.Value, pos)
6886
case "struct":
69-
err = setBool(&retval.Struct, arg.Value)
87+
err = setBool("struct", &dir.Struct, arg.Value, pos)
7088
case "bind":
71-
err = setString(&retval.Bind, arg.Value)
89+
err = setString("bind", &dir.Bind, arg.Value, pos)
7290
case "typename":
73-
err = setString(&retval.TypeName, arg.Value)
91+
err = setString("typename", &dir.TypeName, arg.Value, pos)
7492
default:
75-
return nil, errorf(pos, "unknown argument %v for @genqlient", arg.Name)
93+
return errorf(pos, "unknown argument %v for @genqlient", arg.Name)
7694
}
7795
if err != nil {
78-
return nil, err
96+
return err
7997
}
8098
}
81-
return &retval, nil
99+
return nil
82100
}
83101

84102
func (dir *genqlientDirective) validate(node interface{}, schema *ast.Schema) error {
@@ -185,11 +203,16 @@ func (dir *genqlientDirective) merge(other *genqlientDirective) *genqlientDirect
185203
return &retval
186204
}
187205

206+
// parsePrecedingComment looks at the comment right before this node, and
207+
// returns the genqlient directive applied to it (or an empty one if there is
208+
// none), the remaining human-readable comment (or "" if there is none), and an
209+
// error if the directive is invalid.
188210
func (g *generator) parsePrecedingComment(
189211
node interface{},
190212
pos *ast.Position,
191213
) (comment string, directive *genqlientDirective, err error) {
192-
directive = new(genqlientDirective)
214+
directive = newGenqlientDirective(pos)
215+
hasDirective := false
193216
if pos == nil || pos.Src == nil { // node was added by genqlient itself
194217
return "", directive, nil // treated as if there were no comment
195218
}
@@ -200,26 +223,30 @@ func (g *generator) parsePrecedingComment(
200223
line := strings.TrimSpace(sourceLines[i-1])
201224
trimmed := strings.TrimSpace(strings.TrimPrefix(line, "#"))
202225
if strings.HasPrefix(line, "# @genqlient") {
203-
graphQLDirective, err := parseDirective(trimmed, pos)
226+
hasDirective = true
227+
var graphQLDirective *ast.Directive
228+
graphQLDirective, err = parseDirective(trimmed, pos)
204229
if err != nil {
205230
return "", nil, err
206231
}
207-
genqlientDirective, err := fromGraphQL(graphQLDirective, pos)
232+
err = directive.add(graphQLDirective, pos)
208233
if err != nil {
209234
return "", nil, err
210235
}
211-
err = genqlientDirective.validate(node, g.schema)
212-
if err != nil {
213-
return "", nil, err
214-
}
215-
directive = directive.merge(genqlientDirective)
216236
} else if strings.HasPrefix(line, "#") {
217237
commentLines = append(commentLines, trimmed)
218238
} else {
219239
break
220240
}
221241
}
222242

243+
if hasDirective { // (else directive is empty)
244+
err = directive.validate(node, g.schema)
245+
if err != nil {
246+
return "", nil, err
247+
}
248+
}
249+
223250
reverse(commentLines)
224251

225252
return strings.TrimSpace(strings.Join(commentLines, "\n")), directive, nil
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
# @genqlient(pointer: true, pointer: false)
2+
query ConflictingDirectiveArguments { f }
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
type Query {
2+
f: String
3+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# @genqlient(pointer: true)
2+
# @genqlient(pointer: false)
3+
query ConflictingDirectives { f }
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
type Query {
2+
f: String
3+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
# @genqlient(typename: "MyMultipleDirectivesResponse")
2+
# @genqlient(omitempty: true)
3+
# @genqlient(pointer: true)
4+
query MultipleDirectives(
5+
# @genqlient(pointer: false)
6+
# @genqlient(typename: "MyInput")
7+
$query: UserQueryInput,
8+
$queries: [UserQueryInput],
9+
) {
10+
user(query: $query) { id }
11+
users(query: $queries) { id }
12+
}

0 commit comments

Comments
 (0)