Skip to content

Commit 15d366b

Browse files
authored
Merge pull request #82 from apelisse/change-union
Change union to not "guess" based on difference
2 parents 1c46bef + f55a486 commit 15d366b

File tree

7 files changed

+200
-98
lines changed

7 files changed

+200
-98
lines changed

merge/union_test.go

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -47,16 +47,17 @@ var unionFieldsParser = func() typed.ParseableType {
4747
scalar: string
4848
unions:
4949
- discriminator: type
50+
deduceInvalidDiscriminator: true
5051
fields:
5152
- fieldName: numeric
52-
discriminatedBy: Numeric
53+
discriminatorValue: Numeric
5354
- fieldName: string
54-
discriminatedBy: String
55+
discriminatorValue: String
5556
- fields:
5657
- fieldName: fieldA
57-
discriminatedBy: FieldA
58+
discriminatorValue: FieldA
5859
- fieldName: fieldB
59-
discriminatedBy: FieldB`)
60+
discriminatorValue: FieldB`)
6061
if err != nil {
6162
panic(err)
6263
}

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: 19 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -126,30 +126,35 @@ type UnionField struct {
126126
// FieldName is the name of the field that is part of the union. This
127127
// is the serialized form of the field.
128128
FieldName string `yaml:"fieldName"`
129-
// DiscriminatedBy is the value of the discriminator to select that
130-
// field. If the union doesn't have a discriminator, this field is
131-
// ignored.
132-
DiscriminatedBy string `yaml:"discriminatedBy"`
129+
// Discriminatorvalue is the value of the discriminator to
130+
// select that field. If the union doesn't have a discriminator,
131+
// this field is ignored.
132+
DiscriminatorValue string `yaml:"discriminatorValue"`
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: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ var SchemaSchemaYAML = `types:
9696
- name: fieldName
9797
type:
9898
scalar: string
99-
- name: discriminatedBy
99+
- name: discriminatorValue
100100
type:
101101
scalar: string
102102
- name: union
@@ -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: 63 additions & 19 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 {
@@ -180,9 +203,10 @@ func newUnion(su *schema.Union) *union {
180203
f2d := map[field]discriminated{}
181204
for _, f := range su.Fields {
182205
u.f = append(u.f, field(f.FieldName))
183-
f2d[field(f.FieldName)] = discriminated(f.DiscriminatedBy)
206+
f2d[field(f.FieldName)] = discriminated(f.DiscriminatorValue)
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)