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 }