diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index d16df7cb..a6581e48 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -25,7 +25,7 @@ When releasing a new version: ### 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. ### Bug fixes: - In certain very rare cases involving duplicate fields in fragment spreads, genqlient would generate code that failed to compile due to duplicate methods not getting promoted; genqlient now generates correct types. (See #126 for a more complete description.) diff --git a/docs/genqlient.yaml b/docs/genqlient.yaml index b909d61e..1173f9d1 100644 --- a/docs/genqlient.yaml +++ b/docs/genqlient.yaml @@ -4,6 +4,11 @@ # The filename with the GraphQL schema (in SDL format), relative to # genqlient.yaml. +# This can also be a list of filenames, such as: +# schema: +# - user.graphql +# - ./schema/*.graphql +# - ./another_directory/**/*.graphqls schema: schema.graphql # Filenames or globs with the operations for which to generate code, relative diff --git a/generate/config.go b/generate/config.go index f23e668f..d96cd549 100644 --- a/generate/config.go +++ b/generate/config.go @@ -6,7 +6,10 @@ import ( "io/ioutil" "os" "path/filepath" + "regexp" + "strings" + "github.com/vektah/gqlparser/v2/ast" "gopkg.in/yaml.v2" ) @@ -17,7 +20,7 @@ import ( type Config struct { // The following fields are documented at: // https://github.com/Khan/genqlient/blob/main/docs/genqlient.yaml - Schema string `yaml:"schema"` + Schema StringList `yaml:"schema"` Operations []string `yaml:"operations"` Generated string `yaml:"generated"` Package string `yaml:"package"` @@ -55,8 +58,9 @@ type TypeBinding struct { // typically the directory of the config file. func (c *Config) ValidateAndFillDefaults(baseDir string) error { c.baseDir = baseDir - // Make paths relative to config dir - c.Schema = filepath.Join(baseDir, c.Schema) + for i := range c.Schema { + c.Schema[i] = filepath.Join(baseDir, c.Schema[i]) + } for i := range c.Operations { c.Operations[i] = filepath.Join(baseDir, c.Operations[i]) } @@ -122,3 +126,72 @@ 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/generate_test.go b/generate/generate_test.go index fcc05b5d..8cce225f 100644 --- a/generate/generate_test.go +++ b/generate/generate_test.go @@ -76,7 +76,7 @@ func TestGenerate(t *testing.T) { t.Run(sourceFilename, func(t *testing.T) { generated, err := Generate(&Config{ - Schema: filepath.Join(dataDir, "schema.graphql"), + Schema: []string{filepath.Join(dataDir, "schema.graphql")}, Operations: []string{filepath.Join(dataDir, sourceFilename)}, Package: "test", Generated: goFilename, @@ -197,7 +197,7 @@ func TestGenerateWithConfig(t *testing.T) { baseDir := filepath.Join(dataDir, test.baseDir) t.Run(test.name, func(t *testing.T) { err := config.ValidateAndFillDefaults(baseDir) - config.Schema = filepath.Join(dataDir, "schema.graphql") + config.Schema = []string{filepath.Join(dataDir, "schema.graphql")} config.Operations = []string{filepath.Join(dataDir, sourceFilename)} if err != nil { t.Fatal(err) @@ -256,7 +256,7 @@ func TestGenerateErrors(t *testing.T) { t.Run(testFilename, func(t *testing.T) { _, err := Generate(&Config{ - Schema: filepath.Join(errorsDir, schemaFilename), + Schema: []string{filepath.Join(errorsDir, schemaFilename)}, Operations: []string{filepath.Join(errorsDir, sourceFilename)}, Package: "test", Generated: os.DevNull, diff --git a/generate/parse.go b/generate/parse.go index 39b5b227..bb6e1efe 100644 --- a/generate/parse.go +++ b/generate/parse.go @@ -16,19 +16,16 @@ import ( "github.com/vektah/gqlparser/v2/validator" ) -func getSchema(filename string) (*ast.Schema, error) { - text, err := ioutil.ReadFile(filename) +func getSchema(filePatterns StringList) (*ast.Schema, error) { + sources, err := loadSchemaSources(filePatterns) if err != nil { - return nil, errorf(nil, "unreadable schema file %v: %v", filename, err) + return nil, err } - - schema, graphqlError := gqlparser.LoadSchema( - &ast.Source{Name: filename, Input: string(text)}) + schema, graphqlError := gqlparser.LoadSchema(sources...) if graphqlError != nil { - return nil, errorf(nil, "invalid schema file %v: %v", - filename, graphqlError) + filename, _ := graphqlError.Extensions["file"].(string) + return nil, errorf(nil, "invalid schema file %v: %v", filename, graphqlError) } - return schema, nil } diff --git a/generate/stringlist.go b/generate/stringlist.go new file mode 100644 index 00000000..c49e76ce --- /dev/null +++ b/generate/stringlist.go @@ -0,0 +1,33 @@ +package generate + +// StringList provides yaml unmarshaler to accept both `string` and `[]string` as a valid type. +// Sourced from: +// https://github.com/99designs/gqlgen/blob/1a0b19feff6f02d2af6631c9d847bc243f8ede39/codegen/config/config.go#L302-L329 +type StringList []string + +func (a *StringList) UnmarshalYAML(unmarshal func(interface{}) error) error { + var single string + err := unmarshal(&single) + if err == nil { + *a = []string{single} + return nil + } + + var multi []string + err = unmarshal(&multi) + if err != nil { + return err + } + + *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/go.sum b/go.sum index 48cc5313..53ba6d6e 100644 --- a/go.sum +++ b/go.sum @@ -14,7 +14,6 @@ github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0 h1:jfIu9sQUG6Ig github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0/go.mod h1:t2tdKJDJF9BV14lnkjHmOQgcvEKgtqs5a1N3LNdJhGE= github.com/bradleyjkemp/cupaloy/v2 v2.6.0 h1:knToPYa2xtfg42U3I6punFEjaGFKWQRXJwj0JTv4mTs= github.com/bradleyjkemp/cupaloy/v2 v2.6.0/go.mod h1:bm7JXdkRd4BHJk9HpwqAI8BoAY1lps46Enkdqw6aRX0= -github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d h1:U+s90UTSYgptZMwQh2aRr3LuazLJIa+Pg3Kc1ylSYVY= github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= @@ -43,17 +42,14 @@ github.com/mitchellh/mapstructure v0.0.0-20180203102830-a4e142e9c047 h1:zCoDWFD5 github.com/mitchellh/mapstructure v0.0.0-20180203102830-a4e142e9c047/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/opentracing/basictracer-go v1.0.0/go.mod h1:QfBfYuafItcjQuMwinw9GhYKwFXS9KnPs5lxoYwgW74= github.com/opentracing/opentracing-go v1.0.2/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= -github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rs/cors v1.6.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU= -github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q= github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0= github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= github.com/shurcooL/httpfs v0.0.0-20171119174359-809beceb2371/go.mod h1:ZY1cvUeJuFPAdZ/B6v7RHavJWZn2YPVFQ1OSXhCGOkg= -github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo= github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= github.com/shurcooL/vfsgen v0.0.0-20180121065927-ffb13db8def0/go.mod h1:TrYk7fJVaAttu97ZZKrO9UbRa8izdowaMIZcxYMbVaw= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= @@ -64,7 +60,6 @@ github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81P github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/urfave/cli/v2 v2.1.1 h1:Qt8FeAtxE/vfdrLmR3rxR6JRE0RoVmbXu8+6kZtYU4k= github.com/urfave/cli/v2 v2.1.1/go.mod h1:SE9GqnLQmjVa0iPEY0f1w3ygNIYcIJ0OKPMoW2caLfQ= github.com/vektah/dataloaden v0.2.1-0.20190515034641-a19b9a6e7c9e/go.mod h1:/HUdMve7rvxZma+2ZELQeNh88+003LL7Pf/CZ089j8U= github.com/vektah/gqlparser/v2 v2.1.0 h1:uiKJ+T5HMGGQM2kRKQ8Pxw8+Zq9qhhZhz/lieYvCMns=