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  }