diff --git a/Makefile b/Makefile index 4644868d..2444887c 100644 --- a/Makefile +++ b/Makefile @@ -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 diff --git a/README.md b/README.md index 514e4da9..b5392ea5 100644 --- a/README.md +++ b/README.md @@ -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? @@ -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. @@ -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 @@ -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. diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md new file mode 100644 index 00000000..06b76abe --- /dev/null +++ b/docs/CONTRIBUTING.md @@ -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. diff --git a/DESIGN.md b/docs/DESIGN.md similarity index 100% rename from DESIGN.md rename to docs/DESIGN.md diff --git a/docs/genqlient.yaml b/docs/genqlient.yaml new file mode 100644 index 00000000..d1a12bfe --- /dev/null +++ b/docs/genqlient.yaml @@ -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 }" diff --git a/docs/genqlient_directive.graphql b/docs/genqlient_directive.graphql new file mode 100644 index 00000000..328d8b8c --- /dev/null +++ b/docs/genqlient_directive.graphql @@ -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 diff --git a/genqlient.png b/docs/images/genqlient.png similarity index 100% rename from genqlient.png rename to docs/images/genqlient.png diff --git a/genqlient.svg b/docs/images/genqlient.svg similarity index 100% rename from genqlient.svg rename to docs/images/genqlient.svg diff --git a/generate/config.go b/generate/config.go index 43383dc1..eb8112f7 100644 --- a/generate/config.go +++ b/generate/config.go @@ -15,92 +15,21 @@ import ( // // Callers must call ValidateAndFillDefaults before using the config. type Config struct { - // The filename with the GraphQL schema (in SDL format); defaults to - // schema.graphql - Schema string `yaml:"schema"` - - // Filenames or globs with the operations for which to generate code; - // defaults to genqlient.graphql. - // - // 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 []string `yaml:"operations"` - - // If set, a file at this path 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, especially for cases like queries involving fragments where - // it may not exactly match the input queries. 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. - ExportOperations string `yaml:"export_operations"` - - // The filename to which to write the generated code; defaults to - // generated.go - Generated string `yaml:"generated"` - - // The package name for the output code; defaults to the directory name of - // Generated - Package string `yaml:"package"` - - // 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. - ContextType string `yaml:"context_type"` - - // 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. - ClientGetter string `yaml:"client_getter"` - - // 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 GenqlientDirective.Bind for more details. - Bindings map[string]*TypeBinding `yaml:"bindings"` + // The following fields are documented at: + // https://github.com/Khan/genqlient/blob/main/docs/genqlient.yaml + Schema string `yaml:"schema"` + Operations []string `yaml:"operations"` + Generated string `yaml:"generated"` + Package string `yaml:"package"` + ExportOperations string `yaml:"export_operations"` + ContextType string `yaml:"context_type"` + ClientGetter string `yaml:"client_getter"` + Bindings map[string]*TypeBinding `yaml:"bindings"` // Set to true to use features that aren't fully ready to use. // // This is primarily intended for genqlient's own tests. These features - // are likely BROKEN and come with NO EXPECTATION OF COMPATIBBILITY. Use + // are likely BROKEN and come with NO EXPECTATION OF COMPATIBILITY. Use // them at your own risk! AllowBrokenFeatures bool `yaml:"allow_broken_features"` @@ -110,32 +39,10 @@ type Config struct { } // A TypeBinding represents a Go type to which genqlient will bind a particular -// GraphQL type. See Config.Bind, above, for more details. +// GraphQL type, and is documented further at: +// https://github.com/Khan/genqlient/blob/main/docs/genqlient.yaml type TypeBinding struct { - // 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 string `yaml:"type"` - // 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. You can specify those fields - // like - // MyType: - // type: path/to/my.GoType - // expect_exact_fields: "{ id name }" - // and then 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. + Type string `yaml:"type"` ExpectExactFields string `yaml:"expect_exact_fields"` } diff --git a/generate/convert.go b/generate/convert.go index 51d78cef..3fb2804f 100644 --- a/generate/convert.go +++ b/generate/convert.go @@ -37,7 +37,7 @@ func (g *generator) baseTypeForOperation(operation ast.Operation) (*ast.Definiti // result will be unmarshaled. func (g *generator) convertOperation( operation *ast.OperationDefinition, - queryOptions *GenqlientDirective, + queryOptions *genqlientDirective, ) (goType, error) { name := operation.Name + "Response" @@ -87,7 +87,7 @@ var builtinTypes = map[string]string{ // argument to a GraphQL operation. func (g *generator) convertInputType( typ *ast.Type, - options, queryOptions *GenqlientDirective, + options, queryOptions *genqlientDirective, ) (goType, error) { // note prefix is ignored here (see generator.typeName), as is selectionSet // (for input types we use the whole thing)). @@ -102,7 +102,7 @@ func (g *generator) convertType( namePrefix *prefixList, typ *ast.Type, selectionSet ast.SelectionSet, - options, queryOptions *GenqlientDirective, + options, queryOptions *genqlientDirective, ) (goType, error) { // We check for local bindings here, so that you can bind, say, a // `[String!]` to a struct instead of a slice. Global bindings can only @@ -145,7 +145,7 @@ func (g *generator) convertDefinition( def *ast.Definition, pos *ast.Position, selectionSet ast.SelectionSet, - options, queryOptions *GenqlientDirective, + options, queryOptions *genqlientDirective, ) (goType, error) { // Check if we should use an existing type. (This is usually true for // GraphQL scalars, but we allow you to bind non-scalar types too, if you @@ -315,7 +315,7 @@ func (g *generator) convertSelectionSet( namePrefix *prefixList, selectionSet ast.SelectionSet, containingTypedef *ast.Definition, - queryOptions *GenqlientDirective, + queryOptions *genqlientDirective, ) ([]*goStructField, error) { fields := make([]*goStructField, 0, len(selectionSet)) for _, selection := range selectionSet { @@ -422,7 +422,7 @@ func (g *generator) convertSelectionSet( // the fragment's type. This is distinct from the rules for when a fragment // spread is legal, which is true when the fragment would be active for *any* // of the concrete types the spread-context could have (see -// https://spec.graphql.org/draft/#sec-Fragment-Spreads or DESIGN.md). +// https://spec.graphql.org/draft/#sec-Fragment-Spreads or docs/DESIGN.md). // // containingTypedef is as described in convertInlineFragment, below. // fragmentTypedef is the definition of the fragment's type-condition, i.e. the @@ -456,12 +456,12 @@ func fragmentMatches(containingTypedef, fragmentTypedef *ast.Definition) bool { // // In general, we treat such fragments' fields as if they were fields of the // parent selection-set (except of course they are only included in types the -// fragment matches); see DESIGN.md for more. +// fragment matches); see docs/DESIGN.md for more. func (g *generator) convertInlineFragment( namePrefix *prefixList, fragment *ast.InlineFragment, containingTypedef *ast.Definition, - queryOptions *GenqlientDirective, + queryOptions *genqlientDirective, ) ([]*goStructField, error) { // You might think fragmentTypedef would be fragment.ObjectDefinition, but // actually that's the type into which the fragment is spread. @@ -601,7 +601,7 @@ func (g *generator) convertNamedFragment(fragment *ast.FragmentDefinition) (goTy func (g *generator) convertField( namePrefix *prefixList, field *ast.Field, - fieldOptions, queryOptions *GenqlientDirective, + fieldOptions, queryOptions *genqlientDirective, ) (*goStructField, error) { if field.Definition == nil { // Unclear why gqlparser hasn't already rejected this, diff --git a/generate/default_genqlient.yaml b/generate/default_genqlient.yaml index 66eda357..bfc5f265 100644 --- a/generate/default_genqlient.yaml +++ b/generate/default_genqlient.yaml @@ -1,6 +1,5 @@ -# Default genqlient config, see -# go doc github.com/Khan/genqlient/generate.Config -# for more options. +# Default genqlient config; for full documentation see: +# https://github.com/Khan/genqlient/blob/main/docs/genqlient.yaml schema: schema.graphql operations: - genqlient.graphql diff --git a/generate/generate.go b/generate/generate.go index 986e92a0..7c6614f3 100644 --- a/generate/generate.go +++ b/generate/generate.go @@ -69,7 +69,7 @@ type argument struct { GoType string GraphQLName string IsSlice bool - Options *GenqlientDirective + Options *genqlientDirective } func newGenerator( @@ -139,7 +139,7 @@ func (g *generator) Types() (string, error) { func (g *generator) getArgument( arg *ast.VariableDefinition, - operationDirective *GenqlientDirective, + operationDirective *genqlientDirective, ) (argument, error) { _, directive, err := g.parsePrecedingComment(arg, arg.Position) if err != nil { diff --git a/generate/genqlient_directive.go b/generate/genqlient_directive.go index 99f68cfb..43b8ce41 100644 --- a/generate/genqlient_directive.go +++ b/generate/genqlient_directive.go @@ -8,86 +8,17 @@ import ( "github.com/vektah/gqlparser/v2/parser" ) -// GenqlientDirective represents the @genqlient quasi-directive, used to -// configure genqlient on a query-by-query basis. -// -// The syntax of the directive is just like a GraphQL directive, 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. -type GenqlientDirective struct { - pos *ast.Position - - // 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. +// Represents the genqlient directive, described in detail in +// docs/genqlient_directive.graphql. +type genqlientDirective struct { + pos *ast.Position Omitempty *bool - - // 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 *bool - - // 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 Config.Bindings 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 + Pointer *bool + Bind string } -func (dir *GenqlientDirective) GetOmitempty() bool { return dir.Omitempty != nil && *dir.Omitempty } -func (dir *GenqlientDirective) GetPointer() bool { return dir.Pointer != nil && *dir.Pointer } +func (dir *genqlientDirective) GetOmitempty() bool { return dir.Omitempty != nil && *dir.Omitempty } +func (dir *genqlientDirective) GetPointer() bool { return dir.Pointer != nil && *dir.Pointer } func setBool(dst **bool, v *ast.Value) error { ei, err := v.Value(nil) // no vars allowed @@ -113,14 +44,14 @@ func setString(dst *string, v *ast.Value) error { return errorf(v.Position, "expected string, got non-string value %T(%v)", ei, ei) } -func fromGraphQL(dir *ast.Directive) (*GenqlientDirective, error) { +func fromGraphQL(dir *ast.Directive) (*genqlientDirective, error) { if dir.Name != "genqlient" { // Actually we just won't get here; we only get here if the line starts // with "# @genqlient", unless there's some sort of bug. return nil, errorf(dir.Position, "the only valid comment-directive is @genqlient, got %v", dir.Name) } - var retval GenqlientDirective + var retval genqlientDirective retval.pos = dir.Position var err error @@ -143,7 +74,7 @@ func fromGraphQL(dir *ast.Directive) (*GenqlientDirective, error) { return &retval, nil } -func (dir *GenqlientDirective) validate(node interface{}) error { +func (dir *genqlientDirective) validate(node interface{}) error { switch node := node.(type) { case *ast.OperationDefinition: if dir.Bind != "" { @@ -177,7 +108,7 @@ func (dir *GenqlientDirective) validate(node interface{}) error { } } -func (dir *GenqlientDirective) merge(other *GenqlientDirective) *GenqlientDirective { +func (dir *genqlientDirective) merge(other *genqlientDirective) *genqlientDirective { retval := *dir if other.Omitempty != nil { retval.Omitempty = other.Omitempty @@ -194,8 +125,8 @@ func (dir *GenqlientDirective) merge(other *GenqlientDirective) *GenqlientDirect func (g *generator) parsePrecedingComment( node interface{}, pos *ast.Position, -) (comment string, directive *GenqlientDirective, err error) { - directive = new(GenqlientDirective) +) (comment string, directive *genqlientDirective, err error) { + directive = new(genqlientDirective) if pos == nil || pos.Src == nil { // node was added by genqlient itself return "", directive, nil // treated as if there were no comment } diff --git a/generate/names.go b/generate/names.go index e19419aa..e5173c9f 100644 --- a/generate/names.go +++ b/generate/names.go @@ -2,7 +2,7 @@ package generate // This file generates the names for genqlient's generated types. This is // somewhat tricky because the names need to be unique, stable, and, to the -// extent possible, human-readable and -writable. See DESIGN.md for an +// extent possible, human-readable and -writable. See docs/DESIGN.md for an // overview of the considerations; in short, we need long names. // // Specifically, the names we generate are of the form: @@ -33,9 +33,9 @@ package generate // One subtlety in the above description is: is the "MyType" the interface or // the impelmentation? When it's a suffix, the answer is both: we generate // both MyFieldMyInterface and MyFieldMyImplementation, and the latter, in Go, -// implements the former. (See DESIGN.md for more.) But as an infix, we use -// the type on which the field is requested. Concretely, the following schema -// and query: +// implements the former. (See docs/DESIGN.md for more.) But as an infix, we +// use the type on which the field is requested. Concretely, the following +// schema and query: // type Query { f: I } // interface I { g: G } // type T implements I { g: G, h: H } diff --git a/generate/testdata/queries/ComplexInlineFragments.graphql b/generate/testdata/queries/ComplexInlineFragments.graphql index 99698f35..0e4dbe4c 100644 --- a/generate/testdata/queries/ComplexInlineFragments.graphql +++ b/generate/testdata/queries/ComplexInlineFragments.graphql @@ -1,5 +1,5 @@ -# We test all the spread cases from DESIGN.md, see there for more context on -# each, as well as various other nonsense. But for abstract-in-abstract +# We test all the spread cases from docs/DESIGN.md, see there for more context +# on each, as well as various other nonsense. But for abstract-in-abstract # spreads, we can't test cases (4b) and (4c), where I implements J or vice # versa, because gqlparser doesn't support interfaces that implement other # interfaces yet. diff --git a/generate/testdata/snapshots/TestGenerate-ComplexInlineFragments.graphql-ComplexInlineFragments.graphql.go b/generate/testdata/snapshots/TestGenerate-ComplexInlineFragments.graphql-ComplexInlineFragments.graphql.go index 03bf5385..584d6d72 100644 --- a/generate/testdata/snapshots/TestGenerate-ComplexInlineFragments.graphql-ComplexInlineFragments.graphql.go +++ b/generate/testdata/snapshots/TestGenerate-ComplexInlineFragments.graphql-ComplexInlineFragments.graphql.go @@ -858,8 +858,8 @@ type ComplexInlineFragmentsRootTopic struct { Name string `json:"name"` } -// We test all the spread cases from DESIGN.md, see there for more context on -// each, as well as various other nonsense. But for abstract-in-abstract +// We test all the spread cases from docs/DESIGN.md, see there for more context +// on each, as well as various other nonsense. But for abstract-in-abstract // spreads, we can't test cases (4b) and (4c), where I implements J or vice // versa, because gqlparser doesn't support interfaces that implement other // interfaces yet.