diff --git a/ast/builtins.go b/ast/builtins.go index bc390c4cef..61b1c81982 100644 --- a/ast/builtins.go +++ b/ast/builtins.go @@ -1431,12 +1431,14 @@ var ObjectSubset = &Builtin{ Description: "Determines if an object `sub` is a subset of another object `super`." + "Object `sub` is a subset of object `super` if and only if every key in `sub` is also in `super`, " + "**and** for all keys which `sub` and `super` share, they have the same value. " + - "This function works with objects, sets, and arrays. " + + "This function works with objects, sets, arrays and a set of array and set." + "If both arguments are objects, then the operation is recursive, e.g. " + "`{\"c\": {\"x\": {10, 15, 20}}` is a subset of `{\"a\": \"b\", \"c\": {\"x\": {10, 15, 20, 25}, \"y\": \"z\"}`. " + "If both arguments are sets, then this function checks if every element of `sub` is a member of `super`, " + "but does not attempt to recurse. If both arguments are arrays, " + "then this function checks if `sub` appears contiguously in order within `super`, " + + "and also does not attempt to recurse. If `super` is array and `sub` is set, " + + "then this function checks if `super` contains every element of `sub` with no consideration of ordering, " + "and also does not attempt to recurse.", Decl: types.NewFunction( types.Args( diff --git a/builtin_metadata.json b/builtin_metadata.json index abdb2aa92d..9062cbdf15 100644 --- a/builtin_metadata.json +++ b/builtin_metadata.json @@ -8565,7 +8565,7 @@ "v0.42.1", "edge" ], - "description": "Determines if an object `sub` is a subset of another object `super`.Object `sub` is a subset of object `super` if and only if every key in `sub` is also in `super`, **and** for all keys which `sub` and `super` share, they have the same value. This function works with objects, sets, and arrays. If both arguments are objects, then the operation is recursive, e.g. `{\"c\": {\"x\": {10, 15, 20}}` is a subset of `{\"a\": \"b\", \"c\": {\"x\": {10, 15, 20, 25}, \"y\": \"z\"}`. If both arguments are sets, then this function checks if every element of `sub` is a member of `super`, but does not attempt to recurse. If both arguments are arrays, then this function checks if `sub` appears contiguously in order within `super`, and also does not attempt to recurse.", + "description": "Determines if an object `sub` is a subset of another object `super`.Object `sub` is a subset of object `super` if and only if every key in `sub` is also in `super`, **and** for all keys which `sub` and `super` share, they have the same value. This function works with objects, sets, arrays and a set of array and set.If both arguments are objects, then the operation is recursive, e.g. `{\"c\": {\"x\": {10, 15, 20}}` is a subset of `{\"a\": \"b\", \"c\": {\"x\": {10, 15, 20, 25}, \"y\": \"z\"}`. If both arguments are sets, then this function checks if every element of `sub` is a member of `super`, but does not attempt to recurse. If both arguments are arrays, then this function checks if `sub` appears contiguously in order within `super`, and also does not attempt to recurse. If `super` is array and `sub` is set, then this function checks if `super` contains every element of `sub` with no consideration of ordering, and also does not attempt to recurse.", "introduced": "v0.42.0", "result": { "description": "`true` if `sub` is a subset of `super`", diff --git a/test/cases/testdata/subset/test-subset.yaml b/test/cases/testdata/subset/test-subset.yaml index 34e9464ce9..91c15b3ea3 100644 --- a/test/cases/testdata/subset/test-subset.yaml +++ b/test/cases/testdata/subset/test-subset.yaml @@ -345,3 +345,44 @@ cases: want_result: - x: true + - data: + modules: + - | + package test + + A := [1,2,3,4,5,6] + B := {4,3,2} + + AsubB := object.subset(A, B) + BsubA := object.subset(B, A) + + test_result { + AsubB # B is a subset of A + not BsubA # It is invalid operands + } + + note: subset/array and set 1 + query: data.test.test_result = x + want_result: + - x: true + + - data: + modules: + - | + package test + + A := [1,2,3,4,5,6] + B := {9,8,7} + + AsubB := object.subset(A, B) + BsubA := object.subset(B, A) + + test_result { + not AsubB # B is not a subset of A + not BsubA # It is invalid operands + } + + note: subset/array and set 2 + query: data.test.test_result = x + want_result: + - x: true \ No newline at end of file diff --git a/topdown/subset.go b/topdown/subset.go index 9273ac3998..19c6571c02 100644 --- a/topdown/subset.go +++ b/topdown/subset.go @@ -63,6 +63,24 @@ func bothArrays(t1, t2 *ast.Term) (bool, *ast.Array, *ast.Array) { return true, array1, array2 } +func arraySet(t1, t2 *ast.Term) (bool, *ast.Array, ast.Set) { + if (t1 == nil) || (t2 == nil) { + return false, nil, nil + } + + array, ok := t1.Value.(*ast.Array) + if !ok { + return false, nil, nil + } + + set, ok := t2.Value.(ast.Set) + if !ok { + return false, nil, nil + } + + return true, array, set +} + // objectSubset implements the subset operation on a pair of objects. // // This function will try to recursively apply the subset operation where it @@ -194,6 +212,25 @@ func arraySubset(super, sub *ast.Array) bool { } } +// arraySetSubset implements the subset operation on array and set. +// +// This is defined to mean that the entire "sub" set must appear in +// the "super" array with no consideration of ordering. +// For the same rationale as setSubset(), we do not attempt +// to recurse into values. +func arraySetSubset(super *ast.Array, sub ast.Set) bool { + unmatched := sub.Len() + return super.Until(func(t *ast.Term) bool { + if sub.Contains(t) { + unmatched-- + } + if unmatched == 0 { + return true + } + return false + }) +} + func builtinObjectSubset(_ BuiltinContext, operands []*ast.Term, iter func(*ast.Term) error) error { superTerm := operands[0] subTerm := operands[1] @@ -213,7 +250,12 @@ func builtinObjectSubset(_ BuiltinContext, operands []*ast.Term, iter func(*ast. return iter(ast.BooleanTerm(arraySubset(superArray, subArray))) } - return builtins.ErrOperand("both arguments object.subset must be of the same type") + if ok, superArray, subSet := arraySet(superTerm, subTerm); ok { + // Super operand is array and sub operand is set + return iter(ast.BooleanTerm(arraySetSubset(superArray, subSet))) + } + + return builtins.ErrOperand("both arguments object.subset must be of the same type or array and set") } func init() {