Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add beginning of top down evaluation #26

Merged
merged 1 commit into from
Apr 18, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@
# Use of this source code is governed by an Apache2
# license that can be found in the LICENSE file.

PACKAGES := github.com/open-policy-agent/opa/opalog/.../ \
github.com/open-policy-agent/opa/cmd/.../
PACKAGES := \
github.com/open-policy-agent/opa/cmd/.../ \
github.com/open-policy-agent/opa/eval/.../ \
github.com/open-policy-agent/opa/opalog/.../

BUILD_COMMIT := $(shell ./build/get-build-commit.sh)
BUILD_TIMESTAMP := $(shell ./build/get-build-timestamp.sh)
Expand Down Expand Up @@ -40,7 +42,7 @@ COVER_PACKAGES=$(PACKAGES)
$(COVER_PACKAGES):
@mkdir -p coverage/$(shell dirname $@)
go test -covermode=count -coverprofile=coverage/$(shell dirname $@)/coverage.out $@
go tool cover -html=coverage/$(shell dirname $@)/coverage.out
go tool cover -html=coverage/$(shell dirname $@)/coverage.out || true

cover: $(COVER_PACKAGES)

Expand Down
148 changes: 148 additions & 0 deletions eval/compare.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
// Copyright 2016 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 eval

import "sort"

// Compare returns 0 if a equals b, -1 if a is less than b, and 1 if b is than a.
//
// For comparison between values of different types, the following ordering is used:
// nil < bool < float64 < string < []interface{} < map[string]interface{}. Slices and maps
// are compared recursively. If one slice or map is a subset of the other slice or map
// it is considered "less than". Nil is always equal to nil.
//
func Compare(a, b interface{}) int {
aSortOrder := sortOrder(a)
bSortOrder := sortOrder(b)
if aSortOrder < bSortOrder {
return -1
} else if bSortOrder < aSortOrder {
return 1
}
switch a := a.(type) {
case nil:
return 0
case bool:
switch b := b.(type) {
case bool:
if a == b {
return 0
}
if !a {
return -1
}
return 1
}
case float64:
switch b := b.(type) {
case float64:
if a == b {
return 0
} else if a < b {
return -1
}
return 1
}
case string:
switch b := b.(type) {
case string:
if a == b {
return 0
} else if a < b {
return -1
}
return 1
}
case []interface{}:
switch b := b.(type) {
case []interface{}:
bLen := len(b)
aLen := len(a)
minLen := aLen
if bLen < minLen {
minLen = bLen
}
for i := 0; i < minLen; i++ {
cmp := Compare(a[i], b[i])
if cmp != 0 {
return cmp
}
}
if aLen == bLen {
return 0
} else if aLen < bLen {
return -1
}
return 1
}
case map[string]interface{}:
switch b := b.(type) {
case map[string]interface{}:
var aKeys []string
for k := range a {
aKeys = append(aKeys, k)
}
var bKeys []string
for k := range b {
bKeys = append(bKeys, k)
}
sort.Strings(aKeys)
sort.Strings(bKeys)
aLen := len(aKeys)
bLen := len(bKeys)
minLen := aLen
if bLen < minLen {
minLen = bLen
}
for i := 0; i < minLen; i++ {
if aKeys[i] < bKeys[i] {
return -1
} else if bKeys[i] < aKeys[i] {
return 1
}
aVal := a[aKeys[i]]
bVal := b[bKeys[i]]
cmp := Compare(aVal, bVal)
if cmp != 0 {
return cmp
}
}
if aLen == bLen {
return 0
} else if aLen < bLen {
return -1
}
return 1
}
}
panic("unreachable")
}

const (
nilSort = iota
boolSort = iota
numberSort = iota
stringSort = iota
arraySort = iota
objectSort = iota
)

func sortOrder(v interface{}) int {
switch v.(type) {
case nil:
return nilSort
case bool:
return boolSort
case float64:
return numberSort
case string:
return stringSort
case []interface{}:
return arraySort
case map[string]interface{}:
return objectSort
}
panic("unreachable")
}
51 changes: 51 additions & 0 deletions eval/compare_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
// Copyright 2016 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 eval

