Skip to content

Commit fcae8dd

Browse files
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: #60 Issue: #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: #94
1 parent 5211442 commit fcae8dd

20 files changed

+713
-47
lines changed

docs/FAQ.md

Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,18 @@ if errors.As(err, &errList) {
6666
}
6767
```
6868

69+
### … use custom scalars?
70+
71+
Just tell genqlient via the `bindings` option in `genqlient.yaml`:
72+
73+
```yaml
74+
bindings:
75+
DateTime:
76+
type: time.Time
77+
```
78+
79+
Make sure the given type has whatever logic is needed to convert to/from JSON (e.g. `MarshalJSON`/`UnmarshalJSON` or JSON tags). See the [`genqlient.yaml` documentation](genqlient.yaml) for the full syntax.
80+
6981
### … require 32-bit integers?
7082

7183
The GraphQL spec officially defines the `Int` type to be a [signed 32-bit integer](https://spec.graphql.org/draft/#sec-Int). GraphQL clients and servers vary wildly in their enforcement of this; for example:
@@ -205,6 +217,129 @@ type GetBooksFavoriteBook struct {
205217
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.
206218

207219

220+
### … shared types between different parts of the query?
221+
222+
Suppose you have a query which requests several different fields each of the same GraphQL type, e.g. `User` (or `[User]`):
223+
224+
```graphql
225+
query GetMonopolyPlayers {
226+
game {
227+
winner { id name }
228+
banker { id name }
229+
spectators { id name }
230+
}
231+
}
232+
```
233+
234+
This will produce a Go type like:
235+
```go
236+
type GetMonopolyPlayersGame struct {
237+
Winner GetMonopolyPlayersGameWinnerUser
238+
Banker GetMonopolyPlayersGameBankerUser
239+
Spectators []GetMonopolyPlayersGameSpectatorsUser
240+
}
241+
242+
type GetMonopolyPlayersGameWinnerUser struct {
243+
Id string
244+
Name string
245+
}
246+
247+
// (others similarly)
248+
```
249+
250+
But maybe you wanted to be able to pass all those users to a shared function (defined in your code), say `FormatUser(user ???) string`. That's no good; you need to put three different types as the `???`. genqlient has two ways to deal with this.
251+
252+
One option -- the GraphQL Way, perhaps -- is to use fragments. You'd write your query like:
253+
254+
```graphql
255+
fragment MonopolyUser on User {
256+
id
257+
name
258+
}
259+
260+
query GetMonopolyPlayers {
261+
game {
262+
winner { ...MonopolyUser }
263+
banker { ...MonopolyUser }
264+
spectators { ...MonopolyUser }
265+
}
266+
}
267+
```
268+
269+
genqlient will notice this, and generate a type corresponding to the fragment; `GetMonopolyPlayersGame` will look as before, but each of the field types will have a shared embed:
270+
271+
```go
272+
type MonopolyUser struct {
273+
Id string
274+
Name string
275+
}
276+
277+
type GetMonopolyPlayersGameWinnerUser struct {
278+
MonopolyUser
279+
}
280+
281+
// (others similarly)
282+
```
283+
284+
Thus you can have `FormatUser` accept a `MonopolyUser`, and pass it `game.Winner.MonopolyUser`, `game.Spectators[i].MonopolyUser`, etc. This is convenient if you may later want to add other fields to some of the queries, because you can still do
285+
286+
```graphql
287+
fragment MonopolyUser on User {
288+
id
289+
name
290+
}
291+
292+
query GetMonopolyPlayers {
293+
game {
294+
winner {
295+
winCount
296+
...MonopolyUser
297+
}
298+
banker {
299+
bankerRating
300+
...MonopolyUser
301+
}
302+
spectators { ...MonopolyUser }
303+
}
304+
}
305+
```
306+
307+
and you can even spread the fragment into interface types. It also avoids having to list the fields several times.
308+
309+
Alternately, if you always want exactly the same fields, you can use the simpler but more restrictive genqlient option `typename`:
310+
311+
```graphql
312+
query GetMonopolyPlayers {
313+
game {
314+
# @genqlient(typename: "User")
315+
winner { id name }
316+
# @genqlient(typename: "User")
317+
banker { id name }
318+
# @genqlient(typename: "User")
319+
spectators { id name }
320+
}
321+
}
322+
```
323+
324+
This will tell genqlient to use the same types for each field:
325+
326+
```go
327+
type GetMonopolyPlayersGame struct {
328+
Winner User
329+
Banker User
330+
Spectators []User
331+
}
332+
333+
type User struct {
334+
Id string
335+
Name string
336+
}
337+
```
338+
339+
In this case, genqlient will validate that each type given the name `User` has the exact same fields; see the [full documentation](genqlient_directive.graphql) for details.
340+
341+
Note that it's also possible to use the `bindings` option (see [`genqlient.yaml` documentation](genqlient.yaml)) for a similar purpose, but this is not recommended as it typically requires more work for less gain.
342+
208343
### … documentation on the output types?
209344

210345
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:
@@ -258,6 +393,21 @@ type User = GetFamilyNamesUser
258393
type ChildUser = GetFamilyNamesUserChildrenUser
259394
```
260395

396+
Alternately, you can use the `typename` option: if you query
397+
```graphql
398+
query GetFamilyNames {
399+
# @genqlient(typename: "User")
400+
user {
401+
name
402+
# @genqlient(typename: "ChildUser")
403+
children {
404+
name
405+
}
406+
}
407+
}
408+
```
409+
genqlient will instead generate types with the given names. (You'll need to avoid conflicts; see the [full documentation](genqlient_directive.graphql) for details.)
410+
261411
### … my editor/IDE plugin not know about the code genqlient just generated?
262412

263413
If your tools are backed by [gopls](https://github.com/golang/tools/blob/master/gopls/README.md) (which is most of them), they simply don't know it was updated. In most cases, keeping the generated file (typically `generated.go`) open in the background, and reloading it after each run of `genqlient`, will do the trick.

docs/genqlient_directive.graphql

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,38 @@ directive genqlient(
9191
# genqlient-generated type.
9292
bind: String
9393

94+
# If set, the type of this field will have the given name in Go.
95+
#
96+
# For example, given the following query:
97+
# # @genqlient(typename: "MyResp")
98+
# query MyQuery {
99+
# # @genqlient(typename: "User")
100+
# user {
101+
# id
102+
# }
103+
# }
104+
# genqlient will generate
105+
# type Resp struct {
106+
# User User
107+
# }
108+
# type User struct {
109+
# Id string
110+
# }
111+
# instead of its usual, more verbose type names.
112+
#
113+
# With great power comes great responsibility: when using typename you'll
114+
# need to avoid comments; genqlient will complain if you use the same
115+
# type-name in multiple places unless they request the exact same fields, or
116+
# if your type-name conflicts with an autogenerated one (again, unless they
117+
# request the exact same fields). They must even have the fields in the
118+
# same order. Fragments are often easier to use (see the discussion of
119+
# code-sharing in FAQ.md).
120+
#
121+
# Note that unlike most directives, if applied to the entire operation,
122+
# typename affects the overall response type, rather than being propagated
123+
# down to all child fields (which would cause conflicts).
124+
typename: String
125+
94126
) on
95127
# genqlient directives can go almost anywhere, although some options are only
96128
# applicable in certain locations as described above.

0 commit comments

Comments
 (0)