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  }