Add variables to the grammar

This change set also includes a bit of refactoring:

- Renamed jsonlog to opalog
- Renamed Dictionary to Object
- Split AST into separate files
- Tweaked parser definition to separate terms with whitespace
- Renamed helper functions in parser_test.go to distinguish from cases
- Moved reflection helper into test suite and renamed
- Modified Term.String() to make output more readable
- Reorganized the grammar file
- Allow scalars and variables as object keys. We will deal with this when
serializing to JSON.
This commit is contained in:
Torin Sandall
2016-03-30 09:35:09 -07:00
parent feaa302fce
commit 8ed63b76dd
15 changed files with 1133 additions and 821 deletions
+1 -1
View File
@@ -2,7 +2,7 @@
# 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/jsonlog/.../ \
PACKAGES := github.com/open-policy-agent/opa/opalog/.../ \
github.com/open-policy-agent/opa/cmd/.../
BUILD_COMMIT := $(shell ./build/get-build-commit.sh)
+1 -1
View File
@@ -101,7 +101,7 @@ If you need to update the dependencies:
## Opalog
If you need to modify the Opalog syntax you must update jsonlog/parser.peg
If you need to modify the Opalog syntax you must update opalog/parser.peg
and run `make generate` to re-generate the parser code.
> If you encounter an error because "pigeon" is not installed, run `glide
-113
View File
@@ -1,113 +0,0 @@
{
// Command json parses JSON as defined by [1].
//
// BUGS: the escaped forward solidus (`\/`) is not currently handled for strings.
//
// TODO: check if JSON's numbers are a subset of Go's numbers, since we are
// assuming they are. Currently representing all numbers as float64.
// TODO: think about relaxing a Dictionary to allow numeric keys, as they are common in practice.
// [1]: http://www.ecma-international.org/publications/files/ECMA-ST/ECMA-404.pdf
package jsonlog
func toIfaceSlice(v interface{}) []interface{} {
if v == nil {
return nil
}
return v.([]interface{})
}
}
Prog <- _ vals:Term+ EOF {
if vals == nil {
return make([]interface{}, 0), nil
}
return vals.([]interface{}), nil
}
Term <- val:( Dictionary / Array / Number / String / Bool / Null ) _ {
return val, nil
}
Dictionary <- '{' _ vals:( String _ ':' _ Term ( ',' _ String _ ':' _ Term )* )? '}' {
valsSl := toIfaceSlice(vals)
if len(valsSl) == 0 {
return NewTerm([]*Term{}, DICTIONARY, c.text, "", c.pos.line, c.pos.col), nil
}
restSl := toIfaceSlice(valsSl[5])
// Storing dictionary arguments as a set of KeyValue pairs since we may not be
// able to evaluate the keys (e.g. the key may be a variable)
res := NewKeyValueSet()
res.Add(NewKeyValue(valsSl[0].(*Term), valsSl[4].(*Term)))
for _, v := range restSl {
vSl := toIfaceSlice(v)
res.Add(NewKeyValue(vSl[2].(*Term), vSl[6].(*Term)))
}
t := NewTerm(res, DICTIONARY, c.text, "", c.pos.line, c.pos.col)
return t, nil //res, nil
}
Array <- '[' _ vals:( Value ( ',' _ Value )* )? ']' {
valsSl := toIfaceSlice(vals)
if len(valsSl) == 0 {
return NewTerm([]*Term{}, ARRAY, c.text, "", c.pos.line, c.pos.col), nil
}
restSl := toIfaceSlice(valsSl[1])
res := make([]*Term, 1 + len(valsSl))
for i, v := range restSl {
vSl := toIfaceSlice(v)
res[i] = vSl[2].(*Term)
}
t := NewTerm(res, ARRAY, c.text, "", c.pos.line, c.pos.col)
return t, nil //res, nil
}
Number <- '-'? Integer ( '.' DecimalDigit+ )? Exponent? {
// JSON numbers have the same syntax as Go's, and are parseable using
// strconv.
v, err := strconv.ParseFloat(string(c.text), 64)
t := NewTerm(v, NUMBER, c.text, "", c.pos.line, c.pos.col)
return t, err
}
Integer <- '0' / NonZeroDecimalDigit DecimalDigit*
Exponent <- 'e'i [+-]? DecimalDigit+
String <- '"' ( !EscapedChar . / '\\' EscapeSequence )* '"' {
// TODO : the forward slash (solidus) is not a valid escape in Go, it will
// fail if there's one in the string
v, err := strconv.Unquote(string(c.text))
t := NewTerm(v, STRING, c.text, "", c.pos.line, c.pos.col)
return t, err // v, err
}
EscapedChar <- [\x00-\x1f"\\]
EscapeSequence <- SingleCharEscape / UnicodeEscape
SingleCharEscape <- ["\\/bfnrt]
UnicodeEscape <- 'u' HexDigit HexDigit HexDigit HexDigit
DecimalDigit <- [0-9]
NonZeroDecimalDigit <- [1-9]
HexDigit <- [0-9a-f]i
Bool <- "true" {
t := NewTerm(true, BOOLEAN, c.text, "", c.pos.line, c.pos.col)
return t, nil // true, nil
} / "false" {
t := NewTerm(false, BOOLEAN, c.text, "", c.pos.line, c.pos.col)
return t, nil // false, nil
}
Null <- "null" {
t := NewTerm(nil, NULL, c.text, "", c.pos.line, c.pos.col)
return t, nil //nil, nil
}
_ "whitespace" <- [ \t\r\n]*
EOF <- !.
-117
View File
@@ -1,117 +0,0 @@
// Copyright 2015 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 jsonlog
import (
"fmt"
"testing"
)
var _ = fmt.Printf
func testTermEqual(t *testing.T, x *Term, y *Term) {
if !x.Equal(y) {
t.Errorf("Failure on equality: \n%s and \n%s\n", x, y)
}
}
func testTermNotEqual(t *testing.T, x *Term, y *Term) {
if x.Equal(y) {
t.Errorf("Failure on non-equality: \n%s and \n%s\n", x, y)
}
}
// Test equality on pure-json terms
func TestEqualJsonTerms(t *testing.T) {
testTermEqual(t, NewNull(), NewNull())
testTermEqual(t, GoTerm(true), GoTerm(true))
testTermEqual(t, GoTerm(5), GoTerm(5))
testTermEqual(t, GoTerm("a string"), GoTerm("a string"))
testTermEqual(t, GoTerm(map[int]int{1: 2}), GoTerm(map[int]int{1: 2}))
testTermEqual(t, GoTerm(map[int]int{1: 2, 3: 4}), GoTerm(map[int]int{1: 2, 3: 4}))
testTermEqual(t, GoTerm([]int{1, 2, 3}), GoTerm([]int{1, 2, 3}))
testTermNotEqual(t, NewNull(), GoTerm(true))
testTermNotEqual(t, GoTerm(true), GoTerm(false))
testTermNotEqual(t, GoTerm(5), GoTerm(7))
testTermNotEqual(t, GoTerm("a string"), GoTerm("abc"))
testTermNotEqual(t, GoTerm(map[int]int{3: 2}), GoTerm(map[int]int{1: 2}))
testTermNotEqual(t, GoTerm(map[int]int{1: 2, 3: 7}), GoTerm(map[int]int{1: 2, 3: 4}))
testTermNotEqual(t, GoTerm(5), GoTerm("a string"))
testTermNotEqual(t, GoTerm(1), GoTerm(true))
testTermNotEqual(t, GoTerm(map[int]int{1: 2, 3: 7}), GoTerm([]int{1, 2, 3, 7}))
testTermNotEqual(t, GoTerm([]int{1, 2, 3}), GoTerm([]int{1, 2, 4}))
}
func testParse1Term(t *testing.T, msg string, expr string, correct *Term) interface{} {
p, err := Parse("", []byte(expr))
if err != nil {
t.Errorf("Error on test %s: parse error on %s: %s", msg, expr, err)
return nil
}
parsed := p.([]interface{})
if len(parsed) != 1 {
t.Errorf("Error on test %s: failed to parse 1 element from %s: %v",
msg, expr, parsed)
return nil
}
term := parsed[0].(*Term)
if !term.Equal(correct) {
t.Errorf("Error on test %s: wrong result on %s. Actual = %v; Correct = %v",
msg, expr, term, correct)
return nil
}
return parsed[0]
}
func testParse1TermFail(t *testing.T, msg string, expr string) {
p, err := Parse("", []byte(expr))
if err != nil {
return
}
parsed := p.([]interface{})
if len(parsed) != 1 {
t.Errorf("Error on test %s: failed to parse 1 element from %s: %v", msg, expr, parsed)
} else {
t.Errorf("Error on test %s: failed to error when parsing %v: %v", msg, expr, parsed)
}
}
func TestScalarTerms(t *testing.T) {
testParse1Term(t, "null", "null", NewNull())
testParse1Term(t, "true", "true", GoTerm(true))
testParse1Term(t, "false", "false", GoTerm(false))
testParse1Term(t, "integer", "53", GoTerm(53))
testParse1Term(t, "integer2", "-53", GoTerm(-53))
testParse1Term(t, "float", "16.7", GoTerm(16.7))
testParse1Term(t, "float2", "-16.7", GoTerm(-16.7))
testParse1Term(t, "exponent", "6e7", GoTerm(6e7))
testParse1Term(t, "string", "\"a string\"", GoTerm("a string"))
testParse1Term(t, "string", "\"a string u6abc7def8abc0def with unicode\"",
GoTerm("a string u6abc7def8abc0def with unicode"))
testParse1TermFail(t, "hex", "6abc")
testParse1TermFail(t, "non-string", "'a string'")
testParse1TermFail(t, "non-bool", "True")
testParse1TermFail(t, "non-bool", "False")
testParse1TermFail(t, "non-number", "6zxy")
testParse1TermFail(t, "non-number2", "6d7")
}
func TestDictionaryTerms(t *testing.T) {
correct := GoTerm(map[string]int{"abc": 7, "def": 8})
testParse1Term(t, "simple dict", "{\"abc\": 7, \"def\": 8}", correct)
}
// func TestVariables(t *testing.T) {
// testParse(t, "variable", "\"a string\"")
// }
// NewNull creates a new NULL term for testing.
// Special case since nil could be either NULL or
// an empty array.
func NewNull() *Term {
return NewTerm(nil, NULL, []byte(""), "", 0, 0)
}
-240
View File
@@ -1,240 +0,0 @@
// Copyright 2015 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 jsonlog
import (
"reflect"
"fmt"
)
const (
// note: iota is built into Go and is auto-incremented
NULL = iota
BOOLEAN = iota
NUMBER = iota
STRING = iota
VARIABLE = iota
REFERENCE = iota
ARRAY = iota
DICTIONARY = iota
)
// Set is a collection of objects that we can't use a map for.
type Set struct {
Values []interface{}
EqualFunc func(interface{}, interface{}) bool
}
// NewSet returns a new set
func NewSet(equal func(interface{}, interface{}) bool) *Set {
s := Set{Values: make([]interface{}, 0), EqualFunc: equal}
return &s
}
// NewKeyValueSet returns a set for KeyValue pairs
func NewKeyValueSet() *Set {
f := func(x interface{}, y interface{}) bool {
kvx := x.(*KeyValue)
kvy := y.(*KeyValue)
return kvx.Equal(kvy)
}
return NewSet(f)
}
// Add an element
func (s *Set) Add (x interface{}) {
if !s.Contains(x) {
s.Values = append(s.Values, x)
}
}
// Contains returns true if the set contains element x
func (s *Set) Contains (x interface{}) bool {
for _, elem := range s.Values {
if s.EqualFunc(x, elem) {
return true
}
}
return false
}
// Length returns number of elements
func (s *Set) Length () int {
return len(s.Values)
}
// Equal returns True if the 2 sets have all the same elements
func (set1 *Set) Equal (set2 *Set) bool {
if len(set1.Values) != len(set2.Values) {
return false
}
// TODO: reimplement natively so we don't use memory
diff12 := set1.Difference(set2)
if diff12.Length() > 0 {
return false
}
diff21 := set2.Difference(set1)
if diff21.Length() > 0 {
return false
}
return true
}
// Difference returns a new set that has all the elements of set1 except those in set2
func (set1 *Set) Difference (set2 *Set) *Set {
newset := NewSet(set1.EqualFunc)
for _, elem := range set1.Values {
if !set2.Contains(elem) {
newset.Add(elem)
}
}
return newset
}
// Location records a position in source code
type Location struct {
File string
Row int
Col int
}
// NewLocation creates a new instance of a location
func NewLocation(file string, row int, col int) *Location {
l := Location{File: file, Row: row, Col: col}
return &l
}
// Term is an argument to a function
type Term struct {
Value interface{} // actual value, as represented by Go
Kind int // type of Term: one of the consts defined above
Name []byte // original string representation
Location *Location // text location in original source
}
// NewTerm creates a new Term
func NewTerm(x interface{}, kind int, orig []byte, file string, row int, col int) *Term {
t := Term{Value: x, Kind: kind, Name: orig, Location: NewLocation(file, row, col)}
return &t
}
// String prints out a string version of Term.
func (t *Term) String() string {
return fmt.Sprintf("Term<Value: %v, Kind: %v, Name: %s>", t.Value, t.Kind, t.Name)
}
// KeyValue represents a single key-value pair for a dictionary
type KeyValue struct {
Key *Term
Value *Term
}
// NewKeyValue creates a key-value pair
func NewKeyValue(key *Term, value *Term) *KeyValue {
kv := KeyValue{Key: key, Value: value}
return &kv
}
// String converts a KeyValue into a string
func (kv *KeyValue) String() string {
return fmt.Sprintf("KeyValue<Key: %s, Value: %s>", kv.Key, kv.Value)
}
// Equal returns T if the keys and values are the same
func (kv1 *KeyValue) Equal(kv2 *KeyValue) bool {
return kv1.Key.Equal(kv2.Key) && kv1.Value.Equal(kv2.Value)
}
// Equal checks if two terms are equal for their Value and Kind fields.
// Ignores differences in pointers.
// Will infinite loop on circular Terms (which are never generated by the parser).
func (term1 *Term) Equal (term2 *Term) bool {
// pointer equality
if term1 == term2 {
return true
}
// wrong types
if term1.Kind != term2.Kind {
return false
}
// recursive cases
switch term1.Kind {
case DICTIONARY:
// A dictionary is a list of key/value pairs because
// the keys may not be simple strings in the language
set1 := term1.Value.(*Set)
set2 := term2.Value.(*Set)
return set1.Equal(set2)
case ARRAY:
// Golang Value objs for each of the Terms' .Value fields
arr1 := term1.Value.([]*Term)
arr2 := term2.Value.([]*Term)
if len(arr1) != len(arr2) {
return false
}
for i := 0; i < len(arr1); i++ {
if !arr1[i].Equal(arr2[i]) {
return false
}
}
return true
default:
return term1.Value == term2.Value
}
}
// GoTerm creates a Jsonlog Term from a Go object
func GoTerm(x interface{}) *Term {
var val interface{}
var typ int
switch reflect.TypeOf(x).Kind() {
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64,
reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
val = float64(reflect.ValueOf(x).Int())
typ = NUMBER
case reflect.Float32, reflect.Float64:
val = float64(reflect.ValueOf(x).Float())
typ = NUMBER
case reflect.String:
val = x
typ = STRING
case reflect.Bool:
val = x
typ = BOOLEAN
case reflect.Map:
kvset := NewKeyValueSet()
xval := reflect.ValueOf(x)
for _, key := range xval.MapKeys() {
kvset.Add(NewKeyValue(GoTerm(key.Interface()), GoTerm(xval.MapIndex(key).Interface())))
}
val = kvset
typ = DICTIONARY
case reflect.Slice, reflect.Array:
xval := reflect.ValueOf(x)
length := xval.Len()
arr := make([]*Term, length)
for i := 0; i < length; i++ {
arr[i] = GoTerm(xval.Index(i).Interface())
}
val = arr
typ = ARRAY
default:
val = x
typ = NULL
}
return NewTerm(val, typ, []byte(""), "", 0, 0)
}
// returns the result of dereferencing val and
// any pointers pointed to by val
func dePointer(val reflect.Value) reflect.Value {
switch val.Kind() {
case reflect.Ptr:
return dePointer(val.Elem())
default:
return val
}
}
+2 -2
View File
@@ -17,5 +17,5 @@ func main() {
// Opalog parser generation:
//
//go:generate pigeon -o jsonlog/parser.go jsonlog/jsonlog.peg
//go:generate goimports -w jsonlog/parser.go
//go:generate pigeon -o opalog/parser.go opalog/opalog.peg
//go:generate goimports -w opalog/parser.go
+137
View File
@@ -0,0 +1,137 @@
{
package opalog
//
// BUGS: the escaped forward solidus (`\/`) is not currently handled for strings.
//
}
Prog <- _ head:Term tail:( ws Term )* EOF {
if head == nil {
return make([]interface{}, 0), nil
}
tailSlice := tail.([]interface{})
return append([]interface{}{head}, tailSlice...), nil
}
Term <- val:( Composite / Scalar / Var ) {
return val, nil
}
Composite <- Object / Array
Scalar <- Number / String / Bool / Null
Key <- Scalar / Var
Object <- '{' _ head:(Key _ ':' _ Term)? tail:( _ ',' _ Key _ ':' _ Term )* _ '}' {
set := NewKeyValueSet()
// Empty object.
if head == nil {
return NewTerm(set, OBJECT, c.text, "", c.pos.line, c.pos.col), nil
}
// Non-empty object, first key/value pair.
// The "head" variable is a slice containing exactly 5 elements (see rule definition above):
// [key whitespace colon whitespace key] where the whitespace elements may be nil.
headSlice := head.([]interface{})
set.Add(NewKeyValue(headSlice[0].(*Term), headSlice[len(headSlice) - 1].(*Term)))
// Non-empty object, remaining key/value pairs.
tailSlice := tail.([]interface{})
for _, v := range tailSlice {
s := v.([]interface{})
// The "s" variable is a slice containing exactly 8 elements (see rule definition above).
// This is similar to the "head" variable."
set.Add(NewKeyValue(s[3].(*Term), s[len(s) - 1].(*Term)))
}
result := NewTerm(set, OBJECT, c.text, "", c.pos.line, c.pos.col)
return result, nil
}
Array <- '[' _ head:Term? tail:(_ ',' _ Term)* _ ']' {
// Empty array.
if head == nil {
return NewTerm([]*Term{}, ARRAY, c.text, "", c.pos.line, c.pos.col), nil
}
// Non-empty array, first element.
var arr []*Term
arr = append(arr, head.(*Term))
// Non-empty array, remaining elements.
tailSlice := tail.([]interface{})
for _, v := range tailSlice {
s := v.([]interface{})
// The "s" is a slice containing exactly 4 elements (see rule definition above).
// [whitespace comma whitespace value] where the whitespace elements may be nil.
arr = append(arr, s[len(s) - 1].(*Term))
}
result := NewTerm(arr, ARRAY, c.text, "", c.pos.line, c.pos.col)
return result, nil
}
Var <- vals:( AsciiLetter (AsciiLetter / DecimalDigit)* ) {
v := &Var{string(c.text)}
t := NewTerm(v, VAR, c.text, "", c.pos.line, c.pos.col)
return t, nil
}
Number <- '-'? Integer ( '.' DecimalDigit+ )? Exponent? {
// JSON numbers have the same syntax as Go's, and are parseable using
// strconv.
v, err := strconv.ParseFloat(string(c.text), 64)
t := NewTerm(v, NUMBER, c.text, "", c.pos.line, c.pos.col)
return t, err
}
String <- '"' ( !EscapedChar . / '\\' EscapeSequence )* '"' {
// TODO : the forward slash (solidus) is not a valid escape in Go, it will
// fail if there's one in the string
v, err := strconv.Unquote(string(c.text))
t := NewTerm(v, STRING, c.text, "", c.pos.line, c.pos.col)
return t, err // v, err
}
Bool <- "true" {
t := NewTerm(true, BOOLEAN, c.text, "", c.pos.line, c.pos.col)
return t, nil
} / "false" {
t := NewTerm(false, BOOLEAN, c.text, "", c.pos.line, c.pos.col)
return t, nil
}
Null <- "null" {
t := NewTerm(nil, NULL, c.text, "", c.pos.line, c.pos.col)
return t, nil
}
Integer <- '0' / NonZeroDecimalDigit DecimalDigit*
Exponent <- 'e'i [+-]? DecimalDigit+
AsciiLetter <- [A-Za-z_]
EscapedChar <- [\x00-\x1f"\\]
EscapeSequence <- SingleCharEscape / UnicodeEscape
SingleCharEscape <- ["\\/bfnrt]
UnicodeEscape <- 'u' HexDigit HexDigit HexDigit HexDigit
DecimalDigit <- [0-9]
NonZeroDecimalDigit <- [1-9]
HexDigit <- [0-9a-f]i
_ "whitespace" <- [ \t\r\n]*
ws "whitespace" <- [ \t\r\n]+
EOF <- !.
File diff suppressed because it is too large Load Diff
+207
View File
@@ -0,0 +1,207 @@
// 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 opalog
import (
"fmt"
"reflect"
"testing"
)
var _ = fmt.Printf
func TestScalarTerms(t *testing.T) {
assertParseOneTerm(t, "null", "null", reflectTerm(nil))
assertParseOneTerm(t, "true", "true", reflectTerm(true))
assertParseOneTerm(t, "false", "false", reflectTerm(false))
assertParseOneTerm(t, "integer", "53", reflectTerm(53))
assertParseOneTerm(t, "integer2", "-53", reflectTerm(-53))
assertParseOneTerm(t, "float", "16.7", reflectTerm(16.7))
assertParseOneTerm(t, "float2", "-16.7", reflectTerm(-16.7))
assertParseOneTerm(t, "exponent", "6e7", reflectTerm(6e7))
assertParseOneTerm(t, "string", "\"a string\"", reflectTerm("a string"))
assertParseOneTerm(t, "string", "\"a string u6abc7def8abc0def with unicode\"",
reflectTerm("a string u6abc7def8abc0def with unicode"))
assertParseOneTermFail(t, "hex", "6abc")
assertParseOneTermFail(t, "non-string", "'a string'")
assertParseOneTermFail(t, "non-number", "6zxy")
assertParseOneTermFail(t, "non-number2", "6d7")
assertParseOneTermFail(t, "non-number3", "6\"foo\"")
assertParseOneTermFail(t, "non-number4", "6true")
assertParseOneTermFail(t, "non-number5", "6false")
assertParseOneTermFail(t, "non-number6", "6[null, null]")
assertParseOneTermFail(t, "non-number7", "6{\"foo\": \"bar\"}")
assertParseOneTermFail(t, "out-of-range", "1e1000")
}
func TestVarTerms(t *testing.T) {
assertParseOneTerm(t, "var", "foo", reflectTerm(NewVar("foo")))
assertParseOneTerm(t, "var", "foo_bar", reflectTerm(NewVar("foo_bar")))
assertParseOneTerm(t, "var", "foo0", reflectTerm(NewVar("foo0")))
assertParseOneTermFail(t, "non-var", "foo-bar")
assertParseOneTermFail(t, "non-var2", "foo-7")
}
func TestObjectWithScalars(t *testing.T) {
assertParseOneTerm(t, "number", "{\"abc\": 7, \"def\": 8}", reflectTerm(map[string]int{"abc": 7, "def": 8}))
assertParseOneTerm(t, "bool", "{\"abc\": false, \"def\": true}", reflectTerm(map[string]bool{"abc": false, "def": true}))
assertParseOneTerm(t, "string", "{\"abc\": \"foo\", \"def\": \"bar\"}", reflectTerm(map[string]string{"abc": "foo", "def": "bar"}))
assertParseOneTerm(t, "mixed", "{\"abc\": 7, \"def\": null}", reflectTerm(map[string]interface{}{"abc": 7, "def": nil}))
assertParseOneTerm(t, "number key", "{8: 7, \"def\": null}", reflectTerm(map[interface{}]interface{}{8: 7, "def": nil}))
assertParseOneTerm(t, "number key 2", "{8.5: 7, \"def\": null}", reflectTerm(map[interface{}]interface{}{8.5: 7, "def": nil}))
assertParseOneTerm(t, "bool key", "{true: false}", reflectTerm(map[bool]bool{true: false}))
}
func TestObjectWithVars(t *testing.T) {
assertParseOneTerm(t, "var keys", "{foo: \"bar\", bar: 64}", newObjectTerm([]*KeyValue{
NewKeyValue(reflectTerm(NewVar("foo")), reflectTerm("bar")),
NewKeyValue(reflectTerm(NewVar("bar")), reflectTerm(64)),
}))
assertParseOneTerm(t, "nested var keys", "{baz: {foo: \"bar\", bar: qux}}", newObjectTerm([]*KeyValue{
NewKeyValue(reflectTerm(NewVar("baz")), newObjectTerm([]*KeyValue{
NewKeyValue(reflectTerm(NewVar("foo")), reflectTerm("bar")),
NewKeyValue(reflectTerm(NewVar("bar")), reflectTerm(NewVar("qux"))),
})),
}))
}
func TestArrayWithScalars(t *testing.T) {
assertParseOneTerm(t, "number", "[1,2,3,4.5]", reflectTerm([]float64{1, 2, 3, 4.5}))
assertParseOneTerm(t, "bool", "[true, false, true]", reflectTerm([]bool{true, false, true}))
assertParseOneTerm(t, "string", "[\"foo\", \"bar\"]", reflectTerm([]string{"foo", "bar"}))
assertParseOneTerm(t, "mixed", "[null, true, 42]", reflectTerm([]interface{}{nil, true, 42}))
}
func TestArrayWithVars(t *testing.T) {
assertParseOneTerm(t, "var elements", "[foo, bar, 42]", newArrayTerm([]*Term{reflectTerm(NewVar("foo")), reflectTerm(NewVar("bar")), reflectTerm(42)}))
assertParseOneTerm(t, "nested var elements", "[[foo, true], [null, bar], 42]", newArrayTerm(
[]*Term{
newArrayTerm([]*Term{reflectTerm(NewVar("foo")), reflectTerm(true)}),
newArrayTerm([]*Term{reflectTerm(nil), reflectTerm(NewVar("bar"))}),
reflectTerm(42),
},
))
}
func TestNestedComposites(t *testing.T) {
assertParseOneTerm(t, "nested composites", "[{foo: [\"bar\", baz]}]", newArrayTerm([]*Term{
newObjectTerm([]*KeyValue{
NewKeyValue(reflectTerm(NewVar("foo")), newArrayTerm([]*Term{
reflectTerm("bar"), reflectTerm(NewVar("baz")),
})),
}),
}))
}
func assertTermEqual(t *testing.T, x *Term, y *Term) {
if !x.Equal(y) {
t.Errorf("Failure on equality: \n%s and \n%s\n", x, y)
}
}
func assertTermNotEqual(t *testing.T, x *Term, y *Term) {
if x.Equal(y) {
t.Errorf("Failure on non-equality: \n%s and \n%s\n", x, y)
}
}
func assertParseOneTerm(t *testing.T, msg string, expr string, correct *Term) interface{} {
p, err := Parse("", []byte(expr))
if err != nil {
t.Errorf("Error on test %s: parse error on %s: %s", msg, expr, err)
return nil
}
parsed := p.([]interface{})
if len(parsed) != 1 {
t.Errorf("Error on test %s: failed to parse 1 element from %s: %v",
msg, expr, parsed)
return nil
}
term := parsed[0].(*Term)
if !term.Equal(correct) {
t.Errorf("Error on test %s: wrong result on %s. Actual = %v; Correct = %v",
msg, expr, term, correct)
return nil
}
return parsed[0]
}
func assertParseOneTermFail(t *testing.T, msg string, expr string) {
p, err := Parse("", []byte(expr))
if err != nil {
return
}
parsed := p.([]interface{})
if len(parsed) != 1 {
t.Errorf("Error on test %s: failed to parse 1 element from %s: %v", msg, expr, parsed)
} else {
t.Errorf("Error on test %s: failed to error when parsing %v: %v", msg, expr, parsed)
}
}
func newObjectTerm(o []*KeyValue) *Term {
set := NewKeyValueSet()
for _, v := range o {
set.Add(v)
}
return NewTerm(set, OBJECT, []byte(""), "", 0, 0)
}
func newArrayTerm(arr []*Term) *Term {
return NewTerm(arr, ARRAY, []byte(""), "", 0, 0)
}
func reflectTerm(x interface{}) *Term {
if x == nil {
return NewTerm(nil, NULL, []byte(""), "", 0, 0)
}
if v, ok := x.(*Var); ok {
return NewTerm(v, VAR, []byte(""), "", 0, 0)
}
var val interface{}
var typ int
switch reflect.TypeOf(x).Kind() {
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64,
reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
val = float64(reflect.ValueOf(x).Int())
typ = NUMBER
case reflect.Float32, reflect.Float64:
val = float64(reflect.ValueOf(x).Float())
typ = NUMBER
case reflect.String:
val = x
typ = STRING
case reflect.Bool:
val = x
typ = BOOLEAN
case reflect.Map:
kvset := NewKeyValueSet()
xval := reflect.ValueOf(x)
for _, key := range xval.MapKeys() {
kvset.Add(NewKeyValue(reflectTerm(key.Interface()), reflectTerm(xval.MapIndex(key).Interface())))
}
val = kvset
typ = OBJECT
case reflect.Slice, reflect.Array:
xval := reflect.ValueOf(x)
length := xval.Len()
arr := make([]*Term, length)
for i := 0; i < length; i++ {
arr[i] = reflectTerm(xval.Index(i).Interface())
}
val = arr
typ = ARRAY
default:
panic(fmt.Sprintf("Unexpected type of term: %v", x))
}
return NewTerm(val, typ, []byte(""), "", 0, 0)
}
+102
View File
@@ -0,0 +1,102 @@
// 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 opalog
import "fmt"
// Set is a collection of objects that we can't use a map for.
type Set struct {
Values []interface{}
EqualFunc func(interface{}, interface{}) bool
}
// KeyValue represents a single key-value pair for a dictionary
type KeyValue struct {
Key *Term
Value *Term
}
// NewKeyValue creates a key-value pair
func NewKeyValue(key *Term, value *Term) *KeyValue {
kv := KeyValue{Key: key, Value: value}
return &kv
}
// String converts a KeyValue into a string
func (kv *KeyValue) String() string {
return fmt.Sprintf("%s: %s", kv.Key.String(), kv.Value.String())
}
// Equal returns T if the keys and values are the same
func (kv1 *KeyValue) Equal(kv2 *KeyValue) bool {
return kv1.Key.Equal(kv2.Key) && kv1.Value.Equal(kv2.Value)
}
// NewSet returns a new set
func NewSet(equal func(interface{}, interface{}) bool) *Set {
s := Set{Values: make([]interface{}, 0), EqualFunc: equal}
return &s
}
// NewKeyValueSet returns a set for KeyValue pairs
func NewKeyValueSet() *Set {
f := func(x interface{}, y interface{}) bool {
kvx := x.(*KeyValue)
kvy := y.(*KeyValue)
return kvx.Equal(kvy)
}
return NewSet(f)
}
// Add an element
func (s *Set) Add(x interface{}) {
if !s.Contains(x) {
s.Values = append(s.Values, x)
}
}
// Contains returns true if the set contains element x
func (s *Set) Contains(x interface{}) bool {
for _, elem := range s.Values {
if s.EqualFunc(x, elem) {
return true
}
}
return false
}
// Length returns number of elements
func (s *Set) Length() int {
return len(s.Values)
}
// Equal returns True if the 2 sets have all the same elements
func (set1 *Set) Equal(set2 *Set) bool {
if len(set1.Values) != len(set2.Values) {
return false
}
// TODO: reimplement natively so we don't use memory
diff12 := set1.Difference(set2)
if diff12.Length() > 0 {
return false
}
diff21 := set2.Difference(set1)
if diff21.Length() > 0 {
return false
}
return true
}
// Difference returns a new set that has all the elements of set1 except those in set2
func (set1 *Set) Difference(set2 *Set) *Set {
newset := NewSet(set1.EqualFunc)
for _, elem := range set1.Values {
if !set2.Contains(elem) {
newset.Add(elem)
}
}
return newset
}
@@ -1,15 +1,13 @@
// Copyright 2015 The OPA Authors. All rights reserved.
// 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 jsonlog
package opalog
import (
"testing"
)
func TestSetAdd(t *testing.T) {
eq := func(x interface{}, y interface{}) bool { return x == y }
s1 := NewSet(eq)
@@ -67,4 +65,3 @@ func TestSetEquality(t *testing.T) {
t.Errorf("Equality on sets failed")
}
}
+118
View File
@@ -0,0 +1,118 @@
// 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 opalog
import "strconv"
import "strings"
const (
NULL = iota
BOOLEAN = iota
NUMBER = iota
STRING = iota
ARRAY = iota
OBJECT = iota
VAR = iota
)
// Location records a position in source code
type Location struct {
File string
Row int
Col int
}
// NewLocation creates a new instance of a location
func NewLocation(file string, row int, col int) *Location {
l := Location{File: file, Row: row, Col: col}
return &l
}
// Term is an argument to a function
type Term struct {
Value interface{} // actual value, as represented by Go
Kind int // type of Term: one of the consts defined above
Name []byte // original string representation
Location *Location // text location in original source
}
// NewTerm creates a new Term
func NewTerm(x interface{}, kind int, orig []byte, file string, row int, col int) *Term {
t := Term{Value: x, Kind: kind, Name: orig, Location: NewLocation(file, row, col)}
return &t
}
// String returns the string representation of the Term.
func (t *Term) String() string {
switch t.Kind {
case NULL:
return "null"
case BOOLEAN:
return strconv.FormatBool(t.Value.(bool))
case NUMBER:
return strconv.FormatFloat(t.Value.(float64), 'G', -1, 64)
case STRING:
return "\"" + t.Value.(string) + "\""
case VAR:
return t.Value.(Var).Name
case ARRAY:
var buf []string
for _, v := range t.Value.([]*Term) {
buf = append(buf, v.String())
}
return "[" + strings.Join(buf, ", ") + "]"
case OBJECT:
set := t.Value.(*Set)
var buf []string
for _, v := range set.Values {
buf = append(buf, v.(*KeyValue).String())
}
return "{" + strings.Join(buf, ", ") + "}"
}
panic("unreachable")
return ""
}
// Equal checks if two terms are equal for their Value and Kind fields.
// Ignores differences in pointers.
// Will infinite loop on circular Terms (which are never generated by the parser).
func (term1 *Term) Equal(term2 *Term) bool {
// pointer equality
if term1 == term2 {
return true
}
// wrong types
if term1.Kind != term2.Kind {
return false
}
// recursive cases
switch term1.Kind {
case OBJECT:
// A dictionary is a list of key/value pairs because
// the keys may not be simple strings in the language
set1 := term1.Value.(*Set)
set2 := term2.Value.(*Set)
return set1.Equal(set2)
case ARRAY:
// Golang Value objs for each of the Terms' .Value fields
arr1 := term1.Value.([]*Term)
arr2 := term2.Value.([]*Term)
if len(arr1) != len(arr2) {
return false
}
for i := 0; i < len(arr1); i++ {
if !arr1[i].Equal(arr2[i]) {
return false
}
}
return true
case VAR:
var1 := term1.Value.(*Var)
var2 := term2.Value.(*Var)
return var1.Name == var2.Name
default:
return term1.Value == term2.Value
}
}
+31
View File
@@ -0,0 +1,31 @@
// 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 opalog
import "testing"
func TestEqualTerms(t *testing.T) {
assertTermEqual(t, reflectTerm(nil), reflectTerm(nil))
assertTermEqual(t, reflectTerm(true), reflectTerm(true))
assertTermEqual(t, reflectTerm(5), reflectTerm(5))
assertTermEqual(t, reflectTerm("a string"), reflectTerm("a string"))
assertTermEqual(t, reflectTerm(map[int]int{1: 2}), reflectTerm(map[int]int{1: 2}))
assertTermEqual(t, reflectTerm(map[int]int{1: 2, 3: 4}), reflectTerm(map[int]int{1: 2, 3: 4}))
assertTermEqual(t, reflectTerm([]int{1, 2, 3}), reflectTerm([]int{1, 2, 3}))
assertTermNotEqual(t, reflectTerm(nil), reflectTerm(true))
assertTermNotEqual(t, reflectTerm(true), reflectTerm(false))
assertTermNotEqual(t, reflectTerm(5), reflectTerm(7))
assertTermNotEqual(t, reflectTerm("a string"), reflectTerm("abc"))
assertTermNotEqual(t, reflectTerm(map[int]int{3: 2}), reflectTerm(map[int]int{1: 2}))
assertTermNotEqual(t, reflectTerm(map[int]int{1: 2, 3: 7}), reflectTerm(map[int]int{1: 2, 3: 4}))
assertTermNotEqual(t, reflectTerm(5), reflectTerm("a string"))
assertTermNotEqual(t, reflectTerm(1), reflectTerm(true))
assertTermNotEqual(t, reflectTerm(map[int]int{1: 2, 3: 7}), reflectTerm([]int{1, 2, 3, 7}))
assertTermNotEqual(t, reflectTerm([]int{1, 2, 3}), reflectTerm([]int{1, 2, 4}))
assertTermEqual(t, reflectTerm(NewVar("foo")), reflectTerm(NewVar("foo")))
assertTermNotEqual(t, reflectTerm(NewVar("foo")), reflectTerm(NewVar("bar")))
}
+20
View File
@@ -0,0 +1,20 @@
// 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 opalog
// Var is the AST type representing a variable.
type Var struct {
Name string
}
// NewVar returns a new variable named "name".
func NewVar(name string) *Var {
return &Var{name}
}
// Equal returns true if two variables have the same name.
func (v *Var) Equal(other *Var) bool {
return v.Name == other.Name
}
+12
View File
@@ -0,0 +1,12 @@
// 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 opalog
import "testing"
func TestEqualVarTerms(t *testing.T) {
assertTermEqual(t, reflectTerm(NewVar("foo")), reflectTerm(NewVar("foo")))
assertTermNotEqual(t, reflectTerm(NewVar("foo")), reflectTerm(NewVar("foobar")))
}