import "testing"
import "math"

func TestCompare(t *testing.T) {
tests := []struct {
a interface{}
b interface{}
expected int
}{
{nil, nil, 0},
{nil, true, -1},
{nil, false, -1},
{false, false, 0},
{false, true, -1},
{true, true, 0},
{true, false, 1},
{true, float64(0), -1},
{float64(0), float64(0), 0},
{float64(0), float64(-1), 1},
{float64(-1), float64(0), -1},
{math.MaxFloat64, math.SmallestNonzeroFloat64, 1},
{float64(-1), "", -1},
{"", "", 0},
{"hello", "", 1},
{"hello world", "hello worldz", -1},
{[]interface{}{}, "", 1},
{[]interface{}{}, []interface{}{}, 0},
{[]interface{}{true, false}, []interface{}{true, nil}, 1},
{[]interface{}{true, true}, []interface{}{true, true}, 0},
{[]interface{}{true, false}, []interface{}{true, true}, -1},
{map[string]interface{}{}, []interface{}{}, 1},
{map[string]interface{}{"foo": []interface{}{true, false}, "bar": []interface{}{true, true}}, map[string]interface{}{"foo": []interface{}{true, false}, "bar": []interface{}{true, true}}, 0},
{map[string]interface{}{"foo": []interface{}{true, false}, "bar": []interface{}{true, nil}}, map[string]interface{}{"foo": []interface{}{true, false}, "bar": []interface{}{true, true}}, -1},
{map[string]interface{}{"foo": []interface{}{true, true}, "bar": []interface{}{true, true}}, map[string]interface{}{"foo": []interface{}{true, false}, "bar": []interface{}{true, true}}, 1},
{map[string]interface{}{"foo": true, "barr": false}, map[string]interface{}{"foo": true, "bar": false}, 1},
{map[string]interface{}{"foo": true, "bar": false, "qux": false}, map[string]interface{}{"foo": true, "bar": false}, 1},
{map[string]interface{}{"foo": true, "bar": false, "baz": false}, map[string]interface{}{"foo": true, "bar": false}, -1},
}
for i, tc := range tests {
result := Compare(tc.a, tc.b)
if result != tc.expected {
t.Errorf("Test case %d: expected %d but got: %d", i, tc.expected, result)
}
}
}
115 changes: 115 additions & 0 deletions eval/storage.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
// Copyright 2016 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 eval

import "fmt"
import "github.com/open-policy-agent/opa/opalog"

// StorageErrorCode represents the collection of error types that can be
// returned by Storage.
type StorageErrorCode int

const (
// StorageInternalErr is used to represent an internal error has occurred. These
// errors are unlikely to be recoverable.
StorageInternalErr StorageErrorCode = iota

// StorageNotFoundErr is used when a given reference does not locate a document
// in Storage. In some cases, this may be recoverable.
StorageNotFoundErr = iota

// StorageNonGroundErr is used if the caller attempts to perform a Storage operation
// on an unground reference.
StorageNonGroundErr = iota
)

// StorageError is the error type returned by Storage functions.
type StorageError struct {

// Code is used to identify the specific reason for the error.
Code StorageErrorCode

// Message can be displayed if the error is not recoverable.
Message string
}

func (err *StorageError) Error() string {
return fmt.Sprintf("storage error (code: %d): %v", err.Code, err.Message)
}

func notFoundError(f string, a ...interface{}) *StorageError {
return &StorageError{
Code: StorageNotFoundErr,
Message: fmt.Sprintf(f, a...),
}
}

// Storage is the backend containing rules and data.
type Storage map[interface{}]interface{}

// NewStorageFromJSONObject returns Storage by converting from map[string]interface{}
func NewStorageFromJSONObject(data map[string]interface{}) Storage {
store := Storage(map[interface{}]interface{}{})
for k, v := range data {
store[k] = v
}
return store
}

// Put inserts a value into storage.
func (store Storage) Put(path opalog.Ref, value interface{}) error {
return nil
}

