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.") - } - }) - } -}