Skip to content

Commit

Permalink
More discussion of a few things from Mark brought up:
Browse files Browse the repository at this point in the history
- the option of having an embed as well as an interface to represent
  GraphQL interfaces
- simplifications to the embedding approach when embedding abstract
  spreads; namely in our structs we can embed the concrete type of the
  fragment (but still generate the interface for code sharing)
- more explanation about the four sub-cases of abstract-in-abstract
  spreads
  • Loading branch information
benjaminjkraft committed Aug 25, 2021
1 parent 30f9e42 commit b6a3089
Showing 1 changed file with 31 additions and 20 deletions.
51 changes: 31 additions & 20 deletions DESIGN.md
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ Depending on whether the concrete type returned from `a` is `T`, we can get one

The question is: how do we translate that to Go types?

One natural option is to generate a Go type for every concrete GraphQL type the object might have, and simply inline all the fragments. So here we would have
**Go interfaces:** one natural option is to generate a Go type for every concrete GraphQL type the object might have, and simply inline or embed all the fragments. So here we would have
```go
type T struct{ B, C, D string }
type U struct{ B string }
Expand All @@ -147,9 +147,18 @@ Response{A: T{B: "...", C: "...", D: "..."}}
Response{A: U{B: "..."}}
```

We can also define a getter-method `GetB() string` on `T` and `U`, and include it in `I`, so that if you only need `B` you don't need to type-switch. Note that this option gives a few different ways to represent fragments specifically, discussed in the next section.
We can also define a getter-method `GetB() string` on `T` and `U`, and include it in `I`, so that if you only need `B` you don't need to type-switch. Or, if that's not enough, we can also define a common embedded struct corresponding to the interface, so you can extract and pass that around if you want:

Another natural option, which looks more like the way `shurcooL/graphql` does things, is to generate a type for each fragment, and only fill in the relevant ones:
```go
type IEmbed struct { B string }
type T struct { IEmbed; C, D string }
type U struct { IEmbed }
type I interface { isI(); GetIEmbed() IEmbed }
```

Note that this option gives a few different ways to represent fragments specifically, discussed in the next section.

