Repository: Khan/genqlient
Pull request #137 (2021-10-05 16:36:08)

Align support for multiple operation-files and multiple schema-files
Multiple schema-files are now supported as of #134, but the support was
a bit different from how we did multiple operation-files.  Before anyone
starts to depend on the ways the syntaxes differ, let's just make them
the same.  Since it's easy, I also added support for having just a
single operations-file.

I also realized while writing this that the type-change is technically
breaking (if you call from Go), so documented it as such. I think
this is unlikely to affect many people.

Test plan: make check

benjaminjkraft: cc @hsblhsn if you wanted to take a look!  If it's an issue we could move to using the fancier globbing for both.

Patch:
diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md
index 33bdfaf..f1bd00d 100644
--- a/docs/CHANGELOG.md
+++ b/docs/CHANGELOG.md
@@ -22,10 +22,12 @@ When releasing a new version:
 
 ### Breaking changes:
 
+- The `Config` fields `Schema` and `Operations` are now both of type `StringList`.  This does not affect configuration via `genqlient.yaml`, only via the Go API.
+
 ### New features:
 
 - genqlient now generates getter methods for all fields, even those which do not implement a genqlient-generated interface; this can be useful for callers who wish to define their own interface and have several unrelated genqlient types which have the same fields implement it.
-- genqlient config now accepts either a single or multiple schema files for the `schema` field.
+- genqlient config now accepts either a single or multiple files for the `schema` and `operations` fields (previously it accepted only one `schema`, and required a list of `operations` files).
 - The `typename` option can now be used on basic types (string, int, etc) as well as structs; this can be useful to have genqlient define new types like `type Language string` and use that type for specified fields.
 
 ### Bug fixes:
diff --git a/docs/genqlient.yaml b/docs/genqlient.yaml
index 3cd3799..1ff58b6 100644
--- a/docs/genqlient.yaml
+++ b/docs/genqlient.yaml
@@ -8,15 +8,17 @@
 #  schema:
 #  - user.graphql
 #  - ./schema/*.graphql
-#  - ./another_directory/**/*.graphqls
+#  - ./another_directory/*/*.graphql
 schema: schema.graphql
 
-# Filenames or globs with the operations for which to generate code, relative
+# Filename(s) 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.
+#
+# Like schema, this may be a single file or a list.
 operations:
 - genqlient.graphql
 - "pkg/*.go"
diff --git a/generate/config.go b/generate/config.go
index d96cd54..b6a2865 100644
--- a/generate/config.go
+++ b/generate/config.go
@@ -6,10 +6,7 @@ import (
 	"io/ioutil"
 	"os"
 	"path/filepath"
-	"regexp"
-	"strings"
 
-	"github.com/vektah/gqlparser/v2/ast"
 	"gopkg.in/yaml.v2"
 )
 
