Skip to content

Commit ff00828

Browse files
author
Antoine Pelisse
committed
Implement unions normalization
Normalization can return errors if something very wrong is happening, like setting two new fields, or if setting a new field while also changing the discriminator.
1 parent 1078cd7 commit ff00828

File tree

5 files changed

+568
-9
lines changed

5 files changed

+568
-9
lines changed

schema/elements.go

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -113,8 +113,9 @@ type UnionField struct {
113113
// is the serialized form of the field.
114114
FieldName string `yaml:"fieldName"`
115115
// DiscriminatedBy is the value of the discriminator to select that
116-
// field.
117-
DiscriminatedBy string `yaml:"DiscriminatedBy"`
116+
// field. If the union doesn't have a discriminator, this field is
117+
// ignored.
118+
DiscriminatedBy string `yaml:"discriminatedBy"`
118119
}
119120

120121
// Union, or oneof, means that only one of multiple fields of a structure can be
@@ -135,11 +136,10 @@ type Union struct {
135136
// below.
136137
Discriminator *string `yaml:"discriminator,omitempty"`
137138

138-
// This is the list of fields that belong to this union. This fields are
139-
// required to not be part of another union, or be the discriminator for
140-
// another union. All the fields present in here have to be part of the
141-
// parent structure. Discriminator (if oneOf has one), is NOT included
142-
// in this list. The value for field is how we map the name of the field
139+
// This is the list of fields that belong to this union. All the
140+
// fields present in here have to be part of the parent
141+
// structure. Discriminator (if oneOf has one), is NOT included in
142+
// this list. The value for field is how we map the name of the field
143143
// to actual value for discriminator.
144144
Fields []UnionField `yaml:"fields,omitempty"`
145145
}

schema/schemaschema.go

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,15 @@ var SchemaSchemaYAML = `types:
9090
- name: elementRelationship
9191
type:
9292
scalar: string
93+
- name: unionField
94+
struct:
95+
fields:
96+
- name: fieldName
97+
type:
98+
scalar: string
99+
- name: discriminatedBy
100+
type:
101+
scalar: string
93102
- name: union
94103
struct:
95104
fields:
@@ -98,9 +107,12 @@ var SchemaSchemaYAML = `types:
98107
scalar: string
99108
- name: fields
100109
type:
101-
map:
110+
list:
111+
elementRelationship: associative
102112
elementType:
103-
scalar: string
113+
namedType: unionField
114+
keys:
115+
- fieldName
104116
- name: structField
105117
struct:
106118
fields:

typed/typed.go

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,38 @@ func (tv TypedValue) RemoveItems(items *fieldpath.Set) *TypedValue {
146146
return &tv
147147
}
148148

