Skip to content

Commit

Permalink
Filled out the casting section of builtin functions. I think the to_set
Browse files Browse the repository at this point in the history
and to_array functions require no justification.

For the others: I realized this would be useful to have when I was
doing http calls that returned a types.A and thus when I parsed my code
on occasion it only threw type errors at runtime when I attempted to use
the result in another builtin. It would be great to have some way to
throw these type errors at compile time as well if the
user likes, hence these functions.

Changed names to cast_, added test, added copyright banner, added docs.

Signed-off-by: Varun Mathur <[email protected]>
Signed-off-by: Varun Mathur <[email protected]>
Signed-off-by: Varun Mathur <[email protected]>
  • Loading branch information
vrnmthr authored and tsandall committed Jun 27, 2018
1 parent fd86f74 commit deef34d
Show file tree
Hide file tree
Showing 4 changed files with 227 additions and 1 deletion.
65 changes: 65 additions & 0 deletions ast/builtins.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,12 @@ var DefaultBuiltins = [...]*Builtin{

// Casting
ToNumber,
CastObject,
CastNull,
CastBoolean,
CastString,
CastSet,
CastArray,

// Regular Expressions
RegexMatch,
Expand Down Expand Up @@ -452,6 +458,65 @@ var ToNumber = &Builtin{
),
}

// CastArray checks the underlying type of the input. If it is array or set, an array
// containing the values is returned. If it is not an array, an error is thrown.
var CastArray = &Builtin{
Name: "cast_array",
Decl: types.NewFunction(
types.Args(types.A),
types.NewArray(nil, types.A),
),
}

// CastSet checks the underlying type of the input.
// If it is a set, the set is returned.
// If it is an array, the array is returned in set form (all duplicates removed)
// If neither, an error is thrown
var CastSet = &Builtin{
Name: "cast_set",
Decl: types.NewFunction(
types.Args(types.A),
types.NewSet(types.A),
),
}

// CastString returns input if it is a string; if not returns error.
// For formatting variables, see sprintf
var CastString = &Builtin{
Name: "cast_string",
Decl: types.NewFunction(
types.Args(types.A),
types.S,
),
}

// CastBoolean returns input if it is a boolean; if not returns error.
var CastBoolean = &Builtin{
Name: "cast_boolean",
Decl: types.NewFunction(
types.Args(types.A),
types.B,
),
}

// CastNull returns null if input is null; if not returns error.
var CastNull = &Builtin{
Name: "cast_null",
Decl: types.NewFunction(
types.Args(types.A),
types.NewNull(),
),
}

// CastObject returns the given object if it is null; throws an error otherwise
var CastObject = &Builtin{
Name: "cast_object",
Decl: types.NewFunction(
types.Args(types.A),
types.NewObject(nil, types.NewDynamicProperty(types.A, types.A)),
),
}

/**
* Regular Expressions
*/
Expand Down
6 changes: 6 additions & 0 deletions docs/book/language-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,11 +84,17 @@ complex types.
| <span class="opa-keep-it-together">``to_number(x, output)``</span> | 1 | ``output`` is ``x`` converted to a number |
| <span class="opa-keep-it-together">``is_number(x, output)``</span> | 1 | ``output`` is ``true`` if ``x`` is a number |
| <span class="opa-keep-it-together">``is_string(x, output)``</span> | 1 | ``output`` is ``true`` if ``x`` is a string |
| <span class="opa-keep-it-together">``cast_string(x, output)``</span> | 1 | ``output`` is ``x`` cast to a string |
| <span class="opa-keep-it-together">``is_boolean(x, output)``</span> | 1 | ``output`` is ``true`` if ``x`` is a boolean |
| <span class="opa-keep-it-together">``cast_boolean(x, output)``</span> | 1 | ``output`` is ``x`` cast to a boolean |
| <span class="opa-keep-it-together">``is_array(x, output)``</span> | 1 | ``output`` is ``true`` if ``x`` is an array |
| <span class="opa-keep-it-together">``cast_array(x, output)``</span> | 1 | ``output`` is ``x`` cast to an array |
| <span class="opa-keep-it-together">``is_set(x, output)``</span> | 1 | ``output`` is ``true`` if ``x`` is a set |
| <span class="opa-keep-it-together">``cast_set(x, output)``</span> | 1 | ``output`` is ``x`` cast to a set |
| <span class="opa-keep-it-together">``is_object(x, output)``</span> | 1 | ``output`` is ``true`` if ``x`` is an object |
| <span class="opa-keep-it-together">``cast_object(x, output)``</span> | 1 | ``output`` is ``x`` cast to an object |
| <span class="opa-keep-it-together">``is_null(x, output)``</span> | 1 | ``output`` is ``true`` if ``x`` is null |
| <span class="opa-keep-it-together">``cast_null(x, output)``</span> | 1 | ``output`` is ``x`` cast to null |
| <span class="opa-keep-it-together">``type_name(x, output)``</span> | 1 | ``output`` is the type of ``x`` |

### Encoding
Expand Down
72 changes: 71 additions & 1 deletion topdown/casts.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright 2016 The OPA Authors. All rights reserved.
// Copyright 2018 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.

