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  }