Skip to content

Commit 00b6b52

Browse files
committed
add field-level element relationship which overrides referred type
1 parent 5e8be97 commit 00b6b52

File tree

7 files changed

+412
-7
lines changed

7 files changed

+412
-7
lines changed

merge/field_level_overrides_test.go

Lines changed: 262 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,262 @@
1+
package merge_test
2+
3+
import (
4+
"testing"
5+
6+
"sigs.k8s.io/structured-merge-diff/v4/fieldpath"
7+
"sigs.k8s.io/structured-merge-diff/v4/internal/fixture"
8+
"sigs.k8s.io/structured-merge-diff/v4/merge"
9+
"sigs.k8s.io/structured-merge-diff/v4/typed"
10+
)
11+
12+
func TestFieldLevelOverrides(t *testing.T) {
13+
var overrideStructTypeParser = func() fixture.Parser {
14+
parser, err := typed.NewParser(`
15+
types:
16+
- name: type
17+
map:
18+
fields:
19+
- name: associativeListReference
20+
type:
21+
namedType: associativeList
22+
elementRelationship: atomic
23+
- name: separableInlineList
24+
type:
25+
list:
26+
elementType:
27+
scalar: numeric
28+
elementRelationship: atomic
29+
elementRelationship: associative
30+
- name: separableMapReference
31+
type:
32+
namedType: atomicMap
33+
elementRelationship: separable
34+
- name: atomicMapReference
35+
type:
36+
namedType: unspecifiedMap
37+
elementRelationship: atomic
38+
39+
- name: associativeList
40+
list:
41+
elementType:
42+
namedType: unspecifiedMap
43+
elementRelationship: atomic
44+
elementRelationship: associative
45+
keys:
46+
- name
47+
- name: unspecifiedMap
48+
map:
49+
fields:
50+
- name: name
51+
type:
52+
scalar: string
53+
- name: value
54+
type:
55+
scalar: numeric
56+
- name: atomicMap
57+
map:
58+
elementRelationship: atomic
59+
fields:
60+
- name: name
61+
type:
62+
scalar: string
63+
- name: value
64+
type:
65+
scalar: numeric
66+
`)
67+
if err != nil {
68+
panic(err)
69+
}
70+
return fixture.SameVersionParser{T: parser.Type("type")}
71+
}()
72+
73+
tests := map[string]fixture.TestCase{
74+
"test_override_atomic_map_with_separable": {
75+
// Test that a reference with an separable override to an atomic type
76+
// is treated as separable
77+
Ops: []fixture.Operation{
78+
fixture.Apply{
79+
Manager: "apply_one",
80+
Object: `
81+
separableMapReference:
82+
name: a
83+
`,
84+
APIVersion: "v1",
85+
},
86+
fixture.Apply{
87+
Manager: "apply_two",
88+
Object: `
89+
separableMapReference:
90+
value: 2
91+
`,
92+
APIVersion: "v1",
93+
},
94+
},
95+
Object: `
96+
separableMapReference:
97+
name: a
98+
value: 2
99+
`,
100+
APIVersion: "v1",
101+
Managed: fieldpath.ManagedFields{
102+
"apply_one": fieldpath.NewVersionedSet(
103+
_NS(
104+
_P("separableMapReference", "name"),
105+
),
106+
"v1",
107+
false,
108+
),
109+
"apply_two": fieldpath.NewVersionedSet(
110+
_NS(
111+
_P("separableMapReference", "value"),
112+
),
113+
"v1",
114+
false,
115+
),
116+
},
117+
},
118+
"test_override_unspecified_map_with_atomic": {
119+
// Test that a map which has its element relaetionship left as defualt
120+
// (granular) can be overriden to be atomic
121+
Ops: []fixture.Operation{
122+
fixture.Apply{
123+
Manager: "apply_one",
124+
Object: `
125+
atomicMapReference:
126+
name: a
127+
`,
128+
APIVersion: "v1",
129+
},
130+
fixture.Apply{
131+
Manager: "apply_two",
132+
Object: `
133+
atomicMapReference:
134+
value: 2
135+
`,
136+
APIVersion: "v1",
137+
Conflicts: merge.Conflicts{
138+
merge.Conflict{Manager: "apply_one", Path: _P("atomicMapReference")},
139+
},
140+
},
141+
fixture.Apply{
142+
Manager: "apply_one",
143+
Object: `
144+
atomicMapReference:
145+
name: b
146+
value: 2
147+
`,
148+
APIVersion: "v1",
149+
},
150+
},
151+
Object: `
152+
atomicMapReference:
153+
name: b
154+
value: 2
155+
`,
156+
APIVersion: "v1",
157+
Managed: fieldpath.ManagedFields{
158+
"apply_one": fieldpath.NewVersionedSet(
159+
_NS(
160+
_P("atomicMapReference"),
161+
),
162+
"v1",
163+
false,
164+
),
165+
},
166+
},
167+
"test_override_associative_list_with_atomic": {
168+
// Test that if a list type is listed associative but referred to as atomic
169+
// that attempting to add to the list fauks
170+
Ops: []fixture.Operation{
171+
fixture.Apply{
172+
Manager: "apply_one",
173+
Object: `
174+
associativeListReference:
175+
- name: a
176+
value: 1
177+
`,
178+
APIVersion: "v1",
179+
},
180+
fixture.Apply{
181+
Manager: "apply_two",
182+
Object: `
183+
associativeListReference:
184+
- name: b
185+
value: 2
186+
`,
187+
APIVersion: "v1",
188+
Conflicts: merge.Conflicts{
189+
merge.Conflict{Manager: "apply_one", Path: _P("associativeListReference")},
190+
},
191+
},
192+
},
193+
Object: `
194+
associativeListReference:
195+
- name: a
196+
value: 1
197+
`,
198+
APIVersion: "v1",
199+
Managed: fieldpath.ManagedFields{
200+
"apply_one": fieldpath.NewVersionedSet(
201+
_NS(
202+
_P("associativeListReference"),
203+
),
204+
"v1",
205+
false,
206+
),
207+
},
208+
},
209+
"test_override_inline_atomic_list_with_associative": {
210+
// Tests that an inline atomic list can have its type overridden to be
211+
// associative
212+
Ops: []fixture.Operation{
213+
fixture.Apply{
214+
Manager: "apply_one",
215+
Object: `
216+
separableInlineList:
217+
- 1
218+
`,
219+
APIVersion: "v1",
220+
},
221+
fixture.Apply{
222+
Manager: "apply_two",
223+
Object: `
224+
separableInlineList:
225+
- 2
226+
`,
227+
APIVersion: "v1",
228+
},
229+
},
230+
Object: `
231+
separableInlineList:
232+
- 1
233+
- 2
234+
`,
235+
APIVersion: "v1",
236+
Managed: fieldpath.ManagedFields{
237+
"apply_one": fieldpath.NewVersionedSet(
238+
_NS(
239+
_P("separableInlineList", _V(1)),
240+
),
241+
"v1",
242+
true,
243+
),
244+
"apply_two": fieldpath.NewVersionedSet(
245+
_NS(
246+
_P("separableInlineList", _V(2)),
247+
),
248+
"v1",
249+
true,
250+
),
251+
},
252+
},
253+
}
254+
255+
for name, test := range tests {
256+
t.Run(name, func(t *testing.T) {
257+
if err := test.Test(overrideStructTypeParser); err != nil {
258+
t.Fatal(err)
259+
}
260+
})
261+
}
262+
}