// Lookup returns the value in Storage referenced by path.
// If the lookup fails, an error is returned with a message indicating
// why the failure occurred.
func (store Storage) Lookup(path opalog.Ref) (interface{}, error) {

if !path.IsGround() {
return nil, &StorageError{Code: StorageNonGroundErr, Message: fmt.Sprintf("cannot lookup non-ground reference: %v", path)}
}

var node interface{} = store

for _, v := range path {
switch n := node.(type) {
case Storage:
// The first element in a reference is always a Var so we have
// to handle this special case and use a type conversion from Var to string.
r, ok := n[string(v.Value.(opalog.Var))]
if !ok {
return nil, notFoundError("cannot find path %v in storage, path references object missing key: %v", path, v)
}
node = r
case map[string]interface{}:
k, ok := v.Value.(opalog.String)
if !ok {
return nil, notFoundError("cannot find path %v in storage, path references object with non-string key: %v", path, v)
}
r, ok := n[string(k)]
if !ok {
return nil, notFoundError("cannot find path %v in storage, path references object missing key: %v", path, v)
}
node = r
case []interface{}:
k, ok := v.Value.(opalog.Number)
if !ok {
return nil, notFoundError("cannot find path %v in storage, path references array with non-numeric key: %v", path, v)
}
idx := int(k)
if idx >= len(n) {
return nil, notFoundError("cannot find path %v in storage, path references array with length: %v", path, len(n))
} else if idx < 0 {
return nil, notFoundError("cannot find path %v in storage, path references array using negative index: %v", path, idx)
}
node = n[idx]
default:
return nil, &StorageError{Code: StorageInternalErr, Message: fmt.Sprintf("cannot lookup object with non-string key: %v", path)}
}
}

return node, nil
}
59 changes: 59 additions & 0 deletions eval/storage_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
// Copyright 2016 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 eval

import "testing"
import "reflect"

func TestStorageLookup(t *testing.T) {

data := loadSmallTestData()

var tests = []struct {
ref string
expected interface{}
}{
{"a[0]", float64(1)},
{"a[3]", float64(4)},
{"b.v1", "hello"},
{"b.v2", "goodbye"},
{"c[0].x[1]", false},
{"c[0].y[0]", nil},
{"c[0].y[1]", 3.14159},
{"d.e[1]", "baz"},
{"d.e", []interface{}{"bar", "baz"}},
{"c[0].z", map[string]interface{}{"p": true, "q": false}},
{"d[100]", notFoundError("cannot find path d[100] in storage, path references object with non-string key: 100")},
{"dead.beef", notFoundError("cannot find path dead.beef in storage, path references object missing key: dead")},
{"a.str", notFoundError("cannot find path a.str in storage, path references array with non-numeric key: \"str\"")},
{"a[100]", notFoundError("cannot find path a[100] in storage, path references array with length: 4")},
{"a[-1]", notFoundError("cannot find path a[-1] in storage, path references array using negative index: -1")},
{"b.vdeadbeef", notFoundError("cannot find path b.vdeadbeef in storage, path references object missing key: \"vdeadbeef\"")},
{"a[i]", &StorageError{Code: StorageNonGroundErr, Message: "cannot lookup non-ground reference: a[i]"}},
}

store := NewStorageFromJSONObject(data)

for idx, tc := range tests {
ref := parseRef(tc.ref)
result, err := store.Lookup(ref)
switch e := tc.expected.(type) {
case error:
if err == nil {
t.Errorf("Test case %d: expected error for %v but got %v", idx+1, ref, result)
} else if !reflect.DeepEqual(err, tc.expected) {
t.Errorf("Test case %d: unexpected error for %v: %v, expected: %v", idx+1, ref, err, e)
}
default:
if err != nil {
t.Errorf("Test case %d: expected success for %v but got %v", idx+1, ref, err)
}
if !reflect.DeepEqual(result, tc.expected) {
t.Errorf("Test case %d: expected %f but got %f", idx+1, tc.expected, result)
}
}
}

}
Loading