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  }