github.com/lmorg/murex@v0.0.0-20240217211045-e081c89cd4ef/utils/alter/alter.go (about) 1 package alter 2 3 import ( 4 "context" 5 "errors" 6 "fmt" 7 "strconv" 8 9 "github.com/lmorg/murex/lang/types" 10 ) 11 12 const ( 13 actionAlter int = iota + 1 14 actionMerge 15 actionSum 16 ) 17 18 // Alter a data structure. Requires a path (pre-split) and new structure as a 19 // JSON string. A more seasoned developer will see plenty of room for 20 // optimisation however this function was largely thrown together in a "let's 21 // create something that works first and worry about performance later" kind of 22 // sense (much like a lot of murex's code base). That being said, I will accept 23 // any pull requests from other developers wishing to improve this - or other - 24 // functions. I'm also open to any breaking changes those optimisations might 25 // bring (at least until the project reaches version 1.0). 26 func Alter(ctx context.Context, v interface{}, path []string, new interface{}) (interface{}, error) { 27 return loop(ctx, v, 0, path, &new, actionAlter) 28 } 29 30 // Merge a data structure; like Alter but merges arrays and maps where possible 31 func Merge(ctx context.Context, v interface{}, path []string, new interface{}) (interface{}, error) { 32 if len(path) == 1 && path[0] == "" { 33 path = []string{} 34 } 35 return loop(ctx, v, 0, path, &new, actionMerge) 36 } 37 38 // Sum a data structure; like Merge but sums values in arrays and maps where 39 // duplication exists 40 func Sum(ctx context.Context, v interface{}, path []string, new interface{}) (interface{}, error) { 41 if len(path) == 1 && path[0] == "" { 42 path = []string{} 43 } 44 return loop(ctx, v, 0, path, &new, actionSum) 45 } 46 47 var ( 48 errOverwritePath = errors.New("internal condition: path needs overwriting") 49 errInvalidAction = errors.New("missing or invalid action. Please report this to https://github.com/lmorg/murex/issues") 50 ) 51 52 const ( 53 errExpectingAnArrayIndex = "expecting an array index in path element" 54 errNegativeIndexesNotAllowed = "negative indexes not allowed in arrays: path element" 55 errIndexGreaterThanArray = "index greater than length of array in path element" 56 ) 57 58 func loop(ctx context.Context, v interface{}, i int, path []string, new *interface{}, action int) (ret interface{}, err error) { 59 defer func() { 60 r := recover() 61 if r != nil { 62 err = fmt.Errorf("unhandled error in type conversion: %v", r) 63 } 64 }() 65 66 select { 67 case <-ctx.Done(): 68 return nil, errors.New("cancelled") 69 default: 70 } 71 72 switch { 73 case i < len(path): 74 switch v := v.(type) { 75 case []interface{}: 76 pathI, err := strconv.Atoi(path[i]) 77 if err != nil { 78 return nil, fmt.Errorf("%s '%s': %s", errExpectingAnArrayIndex, path[i], err) 79 } 80 81 if pathI < 0 { 82 return nil, fmt.Errorf("%s '%d'", errNegativeIndexesNotAllowed, pathI) 83 } 84 85 if pathI >= len(v) { 86 return nil, fmt.Errorf("%s '%d' (array length '%d')", errIndexGreaterThanArray, pathI, len(v)) 87 } 88 89 ret, err = loop(ctx, v[pathI], i+1, path, new, action) 90 if err == errOverwritePath { 91 v[pathI] = *new 92 93 } 94 if err == nil { 95 v[pathI] = ret 96 ret = v 97 } 98 99 case []string: 100 pathI, err := strconv.Atoi(path[i]) 101 if err != nil { 102 return nil, fmt.Errorf("%s '%s': %s", errExpectingAnArrayIndex, path[i], err) 103 } 104 105 if pathI < 0 { 106 return nil, fmt.Errorf("%s '%d'", errNegativeIndexesNotAllowed, pathI) 107 } 108 109 if pathI >= len(v) { 110 return nil, fmt.Errorf("%s '%d' (array length '%d')", errIndexGreaterThanArray, pathI, len(v)) 111 } 112 113 ret, err = loop(ctx, v[pathI], i+1, path, new, action) 114 if err == errOverwritePath { 115 s, err := types.ConvertGoType(*new, types.String) 116 if err != nil { 117 return nil, err 118 } 119 v[pathI] = s.(string) 120 121 } 122 if err == nil { 123 v[pathI] = ret.(string) 124 ret = v 125 } 126 127 case []int: 128 pathI, err := strconv.Atoi(path[i]) 129 if err != nil { 130 return nil, fmt.Errorf("%s '%s': %s", errExpectingAnArrayIndex, path[i], err) 131 } 132 133 if pathI < 0 { 134 return nil, fmt.Errorf("%s '%d'", errNegativeIndexesNotAllowed, pathI) 135 } 136 137 if pathI >= len(v) { 138 return nil, fmt.Errorf("%s '%d' (array length '%d')", errIndexGreaterThanArray, pathI, len(v)) 139 } 140 141 ret, err = loop(ctx, v[pathI], i+1, path, new, action) 142 if err == errOverwritePath { 143 i, err := types.ConvertGoType(*new, types.Integer) 144 if err != nil { 145 return nil, err 146 } 147 v[pathI] = i.(int) 148 149 } 150 if err == nil { 151 v[pathI] = ret.(int) 152 ret = v 153 } 154 155 case []float64: 156 pathI, err := strconv.Atoi(path[i]) 157 if err != nil { 158 return nil, fmt.Errorf("%s '%s': %s", errExpectingAnArrayIndex, path[i], err) 159 } 160 161 if pathI < 0 { 162 return nil, fmt.Errorf("%s '%d'", errNegativeIndexesNotAllowed, pathI) 163 } 164 165 if pathI >= len(v) { 166 return nil, fmt.Errorf("%s '%d' (array length '%d')", errIndexGreaterThanArray, pathI, len(v)) 167 } 168 169 ret, err = loop(ctx, v[pathI], i+1, path, new, action) 170 if err == errOverwritePath { 171 f, err := types.ConvertGoType(*new, types.Float) 172 if err != nil { 173 return nil, err 174 } 175 v[pathI] = f.(float64) 176 177 } 178 if err == nil { 179 v[pathI] = ret.(float64) 180 ret = v 181 } 182 183 case []bool: 184 pathI, err := strconv.Atoi(path[i]) 185 if err != nil { 186 return nil, fmt.Errorf("%s '%s': %s", errExpectingAnArrayIndex, path[i], err) 187 } 188 189 if pathI < 0 { 190 return nil, fmt.Errorf("%s '%d'", errNegativeIndexesNotAllowed, pathI) 191 } 192 193 if pathI >= len(v) { 194 return nil, fmt.Errorf("%s '%d' (array length '%d')", errIndexGreaterThanArray, pathI, len(v)) 195 } 196 197 ret, err = loop(ctx, v[pathI], i+1, path, new, action) 198 if err == errOverwritePath { 199 b, err := types.ConvertGoType(*new, types.Boolean) 200 if err != nil { 201 return nil, err 202 } 203 v[pathI] = b.(bool) 204 205 } 206 if err == nil { 207 v[pathI] = ret.(bool) 208 ret = v 209 } 210 211 case map[interface{}]interface{}: 212 ret, err = loop(ctx, v[path[i]], i+1, path, new, action) 213 if err == errOverwritePath { 214 v[path[i]] = *new 215 216 } 217 if err == nil { 218 v[path[i]] = ret 219 ret = v 220 } 221 222 case map[string]interface{}: 223 ret, err = loop(ctx, v[path[i]], i+1, path, new, action) 224 if err == errOverwritePath { 225 v[path[i]] = *new 226 227 } 228 if err == nil { 229 v[path[i]] = ret 230 ret = v 231 } 232 233 case map[interface{}]string: 234 ret, err = loop(ctx, v[path[i]], i+1, path, new, action) 235 if err == errOverwritePath { 236 s, err := types.ConvertGoType(*new, types.String) 237 if err != nil { 238 return nil, err 239 } 240 v[path[i]] = s.(string) 241 242 } 243 if err == nil { 244 v[path[i]] = ret.(string) 245 ret = v 246 } 247 248 case nil: 249 // Let's overwrite part of the path 250 return nil, errOverwritePath 251 252 case string, int, float64, bool: 253 return nil, fmt.Errorf("unable to alter data structure using that path because one of the path elements is an end of tree (%T) rather than a map. Instead please have the full path you want to add as part of the amend JSON string in `alter`", v) 254 255 default: 256 return nil, fmt.Errorf("murex code error: No condition is made for `%T`. Please report this bug to https://github.com/lmorg/murex/issues", v) 257 } 258 259 case i == len(path): 260 switch v.(type) { 261 case string: 262 s, err := types.ConvertGoType(*new, types.String) 263 if err != nil { 264 return nil, err 265 } 266 ret = s.(string) 267 268 case int: 269 i, err := types.ConvertGoType(*new, types.Integer) 270 if err != nil { 271 return nil, err 272 } 273 ret = i.(int) 274 275 case float64: 276 f, err := types.ConvertGoType(*new, types.Float) 277 if err != nil { 278 return nil, err 279 } 280 ret = f.(float64) 281 282 case bool: 283 b, err := types.ConvertGoType(*new, types.Boolean) 284 if err != nil { 285 return nil, err 286 } 287 ret = b.(bool) 288 289 case nil: 290 ret = *new 291 292 case []string, []bool, []float64, []int, []interface{}: 293 switch action { 294 case actionMerge, actionSum: 295 return mergeArray(v, new) 296 case actionAlter: 297 ret = *new 298 default: 299 return nil, errInvalidAction 300 } 301 302 case map[string]interface{}, map[interface{}]interface{}, 303 map[string]int, map[interface{}]int, 304 map[string]float64, map[interface{}]float64: 305 switch action { 306 case actionMerge: 307 return mergeMap(v, new) 308 case actionSum: 309 return sumMap(v, new) 310 case actionAlter: 311 ret = *new 312 default: 313 return nil, errInvalidAction 314 } 315 316 case map[string]string, map[interface{}]string, 317 map[string]bool, map[interface{}]bool: 318 switch action { 319 case actionMerge, actionSum: 320 return mergeMap(v, new) 321 case actionAlter: 322 ret = *new 323 default: 324 return nil, errInvalidAction 325 } 326 327 default: 328 if len(path) == 0 { 329 return nil, fmt.Errorf("path is 0 (zero) length and unable to construct an object path for %T. Possibly due to bad parameters supplied", v) 330 } 331 return nil, fmt.Errorf("cannot locate `%s` in object path or no condition is made for `%T`. Please report this bug to https://github.com/lmorg/murex/issues", path[i-1], v) 332 } 333 334 default: 335 return nil, errors.New("murex code error: default condition calculating the length of the path. I don't know how I got here. Please report this bug to https://github.com/lmorg/murex/issues") 336 } 337 338 if err == errOverwritePath { 339 err = nil 340 } 341 return 342 }