2eba9a2c30
## 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
174 lines
4.8 KiB
Go
174 lines
4.8 KiB
Go
package generate
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
|
|
"github.com/vektah/gqlparser/v2/ast"
|
|
"github.com/vektah/gqlparser/v2/parser"
|
|
)
|
|
|
|
// Represents the genqlient directive, described in detail in
|
|
// docs/genqlient_directive.graphql.
|
|
type genqlientDirective struct {
|
|
pos *ast.Position
|
|
Omitempty *bool
|
|
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 setBool(dst **bool, v *ast.Value) error {
|
|
ei, err := v.Value(nil) // no vars allowed
|
|
if err != nil {
|
|
return errorf(v.Position, "invalid boolean value %v: %v", v, err)
|
|
}
|
|
if b, ok := ei.(bool); ok {
|
|
*dst = &b
|
|
return nil
|
|
}
|
|
return errorf(v.Position, "expected boolean, got non-boolean value %T(%v)", ei, ei)
|
|
}
|
|
|
|
func setString(dst *string, v *ast.Value) error {
|
|
ei, err := v.Value(nil) // no vars allowed
|
|
if err != nil {
|
|
return errorf(v.Position, "invalid string value %v: %v", v, err)
|
|
}
|
|
if b, ok := ei.(string); ok {
|
|
*dst = b
|
|
return nil
|
|
}
|
|
return errorf(v.Position, "expected string, got non-string value %T(%v)", ei, ei)
|
|
}
|
|
|
|
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
|
|
retval.pos = dir.Position
|
|
|
|
var err error
|
|
for _, arg := range dir.Arguments {
|
|
switch arg.Name {
|
|
// TODO: reflect and struct tags?
|
|
case "omitempty":
|
|
err = setBool(&retval.Omitempty, arg.Value)
|
|
case "pointer":
|
|
err = setBool(&retval.Pointer, arg.Value)
|
|
case "bind":
|
|
err = setString(&retval.Bind, arg.Value)
|
|
default:
|
|
return nil, errorf(arg.Position, "unknown argument %v for @genqlient", arg.Name)
|
|
}
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
return &retval, nil
|
|
}
|
|
|
|
func (dir *genqlientDirective) validate(node interface{}) error {
|
|
switch node := node.(type) {
|
|
case *ast.OperationDefinition:
|
|
if dir.Bind != "" {
|
|
return errorf(dir.pos, "bind may not be applied to the entire operation")
|
|
}
|
|
|
|
// Anything else is valid on the entire operation; it will just apply
|
|
// to whatever it is relevant to.
|
|
return nil
|
|
case *ast.FragmentDefinition:
|
|
if dir.Bind != "" {
|
|
// TODO(benkraft): Implement this if people find it useful.
|
|
return errorf(dir.pos, "bind is not implemented for named fragments")
|
|
}
|
|
|
|
// Like operations, anything else will just apply to the entire
|
|
// fragment.
|
|
return nil
|
|
case *ast.VariableDefinition:
|
|
if dir.Omitempty != nil && node.Type.NonNull {
|
|
return errorf(dir.pos, "omitempty may only be used on optional arguments")
|
|
}
|
|
return nil
|
|
case *ast.Field:
|
|
if dir.Omitempty != nil {
|
|
return errorf(dir.pos, "omitempty is not applicable to fields")
|
|
}
|
|
return nil
|
|
default:
|
|
return errorf(dir.pos, "invalid @genqlient directive location: %T", node)
|
|
}
|
|
}
|
|
|
|
func (dir *genqlientDirective) merge(other *genqlientDirective) *genqlientDirective {
|
|
retval := *dir
|
|
if other.Omitempty != nil {
|
|
retval.Omitempty = other.Omitempty
|
|
}
|
|
if other.Pointer != nil {
|
|
retval.Pointer = other.Pointer
|
|
}
|
|
if other.Bind != "" {
|
|
retval.Bind = other.Bind
|
|
}
|
|
return &retval
|
|
}
|
|
|
|
func (g *generator) parsePrecedingComment(
|
|
node interface{},
|
|
pos *ast.Position,
|
|
) (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
|
|
}
|
|
|
|
var commentLines []string
|
|
sourceLines := strings.Split(pos.Src.Input, "\n")
|
|
for i := pos.Line - 1; i > 0; i-- {
|
|
line := strings.TrimSpace(sourceLines[i-1])
|
|
trimmed := strings.TrimSpace(strings.TrimPrefix(line, "#"))
|
|
if strings.HasPrefix(line, "# @genqlient") {
|
|
graphQLDirective, err := parseDirective(trimmed, pos)
|
|
if err != nil {
|
|
return "", nil, err
|
|
}
|
|
genqlientDirective, err := fromGraphQL(graphQLDirective)
|
|
if err != nil {
|
|
return "", nil, err
|
|
}
|
|
err = genqlientDirective.validate(node)
|
|
if err != nil {
|
|
return "", nil, err
|
|
}
|
|
directive = directive.merge(genqlientDirective)
|
|
} else if strings.HasPrefix(line, "#") {
|
|
commentLines = append(commentLines, trimmed)
|
|
} else {
|
|
break
|
|
}
|
|
}
|
|
|
|
reverse(commentLines)
|
|
|
|
return strings.TrimSpace(strings.Join(commentLines, "\n")), directive, nil
|
|
}
|
|
|
|
func parseDirective(line string, pos *ast.Position) (*ast.Directive, error) {
|
|
// HACK: parse the "directive" by making a fake query containing it.
|
|
fakeQuery := fmt.Sprintf("query %v { field }", line)
|
|
doc, err := parser.ParseQuery(&ast.Source{Input: fakeQuery})
|
|
if err != nil {
|
|
return nil, errorf(pos, "invalid genqlient directive: %v", err)
|
|
}
|
|
return doc.Operations[0].Directives[0], nil
|
|
}
|