github.com/fluffy-bunny/viperEx@v0.0.32/surgical-update.go (about) 1 // Copyright © 2020 Herb Stahl <ghstahl@gmail.com>. 2 // 3 // Use of this source code is governed by an MIT-style 4 // license that can be found in the LICENSE file. 5 6 // ViperEx adds some missing gap items from the awesome Viper project is a application configuration system. 7 8 package viperEx 9 10 import ( 11 "fmt" 12 "os" 13 "reflect" 14 "strconv" 15 "strings" 16 17 "github.com/mitchellh/mapstructure" 18 "github.com/rs/zerolog/log" 19 "github.com/spf13/viper" 20 ) 21 22 const defaultKeyDelimiter = "." 23 24 func newChangeAllKeysToLowerCase(m map[string]interface{}) map[string]interface{} { 25 newMap := make(map[string]interface{}) 26 for k, v := range m { 27 switch val := v.(type) { 28 case map[string]interface{}: 29 newMap[strings.ToLower(k)] = newChangeAllKeysToLowerCase(val) 30 case []interface{}: 31 var newSlice []interface{} 32 for _, item := range val { 33 if reflect.TypeOf(item).Kind() == reflect.Map { 34 newSlice = append(newSlice, newChangeAllKeysToLowerCase(item.(map[string]interface{}))) 35 } else { 36 newSlice = append(newSlice, item) 37 } 38 } 39 newMap[strings.ToLower(k)] = newSlice 40 default: 41 newMap[strings.ToLower(k)] = val 42 } 43 } 44 return newMap 45 } 46 47 // WithEnvPrefix sets the prefix for environment variables 48 func WithEnvPrefix(envPrefix string) func(*ViperEx) error { 49 return func(v *ViperEx) error { 50 v.EnvPrefix = envPrefix + "_" 51 return nil 52 } 53 } 54 55 // WithDelimiter sets the delimiter for keys 56 func WithDelimiter(delimiter string) func(*ViperEx) error { 57 return func(v *ViperEx) error { 58 v.KeyDelimiter = delimiter 59 return nil 60 } 61 } 62 63 // New creates a new ViperEx instance with optional options 64 func New(allsettings map[string]interface{}, options ...func(*ViperEx) error) (*ViperEx, error) { 65 changeAllKeysToLowerCase(allsettings) 66 changeStringArrayToInterfaceArray(allsettings) 67 changeStringMapStringToStringMapInterface(allsettings) 68 viperEx := &ViperEx{ 69 KeyDelimiter: defaultKeyDelimiter, 70 AllSettings: newChangeAllKeysToLowerCase(allsettings), 71 } 72 var err error 73 for _, option := range options { 74 err = option(viperEx) 75 if err != nil { 76 return nil, err 77 } 78 } 79 return viperEx, nil 80 } 81 82 // ViperEx type 83 type ViperEx struct { 84 KeyDelimiter string 85 AllSettings map[string]interface{} 86 EnvPrefix string 87 } 88 89 // UpdateFromEnv will find potential ENV candidates to merge in 90 func (ve *ViperEx) UpdateFromEnv() error { 91 potential := ve.getPotentialEnvVariables() 92 for key, value := range potential { 93 ve.UpdateDeepPath(key, value) 94 } 95 return nil 96 } 97 98 // Find will return the interface to the data if it exists 99 func (ve *ViperEx) Find(key string) interface{} { 100 lcaseKey := strings.ToLower(key) 101 path := strings.Split(lcaseKey, ve.KeyDelimiter) 102 103 lastKey := strings.ToLower(path[len(path)-1]) 104 105 fmt.Println(lastKey) 106 path = path[0 : len(path)-1] 107 if len(lastKey) == 0 { 108 return nil 109 } 110 111 deepestEntity := ve.deepSearch(ve.AllSettings, path) 112 deepestMap, ok := deepestEntity.(map[string]interface{}) 113 if ok { 114 return deepestMap[lastKey] 115 } 116 117 deepestArray, ok := deepestEntity.([]interface{}) 118 if ok { 119 // lastKey has to be a num 120 idx, err := strconv.Atoi(lastKey) 121 if err == nil { 122 return deepestArray[idx] 123 } 124 } 125 126 return nil 127 } 128 129 // UpdateDeepPath will update the value if it exists 130 func (ve *ViperEx) UpdateDeepPath(key string, value interface{}) { 131 lcaseKey := strings.ToLower(key) 132 path := strings.Split(lcaseKey, ve.KeyDelimiter) 133 134 lastKey := strings.ToLower(path[len(path)-1]) 135 136 path = path[0 : len(path)-1] 137 if len(lastKey) == 0 { 138 return 139 } 140 141 deepestEntity := ve.deepSearch(ve.AllSettings, path) 142 deepestMap, ok := deepestEntity.(map[string]interface{}) 143 if ok { 144 // set innermost value 145 _, ok := deepestMap[lastKey] 146 if ok { 147 deepestMap[lastKey] = value 148 } 149 } else { 150 // is this an array 151 deepestArray, ok := deepestEntity.([]interface{}) 152 if ok { 153 // lastKey has to be a num 154 idx, err := strconv.Atoi(lastKey) 155 if err == nil { 156 if idx < len(deepestArray) && idx >= 0 { 157 deepestArray[idx] = value 158 } 159 } 160 } 161 } 162 } 163 func (ve *ViperEx) getPotentialEnvVariables() map[string]string { 164 var result map[string]string 165 result = make(map[string]string) 166 for _, element := range os.Environ() { 167 var index = strings.Index(element, "=") 168 key := element[0:index] 169 // check for prefix 170 if len(ve.EnvPrefix) > 0 { 171 if strings.HasPrefix(key, ve.EnvPrefix) { 172 key = key[len(ve.EnvPrefix):] 173 } 174 } 175 value := element[index+1:] 176 if strings.Contains(key, ve.KeyDelimiter) { 177 result[key] = value 178 } 179 } 180 return result 181 } 182 183 func (ve *ViperEx) deepSearch(m map[string]interface{}, path []string) interface{} { 184 if len(path) == 0 { 185 return m 186 } 187 var currentPath string 188 var stepArray = false 189 var currentArray []interface{} 190 var currentEntity interface{} 191 for _, k := range path { 192 if len(currentPath) == 0 { 193 currentPath = k 194 } else { 195 currentPath = fmt.Sprintf("%v.%v", currentPath, k) 196 } 197 if stepArray { 198 idx, err := strconv.Atoi(k) 199 if err != nil { 200 log.Error().Err(err).Msgf("No such path exists, must be an array idx: %v", currentPath) 201 return nil 202 } 203 if len(currentArray) <= idx { 204 log.Error().Msgf("No such path exists: %v", currentPath) 205 return nil 206 } 207 m3, ok := currentArray[idx].(map[string]interface{}) 208 if !ok { 209 log.Error().Msgf("No such path exists: %v, error in mapping to a map[string]interface{}", currentPath) 210 return nil 211 } 212 // continue search from here 213 m = m3 214 currentEntity = m 215 stepArray = false // don't support arrays of arrays 216 } else { 217 m2, ok := m[k] 218 if !ok { 219 // intermediate key does not exist 220 return nil 221 } 222 m3, ok := m2.(map[string]interface{}) 223 if !ok { 224 // is this an array 225 m4, ok := m2.([]interface{}) 226 if ok { 227 // continue search from here 228 currentArray = m4 229 currentEntity = currentArray 230 stepArray = true 231 m3 = nil 232 } else { 233 // intermediate key is a value 234 return nil 235 } 236 } else { 237 // continue search from here 238 m = m3 239 currentEntity = m 240 } 241 } 242 } 243 244 return currentEntity 245 } 246 247 // code copied from the viper project 248 249 // defaultDecoderConfig returns default mapsstructure.DecoderConfig with suppot 250 // of time.Duration values & string slices 251 func defaultDecoderConfig(output interface{}, opts ...viper.DecoderConfigOption) *mapstructure.DecoderConfig { 252 c := &mapstructure.DecoderConfig{ 253 Metadata: nil, 254 Result: output, 255 WeaklyTypedInput: true, 256 DecodeHook: mapstructure.ComposeDecodeHookFunc( 257 mapstructure.StringToTimeDurationHookFunc(), 258 mapstructure.StringToSliceHookFunc(","), 259 ), 260 } 261 for _, opt := range opts { 262 opt(c) 263 } 264 return c 265 } 266 267 // Unmarshal to struct 268 func (ve *ViperEx) Unmarshal(rawVal interface{}, opts ...viper.DecoderConfigOption) error { 269 return decode(ve.AllSettings, defaultDecoderConfig(rawVal, opts...)) 270 } 271 272 // A wrapper around mapstructure.Decode that mimics the WeakDecode functionality 273 func decode(input interface{}, config *mapstructure.DecoderConfig) error { 274 decoder, err := mapstructure.NewDecoder(config) 275 if err != nil { 276 return err 277 } 278 return decoder.Decode(input) 279 } 280 func changeStringMapStringToStringMapInterface(m map[string]interface{}) { 281 var currentKeys []string 282 for key := range m { 283 currentKeys = append(currentKeys, key) 284 } 285 for _, key := range currentKeys { 286 vv, ok := m[key].(map[string]string) 287 if ok { 288 m2 := make(map[string]interface{}) 289 for k, v := range vv { 290 m2[k] = v 291 } 292 m[key] = m2 293 } else { 294 v2, ok := m[key].(map[string]interface{}) 295 if ok { 296 changeStringMapStringToStringMapInterface(v2) 297 } 298 } 299 } 300 } 301 func changeStringArrayToInterfaceArray(m map[string]interface{}) { 302 var currentKeys []string 303 for key := range m { 304 currentKeys = append(currentKeys, key) 305 } 306 307 for _, key := range currentKeys { 308 vv, ok := m[key].([]string) 309 if ok { 310 m2 := make([]interface{}, 0) 311 for idx := range vv { 312 v := vv[idx] 313 m2 = append(m2, &v) 314 } 315 m[key] = m2 316 } else { 317 v2, ok := m[key].(map[string]interface{}) 318 if ok { 319 changeStringArrayToInterfaceArray(v2) 320 } 321 } 322 } 323 } 324 325 func changeAllKeysToLowerCase(m map[string]interface{}) { 326 var lcMap = make(map[string]interface{}) 327 var currentKeys []string 328 for key, value := range m { 329 currentKeys = append(currentKeys, key) 330 lcMap[strings.ToLower(key)] = value 331 } 332 // delete original values 333 for _, k := range currentKeys { 334 delete(m, k) 335 } 336 // put the lowercase ones in the original map 337 for key, value := range lcMap { 338 m[key] = value 339 vMap, ok := value.(map[string]interface{}) 340 if ok { 341 // if the current value is a map[string]interface{}, keep going 342 changeAllKeysToLowerCase(vMap) 343 } 344 } 345 }