diff --git a/README.md b/README.md index 68f43a61..8926093b 100644 --- a/README.md +++ b/README.md @@ -2,74 +2,48 @@ # genqlient: a truly type-safe Go GraphQL client -This is a proof-of-concept of using code-generation to create a truly type-safe GraphQL client in Go. It is certainly not ready for production use nor for contributions (see [docs/CONTRIBUTING.md](docs/CONTRIBUTING.md)). +This is a proof-of-concept of using code-generation to create a truly type-safe GraphQL client in Go. It is not yet ready for contributions (see [docs/CONTRIBUTING.md](docs/CONTRIBUTING.md)). -## Why another GraphQL client? +## What is genqlient? -To understand the issue, consider the example from the documentation of [shurcooL/graphql](https://github.com/shurcooL/graphql/): -```go -// to send a query `{ me { name } }`: -var query struct { - Me struct { - Name graphql.String - } -} -// error handling omitted for brevity -client.Query(context.Background(), &query, nil) -fmt.Println(query.Me.Name) -// Output: Luke Skywalker -``` -While this code may seem type-safe, and at the Go level it is, there's nothing to check that the schema looks like you expect it to. In fact, perhaps here we're querying the GitHub API, in which the field is called `viewer`, not `me`, so this query will fail. More common than misusing the name of the field is mis-capitalizing it, since Go and GraphQL have somewhat different conventions there. And even if you get it right, it adds up to a lot of handwritten boilerplate! And that's the best case; other clients, such as [machinebox/graphql](https://github.com/machinebox/graphql), have even fewer guardrails to help you make the right query and use the result correctly. This isn't a big deal in a small application, but for serious production-grade tools it's not ideal. - -These problems should be entirely avoidable: GraphQL and Go are both typed languages; and GraphQL servers expose their schema in a standard, machine-readable format. We should be able to simply write a query `{ viewer { name } }`, have that automatically validated against the schema and turned into a Go struct which we can use in our code. In fact, there's already good prior art to do this sort of thing: [99designs/gqlgen](https://github.com/99designs/gqlgen) is a popular server library that generates types, and Apollo has a [codegen tool](https://www.apollographql.com/docs/devtools/cli/#supported-commands) to generate similar client-types for several other languages. (See [docs/DESIGN.md](docs/DESIGN.md) for more prior art.) +genqlient is a Go library to easily generate type-safe code to query a GraphQL API. It takes advantage of the fact that both GraphQL and Go are typed languages to ensure at compile-time that your code is making a valid GraphQL query and using the result correctly, all with a minimum of boilerplate. -This is a GraphQL client that does the same sort of thing: you specify the query, and it generates type-safe helpers that make your query. +genqlient provides: -## Usage +- Compile-time validation of GraphQL queries: never ship an invalid GraphQL query again! +- Type-safe response objects: genqlient generates the right type for each query, so you know the response will unmarshal correctly and never need to use `interface{}`. +- Production-readiness: genqlient is used in production at Khan Academy, where it supports millions of learners and teachers around the world. -### Example +## How do I use genqlient? -To use genqlient, put your GraphQL schema (in [SDL format](https://www.apollographql.com/blog/three-ways-to-represent-your-graphql-schema-a41f4175100d/#0c31)) in a file `schema.graphql`, and put a query like the following in `queries.graphql`: +You can download and run genqlient the usual way: `go run github.com/Khan/genqlient`. To set your project up to use genqlient, see the [getting started guide](docs/INTRODUCTION.md), or the [example](example). For more complete documentation, see the [docs](docs). -```graphql -# queries.graphql -query getViewer { - viewer { - name - } -} -``` +## How can I help? -Then run genqlient (`go run github.com/Khan/genqlient`), and it will generate: +genqlient is not yet ready for contributions, but once it is, you'll be able to learn more in the ([Contribution Guidelines](docs/CONTRIBUTING.md)), or file an issue [on GitHub](issues). -```go -// generated.go (auto-generated): -type getViewerResponse struct { ... } -func getViewer(ctx context.Context, client *graphql.Client) (*getViewerResponse, error) { ... } -``` - -Finally, write your code to call genqlient, like so: +## Why another GraphQL client? +Most common Go GraphQL clients have you write code something like this: ```go -// your code (error handling omitted for brevity) -graphqlClient := graphql.NewClient("https://example.com/graphql", nil) -viewerResp, _ := getViewer(context.Background(), graphqlClient) -fmt.Println("you are", viewerResp.Viewer.MyName) - -//go:generate go run github.com/Khan/genqlient +query := `query GetUser($id: ID!) { user(id: $id) { name } }` +variables := map[string]interface{}{"id": "123"} +var resp struct { + Me struct { + Name graphql.String + } +} +client.Query(ctx, query, &resp, variables) +fmt.Println(query.Me.Name) +// Output: Luke Skywalker ``` -For a complete working example, see [`example/`](example). For configuration options, see [docs/genqlient.yaml](https://github.com/Khan/genqlient/blob/main/docs/genqlient.yaml). - -### Documentation for generated code - -For each GraphQL operation (query or mutation), genqlient generates a Go function with the exact same name, which accepts: -- a `context.Context` (unless configured otherwise) -- a `genqlient/graphql.Client` (you might have a global one, or init it inline) -- arguments corresponding to the query arguments +This code works, but it has a few problems: -It returns a pointer to a struct representing the query-result, and an `error`. The struct will always be initialized (never nil), even on error. The error may be a `github.com/vektah/gqlparser/v2/gqlerror.List`, if it was a GraphQL-level error (in this case the returned struct may still contain useful data, if the API returns data even on error), or may be another error if, for example, the whole HTTP request failed (in which case the struct is unlikely to contain useful data). If the GraphQL operation has a comment immediately above it, that comment text will be used as the GoDoc for the generated function. +- While the response struct is type-safe at the Go level; there's nothing to check that the schema looks like you expect. Maybe the field is called `fullName`, not `name`; or maybe you capitalized it wrong (since Go and GraphQL have different conventions); you won't know until runtime. +- The GraphQL variables aren't type-safe at all; you could have passed `{"id": true}` and again you won't know until runtime! +- You have to write everything twice, or hide the query in complicated struct tags, or give up what type safety you do have and resort to `interface{}`. -The generated code may be customized using a directive-like syntax, `# @genqlient(...)`. For full documentation of options, see [docs/genqlient_directive.graphql](docs/genqlient_directive.graphql). +These problems aren't a big deal in a small application, but for serious production-grade tools they're not ideal. And they should be entirely avoidable: GraphQL and Go are both typed languages; and GraphQL servers expose their schema in a standard, machine-readable format. We should be able to simply write a query and have that automatically validated against the schema and turned into a Go struct which we can use in our code. In fact, there's already good prior art to do this sort of thing: [99designs/gqlgen](https://github.com/99designs/gqlgen) is a popular server library that generates types, and Apollo has a [codegen tool](https://www.apollographql.com/docs/devtools/cli/#supported-commands) to generate similar client-types for several other languages. (See [docs/DESIGN.md](docs/DESIGN.md) for more prior art.) -TODO: consider inlining the direct stuff; and document generated types further. +genqlient fills that gap: you just specify the query, and it generates type-safe helpers, validated against the schema, that make the query. diff --git a/docs/DESIGN.md b/docs/DESIGN.md index 806ffe5f..4fdff22a 100644 --- a/docs/DESIGN.md +++ b/docs/DESIGN.md @@ -1,6 +1,6 @@ # Design decisions -This file contains a log of miscellaneous design decisions in genqlient. +This file contains a log of miscellaneous design decisions in genqlient. They aren't all necessarily up to date with the exact implementation details, but are preserved here as context for why genqlient does X thing Y way. ## Types diff --git a/docs/FAQ.md b/docs/FAQ.md new file mode 100644 index 00000000..0e54a347 --- /dev/null +++ b/docs/FAQ.md @@ -0,0 +1,241 @@ +# Frequently Asked Questions + +This document describes common questions about genqlient, and provides an index to how to represent common query structures. For a full list of configuration options, see [genqlient.yaml](genqlient.yaml) and [genqlient_directive.graphql](genqlient_directive.graphql). + +## How do I set up genqlient to … + +### … get started? + +There's a [doc for that](INTRODUCTION.md)! + +### … use an API that requires authentication? + +When you call `graphql.NewClient`, pass in an HTTP client that adds whatever authentication headers you need (typically by wrapping the client's `Transport`). For example: + +```go +type authedTransport struct { + wrapped http.RoundTripper +} + +func (t *authedTransport) RoundTrip(req *http.Request) (*http.Response, error) { + key := ... + req.Header.Set("Authorization", "bearer "+key) + return t.wrapped.RoundTrip(req) +} + +func MakeQuery(...) { + client := graphql.NewClient("https://api.github.com/graphql", + &http.Client{Transport: &authedTransport{wrapped: http.DefaultTransport}}) + + resp, err := MyQuery(ctx, client, ...) +} +``` + +For more on wrapping HTTP clients, see [this post](https://dev.to/stevenacoffman/tripperwares-http-client-middleware-chaining-roundtrippers-3o00). + +### … make requests against a mock server, for tests? + +Testing code that uses genqlient typically involves passing in a special HTTP client that does what you want, similar to authentication. For example, you might write a client whose `RoundTrip` returns a fixed response, constructed with [`httptest`](https://pkg.go.dev/net/http/httptest). Or, you can use `httptest` to start up a temporary server, and point genqlient at that. Many third-party packages provide support for this sort of thing; genqlient should work with any HTTP-level mocking that can expose a regular `http.Client`. + +### … test my GraphQL APIs? + +If you want, you can use genqlient to test your GraphQL APIs; as with mocking you can point genqlient at anything that exposes an ordinary HTTP endpoint or a custom `http.Client`. However, at Khan Academy we've found that genqlient usually isn't the best client for testing; we prefer to use a lightweight (and weakly-typed) client for that, and may separately open-source ours in the future. + +### … handle GraphQL errors? + +Each genqlient-generated helper function returns two results, a pointer to a response-struct, and an error. The response-struct will always be initialized (never nil), even on error. If the request returns a valid GraphQL response containing errors, the returned error will be [`As`-able](https://pkg.go.dev/errors#As) as [`gqlerror.List`](https://pkg.go.dev/github.com/vektah/gqlparser/v2/gqlerror#List), and the struct may be partly-populated (if one field failed but another was computed successfully). If the request fails entirely, the error will be another error (e.g. a [`*url.Error`](https://pkg.go.dev/net/url#Error)), and the response will be blank (but still non-nil). + +For example, you might do one of the following: +```go +// return both error and field: +resp, err := GetUser(...) +return resp.User.Name, err + +// handle different errors differently: +resp, err := GetUser(...) +var errList *gqlerror.List +if errors.As(err, &errList) { + for _, err := range errList { + fmt.Printf("%v at %v\n", err.Message, err.Path) + } + fmt.Printf("partial response: %v\n", resp) +} else if err != nil { + fmt.Printf("http/network error: %v\n", err) +} else { + fmt.Printf("successful response: %v\n", resp) +} +``` + +### … 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: +- [Apollo Server](https://github.com/apollographql/apollo-server/) explicitly checks that integers are at most 32 bits +- [gqlgen](https://github.com/99designs/gqlgen) by default allows any integer that fits in `int` (i.e. 64 bits on most platforms) +- [Apollo Client](https://github.com/apollographql/apollo-client) doesn't check (but implicitly is limited to 53 bits by JavaScript) +- [shurcooL/graphql](https://github.com/shurcooL/graphql) requires integers be passed as a `graphql.Int`, defined to be an `int32` + +By default, genqlient maps GraphQL `Int`s to Go's `int`, meaning that on 64 bit systems there's no client-side restriction. If you prefer to limit integers to `int32`, you can set a binding in your `genqlient.yaml`: + +```yaml +bindings: + Int: + type: int32 +``` + +Or, you can bind it to any other type, perhaps one with size-checked constructors; see the [`genqlient.yaml` documentation](`genqlient.yaml`) for more details. + +## How do I make a query with … + +### … a specific name for a field? + +genqlient supports GraphQL field-aliases, and uses them to determine the Go struct field name. For example, if you do +```graphql +query MyQuery { + myGreatName: myString +} +``` +and genqlient will generate a Go field `MyGreatName string`. Note that the alias will always be uppercased, to ensure the field is visible to the Go JSON library. + +### … nullable fields? + +There are two ways to handle nullable fields in genqlient. One way is to use the Go idiom, where null gets mapped to the zero value; this is the default in genqlient. So if you have a GraphQL field of type `String`, and you do: + +```graphql +query MyQuery(arg: String) { + myString +} +``` + +then genqlient will generate a Go field `MyString string`, and set it to the empty string if the server returns null. This works even for structs: if an object type in GraphQL is null, genqlient will set the corresponding struct to its zero value. It can be helpful to request `id` in such cases, since that’s a field that should always be set, or `__typename` which is guaranteed to be set, so you can use its presence to decide whether to look at the other fields. + +For input fields, you often want to tell genqlient to send null to the server if the argument is set to the zero value, similar to the JSON `omitempty` tag. In this case, you can do: + +```graphql +query MyQuery( + # @genqlient(omitempty: true) + arg: String, +) { + myString +} +``` + +You can also put the `# @genqlient(omitempty: true)` on the first line, which will apply it to all arguments in the query. + +If you need to distinguish null from the empty string (or generally from the Go zero value of your type), you can tell genqlient to use a pointer for the field or argument like this: +```graphql +query MyQuery( + # @genqlient(pointer: true) + arg: String, +) { + # @genqlient(pointer: true) + myString +} +``` + +This will generate a Go field `MyString *string`, and set it to `nil` if the server returns null (and in reverse for arguments). Such fields can be harder to work with in Go, but allow a clear distinction between null and the Go zero value. Again, you can put the directive on the first line to apply it to everything in the query, although this usually gets cumbersome. + +See [genqlient_directive.graphql](genqlient_directive.graphql) for complete documentation on these options. + +### … GraphQL interfaces? + +If you request an interface field, genqlient generates an interface type corresponding to the GraphQL interface, and several struct types corresponding to its implementations. For example, given a query: + +```graphql +query GetBooks { + favorite { + title + ... on Novel { + protagonist + } + ... on Dictionary { + language + } + } +} +``` + +genqlient will generate the following types (see [below](#-genqlient-generate-such-complicated-type-names) for more on the names): + +```go +type GetBooksFavoriteBook interface { + GetTitle() string +} +type GetBooksFavoriteNovel struct { + Title string + Protagonist string +} +type GetBooksFavoriteDictionary struct { + Title string + Language string +} +// (similarly for any other types that implement Book) +``` + +These can be used in the ordinary Go ways: to access shared fields, use the interface methods; to access type-specific fields, use a type switch: + +```go +resp, err := GetBooks(...) +fmt.Println("Favorite book:", resp.Favorite.GetTitle()) +if novel, ok := resp.Favorite.(*GetBooksFavoriteNovel); ok { + fmt.Println("Protagonist:", novel.Protagonist) +} +``` + +The interface-type's GoDoc will include a list of its implementations, for your convenience. + +### … 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: + +```graphql +# This query gets the current user. +# +# If you also need to specify options on the query, you can put +# the @genqlient directive after the docuentation, like this: +# +# @genqlient(omitempty: true) +query GetUser { ... } +``` + +## Why does… + +### … genqlient generate such complicated type-names? + +The short answer is that GraphQL forces our hand. For example, consider a query +```graphql +query GetFamilyNames { + user { + name + children { + name + } + } +} +``` +which returns the following JSON: +```graphql +{ + "user": { + "name": "Ellis Marsalis Jr.", + "children": [ + {"name": "Branford Marsalis"}, + {"name": "Delfeayo Marsalis"}, + {"name": "Jason Marsalis"}, + {"name": "Wynton Marsalis"} + ] + } +} +``` +We need two different `User` types to represent this: one with a `Children` field, and one without. (And there may be more in other queries!) Of course, we could name them `User1` and `User2`, but that's both less descriptive and less stable as the query changes (perhaps to add `parent`), so we call them `GetFamilyNamesUser` and `GetFamilyNamesUserChildrenUser`. + +For the long answer, see [DESIGN.md](DESIGN.md#named-vs-unnamed-types). + +If you find yourself needing to reference long generated names, you can always add type aliases for them, e.g.: +```go +type User = GetFamilyNamesUser +type ChildUser = GetFamilyNamesUserChildrenUser +``` + +### … 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. diff --git a/docs/INTRODUCTION.md b/docs/INTRODUCTION.md new file mode 100644 index 00000000..d949a6d7 --- /dev/null +++ b/docs/INTRODUCTION.md @@ -0,0 +1,66 @@ +# Getting started with genqlient + +This document describes how to set up genqlient and use it for simple queries. See also the full worked [example](../example), the [FAQ](FAQ.md), and the reference for [project-wide](genqlient.yaml) and [query-specific](genqlient_directive.graphql) configuration options. + +## Step 1: Download your schema + +You want the schema in GraphQL [Schema Definition Language (SDL)](https://graphql.org/learn/schema/#type-language) format. For example, to query the GitHub API, you could download the schema from [their documentation](https://docs.github.com/en/graphql/overview/public-schema). Put this in `schema.graphql`. + +## Step 2: Write your queries + +Next, write your GraphQL query. This is often easiest to do in an interactive explorer like [GraphiQL](https://github.com/graphql/graphiql/tree/main/packages/graphiql#readme); the syntax is just standard [GraphQL syntax](https://graphql.org/learn/queries/) and supports both queries and mutations. Put it in `genqlient.graphql`: +```graphql +query getUser($login: String!) { + user(login: $login) { + name + } +} +``` + +## Step 3: Run genqlient + +Now, run `go run github.com/Khan/genqlient --init`. This will create a configuration file, and then run genqlient to produce a file `generated.go` with your queries. + +## Step 4: Use your queries + +Finally, write your code! The generated code will expose a function with the same name as your query, here +```go +func getUser(ctx context.Context, client graphql.Client, login string) (*getUserResponse, error) +``` + +As for the arguments: +- for `ctx`, pass your local context (see [`go doc context`](https://pkg.go.dev/context)) or `context.Background()` if you don't need one +- for `client`, call [`graphql.NewClient`](https://pkg.go.dev/github.com/Khan/genqlient/graphql), e.g. `graphql.NewClient("https://your.api.example/path", http.DefaultClient)` +- for `login`, pass your GitHub username (or whatever the arguments to your query are) + +The response object is a struct with fields corresponding to each GraphQL field; for the exact details check its GoDoc (perhaps via your IDE's autocomplete or hover). For example, you might do: +```go +ctx := context.Background() +client := graphql.NewClient("https://api.github.com/graphql", http.DefaultClient) +resp, err := getUser(ctx, client, "benjaminjkraft") +fmt.Println(resp.User.Name, err) +``` + +Now run your code! + +## Step 5: Repeat + +Over time, as you add or change queries, you'll just need to run `github.com/Khan/genqlient` to re-generate `generated.go`. (Or add a line `// go:generate https://go.dev/blog/generate` in your source, and run [`go generate`](https://go.dev/blog/generate).) If you're using an editor or IDE plugin backed by [gopls](https://github.com/golang/tools/blob/master/gopls/README.md) (which is most of them), keep `generated.go` open in the background, and reload it after each run, so your plugin knows about the automated changes. + +If you prefer, you can specify your queries as string-constants in your Go source, prefixed with `# @genqlient` -- at Khan we put them right next to the calling code, e.g. +```go +_ = `# @genqlient + query getUser($login: String!) { + user(login: $login) { + name + } + } +` + +resp, err := getUser(...) +``` +(You don't need to do anything with the constant, just keep it somewhere in the source as documentation and for the next time you run genqlient.) In this case you'll need to update `genqlient.yaml` to tell it to look at your Go code. + +All the filenames above, and many other aspects of genqlient, are configurable; see [genqlient.yaml](genqlient.yaml) for the full range of options. You can also configure how genqlient converts specific parts of your query with the [`@genqlient` directive](genqlient_directive.graphql). See the [FAQ](FAQ.md) for common options. + +If you want to know even more, and help contribute to genqlient, see [DESIGN.md](DESIGN.md) and [CONTRIBUTING.md](CONTRIBUTING.md). Happy querying! diff --git a/graphql/client.go b/graphql/client.go index 1883db46..ce28c8d5 100644 --- a/graphql/client.go +++ b/graphql/client.go @@ -14,7 +14,8 @@ import ( // Client is the interface that the generated code calls into to actually make // requests. // -// Unstable: This interface is likely to change before v1.0, see #19. +// Unstable: This interface is likely to change before v1.0, see #19. Creating +// a client with NewClient will remain the same. type Client interface { // MakeRequest must make a request to the client's GraphQL API. //