Skip to content

Commit 8e5a6e6

Browse files
committed
feat: Adds ExtractItems option to include key fields
Adds an option to `ExtractItems` to include key fields in the output.
1 parent e96ad23 commit 8e5a6e6

File tree

2 files changed

+148
-1
lines changed

2 files changed

+148
-1
lines changed

Diff for: typed/remove_test.go

+113
Original file line numberDiff line numberDiff line change
@@ -885,3 +885,116 @@ func TestReversibleExtract(t *testing.T) {
885885
})
886886
}
887887
}
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 patial 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.AppendKeyFields)
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+
}

Diff for: typed/typed.go

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

35+
// ExtractItemsOptions is the list of all the options available when extracting items.
36+
type ExtractItemsOptions int
37+
38+
const (
39+
// AppendKeyFields means that when extracting items, the key field would also be included.
40+
AppendKeyFields ExtractItemsOptions = iota
41+
)
42+
3543
// AsTyped accepts a value and a type and returns a TypedValue. 'v' must have
3644
// type 'typeName' in the schema. An error is returned if the v doesn't conform
3745
// to the schema.
@@ -187,7 +195,33 @@ func (tv TypedValue) RemoveItems(items *fieldpath.Set) *TypedValue {
187195
}
188196

189197
// 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 {
198+
func (tv TypedValue) ExtractItems(items *fieldpath.Set, opts ...ExtractItemsOptions) *TypedValue {
199+
for _, opt := range opts {
200+
switch opt {
201+
case AppendKeyFields:
202+
tvPathSet, err := tv.ToFieldSet()
203+
if err != nil {
204+
continue
205+
}
206+
keyFieldPathSet := fieldpath.NewSet()
207+
items.Iterate(func(path fieldpath.Path) {
208+
if !tvPathSet.Has(path) {
209+
return
210+
}
211+
for i, pe := range path {
212+
if pe.Key == nil {
213+
continue
214+
}
215+
for _, keyField := range *pe.Key {
216+
keyName := keyField.Name
217+
keyFieldPath := append(path[:i+1:i+1], fieldpath.PathElement{FieldName: &keyName})
218+
keyFieldPathSet.Insert(keyFieldPath)
219+
}
220+
}
221+
})
222+
items = items.Union(keyFieldPathSet)
223+
}
224+
}
191225
tv.value = removeItemsWithSchema(tv.value, items, tv.schema, tv.typeRef, true)
192226
return &tv
193227
}

0 commit comments

Comments
 (0)