Skip to content

Commit 51def80

Browse files
committed
Expose ExtractItems as the opposite of RemoveItems
This exposes a new method on TypedValue called ExtractItems. Whereas RemoveItems takes a fieldpath.Set adn returns the typed value with the provided set items removed from it, ExtractItems returns ONLY those items provided by the fieldpath.Set, effectively extracting them from the original TypedValue.
1 parent a4e00e9 commit 51def80

File tree

3 files changed

+321
-15
lines changed

3 files changed

+321
-15
lines changed

typed/remove.go

Lines changed: 39 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -20,19 +20,26 @@ import (
2020
)
2121

2222
type removingWalker struct {
23-
value value.Value
24-
out interface{}
25-
schema *schema.Schema
26-
toRemove *fieldpath.Set
27-
allocator value.Allocator
23+
value value.Value
24+
out interface{}
25+
schema *schema.Schema
26+
toRemove *fieldpath.Set
27+
allocator value.Allocator
28+
shouldExtract bool
2829
}
2930

30-
func removeItemsWithSchema(val value.Value, toRemove *fieldpath.Set, schema *schema.Schema, typeRef schema.TypeRef) value.Value {
31+
// removeItemsWithSchema will walk the given value and look for items form the toRemove set.
32+
// Depending on whether shouldExtract is set true or false, it will return a modified version
33+
// of the input value with either:
34+
// 1. only the items in the toRemove set (when shouldExtract is true) or
35+
// 2. the items from the toRemove set removed from the value (when shouldExtract is false).
36+
func removeItemsWithSchema(val value.Value, toRemove *fieldpath.Set, schema *schema.Schema, typeRef schema.TypeRef, shouldExtract bool) value.Value {
3137
w := &removingWalker{
32-
value: val,
33-
schema: schema,
34-
toRemove: toRemove,
35-
allocator: value.NewFreelistAllocator(),
38+
value: val,
39+
schema: schema,
40+
toRemove: toRemove,
41+
allocator: value.NewFreelistAllocator(),
42+
shouldExtract: shouldExtract,
3643
}
3744
resolveSchema(schema, typeRef, val, w)
3845
return value.NewValueInterface(w.out)
@@ -59,13 +66,22 @@ func (w *removingWalker) doList(t *schema.List) (errs ValidationErrors) {
5966
// Ignore error because we have already validated this list
6067
pe, _ := listItemToPathElement(w.allocator, w.schema, t, i, item)
6168
path, _ := fieldpath.MakePath(pe)
69+
// save items that do have the path when we shouldExtract
70+
// but ignore it when we are removing (i.e. !w.shouldExtract)
6271
if w.toRemove.Has(path) {
63-
continue
72+
if w.shouldExtract {
73+
newItems = append(newItems, item.Unstructured())
74+
} else {
75+
continue
76+
}
6477
}
6578
if subset := w.toRemove.WithPrefix(pe); !subset.Empty() {
66-
item = removeItemsWithSchema(item, subset, w.schema, t.ElementType)
79+
item = removeItemsWithSchema(item, subset, w.schema, t.ElementType, w.shouldExtract)
80+
}
81+
// save items that do not have the path only when removing (i.e. !w.shouldExtract)
82+
if !w.shouldExtract {
83+
newItems = append(newItems, item.Unstructured())
6784
}
68-
newItems = append(newItems, item.Unstructured())
6985
}
7086
if len(newItems) > 0 {
7187
w.out = newItems
@@ -96,11 +112,20 @@ func (w *removingWalker) doMap(t *schema.Map) ValidationErrors {
96112
if ft, ok := fieldTypes[k]; ok {
97113
fieldType = ft
98114
}
115+
// save items on the path only when extracting.
99116
if w.toRemove.Has(path) {
117+
if w.shouldExtract {
118+
newMap[k] = val.Unstructured()
119+
}
100120
return true
101121
}
102122
if subset := w.toRemove.WithPrefix(pe); !subset.Empty() {
103-
val = removeItemsWithSchema(val, subset, w.schema, fieldType)
123+
val = removeItemsWithSchema(val, subset, w.schema, fieldType, w.shouldExtract)
124+
} else {
125+
// don't save items not on the path when extracting.
126+
if w.shouldExtract {
127+
return true
128+
}
104129
}
105130
newMap[k] = val.Unstructured()
106131
return true

typed/remove_test.go

Lines changed: 275 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,275 @@
1+
/*
2+
Copyright 2018 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_test
18+
19+
import (
20+
"fmt"
21+
"testing"
22+
23+
"sigs.k8s.io/structured-merge-diff/v4/fieldpath"
24+
"sigs.k8s.io/structured-merge-diff/v4/typed"
25+
"sigs.k8s.io/structured-merge-diff/v4/value"
26+
)
27+
28+
type removeTestCase struct {
29+
name string
30+
rootTypeName string
31+
schema typed.YAMLObject
32+
quadruplets []removeQuadruplet
33+
}
34+
35+
type removeQuadruplet struct {
36+
object typed.YAMLObject
37+
set *fieldpath.Set
38+
removeOutput typed.YAMLObject
39+
extractOutput typed.YAMLObject
40+
}
41+
42+
var simplePairSchema = `types:
43+
- name: stringPair
44+
map:
45+
fields:
46+
- name: key
47+
type:
48+
scalar: string
49+
- name: value
50+
type:
51+
namedType: __untyped_atomic_
52+
- name: __untyped_atomic_
53+
scalar: untyped
54+
list:
55+
elementType:
56+
namedType: __untyped_atomic_
57+
elementRelationship: atomic
58+
map:
59+
elementType:
60+
namedType: __untyped_atomic_
61+
elementRelationship: atomic
62+
`
63+
64+
var structGrabBagSchema = `types:
65+
- name: myStruct
66+
map:
67+
fields:
68+
- name: numeric
69+
type:
70+
scalar: numeric
71+
- name: string
72+
type:
73+
scalar: string
74+
- name: bool
75+
type:
76+
scalar: boolean
77+
- name: setStr
78+
type:
79+
list:
80+
elementType:
81+
scalar: string
82+
elementRelationship: associative
83+
- name: setBool
84+
type:
85+
list:
86+
elementType:
87+
scalar: boolean
88+
elementRelationship: associative
89+
- name: setNumeric
90+
type:
91+
list:
92+
elementType:
93+
scalar: numeric
94+
elementRelationship: associative
95+
`
96+
97+
var associativeListSchema = `types:
98+
- name: myRoot
99+
map:
100+
fields:
101+
- name: list
102+
type:
103+
namedType: myList
104+
- name: atomicList
105+
type:
106+
namedType: mySequence
107+
- name: myList
108+
list:
109+
elementType:
110+
namedType: myElement
111+
elementRelationship: associative
112+
keys:
113+
- key
114+
- id
115+
- name: mySequence
116+
list:
117+
elementType:
118+
scalar: string
119+
elementRelationship: atomic
120+
- name: myElement
121+
map:
122+
fields:
123+
- name: key
124+
type:
125+
scalar: string
126+
- name: id
127+
type:
128+
scalar: numeric
129+
- name: value
130+
type:
131+
namedType: myValue
132+
- name: bv
133+
type:
134+
scalar: boolean
135+
- name: nv
136+
type:
137+
scalar: numeric
138+
- name: myValue
139+
map:
140+
elementType:
141+
scalar: string
142+
`
143+
144+
var removeCases = []removeTestCase{{
145+
name: "simple pair",
146+
rootTypeName: "stringPair",
147+
schema: typed.YAMLObject(simplePairSchema),
148+
quadruplets: []removeQuadruplet{{
149+
`{"key":"foo"}`,
150+
_NS(_P("key")),
151+
``,
152+
`{"key":"foo"}`,
153+
}, {
154+
`{"key":"foo"}`,
155+
_NS(),
156+
`{"key":"foo"}`,
157+
``,
158+
}, {
159+
`{"key":"foo","value":true}`,
160+
_NS(_P("key")),
161+
`{"value":true}`,
162+
`{"key":"foo"}`,
163+
}, {
164+
`{"key":"foo","value":{"a": "b"}}`,
165+
_NS(_P("value")),
166+
`{"key":"foo"}`,
167+
`{"value":{"a": "b"}}`,
168+
}},
169+
}, {
170+
name: "struct grab bag",
171+
rootTypeName: "myStruct",
172+
schema: typed.YAMLObject(structGrabBagSchema),
173+
quadruplets: []removeQuadruplet{{
174+
`{"setBool":[false]}`,
175+
_NS(_P("setBool", _V(false))),
176+
// is this the right remove output?
177+
`{"setBool":null}`,
178+
`{"setBool":[false]}`,
179+
}, {
180+
`{"setBool":[false]}`,
181+
_NS(_P("setBool", _V(true))),
182+
`{"setBool":[false]}`,
183+
`{"setBool":null}`,
184+
}, {
185+
`{"setBool":[true,false]}`,
186+
_NS(_P("setBool", _V(true))),
187+
`{"setBool":[false]}`,
188+
`{"setBool":[true]}`,
189+
}, {
190+
`{"setBool":[true,false]}`,
191+
_NS(_P("setBool")),
192+
``,
193+
`{"setBool":[true,false]}`,
194+
}, {
195+
`{"setNumeric":[1,2,3,4.5]}`,
196+
_NS(_P("setNumeric", _V(1)), _P("setNumeric", _V(4.5))),
197+
`{"setNumeric":[2,3]}`,
198+
`{"setNumeric":[1,4.5]}`,
199+
}, {
200+
`{"setStr":["a","b","c"]}`,
201+
_NS(_P("setStr", _V("a"))),
202+
`{"setStr":["b","c"]}`,
203+
`{"setStr":["a"]}`,
204+
}},
205+
}, {
206+
name: "associative list",
207+
rootTypeName: "myRoot",
208+
schema: typed.YAMLObject(associativeListSchema),
209+
quadruplets: []removeQuadruplet{{
210+
`{"list":[{"key":"a","id":1},{"key":"a","id":2},{"key":"b","id":1}]}`,
211+
_NS(_P("list", _KBF("key", "a", "id", 1))),
212+
`{"list":[{"key":"a","id":2},{"key":"b","id":1}]}`,
213+
`{"list":[{"key":"a","id":1}]}`,
214+
}, {
215+
`{"atomicList":["a", "a", "a"]}`,
216+
_NS(_P("atomicList")),
217+
``,
218+
`{"atomicList":["a", "a", "a"]}`,
219+
}},
220+
}}
221+
222+
func (tt removeTestCase) test(t *testing.T) {
223+
parser, err := typed.NewParser(tt.schema)
224+
if err != nil {
225+
t.Fatalf("failed to create schema: %v", err)
226+
}
227+
228+
for i, quadruplet := range tt.quadruplets {
229+
quadruplet := quadruplet
230+
t.Run(fmt.Sprintf("%v-valid-%v", tt.name, i), func(t *testing.T) {
231+
t.Parallel()
232+
pt := parser.Type(tt.rootTypeName)
233+
234+
tv, err := pt.FromYAML(quadruplet.object)
235+
if err != nil {
236+
t.Fatalf("unable to parser/validate object yaml: %v\n%v", err, quadruplet.object)
237+
}
238+
239+
// test RemoveItems
240+
rmOut, err := pt.FromYAML(quadruplet.removeOutput)
241+
if err != nil {
242+
t.Fatalf("unable to parser/validate removeOutput yaml: %v\n%v", err, quadruplet.removeOutput)
243+
}
244+
245+
rmGot := tv.RemoveItems(quadruplet.set)
246+
if !value.Equals(rmGot.AsValue(), rmOut.AsValue()) {
247+
t.Errorf("Expected\n%v\nbut got\n%v\n",
248+
value.ToString(rmOut.AsValue()), value.ToString(rmGot.AsValue()),
249+
)
250+
}
251+
252+
// test ExtractItems
253+
exOut, err := pt.FromYAML(quadruplet.extractOutput)
254+
if err != nil {
255+
t.Fatalf("unable to parser/validate extractOutput yaml: %v\n%v", err, quadruplet.extractOutput)
256+
}
257+
exGot := tv.ExtractItems(quadruplet.set)
258+
if !value.Equals(exGot.AsValue(), exOut.AsValue()) {
259+
t.Errorf("Expected\n%v\nbut got\n%v\n",
260+
value.ToString(exOut.AsValue()), value.ToString(exGot.AsValue()),
261+
)
262+
}
263+
})
264+
}
265+
}
266+
267+
func TestRemove(t *testing.T) {
268+
for _, tt := range removeCases {
269+
tt := tt
270+
t.Run(tt.name, func(t *testing.T) {
271+
t.Parallel()
272+
tt.test(t)
273+
})
274+
}
275+
}

typed/typed.go

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -150,7 +150,13 @@ func (tv TypedValue) Compare(rhs *TypedValue) (c *Comparison, err error) {
150150

151151
// RemoveItems removes each provided list or map item from the value.
152152
func (tv TypedValue) RemoveItems(items *fieldpath.Set) *TypedValue {
153-
tv.value = removeItemsWithSchema(tv.value, items, tv.schema, tv.typeRef)
153+
tv.value = removeItemsWithSchema(tv.value, items, tv.schema, tv.typeRef, false)
154+
return &tv
155+
}
156+
157+
// ExtractItems returns a value with only the provided list or map items extracted from the value.
158+
func (tv TypedValue) ExtractItems(items *fieldpath.Set) *TypedValue {
159+
tv.value = removeItemsWithSchema(tv.value, items, tv.schema, tv.typeRef, true)
154160
return &tv
155161
}
156162

0 commit comments

Comments
 (0)