@@ -21,7 +18,7 @@ type Config struct {
 	// The following fields are documented at:
 	// https://github.com/Khan/genqlient/blob/main/docs/genqlient.yaml
 	Schema           StringList              `yaml:"schema"`
-	Operations       []string                `yaml:"operations"`
+	Operations       StringList              `yaml:"operations"`
 	Generated        string                  `yaml:"generated"`
 	Package          string                  `yaml:"package"`
 	ExportOperations string                  `yaml:"export_operations"`
@@ -126,72 +123,3 @@ func initConfig(filename string) error {
 	_, err = io.Copy(w, r)
 	return errorf(nil, "unable to write default genqlient.yaml: %v", err)
 }
-
-var path2regex = strings.NewReplacer(
-	`.`, `\.`,
-	`*`, `.+`,
-	`\`, `[\\/]`,
-	`/`, `[\\/]`,
-)
-
-// loadSchemaSources parses the schema file path globs. Parses graphql files,
-// and returns the parsed ast.Source objects.
-// Sourced From:
-// 		https://github.com/99designs/gqlgen/blob/1a0b19feff6f02d2af6631c9d847bc243f8ede39/codegen/config/config.go#L129-L181
-func loadSchemaSources(schemas StringList) ([]*ast.Source, error) {
-	preGlobbing := schemas
-	schemas = StringList{}
-	source := make([]*ast.Source, 0)
-	for _, f := range preGlobbing {
-		var matches []string
-
-		// for ** we want to override default globbing patterns and walk all
-		// subdirectories to match schema files.
-		if strings.Contains(f, "**") {
-			pathParts := strings.SplitN(f, "**", 2)
-			rest := strings.TrimPrefix(strings.TrimPrefix(pathParts[1], `\`), `/`)
-			// turn the rest of the glob into a regex, anchored only at the end because ** allows
-			// for any number of dirs in between and walk will let us match against the full path name
-			globRe := regexp.MustCompile(path2regex.Replace(rest) + `$`)
-
-			if err := filepath.Walk(pathParts[0], func(path string, info os.FileInfo, err error) error {
-				if err != nil {
-					return err
-				}
-
-				if globRe.MatchString(strings.TrimPrefix(path, pathParts[0])) {
-					matches = append(matches, path)
-				}
-
-				return nil
-			}); err != nil {
-				return nil, errorf(nil, "failed to walk schema at root %s: %w", pathParts[0], err)
-			}
-		} else {
-			var err error
-			matches, err = filepath.Glob(f)
-			if err != nil {
-				return nil, errorf(nil, "failed to glob schema filename %s: %w", f, err)
-			}
-		}
-
-		for _, m := range matches {
-			if schemas.Has(m) {
-				continue
-			}
-			schemas = append(schemas, m)
-		}
-	}
-	for _, filename := range schemas {
-		filename = filepath.ToSlash(filename)
-		var err error
-		var schemaRaw []byte
-		schemaRaw, err = ioutil.ReadFile(filename)
-		if err != nil {
-			return nil, errorf(nil, "unable to open schema: %w", err)
-		}
-
-		source = append(source, &ast.Source{Name: filename, Input: string(schemaRaw)})
-	}
-	return source, nil
-}
diff --git a/generate/parse.go b/generate/parse.go
index bb6e1ef..b26b7de 100644
--- a/generate/parse.go
+++ b/generate/parse.go
@@ -16,20 +16,30 @@ import (
 	"github.com/vektah/gqlparser/v2/validator"
 )
 
-func getSchema(filePatterns StringList) (*ast.Schema, error) {
-	sources, err := loadSchemaSources(filePatterns)
+func getSchema(globs StringList) (*ast.Schema, error) {
+	filenames, err := expandFilenames(globs)
 	if err != nil {
 		return nil, err
 	}
+
+	sources := make([]*ast.Source, len(filenames))
+	for i, filename := range filenames {
+		text, err := ioutil.ReadFile(filename)
+		if err != nil {
+			return nil, errorf(nil, "unreadable schema file %v: %v", filename, err)
+		}
+		sources[i] = &ast.Source{Name: filename, Input: string(text)}
+	}
+
 	schema, graphqlError := gqlparser.LoadSchema(sources...)
 	if graphqlError != nil {
-		filename, _ := graphqlError.Extensions["file"].(string)
-		return nil, errorf(nil, "invalid schema file %v: %v", filename, graphqlError)
+		return nil, errorf(nil, "invalid schema: %v", graphqlError)
 	}
+
 	return schema, nil
 }
 
-func getAndValidateQueries(basedir string, filenames []string, schema *ast.Schema) (*ast.QueryDocument, error) {
+func getAndValidateQueries(basedir string, filenames StringList, schema *ast.Schema) (*ast.QueryDocument, error) {
 	queryDoc, err := getQueries(basedir, filenames)
 	if err != nil {
 		return nil, err
@@ -44,7 +54,25 @@ func getAndValidateQueries(basedir string, filenames []string, schema *ast.Schem
 	return queryDoc, nil
 }
 
-func getQueries(basedir string, filenames []string) (*ast.QueryDocument, error) {
+func expandFilenames(globs []string) ([]string, error) {
+	uniqFilenames := make(map[string]bool, len(globs))
+	for _, glob := range globs {
+		matches, err := filepath.Glob(glob)
+		if err != nil {
+			return nil, errorf(nil, "can't expand file-glob %v: %v", glob, err)
+		}
+		for _, match := range matches {
+			uniqFilenames[match] = true
+		}
+	}
+	filenames := make([]string, 0, len(uniqFilenames))
+	for filename := range uniqFilenames {
+		filenames = append(filenames, filename)
+	}
+	return filenames, nil
+}
+
+func getQueries(basedir string, globs StringList) (*ast.QueryDocument, error) {
 	// We merge all the queries into a single query-document, since operations
 	// in one might reference fragments in another.
 	//
@@ -57,16 +85,12 @@ func getQueries(basedir string, filenames []string) (*ast.QueryDocument, error)
 		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, errorf(nil, "can't expand file-glob %v: %v", filename, err)
-		}
-		expandedFilenames = append(expandedFilenames, matches...)
+	filenames, err := expandFilenames(globs)
+	if err != nil {
+		return nil, err
 	}
 
-	for _, filename := range expandedFilenames {
+	for _, filename := range filenames {
 		text, err := ioutil.ReadFile(filename)
 		if err != nil {
 			return nil, errorf(nil, "unreadable query-spec file %v: %v", filename, err)
diff --git a/generate/stringlist.go b/generate/stringlist.go
index c49e76c..a41f221 100644
--- a/generate/stringlist.go
+++ b/generate/stringlist.go
@@ -22,12 +22,3 @@ func (a *StringList) UnmarshalYAML(unmarshal func(interface{}) error) error {
 	*a = multi
 	return nil
 }
-
-func (a StringList) Has(file string) bool {
-	for _, existing := range a {
-		if existing == file {
-			return true
-		}
-	}
-	return false
-}
diff --git a/internal/integration/genqlient.yaml b/internal/integration/genqlient.yaml
index 0fd9fb6..50c2cf4 100644
--- a/internal/integration/genqlient.yaml
+++ b/internal/integration/genqlient.yaml
@@ -1,6 +1,5 @@
 schema: schema.graphql
-operations:
-- "*_test.go"
+operations: "*_test.go"
 generated: generated.go
 allow_broken_features: true
 bindings:


Test patch:
diff --git a/generate/testdata/snapshots/TestGenerateErrors-InvalidSchema-go b/generate/testdata/snapshots/TestGenerateErrors-InvalidSchema-go
index 6aad6cd..6c9eca4 100644
--- a/generate/testdata/snapshots/TestGenerateErrors-InvalidSchema-go
+++ b/generate/testdata/snapshots/TestGenerateErrors-InvalidSchema-go
@@ -1,1 +1,1 @@
-testdata/errors/InvalidSchema.schema.graphql:4: invalid schema file testdata/errors/InvalidSchema.schema.graphql: Expected :, found }
+testdata/errors/InvalidSchema.schema.graphql:4: invalid schema: Expected :, found }
diff --git a/generate/testdata/snapshots/TestGenerateErrors-InvalidSchema-graphql b/generate/testdata/snapshots/TestGenerateErrors-InvalidSchema-graphql
index 6aad6cd..6c9eca4 100644
--- a/generate/testdata/snapshots/TestGenerateErrors-InvalidSchema-graphql
+++ b/generate/testdata/snapshots/TestGenerateErrors-InvalidSchema-graphql
@@ -1,1 +1,1 @@
-testdata/errors/InvalidSchema.schema.graphql:4: invalid schema file testdata/errors/InvalidSchema.schema.graphql: Expected :, found }
+testdata/errors/InvalidSchema.schema.graphql:4: invalid schema: Expected :, found }