Expand Down Expand Up @@ -32,6 +32,76 @@ func builtinToNumber(a ast.Value) (ast.Value, error) {
return nil, builtins.NewOperandTypeErr(1, a, "null", "boolean", "number", "string")
}

func builtinToArray(a ast.Value) (ast.Value, error) {
switch val := a.(type) {
case ast.Array:
return val, nil
case ast.Set:
arr := make(ast.Array, val.Len())
i := 0
val.Foreach(func(term *ast.Term) {
arr[i] = term
i++
})
return arr, nil
default:
return nil, builtins.NewOperandTypeErr(1, a, "array", "set")
}
}

func builtinToSet(a ast.Value) (ast.Value, error) {
switch val := a.(type) {
case ast.Array:
return ast.NewSet(val...), nil
case ast.Set:
return val, nil
default:
return nil, builtins.NewOperandTypeErr(1, a, "array", "set")
}
}

func builtinToString(a ast.Value) (ast.Value, error) {
switch val := a.(type) {
case ast.String:
return val, nil
default:
return nil, builtins.NewOperandTypeErr(1, a, "string")
}
}

func builtinToBoolean(a ast.Value) (ast.Value, error) {
switch val := a.(type) {
case ast.Boolean:
return val, nil
default:
return nil, builtins.NewOperandTypeErr(1, a, "boolean")
}
}

func builtinToNull(a ast.Value) (ast.Value, error) {
switch val := a.(type) {
case ast.Null:
return val, nil
default:
return nil, builtins.NewOperandTypeErr(1, a, "null")
}
}

func builtinToObject(a ast.Value) (ast.Value, error) {
switch val := a.(type) {
case ast.Object:
return val, nil
default:
return nil, builtins.NewOperandTypeErr(1, a, "object")
}
}

func init() {
RegisterFunctionalBuiltin1(ast.ToNumber.Name, builtinToNumber)
RegisterFunctionalBuiltin1(ast.CastArray.Name, builtinToArray)
RegisterFunctionalBuiltin1(ast.CastSet.Name, builtinToSet)
RegisterFunctionalBuiltin1(ast.CastString.Name, builtinToString)
RegisterFunctionalBuiltin1(ast.CastBoolean.Name, builtinToBoolean)
RegisterFunctionalBuiltin1(ast.CastNull.Name, builtinToNull)
RegisterFunctionalBuiltin1(ast.CastObject.Name, builtinToObject)
}
85 changes: 85 additions & 0 deletions topdown/casts_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
// Copyright 2018 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 (
"fmt"
"github.com/open-policy-agent/opa/ast"
"testing"
)

func TestToArray(t *testing.T) {

// expected result
expectedResult := []interface{}{1, 2, 3}
resultObj, err := ast.InterfaceToValue(expectedResult)
if err != nil {
panic(err)
}

typeErr := fmt.Errorf("type")

tests := []struct {
note string
rules []string
expected interface{}
}{
{"array input", []string{`p = x { cast_array([1,2,3], x) }`}, resultObj.String()},
{"set input", []string{`p = x { cast_array({1,2,3}, x) }`}, resultObj.String()},
{"bad type", []string{`p = x { cast_array("hello", x) }`}, typeErr},
}

data := loadSmallTestData()

for _, tc := range tests {
runTopDownTestCase(t, data, tc.note, tc.rules, tc.expected)
}
}

func TestToSet(t *testing.T) {

typeErr := fmt.Errorf("type")

tests := []struct {
note string
rules []string
expected interface{}
}{
{"array input", []string{`p = x { cast_set([1,1,1], x) }`}, "[1]"},
{"set input", []string{`p = x { cast_set({1,1,2,3}, x) }`}, "[1,2,3]"},
{"bad type", []string{`p = x { cast_set("hello", x) }`}, typeErr},
}

data := loadSmallTestData()

for _, tc := range tests {
runTopDownTestCase(t, data, tc.note, tc.rules, tc.expected)
}
}

func TestCasts(t *testing.T) {
typeErr := fmt.Errorf("type")

tests := []struct {
note string
rules []string
expected interface{}
}{
{"null valid", []string{`p = x { cast_null(null, x) }`}, "null"},
{"null invalid", []string{`p = x { cast_null({}, x) }`}, typeErr},
//{"string valid", []string{`p = x { cast_string("potato", x) }`}, "potato"},
{"string invalid", []string{`p = x { cast_string({1,1,2,3}, x) }`}, typeErr},
{"boolean valid", []string{`p = x { cast_boolean(false, x) }`}, "false"},
{"boolean valid", []string{`p = x { cast_boolean(1, x) }`}, typeErr},
{"obj valid", []string{`p = x { cast_object({}, x) }`}, "{}"},
{"obj invalid", []string{`p = x { cast_object([1,2,3], x) }`}, typeErr},
}

data := loadSmallTestData()

for _, tc := range tests {
runTopDownTestCase(t, data, tc.note, tc.rules, tc.expected)
}
}

0 comments on commit deef34d

Please sign in to comment.