github.com/opiuman/genqlient@v1.0.0/generate/config.go (about)

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