allow globbing, and extract queries from .go files
This commit is contained in:
@@ -51,6 +51,8 @@ fmt.Println("you are", viewerResp.Viewer.MyName)
|
|||||||
|
|
||||||
For a complete working example, see `example/`.
|
For a complete working example, see `example/`.
|
||||||
|
|
||||||
|
TODO: document this a bit more, including different ways to specify queries, options once we have those, etc.
|
||||||
|
|
||||||
## Documentation for generated code
|
## Documentation for generated code
|
||||||
|
|
||||||
For each GraphQL operation (query or mutation), genqlient generates a Go function with the exact same name, which accepts:
|
For each GraphQL operation (query or mutation), genqlient generates a Go function with the exact same name, which accepts:
|
||||||
@@ -64,13 +66,11 @@ TODO: document generated types further, especially if they become customizable.
|
|||||||
|
|
||||||
## Tests
|
## Tests
|
||||||
|
|
||||||
`go test ./...` tests code generation. (This is run by GitHub Actions.)
|
`go test ./...` tests code generation. (This is run by GitHub Actions.) Most of the tests are snapshot-based; see `generate/generate_test.go`.
|
||||||
|
|
||||||
Most of the tests are snapshot-based; they use the schema, queries, and snapshots in `generate/testdata`. The schema is in `schema.graphql`; the queries are in `TestName.graphql`. The test by default asserts that the output of the generator matches the snapshot `TestName.graphql.go`. To update the snapshots, run with `UPDATE_SNAPSHOTS=1`; it will fail the tests and print the diffs, but regenerate the snapshots. Make sure to check that the output is sensible!
|
`make example` rebuilds the example, and tests that everything wires up to a real API correctly. This is not currently included in `go test`, since it requires a token.
|
||||||
|
|
||||||
`make example` rebuilds the example, and tests that everything wires up to a real API correctly.
|
TODO(benkraft): Figure out how to get GitHub Actions a token to run the example.
|
||||||
|
|
||||||
TODO(benkraft): Figure out how to get GitHub Actions to run the example -- it needs a token.
|
|
||||||
|
|
||||||
## Design
|
## Design
|
||||||
|
|
||||||
@@ -103,3 +103,4 @@ Other:
|
|||||||
- get a designer to fix my bad logo-thing
|
- get a designer to fix my bad logo-thing
|
||||||
- custom scalar types (or custom mappings for standard scalars, if you want a special ID type say)
|
- custom scalar types (or custom mappings for standard scalars, if you want a special ID type say)
|
||||||
- allow mapping a custom type to a particular val (if you want to use a named type for some string, say)
|
- allow mapping a custom type to a particular val (if you want to use a named type for some string, say)
|
||||||
|
- (optionally?) include full query in generated godoc
|
||||||
|
|||||||
@@ -27,6 +27,10 @@ type Config struct {
|
|||||||
// how to convert that to SDL).
|
// how to convert that to SDL).
|
||||||
Schema string `yaml:"schema"`
|
Schema string `yaml:"schema"`
|
||||||
// Filenames or globs with the queries; defaults to queries.graphql.
|
// Filenames or globs with the queries; defaults to queries.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.
|
||||||
Queries []string `yaml:"queries"`
|
Queries []string `yaml:"queries"`
|
||||||
// The filename to which to write the generated code; defaults to
|
// The filename to which to write the generated code; defaults to
|
||||||
// generated.go
|
// generated.go
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
const dataDir = "testdata"
|
const dataDir = "testdata/queries"
|
||||||
|
|
||||||
func readFile(t *testing.T, filename string, allowNotExist bool) string {
|
func readFile(t *testing.T, filename string, allowNotExist bool) string {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
@@ -34,6 +34,18 @@ func gofmt(src string) (string, error) {
|
|||||||
return string(formatted), nil
|
return string(formatted), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestGenerate is a snapshot-based test of code-generation.
|
||||||
|
//
|
||||||
|
// This file just has the test runner; the actual data is all in
|
||||||
|
// testdata/queries. Specifically, the schema used for all the queries is in
|
||||||
|
// schema.graphql; the queries themselves are in TestName.graphql. The test
|
||||||
|
// asserts that running genqlient on that query produces the generated code in
|
||||||
|
// the snapshot-file TestName.graphql.go.
|
||||||
|
//
|
||||||
|
// To update the snapshots (if the code-generator has changed), run the test
|
||||||
|
// with `UPDATE_SNAPSHOTS=1`; it will fail the tests and print any diffs, but
|
||||||
|
// update the snapshots. Make sure to check that the output is sensible; the
|
||||||
|
// snapshots don't even get compiled!
|
||||||
func TestGenerate(t *testing.T) {
|
func TestGenerate(t *testing.T) {
|
||||||
update := (os.Getenv("UPDATE_SNAPSHOTS") == "1")
|
update := (os.Getenv("UPDATE_SNAPSHOTS") == "1")
|
||||||
|
|
||||||
@@ -56,8 +68,8 @@ func TestGenerate(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
goCode, err := Generate(&Config{
|
goCode, err := Generate(&Config{
|
||||||
Schema: filepath.Join("testdata", "schema.graphql"),
|
Schema: filepath.Join(dataDir, "schema.graphql"),
|
||||||
Queries: []string{filepath.Join("testdata", graphqlFilename)},
|
Queries: []string{filepath.Join(dataDir, graphqlFilename)},
|
||||||
Package: "test",
|
Package: "test",
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
+93
-13
@@ -2,8 +2,14 @@ package generate
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
goAst "go/ast"
|
||||||
|
goParser "go/parser"
|
||||||
|
"go/token"
|
||||||
|
goToken "go/token"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/vektah/gqlparser/v2"
|
"github.com/vektah/gqlparser/v2"
|
||||||
"github.com/vektah/gqlparser/v2/ast"
|
"github.com/vektah/gqlparser/v2/ast"
|
||||||
@@ -28,44 +34,76 @@ func getSchema(filename string) (*ast.Schema, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func getAndValidateQueries(filenames []string, schema *ast.Schema) (*ast.QueryDocument, error) {
|
func getAndValidateQueries(filenames []string, schema *ast.Schema) (*ast.QueryDocument, error) {
|
||||||
|
queryDoc, err := getQueries(filenames)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cf. gqlparser.LoadQuery
|
||||||
|
graphqlErrors := validator.Validate(schema, queryDoc)
|
||||||
|
if graphqlErrors != nil {
|
||||||
|
return nil, fmt.Errorf("query-spec does not match schema: %v", graphqlErrors)
|
||||||
|
}
|
||||||
|
|
||||||
|
return queryDoc, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getQueries(filenames []string) (*ast.QueryDocument, error) {
|
||||||
// We merge all the queries into a single query-document, since operations
|
// We merge all the queries into a single query-document, since operations
|
||||||
// in one might reference fragments in another.
|
// in one might reference fragments in another.
|
||||||
//
|
//
|
||||||
// TODO(benkraft): It might be better to merge just within a filename, so
|
// TODO(benkraft): It might be better to merge just within a filename, so
|
||||||
// that fragment-names don't need to be unique across files.
|
// that fragment-names don't need to be unique across files.
|
||||||
mergedQueryDoc := new(ast.QueryDocument)
|
mergedQueryDoc := new(ast.QueryDocument)
|
||||||
|
addQueryDoc := func(queryDoc *ast.QueryDocument) {
|
||||||
|
mergedQueryDoc.Operations = append(mergedQueryDoc.Operations, queryDoc.Operations...)
|
||||||
|
mergedQueryDoc.Fragments = append(mergedQueryDoc.Fragments, queryDoc.Fragments...)
|
||||||
|
}
|
||||||
|
|
||||||
|
expandedFilenames := make([]string, 0, len(filenames))
|
||||||
for _, filename := range filenames {
|
for _, filename := range filenames {
|
||||||
|
matches, err := filepath.Glob(filename)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("can't expand file-glob %v: %v", filename, err)
|
||||||
|
}
|
||||||
|
expandedFilenames = append(expandedFilenames, matches...)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, filename := range expandedFilenames {
|
||||||
|
text, err := ioutil.ReadFile(filename)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("unreadable query-spec file %v: %v", filename, err)
|
||||||
|
}
|
||||||
|
|
||||||
switch filepath.Ext(filename) {
|
switch filepath.Ext(filename) {
|
||||||
case ".graphql":
|
case ".graphql":
|
||||||
// Cf. gqlparser.LoadQuery
|
|
||||||
text, err := ioutil.ReadFile(filename)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("unreadable query-spec file %v: %v", filename, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
queryDoc, err := getQueriesFromString(string(text), filename)
|
queryDoc, err := getQueriesFromString(string(text), filename)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
mergedQueryDoc.Operations = append(mergedQueryDoc.Operations, queryDoc.Operations...)
|
addQueryDoc(queryDoc)
|
||||||
mergedQueryDoc.Fragments = append(mergedQueryDoc.Fragments, queryDoc.Fragments...)
|
|
||||||
|
case ".go":
|
||||||
|
queryDocs, err := getQueriesFromGo(string(text), filename)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, queryDoc := range queryDocs {
|
||||||
|
addQueryDoc(queryDoc)
|
||||||
|
}
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return nil, fmt.Errorf("unknown file type: %v", filename)
|
return nil, fmt.Errorf("unknown file type: %v", filename)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
graphqlErrors := validator.Validate(schema, mergedQueryDoc)
|
|
||||||
if graphqlErrors != nil {
|
|
||||||
return nil, fmt.Errorf("query-spec does not match schema: %v", graphqlErrors)
|
|
||||||
}
|
|
||||||
|
|
||||||
return mergedQueryDoc, nil
|
return mergedQueryDoc, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func getQueriesFromString(text string, filename string) (*ast.QueryDocument, error) {
|
func getQueriesFromString(text string, filename string) (*ast.QueryDocument, error) {
|
||||||
|
// Cf. gqlparser.LoadQuery
|
||||||
document, graphqlError := parser.ParseQuery(
|
document, graphqlError := parser.ParseQuery(
|
||||||
&ast.Source{Name: filename, Input: text})
|
&ast.Source{Name: filename, Input: text})
|
||||||
if graphqlError != nil { // ParseQuery returns type *graphql.Error, yuck
|
if graphqlError != nil { // ParseQuery returns type *graphql.Error, yuck
|
||||||
@@ -74,3 +112,45 @@ func getQueriesFromString(text string, filename string) (*ast.QueryDocument, err
|
|||||||
|
|
||||||
return document, nil
|
return document, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func getQueriesFromGo(text string, filename string) ([]*ast.QueryDocument, error) {
|
||||||
|
fset := goToken.NewFileSet()
|
||||||
|
f, err := goParser.ParseFile(fset, filename, text, 0)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("invalid Go file %v: %v", filename, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var retval []*ast.QueryDocument
|
||||||
|
goAst.Inspect(f, func(node goAst.Node) bool {
|
||||||
|
if err != nil {
|
||||||
|
return false // don't bother to recurse if something already failed
|
||||||
|
}
|
||||||
|
|
||||||
|
basicLit, ok := node.(*goAst.BasicLit)
|
||||||
|
if !ok || basicLit.Kind != token.STRING {
|
||||||
|
return true // recurse
|
||||||
|
}
|
||||||
|
|
||||||
|
var value string
|
||||||
|
value, err := strconv.Unquote(basicLit.Value)
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if !strings.HasPrefix(strings.TrimSpace(value), "# @genqlient") {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
fakeFilename := fset.Position(basicLit.Pos()).String()
|
||||||
|
var query *ast.QueryDocument
|
||||||
|
query, err = getQueriesFromString(value, fakeFilename)
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
retval = append(retval, query)
|
||||||
|
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
|
||||||
|
return retval, err
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,59 @@
|
|||||||
|
package generate
|
||||||
|
|
||||||
|
import (
|
||||||
|
"path/filepath"
|
||||||
|
"sort"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/vektah/gqlparser/v2/ast"
|
||||||
|
)
|
||||||
|
|
||||||
|
var parseDataDir = "testdata/parsing"
|
||||||
|
|
||||||
|
func sortQueries(queryDoc *ast.QueryDocument) {
|
||||||
|
sort.Slice(queryDoc.Operations, func(i, j int) bool {
|
||||||
|
return queryDoc.Operations[i].Name < queryDoc.Operations[j].Name
|
||||||
|
})
|
||||||
|
sort.Slice(queryDoc.Fragments, func(i, j int) bool {
|
||||||
|
return queryDoc.Fragments[i].Name < queryDoc.Fragments[j].Name
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestParse tests that query-extraction from different language source files
|
||||||
|
// produces equivalent results. We do not test the results it produces (that's
|
||||||
|
// covered by TestGenerate), just that they are equivalent in different
|
||||||
|
// languages (since TestGenerate only uses .graphql as input).
|
||||||
|
func TestParse(t *testing.T) {
|
||||||
|
extensions := []string{"go"}
|
||||||
|
|
||||||
|
graphqlQueries, err := getQueries([]string{filepath.Join(parseDataDir, "*.graphql")})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// check it's at least non-empty
|
||||||
|
if len(graphqlQueries.Operations) == 0 || len(graphqlQueries.Fragments) == 0 {
|
||||||
|
t.Fatalf("Didn't find any queries in *.graphql files")
|
||||||
|
}
|
||||||
|
|
||||||
|
sortQueries(graphqlQueries)
|
||||||
|
|
||||||
|
for _, ext := range extensions {
|
||||||
|
t.Run(ext, func(t *testing.T) {
|
||||||
|
queries, err := getQueries([]string{filepath.Join(parseDataDir, "*."+ext)})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// The different file-types may have the operations/fragments in a
|
||||||
|
// different order.
|
||||||
|
sortQueries(queries)
|
||||||
|
|
||||||
|
got, want := ast.Dump(graphqlQueries), ast.Dump(queries)
|
||||||
|
if got != want {
|
||||||
|
// TODO: nice diffing
|
||||||
|
t.Errorf("got:\n%v\nwant:\n%v\n", got, want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
+46
@@ -0,0 +1,46 @@
|
|||||||
|
package parsing
|
||||||
|
|
||||||
|
const MyFragment = `
|
||||||
|
# @genqlient
|
||||||
|
fragment MyFragment on MyType {
|
||||||
|
myFragmentField
|
||||||
|
...NestedFragment
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
var _ = `
|
||||||
|
# @genqlient
|
||||||
|
fragment NestedFragment on MyType {
|
||||||
|
myOtherFragmentField
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
const MyQuery = `
|
||||||
|
# @genqlient
|
||||||
|
query MyQuery {
|
||||||
|
myField
|
||||||
|
myOtherField {
|
||||||
|
...MyFragment
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
func query(s string) {}
|
||||||
|
|
||||||
|
func MyMutation() {
|
||||||
|
query(`
|
||||||
|
# @genqlient
|
||||||
|
mutation MyMutation {
|
||||||
|
myField
|
||||||
|
myOtherField {
|
||||||
|
...MyFragment
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
NotAString = 1
|
||||||
|
NotAQuery = `query
|
||||||
|
writing with GraphQL is fun!`
|
||||||
|
)
|
||||||
+19
@@ -0,0 +1,19 @@
|
|||||||
|
query MyQuery {
|
||||||
|
myField
|
||||||
|
myOtherField {
|
||||||
|
...MyFragment
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fragment MyFragment on MyType {
|
||||||
|
myFragmentField
|
||||||
|
...NestedFragment
|
||||||
|
}
|
||||||
|
|
||||||
|
fragment UnusedFragment on MyType {
|
||||||
|
myFragmentField
|
||||||
|
}
|
||||||
|
|
||||||
|
fragment NestedFragment on MyType {
|
||||||
|
myOtherFragmentField
|
||||||
|
}
|
||||||
+8
@@ -0,0 +1,8 @@
|
|||||||
|
package parsing
|
||||||
|
|
||||||
|
const _ = `
|
||||||
|
# @genqlient
|
||||||
|
fragment UnusedFragment on MyType {
|
||||||
|
myFragmentField
|
||||||
|
}
|
||||||
|
`
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
mutation MyMutation {
|
||||||
|
myField
|
||||||
|
myOtherField {
|
||||||
|
...MyFragment
|
||||||
|
}
|
||||||
|
}
|
||||||
Vendored
Vendored
Vendored
Vendored
Vendored
Vendored
Vendored
Vendored
Vendored
Vendored
Vendored
Vendored
Reference in New Issue
Block a user