schema/elements.go

Lines changed: 77 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,9 @@ limitations under the License.
1616

1717
package schema
1818

19-
import "sync"
19+
import (
20+
"sync"
21+
)
2022

2123
// Schema is a list of named types.
2224
//
@@ -27,6 +29,15 @@ type Schema struct {
2729

2830
once sync.Once
2931
m map[string]TypeDef
32+
33+
// Once used to protect the initialization of `lock` field.
34+
lockOnce sync.Once
35+
// Lock which protects writes to resolvedTypes. Used as pointer so that
36+
// schema may be used as a value type
37+
lock *sync.Mutex
38+
// Cached results of resolving type references to atoms. Only stores
39+
// type references which require fields of Atom to be overriden.
40+
resolvedTypes map[TypeRef]Atom
3041
}
3142

3243
// A TypeSpecifier references a particular type in a schema.
@@ -48,6 +59,12 @@ type TypeRef struct {
4859
// Either the name or one member of Atom should be set.
4960
NamedType *string `yaml:"namedType,omitempty"`
5061
Inlined Atom `yaml:",inline,omitempty"`
62+
63+
// If this reference refers to a map-type or list-type, this field overrides
64+
// the `ElementRelationship` of the referred type when resolved.
65+
// If this field is nil, then it has no effect.
66+
// See `Map` and `List` for more information about `ElementRelationship`
67+
ElementRelationship *ElementRelationship `yaml:"elementRelationship,omitempty"`
5168
}
5269

