Skip to content

Commit b2ed7e1

Browse files
authored
Merge pull request #72 from apelisse/oneof
Update schema to support oneOf, and normalize objects
2 parents 8b93a2f + ff00828 commit b2ed7e1

File tree

6 files changed

+626
-5
lines changed

6 files changed

+626
-5
lines changed

schema/elements.go

Lines changed: 41 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -83,18 +83,15 @@ const (
8383

8484
// Struct represents a type which is composed of a number of different fields.
8585
// Each field has a name and a type.
86-
//
87-
// TODO: in the future, we will add one-of groups (sometimes called unions).
8886
type Struct struct {
8987
// Each struct field appears exactly once in this list. The order in
9088
// this list defines the canonical field ordering.
9189
Fields []StructField `yaml:"fields,omitempty"`
9290

93-
// TODO: Implement unions, either this way or by inlining.
94-
// Unions are groupings of fields with special rules. They may refer to
91+
// Union is a grouping of fields with special rules. It may refer to
9592
// one or more fields in the above list. A given field from the above
9693
// list may be referenced in exactly 0 or 1 places in the below list.
97-
// Unions []Union `yaml:"unions,omitempty"`
94+
Union *Union `yaml:"union,omitempty"`
9895

9996
// ElementRelationship states the relationship between the struct's items.
10097
// * `separable` (or unset) implies that each element is 100% independent.
@@ -108,6 +105,45 @@ type Struct struct {
108105
ElementRelationship ElementRelationship `yaml:"elementRelationship,omitempty"`
109106
}
110107

108+
// UnionFields are mapping between the fields that are part of the union and
109+
// their discriminated value. The discriminated value has to be set, and
110+
// should not conflict with other discriminated value in the list.
111+
type UnionField struct {
112+
// FieldName is the name of the field that is part of the union. This
113+
// is the serialized form of the field.
114+
FieldName string `yaml:"fieldName"`
115+
// DiscriminatedBy is the value of the discriminator to select that
116+
// field. If the union doesn't have a discriminator, this field is
117+
// ignored.
118+
DiscriminatedBy string `yaml:"discriminatedBy"`
119+
}
120+
121+
// Union, or oneof, means that only one of multiple fields of a structure can be
122+
// set at a time. For backward compatibility reasons, and to help "dumb clients"
123+
// which are not aware of the union (or can't be aware of it because they
124+
// don't know what fields are part of the union), the code tolerates multiple
125+
// fields to be set but will try to detect which fields must be cleared (there
126+
// should never be more than two though):
127+
// - If there is a discriminator and its value has changed, clear all fields
128+
// but the one specified by the discriminator
129+
// - If there is no discriminator, or it hasn't changed, if new has two of the
130+
// fields set, remove the one that was set in old.
131+
// - If there is a discriminator, set it to the value we've kept (if it changed)
132+
type Union struct {
133+
// Discriminator, if present, is the name of the field that
134+
// discriminates fields in the union. The mapping between the value of
135+
// the discriminator and the field is done by using the Fields list
136+
// below.
137+
Discriminator *string `yaml:"discriminator,omitempty"`
138+
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
143+
// to actual value for discriminator.
144+
Fields []UnionField `yaml:"fields,omitempty"`
145+
}
146+
111147
// StructField pairs a field name with a field type.
112148
type StructField struct {
113149
// Name is the field name.

schema/schemaschema.go

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,9 +84,35 @@ var SchemaSchemaYAML = `types:
8484
namedType: structField
8585
elementRelationship: associative
8686
keys: [ "name" ]
87+
- name: union
88+
type:
89+
namedType: union
8790
- name: elementRelationship
8891
type:
8992
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
102+
- name: union
103+
struct:
104+
fields:
105+
- name: discriminator
106+
type:
107+
scalar: string
108+
- name: fields
109+
type:
110+
list:
111+
elementRelationship: associative
112+
elementType:
113+
namedType: unionField
114+
keys:
115+
- fieldName
90116
- name: structField
91117
struct:
92118
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)