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 }