Reorganize documentation to make room to grow (#84)

## 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: https://github.com/Khan/genqlient/issues/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: https://github.com/Khan/genqlient/pull/84
This commit is contained in:
Ben Kraft
2021-09-10 16:03:30 -07:00
committed by GitHub
parent d41cf636af
commit 2eba9a2c30
16 changed files with 275 additions and 237 deletions
+14 -107
View File
@@ -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"`
}
+9 -9
View File
@@ -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,
+2 -3
View File
@@ -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
+2 -2
View File
@@ -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 {
+14 -83
View File
@@ -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
}
+4 -4
View File
@@ -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 }
+2 -2
View File
@@ -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.
@@ -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.