Skip to content

Commit 09abe0b

Browse files
author
Antoine Pelisse
committed
Change union to not "guess" based on difference
We're still using the difference, but only to see if something wrong was sent. The new logic is to only trust the disicriminator, which means that the behavior for non-discriminated union is now back to before. It also means that we can't use that process anymore to normalize applied unions. Applied unions are normalized by trusting the applied fields exclusively: applying two fields doesn't work, applying a discriminator and a different value doesn't work.
1 parent 1c46bef commit 09abe0b

File tree

7 files changed

+186
-84
lines changed

7 files changed

+186
-84
lines changed

merge/union_test.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ var unionFieldsParser = func() typed.ParseableType {
4747
scalar: string
4848
unions:
4949
- discriminator: type
50+
deduceInvalidDiscriminator: true
5051
fields:
5152
- fieldName: numeric
5253
discriminatedBy: Numeric

merge/update.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -151,15 +151,15 @@ func (s *Updater) Update(liveObject, newObject *typed.TypedValue, version fieldp
151151
// and return it.
152152
func (s *Updater) Apply(liveObject, configObject *typed.TypedValue, version fieldpath.APIVersion, managers fieldpath.ManagedFields, manager string, force bool) (*typed.TypedValue, fieldpath.ManagedFields, error) {
153153
managers = shallowCopyManagers(managers)
154-
configObject, err := configObject.Empty().NormalizeUnions(configObject)
154+
configObject, err := configObject.NormalizeUnionsApply(configObject)
155155
if err != nil {
156156
return nil, fieldpath.ManagedFields{}, err
157157
}
158158
newObject, err := liveObject.Merge(configObject)
159159
if err != nil {
160160
return nil, fieldpath.ManagedFields{}, fmt.Errorf("failed to merge config: %v", err)
161161
}
162-
newObject, err = liveObject.NormalizeUnions(newObject)
162+
newObject, err = configObject.NormalizeUnionsApply(newObject)
163163
if err != nil {
164164
return nil, fieldpath.ManagedFields{}, err
165165
}

schema/elements.go

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -133,23 +133,28 @@ type UnionField struct {
133133
}
134134

135135
// Union, or oneof, means that only one of multiple fields of a structure can be
136-
// set at a time. For backward compatibility reasons, and to help "dumb clients"
137-
// which are not aware of the union (or can't be aware of it because they
138-
// don't know what fields are part of the union), the code tolerates multiple
139-
// fields to be set but will try to detect which fields must be cleared (there
140-
// should never be more than two though):
141-
// - If there is a discriminator and its value has changed, clear all fields
142-
// but the one specified by the discriminator
143-
// - If there is no discriminator, or it hasn't changed, if new has two of the
144-
// fields set, remove the one that was set in old.
145-
// - If there is a discriminator, set it to the value we've kept (if it changed)
136+
// set at a time. Setting the discriminator helps clearing oher fields:
137+
// - If discriminator changed to non-nil, and a new field has been added
138+
// that doesn't match, an error is returned,
139+
// - If discriminator hasn't changed and two fields or more are set, an
140+
// error is returned,
141+
// - If discriminator changed to non-nil, all other fields but the
142+
// discriminated one will be cleared,
143+
// - Otherwise, If only one field is left, update discriminator to that value.
146144
type Union struct {
147145
// Discriminator, if present, is the name of the field that
148146
// discriminates fields in the union. The mapping between the value of
149147
// the discriminator and the field is done by using the Fields list
150148
// below.
151149
Discriminator *string `yaml:"discriminator,omitempty"`
152150

151+
// DeduceInvalidDiscriminator indicates if the discriminator
152+
// should be updated automatically based on the fields set. This
153+
// typically defaults to false since we don't want to deduce by
154+
// default (the behavior exists to maintain compatibility on
155+
// existing types and shouldn't be used for new types).
156+
DeduceInvalidDiscriminator bool `yaml:"deduceInvalidDiscriminator,omitempty"`
157+
153158
// This is the list of fields that belong to this union. All the
154159
// fields present in here have to be part of the parent
155160
// structure. Discriminator (if oneOf has one), is NOT included in

schema/schemaschema.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,9 @@ var SchemaSchemaYAML = `types:
105105
- name: discriminator
106106
type:
107107
scalar: string
108+
- name: deduceInvalidDiscriminator
109+
type:
110+
scalar: bool
108111
- name: fields
109112
type:
110113
list:

typed/typed.go

Lines changed: 31 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -147,15 +147,13 @@ func (tv TypedValue) RemoveItems(items *fieldpath.Set) *TypedValue {
147147
}
148148

149149
// NormalizeUnions takes the new object and normalizes the union:
150-
// - If there is a discriminator and its value has changed, clean all
151-
// fields but the one specified by the discriminator
152-
// - If there is no discriminator, or it hasn't changed, if new has two
153-
// of the fields set, remove the one that was set in old.
154-
// - If there is a discriminator, set it to the value we've kept (if it changed)
155-
//
156-
// This can fail if:
157-
// - Multiple new fields are set,
158-
// - The discriminator is changed, and at least one new field is set.
150+
// - If discriminator changed to non-nil, and a new field has been added
151+
// that doesn't match, an error is returned,
152+
// - If discriminator hasn't changed and two fields or more are set, an
153+
// error is returned,
154+
// - If discriminator changed to non-nil, all other fields but the
155+
// discriminated one will be cleared,
156+
// - Otherwise, If only one field is left, update discriminator to that value.
159157
func (tv TypedValue) NormalizeUnions(new *TypedValue) (*TypedValue, error) {
160158
var errs ValidationErrors
161159
var normalizeFn = func(w *mergingWalker) {
@@ -177,6 +175,30 @@ func (tv TypedValue) NormalizeUnions(new *TypedValue) (*TypedValue, error) {
177175
return out, nil
178176
}
179177

178+
// NormalizeUnionsApply specifically normalize unions on apply. It
179+
// validates that the applied union is correct (there should be no
180+
// ambiguity there), and clear the fields according to the sent intent.
181+
func (tv TypedValue) NormalizeUnionsApply(new *TypedValue) (*TypedValue, error) {
182+
var errs ValidationErrors
183+
var normalizeFn = func(w *mergingWalker) {
184+
if w.rhs != nil {
185+
v := *w.rhs
186+
w.out = &v
187+
}
188+
if err := normalizeUnionsApply(w); err != nil {
189+
errs = append(errs, w.error(err)...)
190+
}
191+
}
192+
out, mergeErrs := merge(&tv, new, func(w *mergingWalker) {}, normalizeFn)
193+
if mergeErrs != nil {
194+
errs = append(errs, mergeErrs.(ValidationErrors)...)
195+
}
196+
if len(errs) > 0 {
197+
return nil, errs
198+
}
199+
return out, nil
200+
}
201+
180202
func (tv TypedValue) Empty() *TypedValue {
181203
tv.value = value.Value{Null: true}
182204
return &tv

typed/union.go

Lines changed: 62 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,28 @@ func normalizeUnions(w *mergingWalker) error {
4646
return nil
4747
}
4848

49+
func normalizeUnionsApply(w *mergingWalker) error {
50+
atom, found := w.schema.Resolve(w.typeRef)
51+
if !found {
52+
panic(fmt.Sprintf("Unable to resolve schema in normalize union: %v/%v", w.schema, w.typeRef))
53+
}
54+
// Unions can only be in structures, and the struct must not have been removed
55+
if atom.Map == nil || w.out == nil {
56+
return nil
57+
}
58+
59+
old := &value.Map{}
60+
if w.lhs != nil {
61+
old = w.lhs.MapValue
62+
}
63+
for _, union := range atom.Map.Unions {
64+
if err := newUnion(&union).NormalizeApply(old, w.rhs.MapValue, w.out.MapValue); err != nil {
65+
return err
66+
}
67+
}
68+
return nil
69+
}
70+
4971
type discriminated string
5072
type field string
5173

@@ -167,9 +189,10 @@ func (fs fieldsSet) String() string {
167189
}
168190

169191
type union struct {
170-
d *discriminator
171-
dn discriminatedNames
172-
f []field
192+
deduceInvalidDiscriminator bool
193+
d *discriminator
194+
dn discriminatedNames
195+
f []field
173196
}
174197

175198
func newUnion(su *schema.Union) *union {
@@ -183,6 +206,7 @@ func newUnion(su *schema.Union) *union {
183206
f2d[field(f.FieldName)] = discriminated(f.DiscriminatedBy)
184207
}
185208
u.dn = newDiscriminatedName(f2d)
209+
u.deduceInvalidDiscriminator = su.DeduceInvalidDiscriminator
186210
return u
187211
}
188212

@@ -201,29 +225,49 @@ func (u *union) Normalize(old, new, out *value.Map) error {
201225
ns := newFieldsSet(new, u.f)
202226
diff := ns.Difference(os)
203227

204-
if len(ns) > 1 && len(diff) != 1 {
205-
return fmt.Errorf("unable to guess new discriminator amongst new fields: %v", diff)
228+
if u.d.Get(old) != u.d.Get(new) && u.d.Get(new) != "" {
229+
if len(diff) == 1 && u.d.Get(new) != u.dn.toDiscriminated(*diff.One()) {
230+
return fmt.Errorf("discriminator (%v) and field changed (%v) don't match", u.d.Get(new), diff.One())
231+
}
232+
if len(diff) > 1 {
233+
return fmt.Errorf("multiple new fields added: %v", diff)
234+
}
235+
u.clear(out, u.dn.toField(u.d.Get(new)))
236+
return nil
206237
}
207238

208-
discriminator := field("")
209-
if len(ns) == 1 {
210-
discriminator = *ns.One()
211-
} else if len(diff) == 1 {
212-
discriminator = *diff.One()
239+
if len(ns) > 1 {
240+
return fmt.Errorf("multiple fields set without discriminator change: %v", ns)
213241
}
214242

215-
if u.d.Get(old) != u.d.Get(new) && u.d.Get(new) != "" {
216-
if len(diff) == 1 && u.d.Get(new) != u.dn.toDiscriminated(discriminator) {
217-
return fmt.Errorf("discriminator (%v) and field changed (%v)", u.d.Get(new), discriminator)
218-
}
219-
u.clear(out, u.dn.toField(u.d.Get(new)))
243+
// Update discriminiator if it needs to be deduced.
244+
if u.deduceInvalidDiscriminator && len(ns) == 1 {
245+
u.d.Set(out, u.dn.toDiscriminated(*ns.One()))
246+
}
247+
248+
return nil
249+
}
250+
251+
func (u *union) NormalizeApply(applied, merged, out *value.Map) error {
252+
as := newFieldsSet(applied, u.f)
253+
if len(as) > 1 {
254+
return fmt.Errorf("more than one field of union applied: %v", as)
255+
}
256+
if len(as) == 0 {
257+
// None is set, just leave.
220258
return nil
221259
}
260+
// We have exactly one, discriminiator must match if set
261+
if u.d.Get(applied) != "" && u.d.Get(applied) != u.dn.toDiscriminated(*as.One()) {
262+
return fmt.Errorf("applied discriminator (%v) doesn't match applied field (%v)", u.d.Get(applied), *as.One())
263+
}
222264

223-
if discriminator != "" {
224-
u.clear(out, discriminator)
225-
u.d.Set(out, u.dn.toDiscriminated(discriminator))
265+
// Update discriminiator if needed
266+
if u.deduceInvalidDiscriminator {
267+
u.d.Set(out, u.dn.toDiscriminated(*as.One()))
226268
}
269+
// Clear others fields.
270+
u.clear(out, *as.One())
227271

228272
return nil
229273
}

0 commit comments

Comments
 (0)