5370
// Atom represents the smallest possible pieces of the type system.
@@ -244,20 +261,74 @@ func (s *Schema) FindNamedType(name string) (TypeDef, bool) {
244261
return t, ok
245262
}
246263

264+
func (s *Schema) resolveNoOverrides(tr TypeRef) (Atom, bool) {
265+
result := Atom{}
266+
267+
if tr.NamedType != nil {
268+
t, ok := s.FindNamedType(*tr.NamedType)
269+
if !ok {
270+
return Atom{}, false
271+
}
272+
273+
result = t.Atom
274+
} else {
275+
result = tr.Inlined
276+
}
277+
278+
return result, true
279+
}
280+
247281
// Resolve is a convenience function which returns the atom referenced, whether
248282
// it is inline or named. Returns (Atom{}, false) if the type can't be resolved.
249283
//
250284
// This allows callers to not care about the difference between a (possibly
251285
// inlined) reference and a definition.
252286
func (s *Schema) Resolve(tr TypeRef) (Atom, bool) {
253-
if tr.NamedType != nil {
254-
t, ok := s.FindNamedType(*tr.NamedType)
255-
if !ok {
287+
// If this is a plain reference with no overrides, just return the type
288+
if tr.ElementRelationship == nil {
289+
return s.resolveNoOverrides(tr)
290+
}
291+
292+
// Check to see if we have a cached version of this type
293+
s.lockOnce.Do(func() {
294+
s.lock = &sync.Mutex{}
295+
s.resolvedTypes = make(map[TypeRef]Atom)
296+
})
297+
298+
s.lock.Lock()
299+
defer s.lock.Unlock()
300+
301+
var result Atom
302+
var exists bool
303+
304+
// Return cached result if available
305+
// If not, calculate result and cache it
306+
if result, exists = s.resolvedTypes[tr]; !exists {
307+
if result, exists = s.resolveNoOverrides(tr); exists {
308+
// Allow field-level electives to override the referred type's modifiers
309+
switch {
310+
case result.Map != nil:
311+
mapCopy := *result.Map
312+
mapCopy.ElementRelationship = *tr.ElementRelationship
313+
result.Map = &mapCopy
314+
case result.List != nil:
315+
listCopy := *result.List
316+
listCopy.ElementRelationship = *tr.ElementRelationship
317+
result.List = &listCopy
318+
case result.Scalar != nil:
319+
return Atom{}, false
320+
default:
321+
return Atom{}, false
322+
}
323+
} else {
256324
return Atom{}, false
257325
}
258-
return t.Atom, true
326+
327+
// Save result. If it is nil, that is also recorded as not existing.
328+
s.resolvedTypes[tr] = result
259329
}
260-
return tr.Inlined, true
330+
331+
return result, true
261332
}
262333

263334
// Clones this instance of Schema into the other

0 commit comments

Comments
 (0)