Skip to content

Commit

Permalink
Add support for specifying type-names, and conflict-detection (#94)
Browse files Browse the repository at this point in the history
## 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
  • Loading branch information
benjaminjkraft committed Sep 16, 2021
1 parent 5211442 commit fcae8dd
Show file tree
Hide file tree
Showing 20 changed files with 713 additions and 47 deletions.
150 changes: 150 additions & 0 deletions docs/FAQ.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,18 @@ if errors.As(err, &errList) {
}
```

### … use custom scalars?

Just tell genqlient via the `bindings` option in `genqlient.yaml`:

```yaml
bindings:
DateTime:
type: time.Time
```

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.

### … require 32-bit integers?

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:
Expand Down Expand Up @@ -205,6 +217,129 @@ type GetBooksFavoriteBook struct {
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.


### … shared types between different parts of the query?

Suppose you have a query which requests several different fields each of the same GraphQL type, e.g. `User` (or `[User]`):

```graphql
query GetMonopolyPlayers {
game {
winner { id name }
banker { id name }
spectators { id name }
}
}
```

This will produce a Go type like:
```go
type GetMonopolyPlayersGame struct {
Winner GetMonopolyPlayersGameWinnerUser
Banker GetMonopolyPlayersGameBankerUser
Spectators []GetMonopolyPlayersGameSpectatorsUser
}

type GetMonopolyPlayersGameWinnerUser struct {
Id string
Name string
}

// (others similarly)
```

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.

One option -- the GraphQL Way, perhaps -- is to use fragments. You'd write your query like:

```graphql
fragment MonopolyUser on User {
id
name
}

query GetMonopolyPlayers {
game {
winner { ...MonopolyUser }
banker { ...MonopolyUser }
spectators { ...MonopolyUser }
}
}
```

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:

```go
type MonopolyUser struct {
Id string
Name string
}

type GetMonopolyPlayersGameWinnerUser struct {
MonopolyUser
}

// (others similarly)
```

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

```graphql
fragment MonopolyUser on User {
id
name
}

query GetMonopolyPlayers {
game {
winner {
winCount
...MonopolyUser
}
banker {
bankerRating
...MonopolyUser
}
spectators { ...MonopolyUser }
}
}
```

and you can even spread the fragment into interface types. It also avoids having to list the fields several times.

Alternately, if you always want exactly the same fields, you can use the simpler but more restrictive genqlient option `typename`:

```graphql
query GetMonopolyPlayers {
game {
# @genqlient(typename: "User")
winner { id name }
# @genqlient(typename: "User")
banker { id name }
# @genqlient(typename: "User")
spectators { id name }
}
}
```

This will tell genqlient to use the same types for each field:

```go
type GetMonopolyPlayersGame struct {
Winner User
Banker User
Spectators []User
}

type User struct {
Id string
Name string
}
```

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.

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.

### … documentation on the output types?

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:
Expand Down Expand Up @@ -258,6 +393,21 @@ type User = GetFamilyNamesUser
type ChildUser = GetFamilyNamesUserChildrenUser
```

Alternately, you can use the `typename` option: if you query
```graphql
query GetFamilyNames {
# @genqlient(typename: "User")
user {
name
# @genqlient(typename: "ChildUser")
children {
name
}
}
}
```
genqlient will instead generate types with the given names. (You'll need to avoid conflicts; see the [full documentation](genqlient_directive.graphql) for details.)

### … my editor/IDE plugin not know about the code genqlient just generated?

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.
32 changes: 32 additions & 0 deletions docs/genqlient_directive.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,38 @@ directive genqlient(
# genqlient-generated type.
bind: String

# If set, the type of this field will have the given name in Go.
#
# For example, given the following query:
# # @genqlient(typename: "MyResp")
# query MyQuery {
# # @genqlient(typename: "User")
# user {
# id
# }
# }
# genqlient will generate
# type Resp struct {
# User User
# }
# type User struct {
# Id string
# }
# instead of its usual, more verbose type names.
#
# With great power comes great responsibility: when using typename you'll
# need to avoid comments; genqlient will complain if you use the same
# type-name in multiple places unless they request the exact same fields, or
# if your type-name conflicts with an autogenerated one (again, unless they
# request the exact same fields). They must even have the fields in the
# same order. Fragments are often easier to use (see the discussion of
# code-sharing in FAQ.md).
#
# Note that unlike most directives, if applied to the entire operation,
# typename affects the overall response type, rather than being propagated
# down to all child fields (which would cause conflicts).
typename: String

) on
# genqlient directives can go almost anywhere, although some options are only
# applicable in certain locations as described above.
Expand Down
Loading

0 comments on commit fcae8dd

Please sign in to comment.