github.com/speakeasy-api/sdk-gen-config@v1.14.2/io.go (about) 1 package config 2 3 import ( 4 "bufio" 5 "bytes" 6 "crypto/md5" 7 "encoding/hex" 8 "errors" 9 "fmt" 10 "io/fs" 11 "os" 12 "path/filepath" 13 14 "github.com/speakeasy-api/sdk-gen-config/workspace" 15 "gopkg.in/yaml.v3" 16 ) 17 18 const ( 19 configFile = "gen.yaml" 20 lockFile = "gen.lock" 21 ) 22 23 type Config struct { 24 Config *Configuration 25 ConfigPath string 26 LockFile *LockFile 27 } 28 29 type FS interface { 30 fs.ReadFileFS 31 fs.StatFS 32 WriteFile(name string, data []byte, perm os.FileMode) error 33 } 34 35 type Option func(*options) 36 37 type ( 38 GetLanguageDefaultFunc func(string, bool) (*LanguageConfig, error) 39 TransformerFunc func(*Config) (*Config, error) 40 ) 41 42 type options struct { 43 FS FS 44 UpgradeFunc UpgradeFunc 45 getLanguageDefaultFunc GetLanguageDefaultFunc 46 langs []string 47 transformerFunc TransformerFunc 48 dontWrite bool 49 } 50 51 func WithFileSystem(fs FS) Option { 52 return func(o *options) { 53 o.FS = fs 54 } 55 } 56 57 func WithDontWrite() Option { 58 return func(o *options) { 59 o.dontWrite = true 60 } 61 } 62 63 func WithUpgradeFunc(f UpgradeFunc) Option { 64 return func(o *options) { 65 o.UpgradeFunc = f 66 } 67 } 68 69 func WithLanguageDefaultFunc(f GetLanguageDefaultFunc) Option { 70 return func(o *options) { 71 o.getLanguageDefaultFunc = f 72 } 73 } 74 75 func WithLanguages(langs ...string) Option { 76 return func(o *options) { 77 o.langs = langs 78 } 79 } 80 81 func WithTransformerFunc(f TransformerFunc) Option { 82 return func(o *options) { 83 o.transformerFunc = f 84 } 85 } 86 87 func FindConfigFile(dir string, fileSystem FS) (*workspace.FindWorkspaceResult, error) { 88 configRes, err := workspace.FindWorkspace(dir, workspace.FindWorkspaceOptions{ 89 FindFile: configFile, 90 AllowOutside: true, 91 Recursive: true, 92 FS: fileSystem, 93 }) 94 if err != nil { 95 if errors.Is(err, fs.ErrNotExist) { 96 configRes = &workspace.FindWorkspaceResult{ 97 Path: filepath.Join(dir, workspace.SpeakeasyFolder, configFile), 98 } 99 } else { 100 return nil, err 101 } 102 } 103 104 return configRes, nil 105 } 106 107 func Load(dir string, opts ...Option) (*Config, error) { 108 o := applyOptions(opts) 109 110 newConfig := false 111 newSDK := false 112 newForLang := map[string]bool{} 113 114 // Find existing config file 115 configRes, err := FindConfigFile(dir, o.FS) 116 if err != nil { 117 return nil, err 118 } 119 if configRes.Data == nil { 120 newConfig = true 121 newSDK = true 122 123 for _, lang := range o.langs { 124 newForLang[lang] = true 125 } 126 } 127 128 // Make sure to use the same workspace dir type as the config file 129 workspaceDir := filepath.Base(filepath.Dir(configRes.Path)) 130 if workspaceDir != workspace.SpeakeasyFolder && workspaceDir != workspace.GenFolder { 131 workspaceDir = workspace.SpeakeasyFolder 132 } 133 134 newLockFile := false 135 lockFileRes, err := workspace.FindWorkspace(filepath.Join(dir, workspaceDir), workspace.FindWorkspaceOptions{ 136 FindFile: lockFile, 137 FS: o.FS, 138 }) 139 if err != nil { 140 if !errors.Is(err, fs.ErrNotExist) { 141 return nil, fmt.Errorf("could not read gen.lock: %w", err) 142 } 143 lockFileRes = &workspace.FindWorkspaceResult{ 144 Path: filepath.Join(dir, workspaceDir, lockFile), 145 } 146 newLockFile = true 147 } 148 149 if !newConfig { 150 // Unmarshal config file and check version 151 cfgMap := map[string]any{} 152 if err := yaml.Unmarshal(configRes.Data, &cfgMap); err != nil { 153 return nil, fmt.Errorf("could not unmarshal gen.yaml: %w", err) 154 } 155 156 var lockFileMap map[string]any 157 lockFilePresent := false 158 if lockFileRes.Data != nil { 159 if err := yaml.Unmarshal(lockFileRes.Data, &lockFileMap); err != nil { 160 return nil, fmt.Errorf("could not unmarshal gen.lock: %w", err) 161 } 162 lockFilePresent = true 163 } 164 165 version := "" 166 167 v, ok := cfgMap["configVersion"] 168 if ok { 169 version, ok = v.(string) 170 if !ok { 171 version = "" 172 } 173 } 174 175 // If we aren't upgrading we assume if we are missing a lock file then this is a new SDK 176 if version == Version { 177 newSDK = newSDK || newLockFile 178 } 179 180 if version != Version && o.UpgradeFunc != nil { 181 // Upgrade config file if version is different and write it 182 cfgMap, lockFileMap, err = upgrade(version, cfgMap, lockFileMap, o.UpgradeFunc) 183 if err != nil { 184 return nil, err 185 } 186 187 // Write back out to disk and update data 188 configRes.Data, err = write(configRes.Path, cfgMap, o) 189 if err != nil { 190 return nil, err 191 } 192 193 if lockFileMap != nil { 194 lockFileRes.Data, err = write(lockFileRes.Path, lockFileMap, o) 195 if err != nil { 196 return nil, err 197 } 198 } 199 } 200 201 if lockFileMap != nil { 202 if lockFileMap["features"] == nil && version != "" { 203 for _, lang := range o.langs { 204 newForLang[lang] = true 205 } 206 } else if features, ok := lockFileMap["features"].(map[string]interface{}); ok { 207 for _, lang := range o.langs { 208 if _, ok := features[lang]; !ok { 209 newForLang[lang] = true 210 } 211 } 212 } 213 } else if !lockFilePresent { 214 for _, lang := range o.langs { 215 newForLang[lang] = true 216 } 217 } 218 } 219 220 requiredDefaults := map[string]bool{} 221 for _, lang := range o.langs { 222 requiredDefaults[lang] = newForLang[lang] 223 } 224 225 defaultCfg, err := GetDefaultConfig(newSDK, o.getLanguageDefaultFunc, requiredDefaults) 226 if err != nil { 227 return nil, err 228 } 229 230 cfg, err := GetDefaultConfig(newSDK, o.getLanguageDefaultFunc, requiredDefaults) 231 if err != nil { 232 return nil, err 233 } 234 235 // If this is a totally new config, we need to write out to disk for following operations 236 if newConfig && o.UpgradeFunc != nil { 237 // Write new cfg 238 configRes.Data, err = write(configRes.Path, cfg, o) 239 if err != nil { 240 return nil, err 241 } 242 } 243 244 if lockFileRes.Data == nil && o.UpgradeFunc != nil { 245 lockFile := NewLockFile() 246 lockFileRes.Data, err = write(lockFileRes.Path, lockFile, o) 247 if err != nil { 248 return nil, err 249 } 250 } 251 252 // Okay finally able to unmarshal the config file into expected struct 253 if err := yaml.Unmarshal(configRes.Data, cfg); err != nil { 254 return nil, fmt.Errorf("could not unmarshal gen.yaml: %w", err) 255 } 256 257 var lockFile LockFile 258 if err := yaml.Unmarshal(lockFileRes.Data, &lockFile); err != nil { 259 return nil, fmt.Errorf("could not unmarshal gen.lock: %w", err) 260 } 261 262 cfg.New = newForLang 263 264 // Maps are overwritten by unmarshal, so we need to ensure that the defaults are set 265 for lang, langCfg := range defaultCfg.Languages { 266 if _, ok := cfg.Languages[lang]; !ok { 267 cfg.Languages[lang] = langCfg 268 } 269 270 for k, v := range langCfg.Cfg { 271 if cfg.Languages[lang].Cfg == nil { 272 langCfg = cfg.Languages[lang] 273 langCfg.Cfg = map[string]interface{}{} 274 cfg.Languages[lang] = langCfg 275 } 276 277 if _, ok := cfg.Languages[lang].Cfg[k]; !ok { 278 cfg.Languages[lang].Cfg[k] = v 279 } 280 } 281 } 282 283 if lockFile.Features == nil { 284 lockFile.Features = make(map[string]map[string]string) 285 } 286 287 config := &Config{ 288 Config: cfg, 289 ConfigPath: configRes.Path, 290 LockFile: &lockFile, 291 } 292 293 if o.transformerFunc != nil { 294 config, err = o.transformerFunc(config) 295 if err != nil { 296 return nil, err 297 } 298 } 299 300 if o.UpgradeFunc != nil { 301 // Finally write out the files to solidfy any defaults, upgrades or transformations 302 if _, err := write(configRes.Path, config.Config, o); err != nil { 303 return nil, err 304 } 305 if _, err := write(lockFileRes.Path, config.LockFile, o); err != nil { 306 return nil, err 307 } 308 } 309 310 return config, nil 311 } 312 313 func GetTemplateVersion(dir, target string, opts ...Option) (string, error) { 314 o := applyOptions(opts) 315 316 configRes, err := FindConfigFile(dir, o.FS) 317 if err != nil { 318 return "", err 319 } 320 if configRes.Data == nil { 321 return "", nil 322 } 323 324 cfg := &Configuration{} 325 if err := yaml.Unmarshal(configRes.Data, cfg); err != nil { 326 return "", fmt.Errorf("could not unmarshal gen.yaml: %w", err) 327 } 328 329 if cfg.Languages == nil { 330 return "", nil 331 } 332 333 langCfg, ok := cfg.Languages[target] 334 if !ok { 335 return "", nil 336 } 337 338 tv, ok := langCfg.Cfg["templateVersion"] 339 if !ok { 340 return "", nil 341 } 342 343 return tv.(string), nil 344 } 345 346 func SaveConfig(dir string, cfg *Configuration, opts ...Option) error { 347 o := applyOptions(opts) 348 349 configRes, err := FindConfigFile(dir, o.FS) 350 if err != nil { 351 return err 352 } 353 354 if _, err := write(configRes.Path, cfg, o); err != nil { 355 return err 356 } 357 358 return nil 359 } 360 361 func SaveLockFile(dir string, lf *LockFile, opts ...Option) error { 362 o := applyOptions(opts) 363 364 lockFileRes, err := workspace.FindWorkspace(dir, workspace.FindWorkspaceOptions{ 365 FindFile: lockFile, 366 FS: o.FS, 367 }) 368 if err != nil { 369 if !errors.Is(err, fs.ErrNotExist) { 370 return err 371 } 372 lockFileRes = &workspace.FindWorkspaceResult{ 373 Path: filepath.Join(dir, workspace.SpeakeasyFolder, lockFile), 374 } 375 } 376 377 if _, err := write(lockFileRes.Path, lf, o); err != nil { 378 return err 379 } 380 381 return nil 382 } 383 384 func GetConfigChecksum(dir string, opts ...Option) (string, error) { 385 o := applyOptions(opts) 386 387 configRes, err := FindConfigFile(dir, o.FS) 388 if err != nil { 389 return "", err 390 } 391 if configRes.Data == nil { 392 return "", nil 393 } 394 395 hash := md5.Sum(configRes.Data) 396 return hex.EncodeToString(hash[:]), nil 397 } 398 399 func write(path string, cfg any, o *options) ([]byte, error) { 400 var b bytes.Buffer 401 buf := bufio.NewWriter(&b) 402 403 e := yaml.NewEncoder(buf) 404 e.SetIndent(2) 405 if err := e.Encode(cfg); err != nil { 406 return nil, fmt.Errorf("could not marshal gen.yaml: %w", err) 407 } 408 409 if err := buf.Flush(); err != nil { 410 return nil, fmt.Errorf("could not marshal gen.yaml: %w", err) 411 } 412 413 data := b.Bytes() 414 415 if o.dontWrite { 416 return data, nil 417 } 418 419 writeFileFunc := os.WriteFile 420 if o.FS != nil { 421 writeFileFunc = o.FS.WriteFile 422 } 423 424 if err := writeFileFunc(path, data, os.ModePerm); err != nil { 425 return nil, fmt.Errorf("could not write gen.yaml: %w", err) 426 } 427 428 return data, nil 429 } 430 431 func applyOptions(opts []Option) *options { 432 o := &options{ 433 FS: nil, 434 langs: []string{}, 435 } 436 for _, opt := range opts { 437 opt(o) 438 } 439 440 return o 441 }