Skip to content

Commit

Permalink
Reorganize documentation to make room to grow (#84)
Browse files Browse the repository at this point in the history
## Summary:
In this commit I reorganize much of our documentation into a new `docs`
directory, where there will hopefully be more room to grow and to
organize things in a user-friendly way.  There's almost no net-new
documentation, although of course it's a great time to review it anyway.

In particular:
- I moved the documentation for the `genqlient.yaml` config file into an
  example file instead of GoDoc (which now just points to the example
  file); I think this will be a lot clearer for casual users.
- I moved the documentation for the `@genqlient` directive out of GoDoc
  and into a GraphQL schema file (since while it's a comment it's all
  real syntax), likewise, and made the `GenqlientDirective` type private
  (since there's now nothing useful to do with it).
- I moved `DESIGN.md` and the logo into `docs/` (just to keep the
  toplevel a bit cleaner), and separated the Contributing section of the
  README into `docs/CONTRIBUTING.md` (which github will automatically
  link on various issue and PR pages).

This leaves it so that:
- README.md is the only documentation at the toplevel (and will become
  just the high-level introduction as I add more user docs to `docs/`)
- GoDoc is only documentation for if you want to call genqlient
  programmatically (which is fairly limited as the API surface is quite
  small: it's now just Main, Generate, and Config, plus a constructor, a
  single method, and a bunch of fields on the latter)

In future commits, I'll add some more new documentation to the `docs`
directory.

Issue: #26

## Test plan:
make check (and read the docs)


Author: benjaminjkraft

Reviewers: jvoll, benjaminjkraft, aberkan, dnerdy, MiguelCastillo, mahtabsabet

Required Reviewers: 

Approved By: jvoll

Checks: ✅ Lint, ✅ Test (1.17), ✅ Test (1.16), ✅ Test (1.15), ✅ Test (1.14), ✅ Test (1.17), ✅ Test (1.16), ✅ Test (1.15), ✅ Test (1.14), ✅ Lint

Pull Request URL: #84
  • Loading branch information
benjaminjkraft authored Sep 10, 2021
1 parent d41cf63 commit 2eba9a2
Show file tree
Hide file tree
Showing 16 changed files with 275 additions and 237 deletions.
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ check: lint
go test -cover ./...
go mod tidy

genqlient.png: genqlient.svg
docs/images/genqlient.png: docs/images/genqlient.svg
convert -density 600 -background transparent "$<" "$@"

.PHONY: example
29 changes: 5 additions & 24 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
![generated graphql client ⇒ genqlient](genqlient.png)
![generated graphql client ⇒ genqlient](docs/images/genqlient.png)

# 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 [below](#development)).
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)).

## Why another GraphQL client?

Expand All @@ -21,7 +21,7 @@ fmt.Println(query.Me.Name)
```
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 [DESIGN.md](DESIGN.md) for more prior art.)
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.)

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.

Expand Down Expand Up @@ -59,7 +59,7 @@ fmt.Println("you are", viewerResp.Viewer.MyName)
//go:generate go run github.com/Khan/genqlient
```

