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/`.
|
||||
|
||||
TODO: document this a bit more, including different ways to specify queries, options once we have those, etc.
|
||||
|
||||
## Documentation for generated code
|
||||
|
||||
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
|
||||
|
||||
`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 to run the example -- it needs a token.
|
||||
TODO(benkraft): Figure out how to get GitHub Actions a token to run the example.
|
||||
|
||||
## Design
|
||||
|
||||
@@ -103,3 +103,4 @@ Other:
|
||||
- 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)
|
||||
- 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).
|
||||
Schema string `yaml:"schema"`
|
||||
// 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"`
|
||||
// The filename to which to write the generated code; defaults to
|
||||
// generated.go
|
||||
|
||||
@@ -11,7 +11,7 @@ import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
const dataDir = "testdata"
|
||||
const dataDir = "testdata/queries"
|
||||
|
||||
func readFile(t *testing.T, filename string, allowNotExist bool) string {
|
||||
t.Helper()
|
||||
@@ -34,6 +34,18 @@ func gofmt(src string) (string, error) {
|
||||
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) {
|
||||
update := (os.Getenv("UPDATE_SNAPSHOTS") == "1")
|
||||
|
||||
@@ -56,8 +68,8 @@ func TestGenerate(t *testing.T) {
|
||||
}
|
||||
|
||||
goCode, err := Generate(&Config{
|
||||
Schema: filepath.Join("testdata", "schema.graphql"),
|
||||
Queries: []string{filepath.Join("testdata", graphqlFilename)},
|
||||
Schema: filepath.Join(dataDir, "schema.graphql"),
|
||||
Queries: []string{filepath.Join(dataDir, graphqlFilename)},
|
||||
Package: "test",
|
||||
})
|
||||
if err != nil {
|
||||
|
||||
+93
-13
@@ -2,8 +2,14 @@ package generate
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
goAst "go/ast"
|
||||
goParser "go/parser"
|
||||
"go/token"
|
||||
goToken "go/token"
|
||||
"io/ioutil"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/vektah/gqlparser/v2"
|
||||
"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) {
|
||||
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
|
||||
// in one might reference fragments in another.
|
||||
//
|
||||
// TODO(benkraft): It might be better to merge just within a filename, so
|
||||
// that fragment-names don't need to be unique across files.
|
||||
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 {
|
||||
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) {
|
||||
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)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
mergedQueryDoc.Operations = append(mergedQueryDoc.Operations, queryDoc.Operations...)
|
||||
mergedQueryDoc.Fragments = append(mergedQueryDoc.Fragments, queryDoc.Fragments...)
|
||||
addQueryDoc(queryDoc)
|
||||
|
||||
case ".go":
|
||||
queryDocs, err := getQueriesFromGo(string(text), filename)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, queryDoc := range queryDocs {
|
||||
addQueryDoc(queryDoc)
|
||||
}
|
||||
|
||||
default:
|
||||
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
|
||||
}
|
||||
|
||||
func getQueriesFromString(text string, filename string) (*ast.QueryDocument, error) {
|
||||
// Cf. gqlparser.LoadQuery
|
||||
document, graphqlError := parser.ParseQuery(
|
||||
&ast.Source{Name: filename, Input: text})
|
||||
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
|
||||
}
|
||||
|
||||
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