From 10dc3880169bc531642fd54f22527dcd46f8a7f8 Mon Sep 17 00:00:00 2001 From: John Maguire Date: Thu, 21 Oct 2021 21:43:34 -0400 Subject: [PATCH] Walk parent directories to find config file (#141) Code is mostly borrowed from [gqlgen](https://github.com/99designs/gqlgen). The idea here is that I want to be able to store `genqlient.yaml` at the top-level, but my client code lives down in `graph/client/`. I put the `//go:generate` line in `graph/client/client.go`. --- docs/genqlient.yaml | 4 +- generate/config.go | 45 ++++++++++ generate/config_test.go | 90 +++++++++++++++++++ generate/main.go | 24 +++-- .../find-config/current/genqlient.yaml | 6 ++ .../filenames/dotyaml/.genqlient.yaml | 6 ++ .../filenames/dotyml/.genqlient.yml | 6 ++ .../find-config/filenames/none/.gitkeep | 0 .../find-config/filenames/yaml/genqlient.yaml | 6 ++ .../find-config/filenames/yml/genqlient.yml | 6 ++ generate/testdata/find-config/none/.gitkeep | 0 .../testdata/find-config/none/child/.gitkeep | 0 .../find-config/parent/child/.gitkeep | 0 .../find-config/parent/genqlient.yaml | 6 ++ 14 files changed, 193 insertions(+), 6 deletions(-) create mode 100644 generate/config_test.go create mode 100644 generate/testdata/find-config/current/genqlient.yaml create mode 100644 generate/testdata/find-config/filenames/dotyaml/.genqlient.yaml create mode 100644 generate/testdata/find-config/filenames/dotyml/.genqlient.yml create mode 100644 generate/testdata/find-config/filenames/none/.gitkeep create mode 100644 generate/testdata/find-config/filenames/yaml/genqlient.yaml create mode 100644 generate/testdata/find-config/filenames/yml/genqlient.yml create mode 100644 generate/testdata/find-config/none/.gitkeep create mode 100644 generate/testdata/find-config/none/child/.gitkeep create mode 100644 generate/testdata/find-config/parent/child/.gitkeep create mode 100644 generate/testdata/find-config/parent/genqlient.yaml diff --git a/docs/genqlient.yaml b/docs/genqlient.yaml index 1ff58b67..25aef2e6 100644 --- a/docs/genqlient.yaml +++ b/docs/genqlient.yaml @@ -1,6 +1,8 @@ # genqlient.yaml is genqlient's configuration file. This genqlient.yaml is an # example; use `go run github.com/Khan/genqlient --init` to generate a simple -# starting point. +# starting point. By default, genqlient looks for the configuration file +# named [.]genqlient.y[a]ml in the current directory or any ancestor; or the +# filename may be given as an argument. # The filename with the GraphQL schema (in SDL format), relative to # genqlient.yaml. diff --git a/generate/config.go b/generate/config.go index 03fa6946..dc88eb55 100644 --- a/generate/config.go +++ b/generate/config.go @@ -10,6 +10,8 @@ import ( "gopkg.in/yaml.v2" ) +var cfgFilenames = []string{".genqlient.yml", ".genqlient.yaml", "genqlient.yml", "genqlient.yaml"} + // Config represents genqlient's configuration, generally read from // genqlient.yaml. // @@ -109,6 +111,17 @@ func ReadAndValidateConfig(filename string) (*Config, error) { return &config, nil } +// ReadAndValidateConfigFromDefaultLocations looks for a config file in the +// current directory, and all parent directories walking up the tree. The +// closest config file will be returned. +func ReadAndValidateConfigFromDefaultLocations() (*Config, error) { + cfgFile, err := findCfg() + if err != nil { + return nil, err + } + return ReadAndValidateConfig(cfgFile) +} + func initConfig(filename string) error { // TODO(benkraft): Embed this config file into the binary, see // https://github.com/Khan/genqlient/issues/9. @@ -126,3 +139,35 @@ func initConfig(filename string) error { } return nil } + +// findCfg searches for the config file in this directory and all parents up the tree +// looking for the closest match +func findCfg() (string, error) { + dir, err := os.Getwd() + if err != nil { + return "", errorf(nil, "unable to get working dir to findCfg: %v", err) + } + + cfg := findCfgInDir(dir) + + for cfg == "" && dir != filepath.Dir(dir) { + dir = filepath.Dir(dir) + cfg = findCfgInDir(dir) + } + + if cfg == "" { + return "", os.ErrNotExist + } + + return cfg, nil +} + +func findCfgInDir(dir string) string { + for _, cfgName := range cfgFilenames { + path := filepath.Join(dir, cfgName) + if _, err := os.Stat(path); err == nil { + return path + } + } + return "" +} diff --git a/generate/config_test.go b/generate/config_test.go new file mode 100644 index 00000000..c69f623b --- /dev/null +++ b/generate/config_test.go @@ -0,0 +1,90 @@ +package generate + +import ( + "os" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestFindCfg(t *testing.T) { + cwd, err := os.Getwd() + require.NoError(t, err) + + cases := map[string]struct { + startDir string + expectedCfg string + expectedErr error + }{ + "yaml in parent directory": { + startDir: cwd + "/testdata/find-config/parent/child", + expectedCfg: cwd + "/testdata/find-config/parent/genqlient.yaml", + }, + "yaml in current directory": { + startDir: cwd + "/testdata/find-config/current", + expectedCfg: cwd + "/testdata/find-config/current/genqlient.yaml", + }, + "no yaml": { + startDir: cwd + "/testdata/find-config/none/child", + expectedErr: os.ErrNotExist, + }, + } + + for name, tc := range cases { + t.Run(name, func(t *testing.T) { + defer func() { + require.NoError(t, os.Chdir(cwd), "Test cleanup failed") + }() + + err = os.Chdir(tc.startDir) + require.NoError(t, err) + + path, err := findCfg() + assert.Equal(t, tc.expectedCfg, path) + assert.Equal(t, tc.expectedErr, err) + }) + } +} + +func TestFindCfgInDir(t *testing.T) { + cwd, err := os.Getwd() + require.NoError(t, err) + + cases := map[string]struct { + startDir string + found bool + }{ + "yaml": { + startDir: cwd + "/testdata/find-config/filenames/yaml", + found: true, + }, + "yml": { + startDir: cwd + "/testdata/find-config/filenames/yml", + found: true, + }, + ".yaml": { + startDir: cwd + "/testdata/find-config/filenames/dotyaml", + found: true, + }, + ".yml": { + startDir: cwd + "/testdata/find-config/filenames/dotyml", + found: true, + }, + "none": { + startDir: cwd + "/testdata/find-config/filenames/none", + found: false, + }, + } + + for name, tc := range cases { + t.Run(name, func(t *testing.T) { + path := findCfgInDir(tc.startDir) + if tc.found { + assert.NotEmpty(t, path) + } else { + assert.Empty(t, path) + } + }) + } +} diff --git a/generate/main.go b/generate/main.go index d35a097c..3cd4fc78 100644 --- a/generate/main.go +++ b/generate/main.go @@ -14,9 +14,18 @@ import ( ) func readConfigGenerateAndWrite(configFilename string) error { - config, err := ReadAndValidateConfig(configFilename) - if err != nil { - return err + var config *Config + var err error + if configFilename != "" { + config, err = ReadAndValidateConfig(configFilename) + if err != nil { + return err + } + } else { + config, err = ReadAndValidateConfigFromDefaultLocations() + if err != nil { + return err + } } generated, err := Generate(config) @@ -42,7 +51,7 @@ func readConfigGenerateAndWrite(configFilename string) error { } type cliArgs struct { - ConfigFilename string `arg:"positional" placeholder:"CONFIG" default:"genqlient.yaml" help:"path to genqlient configuration (default genqlient.yaml)"` + ConfigFilename string `arg:"positional" placeholder:"CONFIG" default:"" help:"path to genqlient configuration (default: genqlient.yaml in current or any parent directory)"` Init bool `arg:"--init" help:"write out and use a default config file"` } @@ -67,7 +76,12 @@ func Main() { var args cliArgs arg.MustParse(&args) if args.Init { - err := initConfig(args.ConfigFilename) + filename := args.ConfigFilename + if filename == "" { + filename = "genqlient.yaml" + } + + err := initConfig(filename) exitIfError(err) } err := readConfigGenerateAndWrite(args.ConfigFilename) diff --git a/generate/testdata/find-config/current/genqlient.yaml b/generate/testdata/find-config/current/genqlient.yaml new file mode 100644 index 00000000..bfc5f265 --- /dev/null +++ b/generate/testdata/find-config/current/genqlient.yaml @@ -0,0 +1,6 @@ +# Default genqlient config; for full documentation see: +# https://github.com/Khan/genqlient/blob/main/docs/genqlient.yaml +schema: schema.graphql +operations: +- genqlient.graphql +generated: generated.go diff --git a/generate/testdata/find-config/filenames/dotyaml/.genqlient.yaml b/generate/testdata/find-config/filenames/dotyaml/.genqlient.yaml new file mode 100644 index 00000000..bfc5f265 --- /dev/null +++ b/generate/testdata/find-config/filenames/dotyaml/.genqlient.yaml @@ -0,0 +1,6 @@ +# Default genqlient config; for full documentation see: +# https://github.com/Khan/genqlient/blob/main/docs/genqlient.yaml +schema: schema.graphql +operations: +- genqlient.graphql +generated: generated.go diff --git a/generate/testdata/find-config/filenames/dotyml/.genqlient.yml b/generate/testdata/find-config/filenames/dotyml/.genqlient.yml new file mode 100644 index 00000000..bfc5f265 --- /dev/null +++ b/generate/testdata/find-config/filenames/dotyml/.genqlient.yml @@ -0,0 +1,6 @@ +# Default genqlient config; for full documentation see: +# https://github.com/Khan/genqlient/blob/main/docs/genqlient.yaml +schema: schema.graphql +operations: +- genqlient.graphql +generated: generated.go diff --git a/generate/testdata/find-config/filenames/none/.gitkeep b/generate/testdata/find-config/filenames/none/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/generate/testdata/find-config/filenames/yaml/genqlient.yaml b/generate/testdata/find-config/filenames/yaml/genqlient.yaml new file mode 100644 index 00000000..bfc5f265 --- /dev/null +++ b/generate/testdata/find-config/filenames/yaml/genqlient.yaml @@ -0,0 +1,6 @@ +# Default genqlient config; for full documentation see: +# https://github.com/Khan/genqlient/blob/main/docs/genqlient.yaml +schema: schema.graphql +operations: +- genqlient.graphql +generated: generated.go diff --git a/generate/testdata/find-config/filenames/yml/genqlient.yml b/generate/testdata/find-config/filenames/yml/genqlient.yml new file mode 100644 index 00000000..bfc5f265 --- /dev/null +++ b/generate/testdata/find-config/filenames/yml/genqlient.yml @@ -0,0 +1,6 @@ +# Default genqlient config; for full documentation see: +# https://github.com/Khan/genqlient/blob/main/docs/genqlient.yaml +schema: schema.graphql +operations: +- genqlient.graphql +generated: generated.go diff --git a/generate/testdata/find-config/none/.gitkeep b/generate/testdata/find-config/none/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/generate/testdata/find-config/none/child/.gitkeep b/generate/testdata/find-config/none/child/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/generate/testdata/find-config/parent/child/.gitkeep b/generate/testdata/find-config/parent/child/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/generate/testdata/find-config/parent/genqlient.yaml b/generate/testdata/find-config/parent/genqlient.yaml new file mode 100644 index 00000000..bfc5f265 --- /dev/null +++ b/generate/testdata/find-config/parent/genqlient.yaml @@ -0,0 +1,6 @@ +# Default genqlient config; for full documentation see: +# https://github.com/Khan/genqlient/blob/main/docs/genqlient.yaml +schema: schema.graphql +operations: +- genqlient.graphql +generated: generated.go