github.com/enbility/spine-go@v0.7.0/model/update.go (about) 1 package model 2 3 import ( 4 "reflect" 5 "sort" 6 7 "github.com/enbility/spine-go/util" 8 ) 9 10 type Updater interface { 11 // data model specific update function 12 // 13 // parameters: 14 // - remoteWrite defines if this data came on from a remote service, as that is then to 15 // ignore the "writecheck" tagges fields and should only be allowed to write if the "writecheck" tagged field 16 // boolean is set to true 17 // - persist defines if the data should be persisted, false used for creating full write datasets 18 // - newList is the new data 19 // - filterPartial is the partial filter 20 // - filterDelete is the delete filter 21 // 22 // returns: 23 // - the merged data 24 // - true if everything was successful, false if not 25 UpdateList(remoteWrite, persist bool, newList any, filterPartial, filterDelete *FilterType) (any, bool) 26 } 27 28 // Generates a new list of function items by applying the rules mentioned in the spec 29 // (EEBus_SPINE_TS_ProtocolSpecification.pdf; chapter "5.3.4 Restricted function exchange with cmdOptions"). 30 // The given data provider is used the get the current items and the items and the filters in the payload. 31 // 32 // returns: 33 // - the new data set 34 // - true if everything was successful, false if not 35 func UpdateList[T any](remoteWrite bool, existingData []T, newData []T, filterPartial, filterDelete *FilterType) ([]T, bool) { 36 success := true 37 38 // process delete filter (with selectors and elements) 39 if filterDelete != nil { 40 if filterData, err := filterDelete.Data(); err == nil { 41 updatedData, noErrors := deleteFilteredData(remoteWrite, existingData, filterData) 42 if noErrors { 43 existingData = updatedData 44 } else { 45 success = false 46 } 47 } 48 } 49 50 // process update filter (with selectors and elements) 51 if filterPartial != nil { 52 if filterData, err := filterPartial.Data(); err == nil { 53 newData, noErrors := copyToSelectedData(remoteWrite, existingData, filterData, &newData[0]) 54 if !noErrors { 55 success = false 56 } 57 return newData, success 58 } 59 } 60 61 // check if items have no identifiers 62 // Currently all fields marked as key are required 63 // TODO: check how to handle if only one identifier is provided 64 if len(newData) > 0 && !HasIdentifiers(newData[0]) { 65 // no identifiers specified --> copy data to all existing items 66 // (see EEBus_SPINE_TS_ProtocolSpecification.pdf, Table 7: Considered cmdOptions combinations for classifier "notify") 67 newData, noErrors := copyToAllData(remoteWrite, existingData, &newData[0]) 68 if !noErrors { 69 success = false 70 } 71 return newData, success 72 } 73 74 result, noErrors := Merge(remoteWrite, existingData, newData) 75 if !noErrors { 76 success = false 77 } 78 79 result = SortData(result) 80 81 return result, success 82 } 83 84 // return a list of field names that have the eebus tag 85 func fieldNamesWithEEBusTag(tag EEBusTag, item any) []string { 86 var result []string 87 88 v := reflect.ValueOf(item) 89 t := reflect.TypeOf(item) 90 91 if v.Kind() != reflect.Struct { 92 return result 93 } 94 95 for i := 0; i < v.NumField(); i++ { 96 f := v.Field(i) 97 if f.Kind() != reflect.Ptr { 98 continue 99 } 100 101 sf := v.Type().Field(i) 102 eebusTags := EEBusTags(sf) 103 _, exists := eebusTags[tag] 104 if !exists { 105 continue 106 } 107 108 fieldName := t.Field(i).Name 109 result = append(result, fieldName) 110 } 111 112 return result 113 } 114 115 func HasIdentifiers(data any) bool { 116 keys := fieldNamesWithEEBusTag(EEBusTagKey, data) 117 118 v := reflect.ValueOf(data) 119 120 for _, fieldName := range keys { 121 f := v.FieldByName(fieldName) 122 123 if f.IsNil() || !f.IsValid() { 124 return false 125 } 126 } 127 128 return true 129 } 130 131 // sort slices by fields that have eebus tag "key" 132 func SortData[T any](data []T) []T { 133 if len(data) == 0 { 134 return data 135 } 136 137 keys := fieldNamesWithEEBusTag(EEBusTagKey, data[0]) 138 139 if len(keys) == 0 { 140 return data 141 } 142 143 sort.Slice(data, func(i, j int) bool { 144 item1 := data[i] 145 item2 := data[j] 146 147 item1V := reflect.ValueOf(item1) 148 item2V := reflect.ValueOf(item2) 149 150 // if the fields don't match, don't do anything 151 if item1V.NumField() != item2V.NumField() { 152 return false 153 } 154 155 for _, fieldName := range keys { 156 f1 := item1V.FieldByName(fieldName) 157 f2 := item2V.FieldByName(fieldName) 158 if f1.Type().Kind() != reflect.Ptr || f2.Type().Kind() != reflect.Ptr { 159 return false 160 } 161 162 if f1.IsNil() || f2.IsNil() || !f1.IsValid() || !f2.IsValid() { 163 return false 164 } 165 166 if f1.Elem().Kind() != reflect.Uint || f2.Elem().Kind() != reflect.Uint { 167 return false 168 } 169 170 value1 := f1.Elem().Uint() 171 value2 := f2.Elem().Uint() 172 173 if value1 != value2 { 174 return value1 < value2 175 } 176 } 177 178 return false 179 }) 180 181 return data 182 } 183 184 // Copy data t elements matching the selected items 185 // 186 // Parameter remoteWrite defines if this data came on from a remote service, as that is then to 187 // ignore the "writecheck" tagges fields and should only be allowed to write if the "writecheck" tagged field 188 // boolean is set to true 189 // 190 // returns: 191 // - the new data set 192 // - true if everything was successful, false if not 193 func copyToSelectedData[T any](remoteWrite bool, existingData []T, filterData *FilterData, newData *T) ([]T, bool) { 194 if filterData.Selector == nil { 195 return existingData, true 196 } 197 198 success := true 199 200 for i := range existingData { 201 if filterData.SelectorMatch(util.Ptr(existingData[i])) { 202 writeAllowed := writeAllowed(existingData[i]) 203 if !writeAllowed && remoteWrite { 204 success = false 205 continue 206 } 207 208 CopyNonNilDataFromItemToItem(newData, &existingData[i]) 209 break 210 } 211 } 212 return existingData, success 213 } 214 215 // Copy data to all elements 216 // 217 // Parameter remoteWrite defines if this data came on from a remote service, as that is then to 218 // ignore the "writecheck" tagges fields and should only be allowed to write if the "writecheck" tagged field 219 // boolean is set to true 220 // 221 // returns: 222 // - the new data set 223 // - true if everything was successful, false if not 224 func copyToAllData[T any](remoteWrite bool, existingData []T, newData *T) ([]T, bool) { 225 success := true 226 227 for i := range existingData { 228 writeAllowed := writeAllowed(existingData[i]) 229 if !writeAllowed && remoteWrite { 230 success = false 231 continue 232 } 233 234 CopyNonNilDataFromItemToItem(newData, &existingData[i]) 235 } 236 237 return existingData, success 238 } 239 240 // Execute a partial delete filter 241 // 242 // Parameter remoteWrite defines if this data came on from a remote service, as that is then to 243 // ignore the "writecheck" tagges fields and should only be allowed to write if the "writecheck" tagged field 244 // boolean is set to true 245 // 246 // returns: 247 // - the new data set 248 // - true if everything was successful, false if not 249 func deleteFilteredData[T any](remoteWrite bool, existingData []T, filterData *FilterData) ([]T, bool) { 250 success := true 251 252 if filterData.Elements == nil && filterData.Selector == nil { 253 return existingData, true 254 } 255 256 var result []T 257 for i := range existingData { 258 writeAllowed := writeAllowed(existingData[i]) 259 if !writeAllowed && remoteWrite { 260 success = false 261 continue 262 } 263 264 if filterData.Selector != nil && filterData.Elements != nil { 265 // selector and elements filter 266 267 // remove the fields defined in element if the item matches 268 if filterData.SelectorMatch(util.Ptr(existingData[i])) { 269 RemoveElementFromItem(&existingData[i], filterData.Elements) 270 result = append(result, existingData[i]) 271 } else { 272 result = append(result, existingData[i]) 273 } 274 } else if filterData.Selector != nil { 275 // only selector filter 276 277 // remove the whole item if the item matches 278 if !filterData.SelectorMatch(util.Ptr(existingData[i])) { 279 result = append(result, existingData[i]) 280 } 281 } else { 282 // only elements filter 283 284 // remove the fields defined in element 285 RemoveElementFromItem(&existingData[i], filterData.Elements) 286 result = append(result, existingData[i]) 287 } 288 } 289 290 return result, success 291 } 292 293 func isFieldValueNil(field interface{}) bool { 294 if field == nil { 295 return true 296 } 297 298 switch reflect.TypeOf(field).Kind() { 299 case reflect.Ptr, reflect.Map, reflect.Array, reflect.Chan, reflect.Slice: 300 return reflect.ValueOf(field).IsNil() 301 default: 302 return false 303 } 304 } 305 306 func nonNilElementNames(element any) []string { 307 var result []string 308 309 v := reflect.ValueOf(element).Elem() 310 t := reflect.TypeOf(element).Elem() 311 for i := 0; i < v.NumField(); i++ { 312 isNil := isFieldValueNil(v.Field(i).Interface()) 313 if !isNil { 314 name := t.Field(i).Name 315 result = append(result, name) 316 } 317 } 318 319 return result 320 } 321 322 func isStringValueInSlice(value string, list []string) bool { 323 for _, item := range list { 324 if item == value { 325 return true 326 } 327 } 328 return false 329 } 330 331 func RemoveElementFromItem[T any, E any](item *T, element E) { 332 fieldNamesToBeRemoved := nonNilElementNames(element) 333 334 eV := reflect.ValueOf(element).Elem() 335 eT := reflect.TypeOf(element).Elem() 336 iV := reflect.ValueOf(item).Elem() 337 338 // if the fields don't match, don't do anything 339 if eV.NumField() != iV.NumField() { 340 return 341 } 342 343 for i := 0; i < eV.NumField(); i++ { 344 fieldName := eT.Field(i).Name 345 if isStringValueInSlice(fieldName, fieldNamesToBeRemoved) { 346 f := iV.FieldByName(fieldName) 347 if !f.IsValid() { 348 continue 349 } 350 if !f.CanSet() { 351 continue 352 } 353 354 f.Set(reflect.Zero(f.Type())) 355 } 356 } 357 } 358 359 func CopyNonNilDataFromItemToItem[T any](source *T, destination *T) { 360 if source == nil || destination == nil { 361 return 362 } 363 364 sV := reflect.ValueOf(source).Elem() 365 sT := reflect.TypeOf(source).Elem() 366 dV := reflect.ValueOf(destination).Elem() 367 368 // if the fields don't match, don't do anything 369 if sV.NumField() != dV.NumField() { 370 return 371 } 372 373 for i := 0; i < sV.NumField(); i++ { 374 value := sV.Field(i) 375 if value.IsNil() { 376 continue 377 } 378 379 fieldName := sT.Field(i).Name 380 f := dV.FieldByName(fieldName) 381 382 if !f.IsValid() { 383 continue 384 } 385 if !f.CanSet() { 386 continue 387 } 388 389 f.Set(value) 390 } 391 }