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 }