For a complete working example, see [`example/`](example). For configuration options, see [`go doc github.com/Khan/genqlient/generate.Config`](https://pkg.go.dev/github.com/Khan/genqlient/generate#Config).
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

Expand All @@ -70,25 +70,6 @@ For each GraphQL operation (query or mutation), genqlient generates a Go functio

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.

The generated code may be customized using a directive-like syntax, `# @genqlient(...)`. For full documentation of options, see [`go doc github.com/Khan/genqlient/generate.GenqlientDirective`](https://pkg.go.dev/github.com/Khan/genqlient/generate#GenqlientDirective).
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).

TODO: consider inlining the direct stuff; and document generated types further.

## Development

genqlient is not yet accepting contributions. The library is still in a very rough state, so while we intend to accept contributions in the future, we're not ready for them just yet. Please contact us (via an issue or email) if you are interested in helping out, but please be aware the answer may be that we aren't ready for your help yet, even if such help will be greatly useful once we are! We'll be ready for the rest of the world soon.

Khan Academy is a non-profit organization with a mission to provide a free, world-class education to anyone, anywhere. If you're looking for other ways to help us, You can help us in that mission by [donating](https://khanacademy.org/donate) or looking at [career opportunities](https://khanacademy.org/careers).

### Tests

To run tests and lint, `make check`. (GitHub Actions also runs them.)

Notes for contributors:
- Most of the tests are snapshot-based; see `generate/generate_test.go`. All new code-generation logic should be snapshot-tested. Some code additionally has standalone unit tests, when convenient.
- Integration tests run against a gqlgen server in `internal/integration/integration_test.go`, and should cover everything that snapshot tests can't, including the GraphQL client code and JSON marshaling.
- If `GITHUB_TOKEN` is available in the environment, it also checks that the example returns the expected output when run against the real API. This is configured automatically in GitHub Actions, but you can also use a [personal access token](https://docs.github.com/en/github/authenticating-to-github/creating-a-personal-access-token) with no scopes. There's no need for this to cover anything in particular; it's just to make sure the example in fact works.

### Design

See [DESIGN.md](DESIGN.md) for documentation of major design decisions in this library.
18 changes: 18 additions & 0 deletions docs/CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Contributing to genqlient

genqlient is not yet accepting contributions. The library is still in a very rough state, so while we intend to accept contributions in the future, we're not ready for them just yet. Please contact us (via an issue or email) if you are interested in helping out, but please be aware the answer may be that we aren't ready for your help yet, even if such help will be greatly useful once we are! We'll be ready for the rest of the world soon.

Khan Academy is a non-profit organization with a mission to provide a free, world-class education to anyone, anywhere. If you're looking for other ways to help us, You can help us in that mission by [donating](https://khanacademy.org/donate) or looking at [career opportunities](https://khanacademy.org/careers).

## Tests

To run tests and lint, `make check`. (GitHub Actions also runs them.)

Notes for contributors:
- Most of the tests are snapshot-based; see `generate/generate_test.go`. All new code-generation logic should be snapshot-tested. Some code additionally has standalone unit tests, when convenient.
- Integration tests run against a gqlgen server in `internal/integration/integration_test.go`, and should cover everything that snapshot tests can't, including the GraphQL client code and JSON marshaling.
- If `GITHUB_TOKEN` is available in the environment, it also checks that the example returns the expected output when run against the real API. This is configured automatically in GitHub Actions, but you can also use a [personal access token](https://docs.github.com/en/github/authenticating-to-github/creating-a-personal-access-token) with no scopes. There's no need for this to cover anything in particular; it's just to make sure the example in fact works.

## Design

See [DESIGN.md](DESIGN.md) for documentation of major design decisions in this library.
File renamed without changes.
116 changes: 116 additions & 0 deletions docs/genqlient.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
# genqlient.yaml is genqlient's configuration file. This genqlient.yaml is an
# example; use `go run github.com/Khan/genqlient --init` to generate a simple
# starting point.

# The filename with the GraphQL schema (in SDL format), relative to
# genqlient.yaml.
schema: schema.graphql

# Filenames or globs with the operations for which to generate code, relative
# to genqlient.yaml.
#
# These may be .graphql files, containing the queries in SDL format, or
# Go files, in which case any string-literal starting with (optional
# whitespace and) the string "# @genqlient" will be extracted as a query.
operations:
- genqlient.graphql
- "pkg/*.go"

# The filename to which to write the generated code, relative to
# genqlient.yaml.
generated: generated/genqlient.go

# The package name for the output code; defaults to the directory name of
# the generated-code file.
package: mygenerated

# If set, a file at this path (relative to genqlient.yaml) will be generated
# containing the exact operations that genqlient will send to the server.
#
# This is useful for systems which require queries to be explicitly
# safelisted (e.g. [1]), especially for cases like queries involving fragments
# where it may not exactly match the input queries, or for other static
# analysis. The JSON is an object of the form
# {"operations": [{
# "operationName": "operationname",
# "query": "query operationName { ... }",
# "sourceLocation": "myqueriesfile.graphql",
# }]}
# Keys may be added in the future.
#
# By default, no such file is written.
#
# [1] https://www.apollographql.com/docs/studio/operation-registry/
export_operations: operations.json

# Set to the fully-qualified name of a Go type which generated helpers
# should accept and use as the context.Context for HTTP requests.
#
# Defaults to context.Context; set to "-" to omit context entirely (i.e.
# use context.Background()). Must be a type which implements
# context.Context.
context_type: context.Context

# If set, a function to get a graphql.Client, perhaps from the context.
# By default, the client must be passed explicitly to each genqlient
# generated query-helper.
#
# This is useful if you have a shared client, either a global, or
# available from context, and don't want to pass it explicitly. In this
# case the signature of the genqlient-generated helpers will omit the
# `graphql.Context` and they will call this function instead.
#
# Must be the fully-qualified name of a function which accepts a context
# (of the type configured as ContextType (above), which defaults to
# `context.Context`, or a function of no arguments if ContextType is set
# to the empty string) and returns (graphql.Client, error). If the
# client-getter returns an error, the helper will return the error
# without making a query.
client_getter: "github.com/you/yourpkg.GetClient"

# A map from GraphQL type name to Go fully-qualified type name to override
# the Go type genqlient will use for this GraphQL type.
#
# This is primarily used for custom scalars, or to map builtin scalars to
# a nonstandard type. By default, builtin scalars are mapped to the
# obvious Go types (String and ID to string, Int to int, Float to float64,
# and Boolean to bool), but this setting will extend or override those
# mappings.
#
# genqlient does not validate these types in any way; they must define
# whatever logic is needed (MarshalJSON/UnmarshalJSON or JSON tags) to
# convert to/from JSON. For this reason, it's not recommended to use this
# setting to map object, interface, or union types, because nothing
# guarantees that the fields requested in the query match those present in
# the Go type.
#
# To get equivalent behavior in just one query, use @genqlient(bind: ...);
# see genqlient_directive.graphql for more details.
bindings:
# To bind a scalar:
DateTime:
# The fully-qualified name of the Go type to which to bind. For example:
# time.Time
# map[string]interface{}
# github.com/you/yourpkg/subpkg.MyType
type: time.Time

# To bind an object type:
MyType:
type: github.com/you/yourpkg.GoType
# If set, a GraphQL selection which must exactly match the fields
# requested whenever this type is used. Only applies if the GraphQL type
# is a composite output type (object, interface, or union).
#
# This is useful if Type is a struct whose UnmarshalJSON or other methods
# expect that you requested certain fields. For example, given the below
# config, genqlient will reject if you make a query
# { fieldOfMytype { id title } }
# The fields must match exactly, including the ordering: "{ name id }"
# will be rejected. But the arguments and directives, if any, need not
# match.
#
# TODO(benkraft): Also add ExpectIncludesFields and ExpectSubsetOfFields,
# or something, if you want to say, for example, that you have to request
# certain fields but others are optional.
expect_exact_fields: "{ id name }"
86 changes: 86 additions & 0 deletions docs/genqlient_directive.graphql
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
# The quasi-directive @genqlient is used to configure genqlient on a
# query-by-query basis.
#
# The syntax of the directive is just like a GraphQL directive (as defined
# below), except it goes in a comment on the line immediately preceding the
# field. (This is because GraphQL expects directives in queries to be defined
# by the server, not by the client, so it would reject a real @genqlient
# directive as nonexistent.)
#
# Directives may be applied to fields, arguments, or the entire query.
# Directives on the line preceding the query apply to all relevant nodes in
# the query; other directives apply to all nodes on the following line. (In
# all cases it's fine for there to be other comments in between the directive
# and the node(s) to which it applies.) For example, in the following query:
# # @genqlient(n: "a")
#
# # @genqlient(n: "b")
# #
# # Comment describing the query
# #
# # @genqlient(n: "c")
# query MyQuery(arg1: String,
# # @genqlient(n: "d")
# arg2: String, arg3: String,
# arg4: String,
# ) {
# # @genqlient(n: "e")
# field1, field2
# field3
# }
# the directive "a" is ignored, "b" and "c" apply to all relevant nodes in the
# query, "d" applies to arg2 and arg3, and "e" applies to field1 and field2.
directive genqlient(

# If set, this argument will be omitted if it's equal to its Go zero
# value, or is an empty slice.
#
# For example, given the following query:
# # @genqlient(omitempty: true)
# query MyQuery(arg: String) { ... }
# genqlient will generate a function
# MyQuery(ctx context.Context, client graphql.Client, arg string) ...
# which will pass {"arg": null} to GraphQL if arg is "", and the actual
# value otherwise.
#
# Only applicable to arguments of nullable types.
omitempty: Boolean

# If set, this argument or field will use a pointer type in Go. Response
# types always use pointers, but otherwise we typically do not.
#
# This can be useful if it's a type you'll need to pass around (and want a
# pointer to save copies) or if you wish to distinguish between the Go
# zero value and null (for nullable fields).
pointer: Boolean

# If set, this argument or field will use the given Go type instead of a
# genqlient-generated type.
#
# The value should be the fully-qualified type name to use for the field,
# for example:
# time.Time
# map[string]interface{}
# []github.com/you/yourpkg/subpkg.MyType
# Note that the type is the type of the whole field, e.g. if your field in
# GraphQL has type `[DateTime]`, you'd do
# # @genqlient(bind: "[]time.Time")
# (But you're not required to; if you want to map to some type DateList,
# you can do that, as long as its UnmarshalJSON method can accept a list
# of datetimes.)
#
# See bindings in genqlient.yaml for more details; this is effectively to a
# local version of that global setting and should be used with similar care.
# If set to "-", overrides any such global setting and uses a
# genqlient-generated type.
bind: String

) on
# genqlient directives can go almost anywhere, although some options are only
# applicable in certain locations as described above.
| QUERY
| MUTATION
| SUBSCRIPTION
| FIELD
| FRAGMENT_DEFINITION
| VARIABLE_DEFINITION
File renamed without changes
File renamed without changes
Loading

0 comments on commit 2eba9a2

Please sign in to comment.