diff --git a/ast/builtins.go b/ast/builtins.go index d36fcc362d..5889064785 100644 --- a/ast/builtins.go +++ b/ast/builtins.go @@ -61,6 +61,8 @@ var DefaultBuiltins = [...]*Builtin{ Product, Max, Min, + Any, + All, // Casting ToNumber, @@ -436,6 +438,36 @@ var Min = &Builtin{ ), } +// All takes a list and returns true if all of the items +// are true. A collection of length 0 returns true. +var All = &Builtin{ + Name: "all", + Decl: types.NewFunction( + types.Args( + types.NewAny( + types.NewSet(types.A), + types.NewArray(nil, types.A), + ), + ), + types.B, + ), +} + +// Any takes a collection and returns true if any of the items +// is true. A collection of length 0 returns false. +var Any = &Builtin{ + Name: "any", + Decl: types.NewFunction( + types.Args( + types.NewAny( + types.NewSet(types.A), + types.NewArray(nil, types.A), + ), + ), + types.B, + ), +} + /** * Casting */ diff --git a/docs/book/language-reference.md b/docs/book/language-reference.md index 8450b1cbd7..adab283721 100644 --- a/docs/book/language-reference.md +++ b/docs/book/language-reference.md @@ -42,6 +42,8 @@ complex types. | ``max(array_or_set, output)`` | 1 | ``output`` is the maximum value in ``array_or_set`` | | ``min(array_or_set, output)`` | 1 | ``output`` is the minimum value in ``array_or_set`` | | ``sort(array_or_set, output)`` | 1 | ``output`` is the sorted ``array`` containing elements from ``array_or_set``. | +| ``all(array_or_set, output)`` | 1 | ``output`` is ``true`` if all of the values in ``array_or_set`` are ``true``. A collection of length 0 returns ``true``.| +| ``any(array_or_set, output)`` | 1 | ``output`` is ``true`` if any of the values in ``array_or_set`` is ``true``. A collection of length 0 returns ``false``.| ### Sets diff --git a/topdown/aggregates.go b/topdown/aggregates.go index 68516201c2..0b59487b64 100644 --- a/topdown/aggregates.go +++ b/topdown/aggregates.go @@ -154,6 +154,56 @@ func builtinSort(a ast.Value) (ast.Value, error) { return nil, builtins.NewOperandTypeErr(1, a, "set", "array") } +func builtinAll(a ast.Value) (ast.Value, error) { + switch val := a.(type) { + case ast.Set: + res := true + match := ast.BooleanTerm(true) + val.Foreach(func(term *ast.Term) { + if !term.Equal(match) { + res = false + } + }) + return ast.Boolean(res), nil + case ast.Array: + res := true + match := ast.BooleanTerm(true) + for _, term := range val { + if !term.Equal(match) { + res = false + } + } + return ast.Boolean(res), nil + default: + return nil, builtins.NewOperandTypeErr(1, a, "array", "set") + } +} + +func builtinAny(a ast.Value) (ast.Value, error) { + switch val := a.(type) { + case ast.Set: + res := false + match := ast.BooleanTerm(true) + val.Foreach(func(term *ast.Term) { + if term.Equal(match) { + res = true + } + }) + return ast.Boolean(res), nil + case ast.Array: + res := false + match := ast.BooleanTerm(true) + for _, term := range val { + if term.Equal(match) { + res = true + } + } + return ast.Boolean(res), nil + default: + return nil, builtins.NewOperandTypeErr(1, a, "array", "set") + } +} + func init() { RegisterFunctionalBuiltin1(ast.Count.Name, builtinCount) RegisterFunctionalBuiltin1(ast.Sum.Name, builtinSum) @@ -161,4 +211,6 @@ func init() { RegisterFunctionalBuiltin1(ast.Max.Name, builtinMax) RegisterFunctionalBuiltin1(ast.Min.Name, builtinMin) RegisterFunctionalBuiltin1(ast.Sort.Name, builtinSort) + RegisterFunctionalBuiltin1(ast.Any.Name, builtinAny) + RegisterFunctionalBuiltin1(ast.All.Name, builtinAll) } diff --git a/topdown/aggregates_test.go b/topdown/aggregates_test.go new file mode 100644 index 0000000000..1c89e0c031 --- /dev/null +++ b/topdown/aggregates_test.go @@ -0,0 +1,92 @@ +package topdown + +import ( + "testing" +) + +func TestTopDownAggregates(t *testing.T) { + + tests := []struct { + note string + rules []string + expected interface{} + }{ + {"count", []string{`p[x] { count(a, x) }`}, "[4]"}, + {"count virtual", []string{`p[x] { count([y | q[y]], x) }`, `q[x] { x = a[_] }`}, "[4]"}, + {"count keys", []string{`p[x] { count(b, x) }`}, "[2]"}, + {"count keys virtual", []string{`p[x] { count([k | q[k] = _], x) }`, `q[k] = v { b[k] = v }`}, "[2]"}, + {"count set", []string{`p = x { count(q, x) }`, `q[x] { x = a[_] }`}, "4"}, + {"sum", []string{`p[x] { sum([1, 2, 3, 4], x) }`}, "[10]"}, + {"sum set", []string{`p = x { sum({1, 2, 3, 4}, x) }`}, "10"}, + {"sum virtual", []string{`p[x] { sum([y | q[y]], x) }`, `q[x] { a[_] = x }`}, "[10]"}, + {"sum virtual set", []string{`p = x { sum(q, x) }`, `q[x] { a[_] = x }`}, "10"}, + {"product", []string{"p { product([1,2,3,4], 24) }"}, "true"}, + {"product set", []string{`p = x { product({1, 2, 3, 4}, x) }`}, "24"}, + {"max", []string{`p[x] { max([1, 2, 3, 4], x) }`}, "[4]"}, + {"max set", []string{`p = x { max({1, 2, 3, 4}, x) }`}, "4"}, + {"max virtual", []string{`p[x] { max([y | q[y]], x) }`, `q[x] { a[_] = x }`}, "[4]"}, + {"max virtual set", []string{`p = x { max(q, x) }`, `q[x] { a[_] = x }`}, "4"}, + {"min", []string{`p[x] { min([1, 2, 3, 4], x) }`}, "[1]"}, + {"min dups", []string{`p[x] { min([1, 2, 1, 3, 4], x) }`}, "[1]"}, + {"min out-of-order", []string{`p[x] { min([3, 2, 1, 4, 6, -7, 10], x) }`}, "[-7]"}, + {"min set", []string{`p = x { min({1, 2, 3, 4}, x) }`}, "1"}, + {"min virtual", []string{`p[x] { min([y | q[y]], x) }`, `q[x] { a[_] = x }`}, "[1]"}, + {"min virtual set", []string{`p = x { min(q, x) }`, `q[x] { a[_] = x }`}, "1"}, + {"reduce ref dest", []string{`p = true { max([1, 2, 3, 4], a[3]) }`}, "true"}, + {"reduce ref dest (2)", []string{`p = true { not max([1, 2, 3, 4, 5], a[3]) }`}, "true"}, + {"sort", []string{`p = x { sort([4, 3, 2, 1], x) }`}, "[1 ,2, 3, 4]"}, + {"sort set", []string{`p = x { sort({4,3,2,1}, x) }`}, "[1,2,3,4]"}, + } + + data := loadSmallTestData() + + for _, tc := range tests { + runTopDownTestCase(t, data, tc.note, tc.rules, tc.expected) + } +} + +func TestAll(t *testing.T) { + + tests := []struct { + note string + rules []string + expected interface{} + }{ + {"empty set", []string{`p = x { x := all(set()) }`}, "true"}, + {"empty array", []string{`p = x { x := all([]) }`}, "true"}, + {"set success", []string{`p = x { x := all({true, true, true}) }`}, "true"}, + {"array success", []string{`p = x { x := all( [true, true, true] ) }`}, "true"}, + {"set fail", []string{`p = x { x := all( {true, false, true} ) }`}, "false"}, + {"array fail", []string{`p = x { x := all( [false, true, true] ) }`}, "false"}, + {"other types", []string{`p = x { x := all( [{}, "", true, true, 123] ) }`}, "false"}, + } + + data := loadSmallTestData() + + for _, tc := range tests { + runTopDownTestCase(t, data, tc.note, tc.rules, tc.expected) + } +} + +func TestAny(t *testing.T) { + + tests := []struct { + note string + rules []string + expected interface{} + }{ + {"empty set", []string{`p = x { x := any(set()) }`}, "false"}, + {"empty array", []string{`p = x { x := any([]) }`}, "false"}, + {"set success", []string{`p = x { x := any({false, false, true}) }`}, "true"}, + {"array success", []string{`p = x { x := any( [true, true, true, false, false] ) }`}, "true"}, + {"set fail", []string{`p = x { x := any( {false, false, false} ) }`}, "false"}, + {"array fail", []string{`p = x { x := any( [false] ) }`}, "false"}, + {"other types", []string{`p = x { x := any( [true, {}, "false"] ) }`}, "true"}, + } + + data := loadSmallTestData() + + for _, tc := range tests { + runTopDownTestCase(t, data, tc.note, tc.rules, tc.expected) + } +} diff --git a/topdown/topdown_test.go b/topdown/topdown_test.go index 8f7b6b5348..03aa807538 100644 --- a/topdown/topdown_test.go +++ b/topdown/topdown_test.go @@ -983,47 +983,6 @@ func TestTopDownDefaultKeyword(t *testing.T) { } } -func TestTopDownAggregates(t *testing.T) { - - tests := []struct { - note string - rules []string - expected interface{} - }{ - {"count", []string{`p[x] { count(a, x) }`}, "[4]"}, - {"count virtual", []string{`p[x] { count([y | q[y]], x) }`, `q[x] { x = a[_] }`}, "[4]"}, - {"count keys", []string{`p[x] { count(b, x) }`}, "[2]"}, - {"count keys virtual", []string{`p[x] { count([k | q[k] = _], x) }`, `q[k] = v { b[k] = v }`}, "[2]"}, - {"count set", []string{`p = x { count(q, x) }`, `q[x] { x = a[_] }`}, "4"}, - {"sum", []string{`p[x] { sum([1, 2, 3, 4], x) }`}, "[10]"}, - {"sum set", []string{`p = x { sum({1, 2, 3, 4}, x) }`}, "10"}, - {"sum virtual", []string{`p[x] { sum([y | q[y]], x) }`, `q[x] { a[_] = x }`}, "[10]"}, - {"sum virtual set", []string{`p = x { sum(q, x) }`, `q[x] { a[_] = x }`}, "10"}, - {"product", []string{"p { product([1,2,3,4], 24) }"}, "true"}, - {"product set", []string{`p = x { product({1, 2, 3, 4}, x) }`}, "24"}, - {"max", []string{`p[x] { max([1, 2, 3, 4], x) }`}, "[4]"}, - {"max set", []string{`p = x { max({1, 2, 3, 4}, x) }`}, "4"}, - {"max virtual", []string{`p[x] { max([y | q[y]], x) }`, `q[x] { a[_] = x }`}, "[4]"}, - {"max virtual set", []string{`p = x { max(q, x) }`, `q[x] { a[_] = x }`}, "4"}, - {"min", []string{`p[x] { min([1, 2, 3, 4], x) }`}, "[1]"}, - {"min dups", []string{`p[x] { min([1, 2, 1, 3, 4], x) }`}, "[1]"}, - {"min out-of-order", []string{`p[x] { min([3, 2, 1, 4, 6, -7, 10], x) }`}, "[-7]"}, - {"min set", []string{`p = x { min({1, 2, 3, 4}, x) }`}, "1"}, - {"min virtual", []string{`p[x] { min([y | q[y]], x) }`, `q[x] { a[_] = x }`}, "[1]"}, - {"min virtual set", []string{`p = x { min(q, x) }`, `q[x] { a[_] = x }`}, "1"}, - {"reduce ref dest", []string{`p = true { max([1, 2, 3, 4], a[3]) }`}, "true"}, - {"reduce ref dest (2)", []string{`p = true { not max([1, 2, 3, 4, 5], a[3]) }`}, "true"}, - {"sort", []string{`p = x { sort([4, 3, 2, 1], x) }`}, "[1 ,2, 3, 4]"}, - {"sort set", []string{`p = x { sort({4,3,2,1}, x) }`}, "[1,2,3,4]"}, - } - - data := loadSmallTestData() - - for _, tc := range tests { - runTopDownTestCase(t, data, tc.note, tc.rules, tc.expected) - } -} - func TestTopDownArithmetic(t *testing.T) { tests := []struct { note string