149+
// 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.
159+
func (tv TypedValue) NormalizeUnions(new *TypedValue) (*TypedValue, error) {
160+
var errs ValidationErrors
161+
var normalizeFn = func(w *mergingWalker) {
162+
if err := normalizeUnion(w); err != nil {
163+
errs = append(errs, w.error(err)...)
164+
}
165+
}
166+
out, mergeErrs := merge(&tv, new, func(w *mergingWalker) {
167+
if w.rhs != nil {
168+
v := *w.rhs
169+
w.out = &v
170+
}
171+
}, normalizeFn)
172+
if mergeErrs != nil {
173+
errs = append(errs, mergeErrs.(ValidationErrors)...)
174+
}
175+
if len(errs) > 0 {
176+
return nil, errs
177+
}
178+
return out, nil
179+
}
180+
149181
func merge(lhs, rhs *TypedValue, rule, postRule mergeRule) (*TypedValue, error) {
150182
if lhs.schema != rhs.schema {
151183
return nil, errorFormatter{}.

typed/union.go

Lines changed: 215 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,215 @@
1+
/*
2+
Copyright 2019 The Kubernetes Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package typed
18+
19+
import (
20+
"fmt"
21+
22+
"sigs.k8s.io/structured-merge-diff/schema"
23+
"sigs.k8s.io/structured-merge-diff/value"
24+
)
25+
26+
func normalizeUnion(w *mergingWalker) error {
27+
atom, found := w.schema.Resolve(w.typeRef)
28+
if !found {
29+
panic(fmt.Sprintf("Unable to resolve schema in normalize union: %v/%v", w.schema, w.typeRef))
30+
}
31+
// Unions can only be in structures, and the struct must not have been removed
32+
if atom.Struct == nil || atom.Struct.Union == nil || w.out == nil {
33+
return nil
34+
}
35+
36+
old := &value.Map{}
37+
if w.lhs != nil {
38+
old = w.lhs.MapValue
39+
}
40+
return newUnion(atom.Struct.Union).Normalize(old, w.rhs.MapValue, w.out.MapValue)
41+
}
42+
43+
type discriminated string
44+
type field string
45+
46+
type discriminatedNames struct {
47+
f2d map[field]discriminated
48+
d2f map[discriminated]field
49+
}
50+
51+
func newDiscriminatedName(f2d map[field]discriminated) discriminatedNames {
52+
d2f := map[discriminated]field{}
53+
for key, value := range f2d {
54+
d2f[value] = key
55+
}
56+
return discriminatedNames{
57+
f2d: f2d,
58+
d2f: d2f,
59+
}
60+
}
61+
62+
func (dn discriminatedNames) toField(d discriminated) field {
63+
if f, ok := dn.d2f[d]; ok {
64+
return f
65+
}
66+
return field(d)
67+
}
68+
69+
func (dn discriminatedNames) toDiscriminated(f field) discriminated {
70+
if d, ok := dn.f2d[f]; ok {
71+
return d
72+
}
73+
return discriminated(f)
74+
}
75+
76+
type discriminator struct {
77+
name string
78+
}
79+
80+
func (d *discriminator) Set(m *value.Map, v discriminated) {
81+
if d == nil {
82+
return
83+
}
84+
m.Set(d.name, value.StringValue(string(v)))
85+
}
86+
87+
func (d *discriminator) Get(m *value.Map) discriminated {
88+
if d == nil || m == nil {
89+
return ""
90+
}
91+
f, ok := m.Get(d.name)
92+
if !ok {
93+
return ""
94+
}
95+
if f.Value.StringValue == nil {
96+
return ""
97+
}
98+
return discriminated(*f.Value.StringValue)
99+
}
100+
101+
type fieldsSet map[field]struct{}
102+
103+
// newFieldsSet returns a map of the fields that are part of the union and are set
104+
// in the given map.
105+
func newFieldsSet(m *value.Map, fields []field) fieldsSet {
106+
if m == nil {
107+
return nil
108+
}
109+
set := fieldsSet{}
110+
for _, f := range fields {
111+
if _, ok := m.Get(string(f)); ok {
112+
set.Add(f)
113+
}
114+
}
115+
return set
116+
}
117+
118+
func (fs fieldsSet) Add(f field) {
119+
if fs == nil {
120+
fs = map[field]struct{}{}
121+
}
122+
fs[f] = struct{}{}
123+
}
124+
125+
func (fs fieldsSet) One() *field {
126+
for f := range fs {
127+
return &f
128+
}
129+
return nil
130+
}
131+
132+
func (fs fieldsSet) Has(f field) bool {
133+
_, ok := fs[f]
134+
return ok
135+
}
136+
137+
func (fs fieldsSet) List() []field {
138+
fields := []field{}
139+
for f := range fs {
140+
fields = append(fields, f)
141+
}
142+
return fields
143+
}
144+
145+
func (fs fieldsSet) Difference(o fieldsSet) fieldsSet {
146+
n := fieldsSet{}
147+
for f := range fs {
148+
if !o.Has(f) {
149+
n.Add(f)
150+
}
151+
}
152+
return n
153+
}
154+
155+
type union struct {
156+
d *discriminator
157+
dn discriminatedNames
158+
f []field
159+
}
160+
161+
func newUnion(su *schema.Union) *union {
162+
u := &union{}
163+
if su.Discriminator != nil {
164+
u.d = &discriminator{name: *su.Discriminator}
165+
}
166+
f2d := map[field]discriminated{}
167+
for _, f := range su.Fields {
168+
u.f = append(u.f, field(f.FieldName))
169+
f2d[field(f.FieldName)] = discriminated(f.DiscriminatedBy)
170+
}
171+
u.dn = newDiscriminatedName(f2d)
172+
return u
173+
}
174+
175+
// clear removes all the fields in map that are part of the union, but
176+
// the one we decided to keep.
177+
func (u *union) clear(m *value.Map, f field) {
178+
for _, fieldName := range u.f {
179+
if field(fieldName) != f {
180+
m.Delete(string(fieldName))
181+
}
182+
}
183+
}
184+
185+
func (u *union) Normalize(old, new, out *value.Map) error {
186+
os := newFieldsSet(old, u.f)
187+
ns := newFieldsSet(new, u.f)
188+
diff := ns.Difference(os)
189+
190+
if len(ns) > 1 && len(diff) != 1 {
191+
return fmt.Errorf("unable to guess new discriminator: %v", diff)
192+
}
193+
194+
discriminator := field("")
195+
if len(ns) == 1 {
196+
discriminator = *ns.One()
197+
} else if len(diff) == 1 {
198+
discriminator = *diff.One()
199+
}
200+
201+
if u.d.Get(old) != u.d.Get(new) && u.d.Get(new) != "" {
202+
if len(diff) == 1 && u.d.Get(new) != u.dn.toDiscriminated(discriminator) {
203+
return fmt.Errorf("discriminator and field changed: %v/%v", discriminator, u.d.Get(new))
204+
}
205+
u.clear(out, u.dn.toField(u.d.Get(new)))
206+
return nil
207+
}
208+
209+
if discriminator != "" {
210+
u.clear(out, discriminator)
211+
u.d.Set(out, u.dn.toDiscriminated(discriminator))
212+
}
213+
214+
return nil
215+
}

0 commit comments

Comments
 (0)