677fa94d30
This means we can use generics and various other things. I didn't use any of them yet, this is just bumping the numbers. I added tests for 1.20, and fixed one small bug, I think caused by `go/packages` changes therein. And I bumped the golangci-lint version too while I was in the area. Fixes #256, fixes #257. I have: - [x] Written a clear PR title and description (above) - [x] Signed the [Khan Academy CLA](https://www.khanacademy.org/r/cla) - [x] Added tests covering my changes, if applicable - [x] Included a link to the issue fixed, if applicable - [x] Included documentation, for new features - [x] Added an entry to the changelog
238 lines
7.1 KiB
Go
238 lines
7.1 KiB
Go
package generate
|
|
|
|
import (
|
|
_ "embed"
|
|
"fmt"
|
|
"go/token"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
"golang.org/x/tools/go/packages"
|
|
"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.
|
|
//
|
|
// Callers must call [Config.ValidateAndFillDefaults] before using the config.
|
|
type Config struct {
|
|
// The following fields are documented in the [genqlient.yaml docs].
|
|
//
|
|
// [genqlient.yaml docs]: https://github.com/Khan/genqlient/blob/main/docs/genqlient.yaml
|
|
Schema StringList `yaml:"schema"`
|
|
Operations StringList `yaml:"operations"`
|
|
Generated string `yaml:"generated"`
|
|
Package string `yaml:"package"`
|
|
ExportOperations string `yaml:"export_operations"`
|
|
ContextType string `yaml:"context_type"`
|
|
ClientGetter string `yaml:"client_getter"`
|
|
Bindings map[string]*TypeBinding `yaml:"bindings"`
|
|
PackageBindings []*PackageBinding `yaml:"package_bindings"`
|
|
Optional string `yaml:"optional"`
|
|
StructReferences bool `yaml:"use_struct_references"`
|
|
Extensions bool `yaml:"use_extensions"`
|
|
|
|
// Set to true to use features that aren't fully ready to use.
|
|
//
|
|
// This is primarily intended for genqlient's own tests. These features
|
|
// are likely BROKEN and come with NO EXPECTATION OF COMPATIBILITY. Use
|
|
// them at your own risk!
|
|
AllowBrokenFeatures bool `yaml:"allow_broken_features"`
|
|
|
|
// The directory of the config-file (relative to which all the other paths
|
|
// are resolved). Set by ValidateAndFillDefaults.
|
|
baseDir string
|
|
}
|
|
|
|
// A TypeBinding represents a Go type to which genqlient will bind a particular
|
|
// GraphQL type, and is documented further in the [genqlient.yaml docs].
|
|
//
|
|
// [genqlient.yaml docs]: https://github.com/Khan/genqlient/blob/main/docs/genqlient.yaml
|
|
type TypeBinding struct {
|
|
Type string `yaml:"type"`
|
|
ExpectExactFields string `yaml:"expect_exact_fields"`
|
|
Marshaler string `yaml:"marshaler"`
|
|
Unmarshaler string `yaml:"unmarshaler"`
|
|
}
|
|
|
|
// A PackageBinding represents a Go package for which genqlient will
|
|
// automatically generate [TypeBinding] values, and is documented further in
|
|
// the [genqlient.yaml docs].
|
|
//
|
|
// [genqlient.yaml docs]: https://github.com/Khan/genqlient/blob/main/docs/genqlient.yaml
|
|
type PackageBinding struct {
|
|
Package string `yaml:"package"`
|
|
}
|
|
|
|
// pathJoin is like filepath.Join but 1) it only takes two argsuments,
|
|
// and b) if the second argument is an absolute path the first argument
|
|
// is ignored (similar to how python's os.path.join() works).
|
|
func pathJoin(a, b string) string {
|
|
if filepath.IsAbs(b) {
|
|
return b
|
|
}
|
|
return filepath.Join(a, b)
|
|
}
|
|
|
|
// ValidateAndFillDefaults ensures that the configuration is valid, and fills
|
|
// in any options that were unspecified.
|
|
//
|
|
// The argument is the directory relative to which paths will be interpreted,
|
|
// typically the directory of the config file.
|
|
func (c *Config) ValidateAndFillDefaults(baseDir string) error {
|
|
c.baseDir = baseDir
|
|
for i := range c.Schema {
|
|
c.Schema[i] = pathJoin(baseDir, c.Schema[i])
|
|
}
|
|
for i := range c.Operations {
|
|
c.Operations[i] = pathJoin(baseDir, c.Operations[i])
|
|
}
|
|
c.Generated = pathJoin(baseDir, c.Generated)
|
|
if c.ExportOperations != "" {
|
|
c.ExportOperations = pathJoin(baseDir, c.ExportOperations)
|
|
}
|
|
|
|
if c.ContextType == "" {
|
|
c.ContextType = "context.Context"
|
|
}
|
|
|
|
if c.Package == "" {
|
|
abs, err := filepath.Abs(c.Generated)
|
|
if err != nil {
|
|
return errorf(nil, "unable to guess package-name: %v is not a valid identifier"+
|
|
"\nSet package name in genqlient.yaml"+
|
|
"\nExample: https://github.com/Khan/genqlient/blob/main/example/genqlient.yaml#L6", err)
|
|
}
|
|
|
|
base := filepath.Base(filepath.Dir(abs))
|
|
if !token.IsIdentifier(base) {
|
|
return errorf(nil, "unable to guess package-name: %v is not a valid identifier"+
|
|
"\nSet package name in genqlient.yaml"+
|
|
"\nExample: https://github.com/Khan/genqlient/blob/main/example/genqlient.yaml#L6", base)
|
|
}
|
|
|
|
c.Package = base
|
|
}
|
|
|
|
if len(c.PackageBindings) > 0 {
|
|
for _, binding := range c.PackageBindings {
|
|
if strings.HasSuffix(binding.Package, ".go") {
|
|
// total heuristic -- but this is an easy mistake to make and
|
|
// results in rather bizarre behavior from go/packages.
|
|
return errorf(nil,
|
|
"package %v looks like a file, but should be a package-name",
|
|
binding.Package)
|
|
}
|
|
|
|
mode := packages.NeedDeps | packages.NeedTypes
|
|
pkgs, err := packages.Load(&packages.Config{
|
|
Mode: mode,
|
|
}, binding.Package)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if c.Bindings == nil {
|
|
c.Bindings = map[string]*TypeBinding{}
|
|
}
|
|
|
|
for _, pkg := range pkgs {
|
|
p := pkg.Types
|
|
if p == nil || p.Scope() == nil || p.Scope().Len() == 0 {
|
|
return errorf(nil, "unable to bind package %s: no types found", binding.Package)
|
|
}
|
|
|
|
for _, typ := range p.Scope().Names() {
|
|
if token.IsExported(typ) {
|
|
// Check if type is manual bindings
|
|
_, exist := c.Bindings[typ]
|
|
if !exist {
|
|
pathType := fmt.Sprintf("%s.%s", p.Path(), typ)
|
|
c.Bindings[typ] = &TypeBinding{
|
|
Type: pathType,
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// ReadAndValidateConfig reads the configuration from the given file, validates
|
|
// it, and returns it.
|
|
func ReadAndValidateConfig(filename string) (*Config, error) {
|
|
text, err := os.ReadFile(filename)
|
|
if err != nil {
|
|
return nil, errorf(nil, "unreadable config file %v: %v", filename, err)
|
|
}
|
|
|
|
var config Config
|
|
err = yaml.UnmarshalStrict(text, &config)
|
|
if err != nil {
|
|
return nil, errorf(nil, "invalid config file %v: %v", filename, err)
|
|
}
|
|
|
|
err = config.ValidateAndFillDefaults(filepath.Dir(filename))
|
|
if err != nil {
|
|
return nil, errorf(nil, "invalid config file %v: %v", filename, err)
|
|
}
|
|
|
|
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)
|
|
}
|
|
|
|
//go:embed default_genqlient.yaml
|
|
var defaultConfig []byte
|
|
|
|
func initConfig(filename string) error {
|
|
return os.WriteFile(filename, defaultConfig, 0o644)
|
|
}
|
|
|
|
// 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 := pathJoin(dir, cfgName)
|
|
if _, err := os.Stat(path); err == nil {
|
|
return path
|
|
}
|
|
}
|
|
return ""
|
|
}
|