github.com/nuvolaris/nuv@v0.0.0-20240511174247-a74e3a52bfd8/config/config_map.go (about)

     1  // Licensed to the Apache Software Foundation (ASF) under one
     2  // or more contributor license agreements.  See the NOTICE file
     3  // distributed with this work for additional information
     4  // regarding copyright ownership.  The ASF licenses this file
     5  // to you under the Apache License, Version 2.0 (the
     6  // "License"); you may not use this file except in compliance
     7  // with the License.  You may obtain a copy of the License at
     8  //
     9  //   http://www.apache.org/licenses/LICENSE-2.0
    10  //
    11  // Unless required by applicable law or agreed to in writing,
    12  // software distributed under the License is distributed on an
    13  // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
    14  // KIND, either express or implied.  See the License for the
    15  // specific language governing permissions and limitations
    16  // under the License.
    17  
    18  package config
    19  
    20  import (
    21  	"encoding/json"
    22  	"fmt"
    23  	"log"
    24  	"os"
    25  	"strconv"
    26  	"strings"
    27  )
    28  
    29  // A ConfigMap is a map where the keys are in the form of: A_KEY_WITH_UNDERSCORES.
    30  // The map splits the key by the underscores and creates a nested map that
    31  // represents the key. For example, the key "A_KEY_WITH_UNDERSCORES" would be
    32  // represented as:
    33  //
    34  //	{
    35  //		"a": {
    36  //			"key": {
    37  //				"with": {
    38  //					"underscores": "value",
    39  //				},
    40  //			},
    41  //		},
    42  //	}
    43  //
    44  // To interact with the ConfigMap, use the Insert, Get, and Delete by passing
    45  // keys in the form above. Only the config map is modified by these functions.
    46  // The nuvRootConfig map is only used to read the config keys in nuvroot.json.
    47  // The pluginNuvRootConfigs map is only used to read the config keys in
    48  // plugins (from their nuvroot.json). It is a map that maps the plugin name to
    49  // the config map for that plugin.
    50  type ConfigMap struct {
    51  	pluginNuvRootConfigs map[string]map[string]interface{}
    52  	nuvRootConfig        map[string]interface{}
    53  	config               map[string]interface{}
    54  	configPath           string
    55  }
    56  
    57  // Insert inserts a key and value into the ConfigMap. If the key already exists,
    58  // the value is overwritten. The expected key format is A_KEY_WITH_UNDERSCORES.
    59  func (c *ConfigMap) Insert(key string, value string) error {
    60  	keys, err := parseKey(strings.ToLower(key))
    61  	if err != nil {
    62  		return err
    63  	}
    64  
    65  	currentMap := c.config
    66  	lastIndex := len(keys) - 1
    67  	for i, subKey := range keys {
    68  		// If we are at the last key, set the value
    69  		if i == lastIndex {
    70  			v, err := parseValue(value)
    71  			if err != nil {
    72  				return err
    73  			}
    74  
    75  			currentMap[subKey] = v
    76  		} else {
    77  			// If the sub-map doesn't exist, create it
    78  			if _, ok := currentMap[subKey]; !ok {
    79  				currentMap[subKey] = make(map[string]interface{})
    80  			}
    81  			// Update the current map to the sub-map
    82  			m, ok := currentMap[subKey].(map[string]interface{})
    83  			if !ok {
    84  				return fmt.Errorf("invalid key: '%s' - '%s' is already being used for a value", key, subKey)
    85  			}
    86  			currentMap = m
    87  		}
    88  	}
    89  
    90  	return nil
    91  }
    92  
    93  func (c *ConfigMap) Get(key string) (string, error) {
    94  	cmap := c.Flatten()
    95  
    96  	val, ok := cmap[key]
    97  	if !ok {
    98  		return "", fmt.Errorf("invalid key: '%s' - key does not exist", key)
    99  	}
   100  
   101  	return val, nil
   102  }
   103  
   104  func (c *ConfigMap) Delete(key string) error {
   105  	delFunc := func(config map[string]interface{}, key string) bool {
   106  		if _, ok := config[key]; !ok {
   107  			return false
   108  		}
   109  
   110  		delete(config, key)
   111  		return true
   112  	}
   113  	keys, err := parseKey(strings.ToLower(key))
   114  	if err != nil {
   115  		return err
   116  	}
   117  
   118  	ok := visit(c.config, 0, keys, delFunc)
   119  	if !ok {
   120  		return fmt.Errorf("invalid key: '%s' - key does not exist in config.json", key)
   121  	}
   122  	return nil
   123  }
   124  
   125  func (c *ConfigMap) Flatten() map[string]string {
   126  	outputMap := make(map[string]string)
   127  
   128  	merged := mergeMaps(c.nuvRootConfig, c.config)
   129  
   130  	for name, pluginConfig := range c.pluginNuvRootConfigs {
   131  		// edge case: check that merged does not contain name already
   132  		if _, ok := merged[name]; ok {
   133  			log.Printf("config has key with same name as plugin %s. Plugin config will be ignored.", name)
   134  			continue
   135  		}
   136  
   137  		merged[name] = pluginConfig
   138  	}
   139  
   140  	flatten("", merged, outputMap)
   141  
   142  	return outputMap
   143  }
   144  
   145  func (c *ConfigMap) SaveConfig() error {
   146  	var configJSON, err = json.MarshalIndent(c.config, "", "  ")
   147  	if err != nil {
   148  		return err
   149  	}
   150  
   151  	return os.WriteFile(c.configPath, configJSON, 0644)
   152  }
   153  
   154  // ///
   155  func flatten(prefix string, inputMap map[string]interface{}, outputMap map[string]string) {
   156  	if len(prefix) > 0 {
   157  		prefix += "_"
   158  	}
   159  	for k, v := range inputMap {
   160  		key := strings.ToUpper(prefix + k)
   161  		switch child := v.(type) {
   162  		case map[string]interface{}:
   163  			flatten(key, child, outputMap)
   164  		default:
   165  			outputMap[key] = fmt.Sprintf("%v", v)
   166  		}
   167  	}
   168  }
   169  
   170  type configOperationFunc func(config map[string]interface{}, key string) bool
   171  
   172  func visit(config map[string]interface{}, index int, keys []string, f configOperationFunc) bool {
   173  	// base case: if the key is the last key in the list, call the function f
   174  	if index == len(keys)-1 {
   175  		return f(config, keys[index])
   176  	}
   177  
   178  	// recursive case: if the key is not the last key in the list, call visit on the next key (if cast ok)
   179  	conf, ok := config[keys[index]].(map[string]interface{})
   180  	if !ok {
   181  		return false
   182  	}
   183  	success := visit(conf, index+1, keys, f)
   184  	// if the parent map is empty, clean up
   185  	if success && len(conf) == 0 {
   186  		delete(config, keys[index])
   187  	}
   188  	return success
   189  }
   190  
   191  func parseKey(key string) ([]string, error) {
   192  	parts := strings.Split(key, "_")
   193  	for _, part := range parts {
   194  		if part == "" {
   195  			return nil, fmt.Errorf("invalid key: %s", key)
   196  		}
   197  	}
   198  	return parts, nil
   199  }
   200  
   201  /*
   202  VALUEs are parsed in the following way:
   203  
   204    - try to parse as a jsos first, and if it is a json, store as a json
   205    - then try to parse as a number, and if it is a (float) number store as a number
   206    - then try to parse as true or false and store as a boolean
   207    - then check if it's null and store as a null
   208    - otherwise store as a string
   209  */
   210  func parseValue(value string) (interface{}, error) {
   211  	// Try to parse as json
   212  	var jsonValue interface{}
   213  	if err := json.Unmarshal([]byte(value), &jsonValue); err == nil {
   214  		return jsonValue, nil
   215  	}
   216  
   217  	// Try to parse as a integer with strconv
   218  	if intValue, err := strconv.Atoi(value); err == nil {
   219  		return intValue, nil
   220  	}
   221  
   222  	// Try to parse as a float with strconv
   223  	if floatValue, err := strconv.ParseFloat(value, 64); err == nil {
   224  		return floatValue, nil
   225  	}
   226  
   227  	// Try to parse as a boolean
   228  	if value == "true" || value == "false" {
   229  		return value == "true", nil
   230  	}
   231  
   232  	// Try to parse as null
   233  	if value == "null" {
   234  		return nil, nil
   235  	}
   236  
   237  	// Otherwise, return the string
   238  	return value, nil
   239  }
   240  
   241  // mergeMaps merges map2 into map1 overwriting any values in map1 with values from map2
   242  // when there are conflicts. It returns the merged map.
   243  func mergeMaps(map1, map2 map[string]interface{}) map[string]interface{} {
   244  	if len(map1) == 0 {
   245  		return map2
   246  	}
   247  	if len(map2) == 0 {
   248  		return map1
   249  	}
   250  
   251  	mergedMap := make(map[string]interface{})
   252  
   253  	for key, value := range map1 {
   254  
   255  		map2Value, ok := map2[key]
   256  		// key doesn't exist in map2 so add it to the merged map
   257  		if !ok {
   258  			mergedMap[key] = value
   259  			continue
   260  		}
   261  
   262  		// key exists in map2 but map1 value is NOT a map, so add value from map2
   263  		mapFromMap1, ok := value.(map[string]interface{})
   264  		if !ok {
   265  			mergedMap[key] = map2Value
   266  			continue
   267  		}
   268  
   269  		mapFromMap2, ok := map2Value.(map[string]interface{})
   270  		// key exists in map2, map1 value IS a map but map2 value is not, so overwrite with map2
   271  		if !ok {
   272  			mergedMap[key] = mapFromMap2
   273  			continue
   274  		}
   275  
   276  		// key exists in map2, map1 value IS a map, map2 value IS a map, so merge recursively
   277  		mergedMap[key] = mergeMaps(mapFromMap1, mapFromMap2)
   278  	}
   279  
   280  	// add any keys that exist in map2 but not in map1
   281  	for key, value := range map2 {
   282  		if _, ok := mergedMap[key]; !ok {
   283  			mergedMap[key] = value
   284  		}
   285  	}
   286  
   287  	return mergedMap
   288  }