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  }