Skip to content

Commit 40c8ef9

Browse files
authored
Merge pull request #275 from yongruilin/extend-extractitems
feat: Adds ExtractItems option to include key fields
2 parents 9e64d18 + a88b919 commit 40c8ef9

File tree

2 files changed

+159
-22
lines changed

2 files changed

+159
-22
lines changed

typed/remove_test.go

+113-21
Original file line numberDiff line numberDiff line change
@@ -148,27 +148,6 @@ var associativeAndAtomicSchema = `types:
148148
elementType:
149149
scalar: string
150150
`
151-
var atomicTypesSchema = `types:
152-
- name: myRoot
153-
map:
154-
fields:
155-
- name: atomicMap
156-
type:
157-
namedType: myAtomicMap
158-
- name: atomicList
159-
type:
160-
namedType: mySequence
161-
- name: myAtomicMap
162-
map:
163-
elementType:
164-
scalar: string
165-
elementRelationship: atomic
166-
- name: mySequence
167-
list:
168-
elementType:
169-
scalar: string
170-
elementRelationship: atomic
171-
`
172151

173152
var nestedTypesSchema = `types:
174153
- name: type
@@ -906,3 +885,116 @@ func TestReversibleExtract(t *testing.T) {
906885
})
907886
}
908887
}
888+
889+
type extractWithKeysTestCase struct {
890+
name string
891+
rootTypeName string
892+
schema typed.YAMLObject
893+
triplets []extractTriplet
894+
}
895+
896+
type extractTriplet struct {
897+
object typed.YAMLObject
898+
set *fieldpath.Set
899+
wantOutput interface{}
900+
}
901+
902+
var extractWithKeysCases = []extractWithKeysTestCase{{
903+
name: "associativeAndAtomicSchema",
904+
rootTypeName: "myRoot",
905+
schema: typed.YAMLObject(associativeAndAtomicSchema),
906+
triplets: []extractTriplet{
907+
{
908+
// extract with all key fields included
909+
object: `{"list":[{"key":"nginx","id":1,"nv":2}]}`,
910+
set: _NS(
911+
_P("list", _KBF("key", "nginx", "id", 1), "key"),
912+
_P("list", _KBF("key", "nginx", "id", 1), "id"),
913+
),
914+
wantOutput: typed.YAMLObject(`{"list":[{"key":"nginx","id":1}]}`),
915+
},
916+
{
917+
// extract no key field included
918+
object: `{"list":[{"key":"nginx","id":1,"nv":2}]}`,
919+
set: _NS(
920+
_P("list", _KBF("key", "nginx", "id", 1), "nv"),
921+
),
922+
wantOutput: typed.YAMLObject(`{"list":[{"key":"nginx","id":1, "nv":2}]}`),
923+
},
924+
{
925+
// extract with partial keys included
926+
object: `{"list":[{"key":"nginx","id":1,"nv":2}]}`,
927+
set: _NS(
928+
_P("list", _KBF("key", "nginx", "id", 1), "nv"),
929+
_P("list", _KBF("key", "nginx", "id", 1), "id"),
930+
),
931+
wantOutput: typed.YAMLObject(`{"list":[{"key":"nginx","id":1, "nv":2}]}`),
932+
},
933+
{
934+
// extract with null field value
935+
object: `{"list":[{"key":"nginx","id":1,"nv":2}]}`,
936+
set: _NS(
937+
_P("list", _KBF("key", "nginx", "id", 1), "value"),
938+
),
939+
wantOutput: map[string]interface{}{
940+
"list": []interface{}{nil},
941+
},
942+
},
943+
},
944+
}}
945+
946+
func (tt extractWithKeysTestCase) test(t *testing.T) {
947+
parser, err := typed.NewParser(tt.schema)
948+
if err != nil {
949+
t.Fatalf("failed to create schema: %v", err)
950+
}
951+
pt := parser.Type(tt.rootTypeName)
952+
953+
for i, triplet := range tt.triplets {
954+
triplet := triplet
955+
t.Run(fmt.Sprintf("%v-valid-%v", tt.name, i), func(t *testing.T) {
956+
t.Parallel()
957+
// source typedValue obj
958+
tv, err := pt.FromYAML(triplet.object)
959+
if err != nil {
960+
t.Fatal(err)
961+
}
962+
gotExtracted := tv.ExtractItems(triplet.set, typed.WithAppendKeyFields())
963+
964+
switch triplet.wantOutput.(type) {
965+
case typed.YAMLObject:
966+
wantOut, err := pt.FromYAML(triplet.wantOutput.(typed.YAMLObject))
967+
if err != nil {
968+
t.Fatalf("unable to parser/validate removeOutput yaml: %v\n%v", err, triplet.wantOutput)
969+
}
970+
971+
if !value.Equals(gotExtracted.AsValue(), wantOut.AsValue()) {
972+
t.Errorf("ExtractItems expected\n%v\nbut got\n%v\n",
973+
value.ToString(wantOut.AsValue()), value.ToString(gotExtracted.AsValue()),
974+
)
975+
}
976+
default:
977+
// The extracted result
978+
wantOut := value.NewValueInterface(triplet.wantOutput)
979+
if !value.Equals(gotExtracted.AsValue(), wantOut) {
980+
t.Errorf("ExtractItems expected\n%v\nbut got\n%v\n",
981+
value.ToString(wantOut), value.ToString(gotExtracted.AsValue()),
982+
)
983+
}
984+
}
985+
})
986+
}
987+
}
988+
989+
// TestExtractWithKeys ensures that when you extract
990+
// items from an object with the AppendKeyField option,
991+
// the key fields are also included in the output.
992+
func TestExtractWithKeys(t *testing.T) {
993+
for _, tt := range extractWithKeysCases {
994+
tt := tt
995+
t.Run(tt.name, func(t *testing.T) {
996+
t.Parallel()
997+
tt.test(t)
998+
})
999+
}
1000+
}

typed/typed.go

+46-1
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,21 @@ const (
3232
AllowDuplicates ValidationOptions = iota
3333
)
3434

35+
// extractItemsOptions is the options available when extracting items.
36+
type extractItemsOptions struct {
37+
appendKeyFields bool
38+
}
39+
40+
type ExtractItemsOption func(*extractItemsOptions)
41+
42+
// WithAppendKeyFields configures ExtractItems to include key fields.
43+
// It is exported for use in configuring ExtractItems.
44+
func WithAppendKeyFields() ExtractItemsOption {
45+
return func(opts *extractItemsOptions) {
46+
opts.appendKeyFields = true
47+
}
48+
}
49+
3550
// AsTyped accepts a value and a type and returns a TypedValue. 'v' must have
3651
// type 'typeName' in the schema. An error is returned if the v doesn't conform
3752
// to the schema.
@@ -187,7 +202,37 @@ func (tv TypedValue) RemoveItems(items *fieldpath.Set) *TypedValue {
187202
}
188203

189204
// ExtractItems returns a value with only the provided list or map items extracted from the value.
190-
func (tv TypedValue) ExtractItems(items *fieldpath.Set) *TypedValue {
205+
func (tv TypedValue) ExtractItems(items *fieldpath.Set, opts ...ExtractItemsOption) *TypedValue {
206+
options := &extractItemsOptions{}
207+
for _, opt := range opts {
208+
opt(options)
209+
}
210+
if options.appendKeyFields {
211+
tvPathSet, err := tv.ToFieldSet()
212+
if err == nil {
213+
keyFieldPathSet := fieldpath.NewSet()
214+
items.Iterate(func(path fieldpath.Path) {
215+
if !tvPathSet.Has(path) {
216+
return
217+
}
218+
for i, pe := range path {
219+
if pe.Key == nil {
220+
continue
221+
}
222+
for _, keyField := range *pe.Key {
223+
keyName := keyField.Name
224+
// Create a new slice with the same elements as path[:i+1], but set its capacity to len(path[:i+1]).
225+
// This ensures that appending to keyFieldPath creates a new underlying array, avoiding accidental
226+
// modification of the original slice (path).
227+
keyFieldPath := append(path[:i+1:i+1], fieldpath.PathElement{FieldName: &keyName})
228+
keyFieldPathSet.Insert(keyFieldPath)
229+
}
230+
}
231+
})
232+
items = items.Union(keyFieldPathSet)
233+
}
234+
}
235+
191236
tv.value = removeItemsWithSchema(tv.value, items, tv.schema, tv.typeRef, true)
192237
return &tv
193238
}

0 commit comments

Comments
 (0)