From 35d4e8cac439e20d31e5858386967a5a042d5565 Mon Sep 17 00:00:00 2001 From: Antoine Pelisse Date: Tue, 24 Oct 2023 13:55:42 -0700 Subject: [PATCH] Remove unions logic, keep schema components This removes the generally unused union code from the library in a non-backward incompatible way since the API was not used anyways. The schema isn't changed since we don't want to break existing schema (the unions fields will continue to be just ignored). --- internal/fixture/state.go | 14 -- merge/union_test.go | 234 --------------------------- merge/update.go | 31 ---- typed/typed.go | 57 ------- typed/union.go | 276 -------------------------------- typed/union_test.go | 326 -------------------------------------- 6 files changed, 938 deletions(-) delete mode 100644 merge/union_test.go delete mode 100644 typed/union.go delete mode 100644 typed/union_test.go diff --git a/internal/fixture/state.go b/internal/fixture/state.go index 09463d1a..35fec261 100644 --- a/internal/fixture/state.go +++ b/internal/fixture/state.go @@ -535,8 +535,6 @@ type TestCase struct { // Managed, if not nil, is the ManagedFields as expected // after all operations are run. Managed fieldpath.ManagedFields - // Set to true if the test case needs the union behavior enabled. - RequiresUnions bool // ReportInputOnNoop if we don't want to compare the output and // always return it. ReturnInputOnNoop bool @@ -576,7 +574,6 @@ func (tc TestCase) BenchWithConverter(parser Parser, converter merge.Converter) Converter: converter, IgnoredFields: tc.IgnoredFields, ReturnInputOnNoop: tc.ReturnInputOnNoop, - EnableUnions: tc.RequiresUnions, } state := State{ Updater: updaterBuilder.BuildUpdater(), @@ -599,7 +596,6 @@ func (tc TestCase) TestWithConverter(parser Parser, converter merge.Converter) e Converter: converter, IgnoredFields: tc.IgnoredFields, ReturnInputOnNoop: tc.ReturnInputOnNoop, - EnableUnions: tc.RequiresUnions, } state := State{ Updater: updaterBuilder.BuildUpdater(), @@ -636,15 +632,5 @@ func (tc TestCase) TestWithConverter(parser Parser, converter merge.Converter) e } } - if !tc.RequiresUnions { - // Re-run the test with unions on. - tc2 := tc - tc2.RequiresUnions = true - err := tc2.TestWithConverter(parser, converter) - if err != nil { - return fmt.Errorf("fails if unions are on: %v", err) - } - } - return nil } diff --git a/merge/union_test.go b/merge/union_test.go deleted file mode 100644 index 9a8ba56b..00000000 --- a/merge/union_test.go +++ /dev/null @@ -1,234 +0,0 @@ -/* -Copyright 2019 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 merge_test - -import ( - "testing" - - "sigs.k8s.io/structured-merge-diff/v4/fieldpath" - . "sigs.k8s.io/structured-merge-diff/v4/internal/fixture" - "sigs.k8s.io/structured-merge-diff/v4/merge" - "sigs.k8s.io/structured-merge-diff/v4/typed" -) - -var unionFieldsParser = func() Parser { - parser, err := typed.NewParser(`types: -- name: unionFields - map: - fields: - - name: numeric - type: - scalar: numeric - - name: string - type: - scalar: string - - name: type - type: - scalar: string - - name: fieldA - type: - scalar: string - - name: fieldB - type: - scalar: string - unions: - - discriminator: type - deduceInvalidDiscriminator: true - fields: - - fieldName: numeric - discriminatorValue: Numeric - - fieldName: string - discriminatorValue: String - - fields: - - fieldName: fieldA - discriminatorValue: FieldA - - fieldName: fieldB - discriminatorValue: FieldB`) - if err != nil { - panic(err) - } - return SameVersionParser{T: parser.Type("unionFields")} -}() - -func TestUnion(t *testing.T) { - tests := map[string]TestCase{ - "union_apply_owns_discriminator": { - RequiresUnions: true, - Ops: []Operation{ - Apply{ - Manager: "default", - APIVersion: "v1", - Object: ` - numeric: 1 - `, - }, - }, - Object: ` - numeric: 1 - type: Numeric - `, - APIVersion: "v1", - Managed: fieldpath.ManagedFields{ - "default": fieldpath.NewVersionedSet( - _NS( - _P("numeric"), _P("type"), - ), - "v1", - false, - ), - }, - }, - "union_apply_without_discriminator_conflict": { - RequiresUnions: true, - Ops: []Operation{ - Update{ - Manager: "controller", - APIVersion: "v1", - Object: ` - string: "some string" - `, - }, - Apply{ - Manager: "default", - APIVersion: "v1", - Object: ` - numeric: 1 - `, - Conflicts: merge.Conflicts{ - merge.Conflict{Manager: "controller", Path: _P("type")}, - }, - }, - }, - Object: ` - string: "some string" - type: String - `, - APIVersion: "v1", - Managed: fieldpath.ManagedFields{ - "controller": fieldpath.NewVersionedSet( - _NS( - _P("string"), _P("type"), - ), - "v1", - false, - ), - }, - }, - "union_apply_with_null_value": { - RequiresUnions: true, - Ops: []Operation{ - Apply{ - Manager: "default", - APIVersion: "v1", - Object: ` - type: Numeric - string: null - numeric: 1 - `, - }, - }, - }, - "union_apply_multiple_unions": { - RequiresUnions: true, - Ops: []Operation{ - Apply{ - Manager: "default", - APIVersion: "v1", - Object: ` - string: "some string" - fieldA: "fieldA string" - `, - }, - Apply{ - Manager: "default", - APIVersion: "v1", - Object: ` - numeric: 0 - fieldB: "fieldB string" - `, - }, - }, - Object: ` - type: Numeric - numeric: 0 - fieldB: "fieldB string" - `, - APIVersion: "v1", - }, - } - - for name, test := range tests { - t.Run(name, func(t *testing.T) { - if err := test.Test(unionFieldsParser); err != nil { - t.Fatal(err) - } - }) - } -} - -func TestUnionErrors(t *testing.T) { - tests := map[string]TestCase{ - "union_apply_two": { - RequiresUnions: true, - Ops: []Operation{ - Apply{ - Manager: "default", - APIVersion: "v1", - Object: ` - numeric: 1 - string: "some string" - `, - }, - }, - }, - "union_apply_two_and_discriminator": { - RequiresUnions: true, - Ops: []Operation{ - Apply{ - Manager: "default", - APIVersion: "v1", - Object: ` - type: Numeric - string: "some string" - numeric: 1 - `, - }, - }, - }, - "union_apply_wrong_discriminator": { - RequiresUnions: true, - Ops: []Operation{ - Apply{ - Manager: "default", - APIVersion: "v1", - Object: ` - type: Numeric - string: "some string" - `, - }, - }, - }, - } - - for name, test := range tests { - t.Run(name, func(t *testing.T) { - if test.Test(unionFieldsParser) == nil { - t.Fatal("Should fail") - } - }) - } -} diff --git a/merge/update.go b/merge/update.go index e1540841..7d06eb8a 100644 --- a/merge/update.go +++ b/merge/update.go @@ -34,8 +34,6 @@ type UpdaterBuilder struct { Converter Converter IgnoredFields map[fieldpath.APIVersion]*fieldpath.Set - EnableUnions bool - // Stop comparing the new object with old object after applying. // This was initially used to avoid spurious etcd update, but // since that's vastly inefficient, we've come-up with a better @@ -49,7 +47,6 @@ func (u *UpdaterBuilder) BuildUpdater() *Updater { return &Updater{ Converter: u.Converter, IgnoredFields: u.IgnoredFields, - enableUnions: u.EnableUnions, returnInputOnNoop: u.ReturnInputOnNoop, } } @@ -63,19 +60,9 @@ type Updater struct { // Deprecated: This will eventually become private. IgnoredFields map[fieldpath.APIVersion]*fieldpath.Set - enableUnions bool - returnInputOnNoop bool } -// EnableUnionFeature turns on union handling. It is disabled by default until the -// feature is complete. -// -// Deprecated: Use the builder instead. -func (s *Updater) EnableUnionFeature() { - s.enableUnions = true -} - func (s *Updater) update(oldObject, newObject *typed.TypedValue, version fieldpath.APIVersion, managers fieldpath.ManagedFields, workflow string, force bool) (fieldpath.ManagedFields, *typed.Comparison, error) { conflicts := fieldpath.ManagedFields{} removed := fieldpath.ManagedFields{} @@ -160,12 +147,6 @@ func (s *Updater) Update(liveObject, newObject *typed.TypedValue, version fieldp if err != nil { return nil, fieldpath.ManagedFields{}, err } - if s.enableUnions { - newObject, err = liveObject.NormalizeUnions(newObject) - if err != nil { - return nil, fieldpath.ManagedFields{}, err - } - } managers, compare, err := s.update(liveObject, newObject, version, managers, manager, true) if err != nil { return nil, fieldpath.ManagedFields{}, err @@ -198,22 +179,10 @@ func (s *Updater) Apply(liveObject, configObject *typed.TypedValue, version fiel if err != nil { return nil, fieldpath.ManagedFields{}, err } - if s.enableUnions { - configObject, err = configObject.NormalizeUnionsApply(configObject) - if err != nil { - return nil, fieldpath.ManagedFields{}, err - } - } newObject, err := liveObject.Merge(configObject) if err != nil { return nil, fieldpath.ManagedFields{}, fmt.Errorf("failed to merge config: %v", err) } - if s.enableUnions { - newObject, err = configObject.NormalizeUnionsApply(newObject) - if err != nil { - return nil, fieldpath.ManagedFields{}, err - } - } lastSet := managers[manager] set, err := configObject.ToFieldSet() if err != nil { diff --git a/typed/typed.go b/typed/typed.go index 035c14bd..9be90282 100644 --- a/typed/typed.go +++ b/typed/typed.go @@ -192,63 +192,6 @@ func (tv TypedValue) ExtractItems(items *fieldpath.Set) *TypedValue { return &tv } -// NormalizeUnions takes the new object and normalizes the union: -// - If discriminator changed to non-nil, and a new field has been added -// that doesn't match, an error is returned, -// - If discriminator hasn't changed and two fields or more are set, an -// error is returned, -// - If discriminator changed to non-nil, all other fields but the -// discriminated one will be cleared, -// - Otherwise, If only one field is left, update discriminator to that value. -// -// Please note: union behavior isn't finalized yet and this is still experimental. -func (tv TypedValue) NormalizeUnions(new *TypedValue) (*TypedValue, error) { - var errs ValidationErrors - var normalizeFn = func(w *mergingWalker) { - if w.rhs != nil { - v := w.rhs.Unstructured() - w.out = &v - } - if err := normalizeUnions(w); err != nil { - errs = append(errs, errorf(err.Error())...) - } - } - out, mergeErrs := merge(&tv, new, func(w *mergingWalker) {}, normalizeFn) - if mergeErrs != nil { - errs = append(errs, mergeErrs.(ValidationErrors)...) - } - if len(errs) > 0 { - return nil, errs - } - return out, nil -} - -// NormalizeUnionsApply specifically normalize unions on apply. It -// validates that the applied union is correct (there should be no -// ambiguity there), and clear the fields according to the sent intent. -// -// Please note: union behavior isn't finalized yet and this is still experimental. -func (tv TypedValue) NormalizeUnionsApply(new *TypedValue) (*TypedValue, error) { - var errs ValidationErrors - var normalizeFn = func(w *mergingWalker) { - if w.rhs != nil { - v := w.rhs.Unstructured() - w.out = &v - } - if err := normalizeUnionsApply(w); err != nil { - errs = append(errs, errorf(err.Error())...) - } - } - out, mergeErrs := merge(&tv, new, func(w *mergingWalker) {}, normalizeFn) - if mergeErrs != nil { - errs = append(errs, mergeErrs.(ValidationErrors)...) - } - if len(errs) > 0 { - return nil, errs - } - return out, nil -} - func (tv TypedValue) Empty() *TypedValue { tv.value = value.NewValueInterface(nil) return &tv diff --git a/typed/union.go b/typed/union.go deleted file mode 100644 index 1fa5d88a..00000000 --- a/typed/union.go +++ /dev/null @@ -1,276 +0,0 @@ -/* -Copyright 2019 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 typed - -import ( - "fmt" - "strings" - - "sigs.k8s.io/structured-merge-diff/v4/schema" - "sigs.k8s.io/structured-merge-diff/v4/value" -) - -func normalizeUnions(w *mergingWalker) error { - atom, found := w.schema.Resolve(w.typeRef) - if !found { - panic(fmt.Sprintf("Unable to resolve schema in normalize union: %v/%v", w.schema, w.typeRef)) - } - // Unions can only be in structures, and the struct must not have been removed - if atom.Map == nil || w.out == nil { - return nil - } - - var old value.Map - if w.lhs != nil && !w.lhs.IsNull() { - old = w.lhs.AsMap() - } - for _, union := range atom.Map.Unions { - if err := newUnion(&union).Normalize(old, w.rhs.AsMap(), value.NewValueInterface(*w.out).AsMap()); err != nil { - return err - } - } - return nil -} - -func normalizeUnionsApply(w *mergingWalker) error { - atom, found := w.schema.Resolve(w.typeRef) - if !found { - panic(fmt.Sprintf("Unable to resolve schema in normalize union: %v/%v", w.schema, w.typeRef)) - } - // Unions can only be in structures, and the struct must not have been removed - if atom.Map == nil || w.out == nil { - return nil - } - - var old value.Map - if w.lhs != nil && !w.lhs.IsNull() { - old = w.lhs.AsMap() - } - - for _, union := range atom.Map.Unions { - out := value.NewValueInterface(*w.out) - if err := newUnion(&union).NormalizeApply(old, w.rhs.AsMap(), out.AsMap()); err != nil { - return err - } - *w.out = out.Unstructured() - } - return nil -} - -type discriminated string -type field string - -type discriminatedNames struct { - f2d map[field]discriminated - d2f map[discriminated]field -} - -func newDiscriminatedName(f2d map[field]discriminated) discriminatedNames { - d2f := map[discriminated]field{} - for key, value := range f2d { - d2f[value] = key - } - return discriminatedNames{ - f2d: f2d, - d2f: d2f, - } -} - -func (dn discriminatedNames) toField(d discriminated) field { - if f, ok := dn.d2f[d]; ok { - return f - } - return field(d) -} - -func (dn discriminatedNames) toDiscriminated(f field) discriminated { - if d, ok := dn.f2d[f]; ok { - return d - } - return discriminated(f) -} - -type discriminator struct { - name string -} - -func (d *discriminator) Set(m value.Map, v discriminated) { - if d == nil { - return - } - m.Set(d.name, value.NewValueInterface(string(v))) -} - -func (d *discriminator) Get(m value.Map) discriminated { - if d == nil || m == nil { - return "" - } - val, ok := m.Get(d.name) - if !ok { - return "" - } - if !val.IsString() { - return "" - } - return discriminated(val.AsString()) -} - -type fieldsSet map[field]struct{} - -// newFieldsSet returns a map of the fields that are part of the union and are set -// in the given map. -func newFieldsSet(m value.Map, fields []field) fieldsSet { - if m == nil { - return nil - } - set := fieldsSet{} - for _, f := range fields { - if subField, ok := m.Get(string(f)); ok && !subField.IsNull() { - set.Add(f) - } - } - return set -} - -func (fs fieldsSet) Add(f field) { - if fs == nil { - fs = map[field]struct{}{} - } - fs[f] = struct{}{} -} - -func (fs fieldsSet) One() *field { - for f := range fs { - return &f - } - return nil -} - -func (fs fieldsSet) Has(f field) bool { - _, ok := fs[f] - return ok -} - -func (fs fieldsSet) List() []field { - fields := []field{} - for f := range fs { - fields = append(fields, f) - } - return fields -} - -func (fs fieldsSet) Difference(o fieldsSet) fieldsSet { - n := fieldsSet{} - for f := range fs { - if !o.Has(f) { - n.Add(f) - } - } - return n -} - -func (fs fieldsSet) String() string { - s := []string{} - for k := range fs { - s = append(s, string(k)) - } - return strings.Join(s, ", ") -} - -type union struct { - deduceInvalidDiscriminator bool - d *discriminator - dn discriminatedNames - f []field -} - -func newUnion(su *schema.Union) *union { - u := &union{} - if su.Discriminator != nil { - u.d = &discriminator{name: *su.Discriminator} - } - f2d := map[field]discriminated{} - for _, f := range su.Fields { - u.f = append(u.f, field(f.FieldName)) - f2d[field(f.FieldName)] = discriminated(f.DiscriminatorValue) - } - u.dn = newDiscriminatedName(f2d) - u.deduceInvalidDiscriminator = su.DeduceInvalidDiscriminator - return u -} - -// clear removes all the fields in map that are part of the union, but -// the one we decided to keep. -func (u *union) clear(m value.Map, f field) { - for _, fieldName := range u.f { - if field(fieldName) != f { - m.Delete(string(fieldName)) - } - } -} - -func (u *union) Normalize(old, new, out value.Map) error { - os := newFieldsSet(old, u.f) - ns := newFieldsSet(new, u.f) - diff := ns.Difference(os) - - if u.d.Get(old) != u.d.Get(new) && u.d.Get(new) != "" { - if len(diff) == 1 && u.d.Get(new) != u.dn.toDiscriminated(*diff.One()) { - return fmt.Errorf("discriminator (%v) and field changed (%v) don't match", u.d.Get(new), diff.One()) - } - if len(diff) > 1 { - return fmt.Errorf("multiple new fields added: %v", diff) - } - u.clear(out, u.dn.toField(u.d.Get(new))) - return nil - } - - if len(ns) > 1 { - return fmt.Errorf("multiple fields set without discriminator change: %v", ns) - } - - // Set discriminiator if it needs to be deduced. - if u.deduceInvalidDiscriminator && len(ns) == 1 { - u.d.Set(out, u.dn.toDiscriminated(*ns.One())) - } - - return nil -} - -func (u *union) NormalizeApply(applied, merged, out value.Map) error { - as := newFieldsSet(applied, u.f) - if len(as) > 1 { - return fmt.Errorf("more than one field of union applied: %v", as) - } - if len(as) == 0 { - // None is set, just leave. - return nil - } - // We have exactly one, discriminiator must match if set - if u.d.Get(applied) != "" && u.d.Get(applied) != u.dn.toDiscriminated(*as.One()) { - return fmt.Errorf("applied discriminator (%v) doesn't match applied field (%v)", u.d.Get(applied), *as.One()) - } - - // Update discriminiator if needed - if u.deduceInvalidDiscriminator { - u.d.Set(out, u.dn.toDiscriminated(*as.One())) - } - // Clear others fields. - u.clear(out, *as.One()) - - return nil -} diff --git a/typed/union_test.go b/typed/union_test.go deleted file mode 100644 index a3ed129a..00000000 --- a/typed/union_test.go +++ /dev/null @@ -1,326 +0,0 @@ -/* -Copyright 2019 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 typed_test - -import ( - "testing" - - "sigs.k8s.io/structured-merge-diff/v4/typed" -) - -var unionParser = func() typed.ParseableType { - parser, err := typed.NewParser(`types: -- name: union - map: - fields: - - name: discriminator - type: - scalar: string - - name: one - type: - scalar: numeric - - name: two - type: - scalar: numeric - - name: three - type: - scalar: numeric - - name: letter - type: - scalar: string - - name: a - type: - scalar: numeric - - name: b - type: - scalar: numeric - unions: - - discriminator: discriminator - deduceInvalidDiscriminator: true - fields: - - fieldName: one - discriminatorValue: One - - fieldName: two - discriminatorValue: TWO - - fieldName: three - discriminatorValue: three - - discriminator: letter - fields: - - fieldName: a - discriminatorValue: A - - fieldName: b - discriminatorValue: b`) - if err != nil { - panic(err) - } - return parser.Type("union") -}() - -func TestNormalizeUnions(t *testing.T) { - tests := []struct { - name string - old typed.YAMLObject - new typed.YAMLObject - out typed.YAMLObject - }{ - { - name: "nothing changed, add discriminator", - old: `{"one": 1}`, - new: `{"one": 1}`, - out: `{"one": 1, "discriminator": "One"}`, - }, - { - name: "nothing changed, non-deduced", - old: `{"a": 1}`, - new: `{"a": 1}`, - out: `{"a": 1}`, - }, - { - name: "proper union update, setting discriminator", - old: `{"one": 1}`, - new: `{"two": 1}`, - out: `{"two": 1, "discriminator": "TWO"}`, - }, - { - name: "proper union update, non-deduced", - old: `{"a": 1}`, - new: `{"b": 1}`, - out: `{"b": 1}`, - }, - { - name: "proper union update from not-set, setting discriminator", - old: `{}`, - new: `{"two": 1}`, - out: `{"two": 1, "discriminator": "TWO"}`, - }, - { - name: "proper union update from not-set, non-deduced", - old: `{}`, - new: `{"b": 1}`, - out: `{"b": 1}`, - }, - { - name: "remove union, with discriminator", - old: `{"one": 1}`, - new: `{}`, - out: `{}`, - }, - { - name: "remove union and discriminator", - old: `{"one": 1, "discriminator": "One"}`, - new: `{}`, - out: `{}`, - }, - { - name: "remove union, not discriminator", - old: `{"one": 1, "discriminator": "One"}`, - new: `{"discriminator": "One"}`, - out: `{"discriminator": "One"}`, - }, - { - name: "remove union, not discriminator, non-deduced", - old: `{"a": 1, "letter": "A"}`, - new: `{"letter": "A"}`, - out: `{"letter": "A"}`, - }, - { - name: "change discriminator, nothing else", - old: `{"discriminator": "One"}`, - new: `{"discriminator": "random"}`, - out: `{"discriminator": "random"}`, - }, - { - name: "change discriminator, nothing else, non-deduced", - old: `{"letter": "A"}`, - new: `{"letter": "b"}`, - out: `{"letter": "b"}`, - }, - { - name: "change discriminator, nothing else, it drops other field", - old: `{"discriminator": "One", "one": 1}`, - new: `{"discriminator": "random", "one": 1}`, - out: `{"discriminator": "random"}`, - }, - { - name: "change discriminator, nothing else, it drops other field, non-deduced", - old: `{"letter": "A", "a": 1}`, - new: `{"letter": "b", "a": 1}`, - out: `{"letter": "b"}`, - }, - { - name: "remove discriminator, nothing else", - old: `{"discriminator": "One", "one": 1}`, - new: `{"one": 1}`, - out: `{"one": 1, "discriminator": "One"}`, - }, - { - name: "remove discriminator, nothing else, non-deduced", - old: `{"letter": "A", "a": 1}`, - new: `{"a": 1}`, - out: `{"a": 1}`, - }, - { - name: "remove discriminator, add new field", - old: `{"discriminator": "One", "one": 1}`, - new: `{"two": 1}`, - out: `{"two": 1, "discriminator": "TWO"}`, - }, - { - name: "remove discriminator, add new field, non-deduced", - old: `{"letter": "A", "a": 1}`, - new: `{"b": 1}`, - out: `{"b": 1}`, - }, - { - name: "both fields removed", - old: `{"one": 1, "two": 1}`, - new: `{}`, - out: `{}`, - }, - { - name: "one field removed", - old: `{"one": 1, "two": 1}`, - new: `{"one": 1}`, - out: `{"one": 1, "discriminator": "One"}`, - }, - { - name: "one field removed, non-deduced", - old: `{"a": 1, "b": 1}`, - new: `{"a": 1}`, - out: `{"a": 1}`, - }, - // These use-cases shouldn't happen: - { - name: "one field removed, discriminator unchanged", - old: `{"one": 1, "two": 1, "discriminator": "TWO"}`, - new: `{"one": 1, "discriminator": "TWO"}`, - out: `{"one": 1, "discriminator": "One"}`, - }, - { - name: "one field removed, discriminator unchanged, non-deduced", - old: `{"a": 1, "b": 1, "letter": "b"}`, - new: `{"a": 1, "letter": "b"}`, - out: `{"a": 1, "letter": "b"}`, - }, - { - name: "one field removed, discriminator added", - old: `{"two": 2, "one": 1}`, - new: `{"one": 1, "discriminator": "TWO"}`, - out: `{"discriminator": "TWO"}`, - }, - { - name: "one field removed, discriminator added, non-deduced", - old: `{"b": 2, "a": 1}`, - new: `{"a": 1, "letter": "b"}`, - out: `{"letter": "b"}`, - }, - } - - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - old, err := unionParser.FromYAML(test.old) - if err != nil { - t.Fatalf("Failed to parse old object: %v", err) - } - new, err := unionParser.FromYAML(test.new) - if err != nil { - t.Fatalf("failed to parse new object: %v", err) - } - out, err := unionParser.FromYAML(test.out) - if err != nil { - t.Fatalf("failed to parse out object: %v", err) - } - got, err := old.NormalizeUnions(new) - if err != nil { - t.Fatalf("failed to normalize unions: %v", err) - } - comparison, err := out.Compare(got) - if err != nil { - t.Fatalf("failed to compare result and expected: %v", err) - } - if !comparison.IsSame() { - t.Errorf("Result is different from expected:\n%v", comparison) - } - }) - } -} - -func TestNormalizeUnionError(t *testing.T) { - tests := []struct { - name string - old typed.YAMLObject - new typed.YAMLObject - }{ - { - name: "dumb client update, no discriminator", - old: `{"one": 1}`, - new: `{"one": 2, "two": 1}`, - }, - { - name: "new object has three of same union set", - old: `{"one": 1}`, - new: `{"one": 2, "two": 1, "three": 3}`, - }, - { - name: "dumb client doesn't update discriminator", - old: `{"one": 1, "discriminator": "One"}`, - new: `{"one": 2, "two": 1, "discriminator": "One"}`, - }, - { - name: "client sends new field that and discriminator change", - old: `{}`, - new: `{"one": 1, "discriminator": "Two"}`, - }, - { - name: "client sends new fields that don't match discriminator change", - old: `{}`, - new: `{"one": 1, "two": 1, "discriminator": "One"}`, - }, - { - name: "old object has two of same union set", - old: `{"one": 1, "two": 2}`, - new: `{"one": 2, "two": 1}`, - }, - { - name: "old object has two of same union, but we add third", - old: `{"discriminator": "One", "one": 1, "two": 1}`, - new: `{"discriminator": "One", "one": 1, "two": 1, "three": 1}`, - }, - { - name: "one field removed, 2 left, discriminator unchanged", - old: `{"one": 1, "two": 1, "three": 1, "discriminator": "TWO"}`, - new: `{"one": 1, "two": 1, "discriminator": "TWO"}`, - }, - } - - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - old, err := unionParser.FromYAML(test.old) - if err != nil { - t.Fatalf("Failed to parse old object: %v", err) - } - new, err := unionParser.FromYAML(test.new) - if err != nil { - t.Fatalf("failed to parse new object: %v", err) - } - _, err = old.NormalizeUnions(new) - if err == nil { - t.Fatal("Normalization should have failed, but hasn't.") - } - }) - } -}