Skip to content

Commit

Permalink
topdown: Add new numbers.range built-in function
Browse files Browse the repository at this point in the history
This commit adds a new built-in function to generate a range of
integers between two values (inclusive). This is useful in certain
cases where users need to enumerate a set of values (e.g., port
numbers).

Fixes #2479

Signed-off-by: Torin Sandall <torinsandall@gmail.com>
  • Loading branch information
tsandall authored and patrick-east committed Jul 14, 2020
1 parent 338583c commit 616d947
Show file tree
Hide file tree
Showing 4 changed files with 121 additions and 0 deletions.
23 changes: 23 additions & 0 deletions ast/builtins.go
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,9 @@ var DefaultBuiltins = [...]*Builtin{
TrimSpace,
Sprintf,

// Numbers
NumbersRange,

// Encoding
JSONMarshal,
JSONUnmarshal,
Expand Down Expand Up @@ -984,6 +987,26 @@ var Sprintf = &Builtin{
),
}

/**
* Numbers
*/

// NumbersRange returns an array of numbers in the given inclusive range.
var NumbersRange = &Builtin{
Name: "numbers.range",
Decl: types.NewFunction(
types.Args(
types.N,
types.N,
),
types.NewArray(nil, types.N),
),
}

/**
* Units
*/

// UnitsParseBytes converts strings like 10GB, 5K, 4mb, and the like into an
// integer number of bytes.
var UnitsParseBytes = &Builtin{
Expand Down
1 change: 1 addition & 0 deletions docs/content/policy-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -273,6 +273,7 @@ complex types.
| <span class="opa-keep-it-together">``z := x % y``</span> | ``z`` is the remainder from the division of ``x`` and ``y`` |
| <span class="opa-keep-it-together">``output := round(x)``</span> | ``output`` is ``x`` rounded to the nearest integer |
| <span class="opa-keep-it-together">``output := abs(x)``</span> | ``output`` is the absolute value of ``x`` |
| <span class="opa-keep-it-together">``output := numbers.range(a, b)``</span> | ``output`` is the range of integer numbers between ``a`` and ``b`` (inclusive). If ``a`` == ``b`` then ``output`` == ``[a]``. If ``a`` < ``b`` the range is in ascending order. If ``a`` > ``b`` the range is in descending order. |

### Aggregates

Expand Down
46 changes: 46 additions & 0 deletions topdown/numbers.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
// Copyright 2020 The OPA Authors. All rights reserved.
// Use of this source code is governed by an Apache2
// license that can be found in the LICENSE file.

package topdown

import (
"math/big"

"github.com/open-policy-agent/opa/ast"
"github.com/open-policy-agent/opa/topdown/builtins"
)

var one = big.NewInt(1)

func builtinNumbersRange(_ BuiltinContext, operands []*ast.Term, iter func(*ast.Term) error) error {

x, err := builtins.BigIntOperand(operands[0].Value, 1)
if err != nil {
return err
}

y, err := builtins.BigIntOperand(operands[1].Value, 2)
if err != nil {
return err
}

var result ast.Array
cmp := x.Cmp(y)

if cmp <= 0 {
for i := new(big.Int).Set(x); i.Cmp(y) <= 0; i = i.Add(i, one) {
result = append(result, ast.NewTerm(builtins.IntToNumber(i)))
}
} else {
for i := new(big.Int).Set(x); i.Cmp(y) >= 0; i = i.Sub(i, one) {
result = append(result, ast.NewTerm(builtins.IntToNumber(i)))
}
}

return iter(ast.NewTerm(result))
}

func init() {
RegisterBuiltinFunc(ast.NumbersRange.Name, builtinNumbersRange)
}
51 changes: 51 additions & 0 deletions topdown/numbers_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
// Copyright 2020 The OPA Authors. All rights reserved.
// Use of this source code is governed by an Apache2
// license that can be found in the LICENSE file.
package topdown

import (
"testing"
)

func TestBuiltinNumbersRange(t *testing.T) {
cases := []struct {
note string
stmt string
exp interface{}
}{
{
note: "one",
stmt: "p = x { x := numbers.range(0, 0) }",
exp: "[0]",
},
{
note: "ascending",
stmt: "p = x { x := numbers.range(-2, 3) }",
exp: "[-2, -1, 0, 1, 2, 3]",
},
{
note: "descending",
stmt: "p = x { x := numbers.range(2, -3) }",
exp: "[2, 1, 0, -1, -2, -3]",
},
{
note: "precision",
stmt: "p { numbers.range(49649733057, 49649733060, [49649733057, 49649733058, 49649733059, 49649733060]) }",
exp: "true",
},
{
note: "error: floating-point number pos 1",
stmt: "p { numbers.range(3.14, 4) }",
exp: &Error{Code: TypeErr, Message: "numbers.range: operand 1 must be integer number but got floating-point number"},
},
{
note: "error: floating-point number pos 2",
stmt: "p { numbers.range(3, 3.14) }",
exp: &Error{Code: TypeErr, Message: "numbers.range: operand 2 must be integer number but got floating-point number"},
},
}

for _, tc := range cases {
runTopDownTestCase(t, map[string]interface{}{}, tc.note, []string{tc.stmt}, tc.exp)
}
}

0 comments on commit 616d947

Please sign in to comment.