github.com/nathanstitt/genqlient@v0.3.1-0.20211028004951-a2bda3c41ab8/generate/config.go (about) 1 package generate 2 3 import ( 4 "go/token" 5 "io" 6 "io/ioutil" 7 "os" 8 "path/filepath" 9 10 "gopkg.in/yaml.v2" 11 ) 12 13 var cfgFilenames = []string{".genqlient.yml", ".genqlient.yaml", "genqlient.yml", "genqlient.yaml"} 14 15 // Config represents genqlient's configuration, generally read from 16 // genqlient.yaml. 17 // 18 // Callers must call ValidateAndFillDefaults before using the config. 19 type Config struct { 20 // The following fields are documented at: 21 // https://github.com/Khan/genqlient/blob/main/docs/genqlient.yaml 22 Schema StringList `yaml:"schema"` 23 Operations StringList `yaml:"operations"` 24 Generated string `yaml:"generated"` 25 Package string `yaml:"package"` 26 ExportOperations string `yaml:"export_operations"` 27 ContextType string `yaml:"context_type"` 28 ClientGetter string `yaml:"client_getter"` 29 Bindings map[string]*TypeBinding `yaml:"bindings"` 30 31 // Set to true to use features that aren't fully ready to use. 32 // 33 // This is primarily intended for genqlient's own tests. These features 34 // are likely BROKEN and come with NO EXPECTATION OF COMPATIBILITY. Use 35 // them at your own risk! 36 AllowBrokenFeatures bool `yaml:"allow_broken_features"` 37 38 // The directory of the config-file (relative to which all the other paths 39 // are resolved). Set by ValidateAndFillDefaults. 40 baseDir string 41 } 42 43 // A TypeBinding represents a Go type to which genqlient will bind a particular 44 // GraphQL type, and is documented further at: 45 // https://github.com/Khan/genqlient/blob/main/docs/genqlient.yaml 46 type TypeBinding struct { 47 Type string `yaml:"type"` 48 ExpectExactFields string `yaml:"expect_exact_fields"` 49 Marshaler string `yaml:"marshaler"` 50 Unmarshaler string `yaml:"unmarshaler"` 51 } 52 53 // ValidateAndFillDefaults ensures that the configuration is valid, and fills 54 // in any options that were unspecified. 55 // 56 // The argument is the directory relative to which paths will be interpreted, 57 // typically the directory of the config file. 58 func (c *Config) ValidateAndFillDefaults(baseDir string) error { 59 c.baseDir = baseDir 60 for i := range c.Schema { 61 c.Schema[i] = filepath.Join(baseDir, c.Schema[i]) 62 } 63 for i := range c.Operations { 64 c.Operations[i] = filepath.Join(baseDir, c.Operations[i]) 65 } 66 c.Generated = filepath.Join(baseDir, c.Generated) 67 if c.ExportOperations != "" { 68 c.ExportOperations = filepath.Join(baseDir, c.ExportOperations) 69 } 70 71 if c.ContextType == "" { 72 c.ContextType = "context.Context" 73 } 74 75 if c.Package == "" { 76 abs, err := filepath.Abs(c.Generated) 77 if err != nil { 78 return errorf(nil, "unable to guess package-name: %v", err) 79 } 80 81 base := filepath.Base(filepath.Dir(abs)) 82 if !token.IsIdentifier(base) { 83 return errorf(nil, "unable to guess package-name: %v is not a valid identifier", base) 84 } 85 86 c.Package = base 87 } 88 89 return nil 90 } 91 92 // ReadAndValidateConfig reads the configuration from the given file, validates 93 // it, and returns it. 94 func ReadAndValidateConfig(filename string) (*Config, error) { 95 text, err := ioutil.ReadFile(filename) 96 if err != nil { 97 return nil, errorf(nil, "unreadable config file %v: %v", filename, err) 98 } 99 100 var config Config 101 err = yaml.UnmarshalStrict(text, &config) 102 if err != nil { 103 return nil, errorf(nil, "invalid config file %v: %v", filename, err) 104 } 105 106 err = config.ValidateAndFillDefaults(filepath.Dir(filename)) 107 if err != nil { 108 return nil, errorf(nil, "invalid config file %v: %v", filename, err) 109 } 110 111 return &config, nil 112 } 113 114 // ReadAndValidateConfigFromDefaultLocations looks for a config file in the 115 // current directory, and all parent directories walking up the tree. The 116 // closest config file will be returned. 117 func ReadAndValidateConfigFromDefaultLocations() (*Config, error) { 118 cfgFile, err := findCfg() 119 if err != nil { 120 return nil, err 121 } 122 return ReadAndValidateConfig(cfgFile) 123 } 124 125 func initConfig(filename string) error { 126 // TODO(benkraft): Embed this config file into the binary, see 127 // https://github.com/Khan/genqlient/issues/9. 128 r, err := os.Open(filepath.Join(thisDir, "default_genqlient.yaml")) 129 if err != nil { 130 return err 131 } 132 w, err := os.OpenFile(filename, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0o644) 133 if err != nil { 134 return errorf(nil, "unable to write default genqlient.yaml: %v", err) 135 } 136 _, err = io.Copy(w, r) 137 if err != nil { 138 return errorf(nil, "unable to write default genqlient.yaml: %v", err) 139 } 140 return nil 141 } 142 143 // findCfg searches for the config file in this directory and all parents up the tree 144 // looking for the closest match 145 func findCfg() (string, error) { 146 dir, err := os.Getwd() 147 if err != nil { 148 return "", errorf(nil, "unable to get working dir to findCfg: %v", err) 149 } 150 151 cfg := findCfgInDir(dir) 152 153 for cfg == "" && dir != filepath.Dir(dir) { 154 dir = filepath.Dir(dir) 155 cfg = findCfgInDir(dir) 156 } 157 158 if cfg == "" { 159 return "", os.ErrNotExist 160 } 161 162 return cfg, nil 163 } 164 165 func findCfgInDir(dir string) string { 166 for _, cfgName := range cfgFilenames { 167 path := filepath.Join(dir, cfgName) 168 if _, err := os.Stat(path); err == nil { 169 return path 170 } 171 } 172 return "" 173 }