**Fragment fields:** another natural option, which looks more like the way `shurcooL/graphql` does things, is to generate a type for each fragment, and only fill in the relevant ones:
```go
type Response struct {
A struct {
Expand Down Expand Up @@ -179,21 +188,21 @@ query { a { b } }
using the same schema as above. In the former approach, we still define three types (plus two receivers); and `resp.A` is still of an interface type; it might be either `T` or `U`. In the latter approach, this looks just like any other query: `resp.A` is a `struct{ B string }`. This has implications for how we use this data: the latter approach lets us just do `resp.A.B`, whereas the former requires we do a type-switch, or add a `GetB()` method to `I`, and do `resp.A.GetB()`.


Pros of the first approach:
**Pros of Go interfaces:**

- it's the most natural translation of how GraphQL does things, and provides the clearest guarantees about what is set when, what is mutually exclusive, etc.
- you always know what type you got back
- you always know which fields are there -- you don't have to encode at the application level an assumption that if fragment A was defined, fragment B also will be, because all types that match A also match B

Pros of the second approach:
**Pros of fragment fields:**

- the types are simpler, and allow the option of unnamed types
- if you query an interface, but don't care about the types, just the shared fields, you don't even have to think about any of this stuff
- for callers accessing shared fields (of interfaces or of fragments spread into several places) we avoid having to make them do a type switch or use getter methods

Note that both approaches require that we add `__typename` to every selection set which has fragments (unless the types match exactly). This seems fine since Apollo client also does so for all selection sets. We also need to define `UnmarshalJSON` on every type with fragment-spreads; in the former case Go doesn't know how to unmarshal into an interface type, while in the latter the Go type structure is too different from that of the JSON. (Note that `shurcooL/graphql` actually has its own JSON-decoder to solve this problem.)

A third non-approach is to simplify define all the fields on the same struct, with some optional:
**Flatten everything:** a third non-approach is to simplify define all the fields on the same struct, with some optional:

```go
type Response struct {
Expand Down Expand Up @@ -222,15 +231,15 @@ query {

What type is `resp.A.F`? It has to be both `string` and `int`.

In other libraries:
- Apollo does basically option 1, except with TypeScript/Flow's better support for sum types.
- GraphQL Code Generator does basically option 1 (except with sum types instead of interfaces), except with unnamed types. It definitely generates some ugly types, even with TypeScript/Flow's better support for sum types!
- Khan's mobile autogen basically does option 1 (again with unnamed types, and sum types).
**In other libraries:**
- Apollo does basically the interface approach, except with TypeScript/Flow's better support for sum types.
- GraphQL Code Generator does basically the interface approach (except with sum types instead of interfaces), except with unnamed types. It definitely generates some ugly types, even with TypeScript/Flow's better support for sum types!
- Khan's mobile autogen basically does the interface approach (again with unnamed types, and sum types).
- gqlgen doesn't have this problem; on the server, fragments and interfaces are handled entirely in the framework and need not even be visible in user-land.
- shurcooL/graphql uses option 2.
- protobuf has a similar problem, and uses basically option 1 in Go (even though in other languages it uses something more like option 3).
- shurcooL/graphql uses fragment fields.
- protobuf has a similar problem, and uses basically the interface approach in Go (even though in other languages it uses something more like flattening everything).

**Decision:** In general, it seems like the GraphQL Way, and perhaps also the Go Way is Option 1; I've always found the way shurcooL/graphql handles this to be a bit strange. So I think it has to be at least the default. In principle we could allow both though, since option 2 is legitimately convenient for some cases, especially if you are querying shared fields of interfaces or using fragments as a code-sharing mechanism, since option 1 handles those only through getter methods.
**Decision:** In general, it seems like the GraphQL Way, and perhaps also the Go Way is to use Go interfaces; I've always found the way shurcooL/graphql handles this to be a bit strange. So I think it has to be at least the default. In principle we could allow both though, since fragment fields are legitimately convenient for some cases, especially if you are querying shared fields of interfaces or using fragments as a code-sharing mechanism, since using Go interfaces handles those only through getter methods (although see below).

### How to support fragments

Expand All @@ -241,7 +250,7 @@ query MyQuery { a { b ...F } }
fragment F on T { c d }
```

We'll have some struct (potentially several structs, one per implementation) representing the type of the value of `a`. We can handle the fragment one of two ways: we can either flatten it into that type, such that the code is equivalent to writing `query Q { a { b c d } }` (except that `c` and `d` might be included for only some implementations, if `a` returns an interface), or we can represent it as a separate type `F` and embed it in the relevant structs. (Or it could be a named field of said structs, but this seems to have little benefit, and it's nice to be able to access fields without knowing what fragment they're on.)
We'll have some struct (potentially several structs, one per implementation) representing the type of the value of `a`. We can handle the fragment one of two ways: we can either flatten it into that type, such that the code is equivalent to writing `query Q { a { b c d } }` (except that `c` and `d` might be included for only some implementations, if `a` returns an interface), or we can represent it as a separate type `F` and embed it in the relevant structs. (Or it could be a named field of said structs, but this seems to have little benefit, and it's nice to be able to access fields without knowing what fragment they're on.) When the spread itself is abstract, there are a few sub-cases of the embed option.

To get more concrete, there are [four cases](https://spec.graphql.org/June2018/#sec-Fragment-spread-is-possible) depending on the types in question. (In all cases below, the methods, and other potential implementations of the interfaces, are elided. Note also that I'm not sure I got all the type-names right exactly as genqlient would, but they should match approximately.)

Expand Down Expand Up @@ -279,9 +288,9 @@ fragment F on I { c d }
// flattened:
type MyQueryA struct { B, C, D string }

// embedded
type MyQueryA struct { B string; F }
type F interface { isF(); GetC() string; GetD() string }
// embedded:
type MyQueryA struct { B string; FA }
type F interface { isF(); GetC() string; GetD() string } // for code-sharing purposes
type FA struct { C, D string } // implements F
```

Expand Down Expand Up @@ -312,7 +321,7 @@ type F struct { C, D string }
type G struct { U, V string }
```

**Abstract spread in abstract scope:** This is a sort of combination of the last two, where you spread one interface's fragment into another interface, and can be used for code-sharing and/or to conditionally request fields. (Perhaps surprisingly, this is legal any time the two interfaces share an implementation, and neither need implement the other.)
**Abstract spread in abstract scope:** This is a sort of combination of the last two, where you spread one interface's fragment into another interface, and can be used for code-sharing and/or to conditionally request fields. Perhaps surprisingly, this is legal any time the two interfaces share an implementation, and neither need implement the other; this means there are arguably four cases of spreading a fragment of type `I` into a scope of type `J`: `I = J` (similar to object-in-object), `I implements J` (similar to abstract-in-object), `J implements I` (similar to object-in-abstract), and none of the above (which is quite rare).

```graphql
type Query { a: I }
Expand All @@ -334,11 +343,13 @@ type MyQueryAIT struct { B } // implements MyQueryAI

// embedded:
type MyQueryAI interface { isMyQueryAI(); GetB() string }
type MyQueryAIA struct { B string; F } // implements MyQueryAI
type MyQueryAIA struct { B string; FA } // implements MyQueryAI
type MyQueryAIT struct { B string } // implements MyQueryAI
type F interface { isF(); GetC() string; GetD() string }
type FA struct { C, D string } // implements F
type FJ struct { C, D, V string } // implements F (never used; might be omitted)
type FU struct { C, D string } // implements F (never used; might be omitted)
// if I == J or I implements J, perhaps additionally:
type MyQueryAI interface { isMyQueryAI(); GetB() string; F }
```

Note in this case a third non-approach is
Expand Down

0 comments on commit b6a3089

Please sign in to comment.