From d7c26a3477a08fd21a45c36e6b7b0bc274f96b62 Mon Sep 17 00:00:00 2001 From: Alexander Zielenski <351783+alexzielenski@users.noreply.github.com> Date: Thu, 23 May 2024 07:02:57 -0700 Subject: [PATCH] POC --- typed/remove.go | 20 +++++- typed/update_test.go | 141 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 159 insertions(+), 2 deletions(-) create mode 100644 typed/update_test.go diff --git a/typed/remove.go b/typed/remove.go index ad071ee8..fb9d2e8b 100644 --- a/typed/remove.go +++ b/typed/remove.go @@ -19,6 +19,8 @@ import ( "sigs.k8s.io/structured-merge-diff/v4/value" ) +var REMOVEKEEPEMPTYCOLLECTIONS = false + type removingWalker struct { value value.Value out interface{} @@ -58,6 +60,9 @@ func (w *removingWalker) doList(t *schema.List) (errs ValidationErrors) { defer w.allocator.Free(l) // If list is null or empty just return if l == nil || l.Length() == 0 { + if REMOVEKEEPEMPTYCOLLECTIONS { + w.out = w.value.Unstructured() + } return nil } @@ -66,6 +71,10 @@ func (w *removingWalker) doList(t *schema.List) (errs ValidationErrors) { if t.ElementRelationship == schema.Atomic { if w.shouldExtract { w.out = w.value.Unstructured() + } else if !w.toRemove.Has(fieldpath.Path{}) && REMOVEKEEPEMPTYCOLLECTIONS { + // If the atomic list itself wasn't being removed, then it + // is being set to empty list (we only get here if a prefix of this fieldPath is in toRemove) + w.out = []interface{}{} } return nil } @@ -97,7 +106,7 @@ func (w *removingWalker) doList(t *schema.List) (errs ValidationErrors) { } newItems = append(newItems, item.Unstructured()) } - if len(newItems) > 0 { + if len(newItems) > 0 || REMOVEKEEPEMPTYCOLLECTIONS { w.out = newItems } return nil @@ -113,6 +122,9 @@ func (w *removingWalker) doMap(t *schema.Map) ValidationErrors { } // If map is null or empty just return if m == nil || m.Empty() { + if REMOVEKEEPEMPTYCOLLECTIONS { + w.out = w.value + } return nil } @@ -121,6 +133,10 @@ func (w *removingWalker) doMap(t *schema.Map) ValidationErrors { if t.ElementRelationship == schema.Atomic { if w.shouldExtract { w.out = w.value.Unstructured() + } else if !w.toRemove.Has(fieldpath.Path{}) && REMOVEKEEPEMPTYCOLLECTIONS { + // If the atomic map itself wasn't being removed, then it + // is being set to empty map (we only get here if a prefix of this fieldPath is in toRemove) + w.out = map[string]interface{}{} } return nil } @@ -158,7 +174,7 @@ func (w *removingWalker) doMap(t *schema.Map) ValidationErrors { newMap[k] = val.Unstructured() return true }) - if len(newMap) > 0 { + if len(newMap) > 0 || REMOVEKEEPEMPTYCOLLECTIONS { w.out = newMap } return nil diff --git a/typed/update_test.go b/typed/update_test.go new file mode 100644 index 00000000..0ec808bc --- /dev/null +++ b/typed/update_test.go @@ -0,0 +1,141 @@ +package typed_test + +import ( + "testing" + + "sigs.k8s.io/structured-merge-diff/v4/internal/fixture" + "sigs.k8s.io/structured-merge-diff/v4/typed" +) + +var updateParser = func() fixture.Parser { + parser, err := typed.NewParser(` +types: +- name: nestedOptionalFields + map: + fields: + - name: nestedList + type: + list: + elementRelationship: associative + keys: + - name + elementType: + map: + fields: + - name: name + type: + scalar: string + - name: value + type: + scalar: numeric + - name: nestedMap + type: + map: + elementType: + scalar: numeric + - name: nested + type: + map: + fields: + - name: numeric + type: + scalar: numeric + - name: string + type: + scalar: string +`) + if err != nil { + panic(err) + } + return fixture.SameVersionParser{T: parser.Type("nestedOptionalFields")} +}() + +func TestUpdate(t *testing.T) { + tests := map[string]fixture.TestCase{ + "delete_nested_fields_struct": { + Ops: []fixture.Operation{ + fixture.Apply{ + Manager: "default", + APIVersion: "v1", + Object: ` + nested: + numeric: 1 + string: my string + `, + }, + fixture.Apply{ + Manager: "default", + APIVersion: "v1", + Object: ` + nested: {} + `, + }, + }, + APIVersion: `v1`, + Object: `{nested: {}}`, + }, + "delete_nested_fields_list": { + Ops: []fixture.Operation{ + fixture.Apply{ + Manager: "default", + APIVersion: "v1", + Object: ` + nestedList: + - name: first + - name: second + `, + }, + fixture.Apply{ + Manager: "default", + APIVersion: "v1", + Object: ` + nestedList: [] + `, + }, + }, + APIVersion: `v1`, + Object: `{nestedList: []}`, + }, + "delete_nested_fields_map": { + Ops: []fixture.Operation{ + fixture.Apply{ + Manager: "default", + APIVersion: "v1", + Object: ` + nestedMap: + first: 1 + second: 2 + + `, + }, + fixture.Apply{ + Manager: "default", + APIVersion: "v1", + Object: ` + nestedMap: {} + `, + }, + }, + APIVersion: `v1`, + Object: `{nestedMap: {}}`, + }, + } + + for name, tc := range tests { + tc2 := tc + t.Run(name, func(t *testing.T) { + typed.REMOVEKEEPEMPTYCOLLECTIONS = true + if err := tc.Test(updateParser); err != nil { + t.Fatal(err) + } + }) + + t.Run(name+"Nil", func(t *testing.T) { + typed.REMOVEKEEPEMPTYCOLLECTIONS = false + if err := tc2.Test(updateParser); err != nil { + t.Fatal(err) + } + }) + } + +}