| /* |
| Copyright 2017 The Kubernetes Authors. |
| |
| Licensed under the Apache License, Version 2.0 (the "License"); |
| you may not use this file except in compliance with the License. |
| You may obtain a copy of the License at |
| |
| http://www.apache.org/licenses/LICENSE-2.0 |
| |
| Unless required by applicable law or agreed to in writing, software |
| distributed under the License is distributed on an "AS IS" BASIS, |
| WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| See the License for the specific language governing permissions and |
| limitations under the License. |
| */ |
| |
| package apply |
| |
| import ( |
| "fmt" |
| ) |
| |
| // Element contains the record, local, and remote value for a field in an object |
| // and metadata about the field read from openapi. |
| // Calling Merge on an element will apply the passed in strategy to Element - |
| // e.g. either replacing the whole element with the local copy or merging each |
| // of the recorded, local and remote fields of the element. |
| type Element interface { |
| // FieldMeta specifies which merge strategy to use for this element |
| FieldMeta |
| |
| // Merge merges the recorded, local and remote values in the element using the Strategy |
| // provided as an argument. Calls the type specific method on the Strategy - following the |
| // "Accept" method from the "Visitor" pattern. |
| // e.g. Merge on a ListElement will call Strategy.MergeList(self) |
| // Returns the Result of the merged elements |
| Merge(Strategy) (Result, error) |
| |
| // HasRecorded returns true if the field was explicitly |
| // present in the recorded source. This is to differentiate between |
| // undefined and set to null |
| HasRecorded() bool |
| |
| // GetRecorded returns the field value from the recorded source of the object |
| GetRecorded() interface{} |
| |
| // HasLocal returns true if the field was explicitly |
| // present in the local source. This is to differentiate between |
| // undefined and set to null |
| HasLocal() bool |
| |
| // GetLocal returns the field value from the local source of the object |
| GetLocal() interface{} |
| |
| // HasRemote returns true if the field was explicitly |
| // present in the remote source. This is to differentiate between |
| // undefined and set to null |
| HasRemote() bool |
| |
| // GetRemote returns the field value from the remote source of the object |
| GetRemote() interface{} |
| } |
| |
| // FieldMeta defines the strategy used to apply a Patch for an element |
| type FieldMeta interface { |
| // GetFieldMergeType returns the type of merge strategy to use for this field |
| // maybe "merge", "replace" or "retainkeys" |
| // TODO: There maybe multiple strategies, so this may need to be a slice, map, or struct |
| // Address this in a follow up in the PR to introduce retainkeys strategy |
| GetFieldMergeType() string |
| |
| // GetFieldMergeKeys returns the merge key to use when the MergeType is "merge" and underlying type is a list |
| GetFieldMergeKeys() MergeKeys |
| |
| // GetFieldType returns the openapi field type - e.g. primitive, array, map, type, reference |
| GetFieldType() string |
| } |
| |
| // FieldMetaImpl implements FieldMeta |
| type FieldMetaImpl struct { |
| // MergeType is the type of merge strategy to use for this field |
| // maybe "merge", "replace" or "retainkeys" |
| MergeType string |
| |
| // MergeKeys are the merge keys to use when the MergeType is "merge" and underlying type is a list |
| MergeKeys MergeKeys |
| |
| // Type is the openapi type of the field - "list", "primitive", "map" |
| Type string |
| |
| // Name contains name of the field |
| Name string |
| } |
| |
| // GetFieldMergeType implements FieldMeta.GetFieldMergeType |
| func (s FieldMetaImpl) GetFieldMergeType() string { |
| return s.MergeType |
| } |
| |
| // GetFieldMergeKeys implements FieldMeta.GetFieldMergeKeys |
| func (s FieldMetaImpl) GetFieldMergeKeys() MergeKeys { |
| return s.MergeKeys |
| } |
| |
| // GetFieldType implements FieldMeta.GetFieldType |
| func (s FieldMetaImpl) GetFieldType() string { |
| return s.Type |
| } |
| |
| // MergeKeyValue records the value of the mergekey for an item in a list |
| type MergeKeyValue map[string]string |
| |
| // Equal returns true if the MergeKeyValues share the same value, |
| // representing the same item in a list |
| func (v MergeKeyValue) Equal(o MergeKeyValue) bool { |
| if len(v) != len(o) { |
| return false |
| } |
| |
| for key, v1 := range v { |
| if v2, found := o[key]; !found || v1 != v2 { |
| return false |
| } |
| } |
| |
| return true |
| } |
| |
| // MergeKeys is the set of fields on an object that uniquely identify |
| // and is used when merging lists to identify the "same" object |
| // independent of the ordering of the objects |
| type MergeKeys []string |
| |
| // GetMergeKeyValue parses the MergeKeyValue from an item in a list |
| func (mk MergeKeys) GetMergeKeyValue(i interface{}) (MergeKeyValue, error) { |
| result := MergeKeyValue{} |
| if len(mk) <= 0 { |
| return result, fmt.Errorf("merge key must have at least 1 value to merge") |
| } |
| m, ok := i.(map[string]interface{}) |
| if !ok { |
| return result, fmt.Errorf("cannot use mergekey %v for primitive item in list %v", mk, i) |
| } |
| for _, field := range mk { |
| if value, found := m[field]; !found { |
| result[field] = "" |
| } else { |
| result[field] = fmt.Sprintf("%v", value) |
| } |
| } |
| return result, nil |
| } |
| |
| type source int |
| |
| const ( |
| recorded source = iota |
| local |
| remote |
| ) |
| |
| // CombinedPrimitiveSlice implements a slice of primitives |
| type CombinedPrimitiveSlice struct { |
| Items []*PrimitiveListItem |
| } |
| |
| // PrimitiveListItem represents a single value in a slice of primitives |
| type PrimitiveListItem struct { |
| // Value is the value of the primitive, should match recorded, local and remote |
| Value interface{} |
| |
| RawElementData |
| } |
| |
| // Contains returns true if the slice contains the l |
| func (s *CombinedPrimitiveSlice) lookup(l interface{}) *PrimitiveListItem { |
| val := fmt.Sprintf("%v", l) |
| for _, i := range s.Items { |
| if fmt.Sprintf("%v", i.Value) == val { |
| return i |
| } |
| } |
| return nil |
| } |
| |
| func (s *CombinedPrimitiveSlice) upsert(l interface{}) *PrimitiveListItem { |
| // Return the item if it exists |
| if item := s.lookup(l); item != nil { |
| return item |
| } |
| |
| // Otherwise create a new item and append to the list |
| item := &PrimitiveListItem{ |
| Value: l, |
| } |
| s.Items = append(s.Items, item) |
| return item |
| } |
| |
| // UpsertRecorded adds l to the slice. If there is already a value of l in the |
| // slice for either the local or remote, set on that value as the recorded value |
| // Otherwise append a new item to the list with the recorded value. |
| func (s *CombinedPrimitiveSlice) UpsertRecorded(l interface{}) { |
| v := s.upsert(l) |
| v.recorded = l |
| v.recordedSet = true |
| } |
| |
| // UpsertLocal adds l to the slice. If there is already a value of l in the |
| // slice for either the recorded or remote, set on that value as the local value |
| // Otherwise append a new item to the list with the local value. |
| func (s *CombinedPrimitiveSlice) UpsertLocal(l interface{}) { |
| v := s.upsert(l) |
| v.local = l |
| v.localSet = true |
| } |
| |
| // UpsertRemote adds l to the slice. If there is already a value of l in the |
| // slice for either the local or recorded, set on that value as the remote value |
| // Otherwise append a new item to the list with the remote value. |
| func (s *CombinedPrimitiveSlice) UpsertRemote(l interface{}) { |
| v := s.upsert(l) |
| v.remote = l |
| v.remoteSet = true |
| } |
| |
| // ListItem represents a single value in a slice of maps or types |
| type ListItem struct { |
| // KeyValue is the merge key value of the item |
| KeyValue MergeKeyValue |
| |
| // RawElementData contains the field values |
| RawElementData |
| } |
| |
| // CombinedMapSlice is a slice of maps or types with merge keys |
| type CombinedMapSlice struct { |
| Items []*ListItem |
| } |
| |
| // Lookup returns the ListItem matching the merge key, or nil if not found. |
| func (s *CombinedMapSlice) lookup(v MergeKeyValue) *ListItem { |
| for _, i := range s.Items { |
| if i.KeyValue.Equal(v) { |
| return i |
| } |
| } |
| return nil |
| } |
| |
| func (s *CombinedMapSlice) upsert(key MergeKeys, l interface{}) (*ListItem, error) { |
| // Get the identity of the item |
| val, err := key.GetMergeKeyValue(l) |
| if err != nil { |
| return nil, err |
| } |
| |
| // Return the item if it exists |
| if item := s.lookup(val); item != nil { |
| return item, nil |
| } |
| |
| // Otherwise create a new item and append to the list |
| item := &ListItem{ |
| KeyValue: val, |
| } |
| s.Items = append(s.Items, item) |
| return item, nil |
| } |
| |
| // UpsertRecorded adds l to the slice. If there is already a value of l sharing |
| // l's merge key in the slice for either the local or remote, set l the recorded value |
| // Otherwise append a new item to the list with the recorded value. |
| func (s *CombinedMapSlice) UpsertRecorded(key MergeKeys, l interface{}) error { |
| item, err := s.upsert(key, l) |
| if err != nil { |
| return err |
| } |
| item.SetRecorded(l) |
| return nil |
| } |
| |
| // UpsertLocal adds l to the slice. If there is already a value of l sharing |
| // l's merge key in the slice for either the recorded or remote, set l the local value |
| // Otherwise append a new item to the list with the local value. |
| func (s *CombinedMapSlice) UpsertLocal(key MergeKeys, l interface{}) error { |
| item, err := s.upsert(key, l) |
| if err != nil { |
| return err |
| } |
| item.SetLocal(l) |
| return nil |
| } |
| |
| // UpsertRemote adds l to the slice. If there is already a value of l sharing |
| // l's merge key in the slice for either the recorded or local, set l the remote value |
| // Otherwise append a new item to the list with the remote value. |
| func (s *CombinedMapSlice) UpsertRemote(key MergeKeys, l interface{}) error { |
| item, err := s.upsert(key, l) |
| if err != nil { |
| return err |
| } |
| item.SetRemote(l) |
| return nil |
| } |
| |
| // IsDrop returns true if the field represented by e should be dropped from the merged object |
| func IsDrop(e Element) bool { |
| // Specified in the last value recorded value and since deleted from the local |
| removed := e.HasRecorded() && !e.HasLocal() |
| |
| // Specified locally and explicitly set to null |
| setToNil := e.HasLocal() && e.GetLocal() == nil |
| |
| return removed || setToNil |
| } |
| |
| // IsAdd returns true if the field represented by e should have the local value directly |
| // added to the merged object instead of merging the recorded, local and remote values |
| func IsAdd(e Element) bool { |
| // If it isn't already present in the remote value and is present in the local value |
| return e.HasLocal() && !e.HasRemote() |
| } |
| |
| // NewRawElementData returns a new RawElementData, setting IsSet to true for |
| // non-nil values, and leaving IsSet false for nil values. |
| // Note: use this only when you want a nil-value to be considered "unspecified" |
| // (ignore) and not "unset" (deleted). |
| func NewRawElementData(recorded, local, remote interface{}) RawElementData { |
| data := RawElementData{} |
| if recorded != nil { |
| data.SetRecorded(recorded) |
| } |
| if local != nil { |
| data.SetLocal(local) |
| } |
| if remote != nil { |
| data.SetRemote(remote) |
| } |
| return data |
| } |
| |
| // RawElementData contains the raw recorded, local and remote data |
| // and metadata about whethere or not each was set |
| type RawElementData struct { |
| HasElementData |
| |
| recorded interface{} |
| local interface{} |
| remote interface{} |
| } |
| |
| // SetRecorded sets the recorded value |
| func (b *RawElementData) SetRecorded(value interface{}) { |
| b.recorded = value |
| b.recordedSet = true |
| } |
| |
| // SetLocal sets the local value |
| func (b *RawElementData) SetLocal(value interface{}) { |
| b.local = value |
| b.localSet = true |
| } |
| |
| // SetRemote sets the remote value |
| func (b *RawElementData) SetRemote(value interface{}) { |
| b.remote = value |
| b.remoteSet = true |
| } |
| |
| // GetRecorded implements Element.GetRecorded |
| func (b RawElementData) GetRecorded() interface{} { |
| // https://golang.org/doc/faq#nil_error |
| if b.recorded == nil { |
| return nil |
| } |
| return b.recorded |
| } |
| |
| // GetLocal implements Element.GetLocal |
| func (b RawElementData) GetLocal() interface{} { |
| // https://golang.org/doc/faq#nil_error |
| if b.local == nil { |
| return nil |
| } |
| return b.local |
| } |
| |
| // GetRemote implements Element.GetRemote |
| func (b RawElementData) GetRemote() interface{} { |
| // https://golang.org/doc/faq#nil_error |
| if b.remote == nil { |
| return nil |
| } |
| return b.remote |
| } |
| |
| // HasElementData contains whether a field was set in the recorded, local and remote sources |
| type HasElementData struct { |
| recordedSet bool |
| localSet bool |
| remoteSet bool |
| } |
| |
| // HasRecorded implements Element.HasRecorded |
| func (e HasElementData) HasRecorded() bool { |
| return e.recordedSet |
| } |
| |
| // HasLocal implements Element.HasLocal |
| func (e HasElementData) HasLocal() bool { |
| return e.localSet |
| } |
| |
| // HasRemote implements Element.HasRemote |
| func (e HasElementData) HasRemote() bool { |
| return e.remoteSet |
| } |
| |
| // ConflictDetector defines the capability to detect conflict. An element can examine remote/recorded value to detect conflict. |
| type ConflictDetector interface { |
| HasConflict() error |
| } |