github.com/codykaup/genqlient@v0.6.2/generate/config.go (about) 1 package generate 2 3 import ( 4 _ "embed" 5 "fmt" 6 "go/token" 7 "os" 8 "path/filepath" 9 "strings" 10 11 "golang.org/x/tools/go/packages" 12 "gopkg.in/yaml.v2" 13 ) 14 15 var cfgFilenames = []string{".genqlient.yml", ".genqlient.yaml", "genqlient.yml", "genqlient.yaml"} 16 17 // Config represents genqlient's configuration, generally read from 18 // genqlient.yaml. 19 // 20 // Callers must call [Config.ValidateAndFillDefaults] before using the config. 21 type Config struct { 22 // The following fields are documented in the [genqlient.yaml docs]. 23 // 24 // [genqlient.yaml docs]: https://github.com/codykaup/genqlient/blob/main/docs/genqlient.yaml 25 Schema StringList `yaml:"schema"` 26 Operations StringList `yaml:"operations"` 27 Generated string `yaml:"generated"` 28 Package string `yaml:"package"` 29 ExportOperations string `yaml:"export_operations"` 30 ContextType string `yaml:"context_type"` 31 ClientGetter string `yaml:"client_getter"` 32 Bindings map[string]*TypeBinding `yaml:"bindings"` 33 PackageBindings []*PackageBinding `yaml:"package_bindings"` 34 Casing Casing `yaml:"casing"` 35 Optional string `yaml:"optional"` 36 OptionalGenericType string `yaml:"optional_generic_type"` 37 StructReferences bool `yaml:"use_struct_references"` 38 Extensions bool `yaml:"use_extensions"` 39 40 // Set to true to use features that aren't fully ready to use. 41 // 42 // This is primarily intended for genqlient's own tests. These features 43 // are likely BROKEN and come with NO EXPECTATION OF COMPATIBILITY. Use 44 // them at your own risk! 45 AllowBrokenFeatures bool `yaml:"allow_broken_features"` 46 47 // The directory of the config-file (relative to which all the other paths 48 // are resolved). Set by ValidateAndFillDefaults. 49 baseDir string 50 } 51 52 // A TypeBinding represents a Go type to which genqlient will bind a particular 53 // GraphQL type, and is documented further in the [genqlient.yaml docs]. 54 // 55 // [genqlient.yaml docs]: https://github.com/codykaup/genqlient/blob/main/docs/genqlient.yaml 56 type TypeBinding struct { 57 Type string `yaml:"type"` 58 ExpectExactFields string `yaml:"expect_exact_fields"` 59 Marshaler string `yaml:"marshaler"` 60 Unmarshaler string `yaml:"unmarshaler"` 61 } 62 63 // A PackageBinding represents a Go package for which genqlient will 64 // automatically generate [TypeBinding] values, and is documented further in 65 // the [genqlient.yaml docs]. 66 // 67 // [genqlient.yaml docs]: https://github.com/codykaup/genqlient/blob/main/docs/genqlient.yaml 68 type PackageBinding struct { 69 Package string `yaml:"package"` 70 } 71 72 // CasingAlgorithm represents a way that genqlient can handle casing, and is 73 // documented further in the [genqlient.yaml docs]. 74 // 75 // [genqlient.yaml docs]: https://github.com/codykaup/genqlient/blob/main/docs/genqlient.yaml 76 type CasingAlgorithm string 77 78 const ( 79 CasingDefault CasingAlgorithm = "default" 80 CasingRaw CasingAlgorithm = "raw" 81 ) 82 83 func (algo CasingAlgorithm) validate() error { 84 switch algo { 85 case CasingDefault, CasingRaw: 86 return nil 87 default: 88 return errorf(nil, "unknown casing algorithm: %s", algo) 89 } 90 } 91 92 // Casing wraps the casing-related options, and is documented further in 93 // the [genqlient.yaml docs]. 94 // 95 // [genqlient.yaml docs]: https://github.com/codykaup/genqlient/blob/main/docs/genqlient.yaml 96 type Casing struct { 97 AllEnums CasingAlgorithm `yaml:"all_enums"` 98 Enums map[string]CasingAlgorithm `yaml:"enums"` 99 } 100 101 func (casing *Casing) validate() error { 102 if casing.AllEnums != "" { 103 if err := casing.AllEnums.validate(); err != nil { 104 return err 105 } 106 } 107 for _, algo := range casing.Enums { 108 if err := algo.validate(); err != nil { 109 return err 110 } 111 } 112 return nil 113 } 114 115 func (casing *Casing) forEnum(graphQLTypeName string) CasingAlgorithm { 116 if specificConfig, ok := casing.Enums[graphQLTypeName]; ok { 117 return specificConfig 118 } 119 if casing.AllEnums != "" { 120 return casing.AllEnums 121 } 122 return CasingDefault 123 } 124 125 // pathJoin is like filepath.Join but 1) it only takes two argsuments, 126 // and b) if the second argument is an absolute path the first argument 127 // is ignored (similar to how python's os.path.join() works). 128 func pathJoin(a, b string) string { 129 if filepath.IsAbs(b) { 130 return b 131 } 132 return filepath.Join(a, b) 133 } 134 135 // ValidateAndFillDefaults ensures that the configuration is valid, and fills 136 // in any options that were unspecified. 137 // 138 // The argument is the directory relative to which paths will be interpreted, 139 // typically the directory of the config file. 140 func (c *Config) ValidateAndFillDefaults(baseDir string) error { 141 c.baseDir = baseDir 142 for i := range c.Schema { 143 c.Schema[i] = pathJoin(baseDir, c.Schema[i]) 144 } 145 for i := range c.Operations { 146 c.Operations[i] = pathJoin(baseDir, c.Operations[i]) 147 } 148 if c.Generated == "" { 149 c.Generated = "generated.go" 150 } 151 c.Generated = pathJoin(baseDir, c.Generated) 152 if c.ExportOperations != "" { 153 c.ExportOperations = pathJoin(baseDir, c.ExportOperations) 154 } 155 156 if c.ContextType == "" { 157 c.ContextType = "context.Context" 158 } 159 160 if c.Optional != "" && c.Optional != "value" && c.Optional != "pointer" && c.Optional != "generic" { 161 return errorf(nil, "optional must be one of: 'value' (default), 'pointer', or 'generic'") 162 } 163 164 if c.Optional == "generic" && c.OptionalGenericType == "" { 165 return errorf(nil, "if optional is set to 'generic', optional_generic_type must be set to the fully"+ 166 "qualified name of a type with a single generic parameter"+ 167 "\nExample: \"github.com/Org/Repo/optional.Value\"") 168 } 169 170 if c.Package != "" { 171 if !token.IsIdentifier(c.Package) { 172 // No need for link here -- if you're already setting the package 173 // you know where to set the package. 174 return errorf(nil, "invalid package in genqlient.yaml: '%v' is not a valid identifier", c.Package) 175 } 176 } else { 177 abs, err := filepath.Abs(c.Generated) 178 if err != nil { 179 return errorf(nil, "unable to guess package-name: %v"+ 180 "\nSet package name in genqlient.yaml"+ 181 "\nExample: https://github.com/codykaup/genqlient/blob/main/example/genqlient.yaml#L6", err) 182 } 183 184 base := filepath.Base(filepath.Dir(abs)) 185 if !token.IsIdentifier(base) { 186 return errorf(nil, "unable to guess package-name: '%v' is not a valid identifier"+ 187 "\nSet package name in genqlient.yaml"+ 188 "\nExample: https://github.com/codykaup/genqlient/blob/main/example/genqlient.yaml#L6", base) 189 } 190 191 c.Package = base 192 } 193 194 if len(c.PackageBindings) > 0 { 195 for _, binding := range c.PackageBindings { 196 if strings.HasSuffix(binding.Package, ".go") { 197 // total heuristic -- but this is an easy mistake to make and 198 // results in rather bizarre behavior from go/packages. 199 return errorf(nil, 200 "package %v looks like a file, but should be a package-name", 201 binding.Package) 202 } 203 204 mode := packages.NeedDeps | packages.NeedTypes 205 pkgs, err := packages.Load(&packages.Config{ 206 Mode: mode, 207 }, binding.Package) 208 if err != nil { 209 return err 210 } 211 212 if c.Bindings == nil { 213 c.Bindings = map[string]*TypeBinding{} 214 } 215 216 for _, pkg := range pkgs { 217 p := pkg.Types 218 if p == nil || p.Scope() == nil || p.Scope().Len() == 0 { 219 return errorf(nil, "unable to bind package %s: no types found", binding.Package) 220 } 221 222 for _, typ := range p.Scope().Names() { 223 if token.IsExported(typ) { 224 // Check if type is manual bindings 225 _, exist := c.Bindings[typ] 226 if !exist { 227 pathType := fmt.Sprintf("%s.%s", p.Path(), typ) 228 c.Bindings[typ] = &TypeBinding{ 229 Type: pathType, 230 } 231 } 232 } 233 } 234 } 235 } 236 } 237 238 if err := c.Casing.validate(); err != nil { 239 return err 240 } 241 242 return nil 243 } 244 245 // ReadAndValidateConfig reads the configuration from the given file, validates 246 // it, and returns it. 247 func ReadAndValidateConfig(filename string) (*Config, error) { 248 text, err := os.ReadFile(filename) 249 if err != nil { 250 return nil, errorf(nil, "unreadable config file %v: %v", filename, err) 251 } 252 253 var config Config 254 err = yaml.UnmarshalStrict(text, &config) 255 if err != nil { 256 return nil, errorf(nil, "invalid config file %v: %v", filename, err) 257 } 258 259 err = config.ValidateAndFillDefaults(filepath.Dir(filename)) 260 if err != nil { 261 return nil, errorf(nil, "invalid config file %v: %v", filename, err) 262 } 263 264 return &config, nil 265 } 266 267 // ReadAndValidateConfigFromDefaultLocations looks for a config file in the 268 // current directory, and all parent directories walking up the tree. The 269 // closest config file will be returned. 270 func ReadAndValidateConfigFromDefaultLocations() (*Config, error) { 271 cfgFile, err := findCfg() 272 if err != nil { 273 return nil, err 274 } 275 return ReadAndValidateConfig(cfgFile) 276 } 277 278 //go:embed default_genqlient.yaml 279 var defaultConfig []byte 280 281 func initConfig(filename string) error { 282 return os.WriteFile(filename, defaultConfig, 0o644) 283 } 284 285 // findCfg searches for the config file in this directory and all parents up the tree 286 // looking for the closest match 287 func findCfg() (string, error) { 288 dir, err := os.Getwd() 289 if err != nil { 290 return "", errorf(nil, "unable to get working dir to findCfg: %v", err) 291 } 292 293 cfg := findCfgInDir(dir) 294 295 for cfg == "" && dir != filepath.Dir(dir) { 296 dir = filepath.Dir(dir) 297 cfg = findCfgInDir(dir) 298 } 299 300 if cfg == "" { 301 return "", os.ErrNotExist 302 } 303 304 return cfg, nil 305 } 306 307 func findCfgInDir(dir string) string { 308 for _, cfgName := range cfgFilenames { 309 path := pathJoin(dir, cfgName) 310 if _, err := os.Stat(path); err == nil { 311 return path 312 } 313 } 314 return "" 315 }