go.charczuk.com@v0.0.0-20240327042549-bc490516bd1a/sdk/configutil/read.go (about) 1 /* 2 3 Copyright (c) 2023 - Present. Will Charczuk. All rights reserved. 4 Use of this source code is governed by a MIT license that can be found in the LICENSE file at the root of the repository. 5 6 */ 7 8 package configutil 9 10 import ( 11 "encoding/json" 12 "io" 13 "os" 14 15 "gopkg.in/yaml.v3" 16 ) 17 18 // MustRead reads a config from optional path(s) and panics on error. 19 // 20 // It is functionally equivalent to `Read` outside error handling; see this function for more information. 21 func MustRead(ref any, options ...Option) (filePaths []string) { 22 var err error 23 filePaths, err = Read(ref, options...) 24 if !IsIgnored(err) { 25 panic(err) 26 } 27 return 28 } 29 30 /* 31 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. 32 33 If the ref type is a `Resolver` the `Resolve(context.Context) error` method will 34 be called on the ref and passed a context configured from the given options. 35 36 By default, a well known set of paths will be read from (including a path read from the environment variable `CONFIG_PATH`). 37 38 You can override this by providing options to specify which paths will be read from: 39 40 paths, err := configutil.Read(&cfg, configutil.OptPaths("foo.yml")) 41 42 The above will _only_ read from `foo.yml` to populate the `cfg` reference. 43 */ 44 func Read(ref any, options ...Option) (paths []string, err error) { 45 var configOptions ConfigOptions 46 configOptions, err = createConfigOptions(options...) 47 if err != nil { 48 return 49 } 50 51 for _, contents := range configOptions.Contents { 52 err = configOptions.Deserializer(contents, ref) 53 if err != nil { 54 return 55 } 56 } 57 58 var f *os.File 59 var path string 60 var resolveErr error 61 for _, path = range configOptions.FilePaths { 62 if path == "" { 63 continue 64 } 65 f, resolveErr = os.Open(path) 66 if IsNotExist(resolveErr) { 67 continue 68 } 69 if resolveErr != nil { 70 err = resolveErr 71 break 72 } 73 defer f.Close() 74 75 resolveErr = configOptions.Deserializer(f, ref) 76 if resolveErr != nil { 77 err = resolveErr 78 return 79 } 80 81 paths = append(paths, path) 82 } 83 84 if typed, ok := ref.(Resolver); ok { 85 if resolveErr := typed.Resolve(configOptions.Background()); resolveErr != nil { 86 err = resolveErr 87 return 88 } 89 } 90 return 91 } 92 93 func createConfigOptions(options ...Option) (configOptions ConfigOptions, err error) { 94 configOptions.Env = parseEnv() 95 configOptions.FilePaths = DefaultPaths 96 configOptions.Deserializer = deserializeYAML 97 if configPath, ok := configOptions.Env[EnvVarConfigPath]; ok && configPath != "" { 98 configOptions.FilePaths = []string{configPath} 99 } 100 for _, option := range options { 101 if err = option(&configOptions); err != nil { 102 return 103 } 104 } 105 return 106 } 107 108 func deserializeJSON(r io.Reader, ref any) error { 109 return json.NewDecoder(r).Decode(ref) 110 } 111 112 func deserializeYAML(r io.Reader, ref any) error { 113 return yaml.NewDecoder(r).Decode(ref) 114 } 115 116 func parseEnv() map[string]string { 117 var key, value string 118 vars := make(map[string]string) 119 for _, ev := range os.Environ() { 120 key, value = splitVar(ev) 121 if key != "" { 122 vars[key] = value 123 } 124 } 125 return vars 126 } 127 128 func splitVar(s string) (key, value string) { 129 for i := 0; i < len(s); i++ { 130 if s[i] == '=' { 131 key = s[:i] 132 value = s[i+1:] 133 return 134 } 135 } 136 return 137 }