github.com/enbility/spine-go@v0.7.0/model/collection_operations.go (about)

     1  package model
     2  
     3  import (
     4  	"fmt"
     5  	"reflect"
     6  	"slices"
     7  )
     8  
     9  // creates an hash key by using fields that have eebus tag "key"
    10  func hashKey(data any) string {
    11  	result := ""
    12  
    13  	keys := fieldNamesWithEEBusTag(EEBusTagKey, data)
    14  
    15  	if len(keys) == 0 {
    16  		return result
    17  	}
    18  
    19  	v := reflect.ValueOf(data)
    20  
    21  	for _, fieldName := range keys {
    22  		f := v.FieldByName(fieldName)
    23  
    24  		if f.IsNil() || !f.IsValid() {
    25  			return result
    26  		}
    27  
    28  		switch f.Elem().Kind() {
    29  		case reflect.String:
    30  			value := f.Elem().String()
    31  
    32  			if len(result) > 0 {
    33  				result = fmt.Sprintf("%s|", result)
    34  			}
    35  			result = fmt.Sprintf("%s%s", result, value)
    36  
    37  		case reflect.Uint:
    38  			value := f.Elem().Uint()
    39  
    40  			if len(result) > 0 {
    41  				result = fmt.Sprintf("%s|", result)
    42  			}
    43  			result = fmt.Sprintf("%s%d", result, value)
    44  
    45  		case reflect.Struct:
    46  			value := f.Type()
    47  			fmt.Println(value)
    48  
    49  			if !f.CanInterface() {
    50  				return result
    51  			}
    52  
    53  			c, ok := f.Interface().(UpdateHelper)
    54  			if !ok {
    55  				return result
    56  			}
    57  
    58  			if len(result) > 0 {
    59  				result = fmt.Sprintf("%s|", result)
    60  			}
    61  			result = fmt.Sprintf("%s%s", result, c.String())
    62  
    63  			return result
    64  		default:
    65  			return result
    66  		}
    67  	}
    68  
    69  	return result
    70  }
    71  
    72  // check the eebus tag if is has a "writecheck" item
    73  // and if so, if the value of that field is true
    74  func writeAllowed(data any) bool {
    75  	fields := fieldNamesWithEEBusTag(EEBusTagWriteCheck, data)
    76  	// only one field in a struct may have this tag
    77  	if len(fields) != 1 {
    78  		return true
    79  	}
    80  
    81  	fieldName := fields[0]
    82  	v := reflect.ValueOf(data)
    83  	f := v.FieldByName(fieldName)
    84  
    85  	if f.IsNil() || !f.IsValid() {
    86  		return false
    87  	}
    88  
    89  	// if this is not a boolean, the tag is wrong which shouldn't happen
    90  	// and we allow overwriting
    91  	if f.Elem().Kind() != reflect.Bool {
    92  		return true
    93  	}
    94  
    95  	value := f.Elem().Bool()
    96  	return value
    97  }
    98  
    99  // update missing fields in destination with values from source
   100  func updateFields[T any](remoteWrite bool, source T, destination *T) {
   101  	if destination == nil {
   102  		return
   103  	}
   104  
   105  	writeCheckFields := fieldNamesWithEEBusTag(EEBusTagWriteCheck, source)
   106  
   107  	sV := reflect.ValueOf(source)
   108  	sT := reflect.TypeOf(source)
   109  	dV := reflect.ValueOf(destination).Elem()
   110  
   111  	// if the fields don't match, don't do anything
   112  	if sV.Kind() != reflect.Struct || sV.NumField() != dV.NumField() {
   113  		return
   114  	}
   115  
   116  	for i := 0; i < sV.NumField(); i++ {
   117  		value := sV.Field(i)
   118  		fieldName := sT.Field(i).Name
   119  		f := dV.FieldByName(fieldName)
   120  
   121  		if !f.IsValid() ||
   122  			!f.CanSet() {
   123  			continue
   124  		}
   125  
   126  		// on local merge set all nil values
   127  		// on remote writes only set nil values if it is not a "writecheck" tagged field
   128  		if f.IsNil() ||
   129  			(remoteWrite && len(writeCheckFields) > 0 && slices.Contains(writeCheckFields, fieldName)) {
   130  			f.Set(value)
   131  		}
   132  	}
   133  }
   134  
   135  // Merges two slices into one. The item in the first slice will be replaced by the one in the second slice
   136  // if the hash key is the same. Items in the second slice which are not in the first will be added.
   137  //
   138  // Parameter remoteWrite defines if this data came on from a remote service, as that is then to
   139  // ignore the "writecheck" tagges fields and should only be allowed to write if the "writecheck" tagged field
   140  // boolean is set to true
   141  //
   142  // returns:
   143  //   - the new data set
   144  //   - true if everything was successful, false if not
   145  func Merge[T any](remoteWrite bool, s1 []T, s2 []T) ([]T, bool) {
   146  	var result []T
   147  	success := true
   148  
   149  	m2 := ToMap(s2)
   150  
   151  	// go through the first slice
   152  	m1 := make(map[string]T, len(s1))
   153  	for _, s1Item := range s1 {
   154  		s1ItemHash := hashKey(s1Item)
   155  		s2Item, exist := m2[s1ItemHash]
   156  		writeAllowed := writeAllowed(s1Item)
   157  		if !writeAllowed && remoteWrite {
   158  			success = false
   159  		}
   160  		// if exists and overwriting is allowed
   161  		if exist && (!remoteWrite || writeAllowed) {
   162  			// add values from s1Item that don't exist in s2Item or shouldn't be
   163  			// set in s2Item
   164  			updateFields(remoteWrite, s1Item, &s2Item)
   165  
   166  			// the item in the first slice will be replaced by the one of the second slice
   167  			result = append(result, s2Item)
   168  		} else {
   169  			result = append(result, s1Item)
   170  		}
   171  
   172  		m1[s1ItemHash] = s1Item
   173  	}
   174  
   175  	// append items which were not in the first slice
   176  	for _, s2Item := range s2 {
   177  		s2ItemHash := hashKey(s2Item)
   178  		_, exist := m1[s2ItemHash]
   179  		if !exist && !remoteWrite {
   180  			// only local updates can append data
   181  			result = append(result, s2Item)
   182  		}
   183  	}
   184  
   185  	return result, success
   186  }
   187  
   188  func ToMap[T any](s []T) map[string]T {
   189  	result := make(map[string]T, len(s))
   190  	for _, item := range s {
   191  		result[hashKey(item)] = item
   192  	}
   193  	return result
   194  }