github.com/blend/go-sdk@v1.20240719.1/configutil/read.go (about) 1 /* 2 3 Copyright (c) 2024 - Present. Blend Labs, Inc. All rights reserved 4 Use of this source code is governed by a MIT license that can be found in the LICENSE file. 5 6 */ 7 8 package configutil 9 10 import ( 11 "encoding/json" 12 "io" 13 "os" 14 "path/filepath" 15 "strings" 16 17 "gopkg.in/yaml.v3" 18 19 "github.com/blend/go-sdk/env" 20 "github.com/blend/go-sdk/ex" 21 ) 22 23 // MustRead reads a config from optional path(s) and panics on error. 24 // 25 // It is functionally equivalent to `Read` outside error handling; see this function for more information. 26 func MustRead(ref Any, options ...Option) (filePaths []string) { 27 var err error 28 filePaths, err = Read(ref, options...) 29 if !IsIgnored(err) { 30 panic(err) 31 } 32 return 33 } 34 35 // Read reads a config from optional path(s), returning the paths read from (in the order visited), and an error if there were any issues. 36 /* 37 If the ref type is a `Resolver` the `Resolve(context.Context) error` method will 38 be called on the ref and passed a context configured from the given options. 39 40 By default, a well known set of paths will be read from (including a path read from the environment variable `CONFIG_PATH`). 41 42 You can override this by providing options to specify which paths will be read from: 43 44 paths, err := configutil.Read(&cfg, configutil.OptPaths("foo.yml")) 45 46 The above will _only_ read from `foo.yml` to populate the `cfg` reference. 47 */ 48 func Read(ref Any, options ...Option) (paths []string, err error) { 49 var configOptions ConfigOptions 50 configOptions, err = createConfigOptions(options...) 51 if err != nil { 52 return 53 } 54 55 for _, contents := range configOptions.Contents { 56 MaybeDebugf(configOptions.Log, "reading config contents with extension `%s`", contents.Ext) 57 err = deserialize(contents.Ext, contents.Contents, ref) 58 if err != nil { 59 return 60 } 61 } 62 63 var f *os.File 64 var path string 65 var resolveErr error 66 for _, path = range configOptions.FilePaths { 67 if path == "" { 68 continue 69 } 70 MaybeDebugf(configOptions.Log, "checking for config path: %s", path) 71 f, resolveErr = os.Open(path) 72 if IsNotExist(resolveErr) { 73 continue 74 } 75 if resolveErr != nil { 76 err = ex.New(resolveErr) 77 break 78 } 79 defer f.Close() 80 81 MaybeDebugf(configOptions.Log, "reading config path: %s", path) 82 resolveErr = deserialize(filepath.Ext(path), f, ref) 83 if resolveErr != nil { 84 err = ex.New(resolveErr) 85 return 86 } 87 88 paths = append(paths, path) 89 } 90 91 if typed, ok := ref.(Resolver); ok { 92 MaybeDebugf(configOptions.Log, "calling config resolver") 93 if resolveErr := typed.Resolve(configOptions.Background()); resolveErr != nil { 94 MaybeErrorf(configOptions.Log, "calling resolver error: %+v", resolveErr) 95 err = resolveErr 96 return 97 } 98 } 99 return 100 } 101 102 func createConfigOptions(options ...Option) (configOptions ConfigOptions, err error) { 103 configOptions.Env = env.Env() 104 configOptions.FilePaths = DefaultPaths 105 if configOptions.Env.Has(EnvVarConfigPath) { 106 configOptions.FilePaths = append(configOptions.Env.CSV(EnvVarConfigPath), configOptions.FilePaths...) 107 } 108 for _, option := range options { 109 if err = option(&configOptions); err != nil { 110 return 111 } 112 } 113 return 114 } 115 116 // deserialize deserializes a config. 117 func deserialize(ext string, r io.Reader, ref Any) error { 118 // make sure the extension starts with a "." 119 if !strings.HasPrefix(ext, ".") { 120 ext = "." + ext 121 } 122 123 // based off the extension, use the appropriate deserializer 124 switch strings.ToLower(ext) { 125 case ExtensionJSON: 126 return ex.New(json.NewDecoder(r).Decode(ref)) 127 case ExtensionYAML, ExtensionYML: 128 return ex.New(yaml.NewDecoder(r).Decode(ref)) 129 default: // return an error if we're passed a weird extension 130 return ex.New(ErrInvalidConfigExtension, ex.OptMessagef("extension: %s", ext)) 131 } 132 }