mirror of
https://github.com/open-policy-agent/opa.git
synced 2026-08-12 19:32:48 -06:00
Merge pull request #24 from tsandall/initial-grammar
Flesh out the grammar
This commit is contained in:
@@ -18,7 +18,7 @@ GO := go
|
||||
GO15VENDOREXPERIMENT := 1
|
||||
export GO15VENDOREXPERIMENT
|
||||
|
||||
.PHONY: all deps generate build test check check-fmt check-vet check-lint clean
|
||||
.PHONY: all deps generate build test check check-fmt check-vet check-lint fmt clean
|
||||
|
||||
all: build test check
|
||||
|
||||
@@ -47,5 +47,8 @@ check-vet:
|
||||
check-lint:
|
||||
./build/check-lint.sh
|
||||
|
||||
fmt:
|
||||
$(GO) fmt $(PACKAGES)
|
||||
|
||||
clean:
|
||||
rm -f ./opa
|
||||
|
||||
+202
-36
@@ -5,19 +5,166 @@ package opalog
|
||||
// BUGS: the escaped forward solidus (`\/`) is not currently handled for strings.
|
||||
//
|
||||
|
||||
// currentLocation converts the parser context to a Location object.
|
||||
func currentLocation(c *current) *Location {
|
||||
// TODO: Is it possible to propagate file names into the parser?
|
||||
// TODO(tsandall): is it possible to access the filename from inside the parser?
|
||||
return NewLocation(c.text, "", c.pos.line, c.pos.col)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Prog <- _ head:Term tail:( ws Term )* EOF {
|
||||
if head == nil {
|
||||
return make([]interface{}, 0), nil
|
||||
Program <- _ vals:(head:Stmt tail:(ws Stmt)*)? _ EOF {
|
||||
var buf []interface{}
|
||||
|
||||
if vals == nil {
|
||||
return buf, nil
|
||||
}
|
||||
|
||||
ifaceSlice := vals.([]interface{})
|
||||
head := ifaceSlice[0]
|
||||
buf = append(buf, head)
|
||||
for _, tail := range ifaceSlice[1].([]interface{}) {
|
||||
stmt := tail.([]interface{})[1]
|
||||
buf = append(buf, stmt)
|
||||
}
|
||||
|
||||
return buf, nil
|
||||
}
|
||||
|
||||
Stmt <- val:(Package / Import / Rule / Body / Comment) {
|
||||
return val, nil
|
||||
}
|
||||
|
||||
Package <- "package" ws val:(Ref / Var) {
|
||||
// All packages are implicitly declared under the default root document.
|
||||
path := RefTerm(DefaultRootDocument)
|
||||
switch v := val.(*Term).Value.(type) {
|
||||
case Ref:
|
||||
// Convert head of package Ref to String because it will be prefixed
|
||||
// with the root document variable.
|
||||
head := v[0]
|
||||
head = StringTerm(string(head.Value.(Var)))
|
||||
head.Location = v[0].Location
|
||||
tail := v[1:]
|
||||
if !tail.IsGround() {
|
||||
return nil, fmt.Errorf("package name cannot contain variables: %v", v)
|
||||
}
|
||||
|
||||
// We do not allow non-string values in package names.
|
||||
// Because documents are typically represented as JSON, non-string keys are
|
||||
// not allowed for now.
|
||||
// TODO(tsandall): consider special syntax for namespacing under arrays.
|
||||
for _, p := range tail {
|
||||
_, ok := p.Value.(String)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("package name cannot contain non-string values: %v", v)
|
||||
}
|
||||
}
|
||||
path.Value = append(path.Value.(Ref), head)
|
||||
path.Value = append(path.Value.(Ref), tail...)
|
||||
case Var:
|
||||
s := StringTerm(string(v))
|
||||
s.Location = val.(*Term).Location
|
||||
path.Value = append(path.Value.(Ref), s)
|
||||
}
|
||||
pkg := &Package{Location: currentLocation(c), Path: path.Value.(Ref)}
|
||||
return pkg, nil
|
||||
}
|
||||
|
||||
Import <- "import" ws path:(Ref / Var) alias:(ws "as" ws Var)? {
|
||||
imp := &Import{}
|
||||
imp.Location = currentLocation(c)
|
||||
imp.Path = path.(*Term).Value
|
||||
switch p := imp.Path.(type) {
|
||||
case Ref:
|
||||
if !p[1:].IsGround() {
|
||||
return nil, fmt.Errorf("import cannot contain variables in tail: %v", p)
|
||||
}
|
||||
}
|
||||
if alias == nil {
|
||||
return imp, nil
|
||||
}
|
||||
aliasSlice := alias.([]interface{})
|
||||
// Import definition above describes the "alias" slice. We only care about the "Var" element.
|
||||
imp.Alias = aliasSlice[3].(*Term).Value.(Var)
|
||||
return imp, nil
|
||||
}
|
||||
|
||||
// TODO(tsandall): update to handle underscore variables
|
||||
Rule <- name:Var key:( _ "[" _ Term _ "]" _ )? value:( _ "=" _ Term )? body:( _ ":-" _ Body) {
|
||||
|
||||
if key == nil && value == nil {
|
||||
return nil, fmt.Errorf("rule head must include key and/or value: %v", name)
|
||||
}
|
||||
|
||||
rule := &Rule{}
|
||||
rule.Location = currentLocation(c)
|
||||
rule.Name = name.(*Term).Value.(Var)
|
||||
|
||||
if key != nil {
|
||||
keySlice := key.([]interface{})
|
||||
// Rule definition above describes the "key" slice. We care about the "Term" element.
|
||||
rule.Key = keySlice[3].(*Term)
|
||||
_, ok := rule.Key.Value.(Var)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("rule key must be a variable: %v", rule.Key)
|
||||
}
|
||||
}
|
||||
|
||||
if value != nil {
|
||||
valueSlice := value.([]interface{})
|
||||
// Rule definition above describes the "value" slice. We care about the "Term" element.
|
||||
rule.Value = valueSlice[len(valueSlice)-1].(*Term)
|
||||
}
|
||||
|
||||
// Rule definition above describes the "body" slice. We only care about the "Body" element.
|
||||
rule.Body = body.([]interface{})[3].(Body)
|
||||
|
||||
return rule, nil
|
||||
}
|
||||
|
||||
Body <- head:Expr tail:( _ "," _ Expr)* {
|
||||
var buf Body
|
||||
buf = append(buf, head.(*Expr))
|
||||
for _, s := range tail.([]interface{}) {
|
||||
expr := s.([]interface{})[3].(*Expr)
|
||||
buf = append(buf, expr)
|
||||
}
|
||||
return buf, nil
|
||||
}
|
||||
|
||||
Expr <- neg:( "not" ws )? val:(InfixExpr / PrefixExpr / Term) {
|
||||
expr := &Expr{}
|
||||
expr.Location = currentLocation(c)
|
||||
expr.Negated = neg != nil
|
||||
expr.Terms = val
|
||||
return expr, nil
|
||||
}
|
||||
|
||||
InfixExpr <- left:Term _ op:InfixOp _ right:Term {
|
||||
return []*Term{op.(*Term), left.(*Term), right.(*Term)}, nil
|
||||
}
|
||||
|
||||
InfixOp <- val:("=" / "!=" / "<=" / ">=" / "<" / ">") {
|
||||
operator := VarTerm(string(c.text))
|
||||
operator.Location = currentLocation(c)
|
||||
return operator, nil
|
||||
}
|
||||
|
||||
PrefixExpr <- op:Var "(" _ head:Term? tail:( _ "," _ Term )* _ ")" {
|
||||
buf := []*Term{op.(*Term)}
|
||||
if head == nil {
|
||||
return buf, nil
|
||||
}
|
||||
buf = append(buf, head.(*Term))
|
||||
|
||||
// PrefixExpr above describes the "tail" structure. We only care about the "Term" elements.
|
||||
tailSlice := tail.([]interface{})
|
||||
return append([]interface{}{head}, tailSlice...), nil
|
||||
for _, v := range tailSlice {
|
||||
s := v.([]interface{})
|
||||
buf = append(buf, s[len(s) - 1].(*Term))
|
||||
}
|
||||
return buf, nil
|
||||
}
|
||||
|
||||
Term <- val:( Composite / Scalar / Ref / Var ) {
|
||||
@@ -31,103 +178,120 @@ Scalar <- Number / String / Bool / Null
|
||||
Key <- Scalar / Ref / Var
|
||||
|
||||
Object <- '{' _ head:(Key _ ':' _ Term)? tail:( _ ',' _ Key _ ':' _ Term )* _ '}' {
|
||||
var buf [][2]*Term
|
||||
obj := ObjectTerm()
|
||||
obj.Location = currentLocation(c)
|
||||
|
||||
// Empty object.
|
||||
if head == nil {
|
||||
return ObjectTermWithLoc(buf, currentLocation(c)), nil
|
||||
return obj, 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.
|
||||
// Object definition above describes the "head" structure. We only care about the "Key" and "Term" elements.
|
||||
headSlice := head.([]interface{})
|
||||
buf = append(buf, Item(headSlice[0].(*Term), headSlice[len(headSlice) - 1].(*Term)))
|
||||
obj.Value = append(obj.Value.(Object), Item(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."
|
||||
buf = append(buf, Item(s[3].(*Term), s[len(s) - 1].(*Term)))
|
||||
// Object definition above describes the "tail" structure. We only care about the "Key" and "Term" elements.
|
||||
obj.Value = append(obj.Value.(Object), Item(s[3].(*Term), s[len(s) - 1].(*Term)))
|
||||
}
|
||||
|
||||
return ObjectTermWithLoc(buf, currentLocation(c)), nil
|
||||
return obj, nil
|
||||
}
|
||||
|
||||
Array <- '[' _ head:Term? tail:(_ ',' _ Term)* _ ']' {
|
||||
|
||||
var buf []*Term
|
||||
arr := ArrayTerm()
|
||||
arr.Location = currentLocation(c)
|
||||
|
||||
// Empty array.
|
||||
if head == nil {
|
||||
return ArrayTermWithLoc(buf, currentLocation(c)), nil
|
||||
return arr, nil
|
||||
}
|
||||
|
||||
// Non-empty array, first element.
|
||||
buf = append(buf, head.(*Term))
|
||||
arr.Value = append(arr.Value.(Array), 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.
|
||||
buf = append(buf, s[len(s) - 1].(*Term))
|
||||
// Array definition above describes the "tail" structure. We only care about the "Term" elements.
|
||||
arr.Value = append(arr.Value.(Array), s[len(s) - 1].(*Term))
|
||||
}
|
||||
|
||||
return ArrayTermWithLoc(buf, currentLocation(c)), nil
|
||||
return arr, nil
|
||||
}
|
||||
|
||||
Ref <- head:Var tail:( RefDot / RefBracket )+ {
|
||||
buf := []*Term{head.(*Term)}
|
||||
|
||||
ref := RefTerm(head.(*Term))
|
||||
ref.Location = currentLocation(c)
|
||||
|
||||
tailSlice := tail.([]interface{})
|
||||
for _, v := range tailSlice {
|
||||
buf = append(buf, v.(*Term))
|
||||
ref.Value = append(ref.Value.(Ref), v.(*Term))
|
||||
}
|
||||
|
||||
return RefTermWithLoc(buf, currentLocation(c)), nil
|
||||
return ref, nil
|
||||
}
|
||||
|
||||
RefDot <- "." val:Var {
|
||||
// Convert the Var into a string because 'foo.bar.baz' is equivalent to 'foo["bar"]["baz"]'.
|
||||
return StringTermWithLoc(string(val.(*Term).Value.(Var)), currentLocation(c)), nil
|
||||
str := StringTerm(string(val.(*Term).Value.(Var)))
|
||||
str.Location = currentLocation(c)
|
||||
return str, nil
|
||||
}
|
||||
|
||||
RefBracket <- "[" val:(Scalar / Var) "]" {
|
||||
return val, nil
|
||||
}
|
||||
|
||||
Var <- vals:( AsciiLetter (AsciiLetter / DecimalDigit)* ) {
|
||||
return VarTermWithLoc(string(c.text), currentLocation(c)), nil
|
||||
Var <- !Reserved AsciiLetter (AsciiLetter / DecimalDigit)* {
|
||||
str := string(c.text)
|
||||
variable := VarTerm(str)
|
||||
variable.Location = currentLocation(c)
|
||||
return variable, 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)
|
||||
return NumberTermWithLoc(v, currentLocation(c)), err
|
||||
num := NumberTerm(v)
|
||||
num.Location = currentLocation(c)
|
||||
return num, 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))
|
||||
return StringTermWithLoc(v, currentLocation(c)), err
|
||||
str := StringTerm(v)
|
||||
str.Location = currentLocation(c)
|
||||
return str, err
|
||||
}
|
||||
|
||||
Bool <- "true" {
|
||||
return BooleanTermWithLoc(true, currentLocation(c)), nil
|
||||
bol := BooleanTerm(true)
|
||||
bol.Location = currentLocation(c)
|
||||
return bol, nil
|
||||
} / "false" {
|
||||
return BooleanTermWithLoc(false, currentLocation(c)), nil
|
||||
bol := BooleanTerm(false)
|
||||
bol.Location = currentLocation(c)
|
||||
return bol, nil
|
||||
}
|
||||
|
||||
Null <- "null" {
|
||||
return NullTermWithLoc(currentLocation(c)), nil
|
||||
null := NullTerm()
|
||||
null.Location = currentLocation(c)
|
||||
return null, nil
|
||||
}
|
||||
|
||||
Reserved <- ("not" / "package" / "import" / "null" / "true" / "false")
|
||||
|
||||
Integer <- '0' / NonZeroDecimalDigit DecimalDigit*
|
||||
|
||||
Exponent <- 'e'i [+-]? DecimalDigit+
|
||||
@@ -146,10 +310,12 @@ DecimalDigit <- [0-9]
|
||||
|
||||
NonZeroDecimalDigit <- [1-9]
|
||||
|
||||
HexDigit <- [0-9a-f]i
|
||||
|
||||
_ "whitespace" <- [ \t\r\n]*
|
||||
HexDigit <- [0-9a-f]
|
||||
|
||||
ws "whitespace" <- [ \t\r\n]+
|
||||
|
||||
_ "whitespace" <- ( [ \t\r\n] / Comment )*
|
||||
|
||||
Comment <- [ \t]* "#" [^\r\n]*
|
||||
|
||||
EOF <- !.
|
||||
|
||||
@@ -0,0 +1,135 @@
|
||||
// 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.
|
||||
|
||||
// This file contains extra functions for parsing Opalog.
|
||||
// Most of the parsing is handled by the auto-generated code in
|
||||
// parser.go, however, there are additional utilities that are
|
||||
// helpful for dealing with Opalog source inputs (e.g., REPL
|
||||
// statements, source files, etc.)
|
||||
|
||||
package opalog
|
||||
|
||||
import "fmt"
|
||||
|
||||
// MustParseStatements returns a slice of parsed statements.
|
||||
// If an error occurs during parsing, an exception is raised.
|
||||
func MustParseStatements(input string) []interface{} {
|
||||
parsed, err := ParseStatements(input)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return parsed
|
||||
}
|
||||
|
||||
// MustParseStatement returns exactly one statement.
|
||||
// If an error occurs during parsing or multiple statements are parsed,
|
||||
// panic(err) is called.
|
||||
func MustParseStatement(input string) interface{} {
|
||||
parsed, err := ParseStatement(input)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return parsed
|
||||
}
|
||||
|
||||
// ParseStatements returns a slice of parsed statements.
|
||||
// This is the default return value from the parser.
|
||||
func ParseStatements(input string) ([]interface{}, error) {
|
||||
parsed, err := Parse("", []byte(input))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
stmts := parsed.([]interface{})
|
||||
return stmts, err
|
||||
}
|
||||
|
||||
// ParseStatement returns exactly one statement.
|
||||
// A statement might be a term, expression, rule, etc. Regardless,
|
||||
// this function expects *exactly* one statement. If multiple
|
||||
// statements are parsed, an error is returned.
|
||||
func ParseStatement(input string) (interface{}, error) {
|
||||
stmts, err := ParseStatements(input)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(stmts) != 1 {
|
||||
return nil, fmt.Errorf("expected exactly one statement")
|
||||
}
|
||||
return stmts[0], nil
|
||||
}
|
||||
|
||||
// ParseModule returns a parsed Module object.
|
||||
// For details on Module objects and their fields, see policy.go.
|
||||
// Empty input will return nil, nil.
|
||||
func ParseModule(input string) (*Module, error) {
|
||||
stmts, err := ParseStatements(input)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(stmts) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
_package, ok := stmts[0].(*Package)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("first statement must be package")
|
||||
}
|
||||
|
||||
mod := &Module{
|
||||
Package: _package,
|
||||
}
|
||||
|
||||
for _, stmt := range stmts[1:] {
|
||||
switch stmt := stmt.(type) {
|
||||
case *Import:
|
||||
mod.Imports = append(mod.Imports, stmt)
|
||||
case *Rule:
|
||||
mod.Rules = append(mod.Rules, stmt)
|
||||
case Body:
|
||||
rule, err := parseConstantRule(stmt)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
mod.Rules = append(mod.Rules, rule)
|
||||
}
|
||||
}
|
||||
|
||||
return mod, nil
|
||||
}
|
||||
|
||||
// parseConstantRule attempts to return a rule from a Body.
|
||||
// Equality expressions of the form <var> = <ground term> can be
|
||||
// converted into rules of the form <var> = <ground term> :- true.
|
||||
// This is a concise way of defining constants inside modules.
|
||||
// This function handles the conversion.
|
||||
func parseConstantRule(stmt Body) (*Rule, error) {
|
||||
if len(stmt) > 1 {
|
||||
return nil, fmt.Errorf("expression must be contained inside rule: %v", stmt)
|
||||
} else if len(stmt) == 1 {
|
||||
stmt := stmt[0]
|
||||
if !stmt.IsEquality() {
|
||||
return nil, fmt.Errorf("non-equality expression must be contained inside rule: %v", stmt)
|
||||
}
|
||||
terms := stmt.Terms.([]*Term)
|
||||
if !terms[2].IsGround() {
|
||||
return nil, fmt.Errorf("constant rule value must be ground: %v", stmt)
|
||||
}
|
||||
switch name := terms[1].Value.(type) {
|
||||
case Var:
|
||||
rule := &Rule{
|
||||
Location: stmt.Location,
|
||||
Name: name,
|
||||
Value: terms[2],
|
||||
Body: []*Expr{
|
||||
&Expr{Terms: BooleanTerm(true)},
|
||||
},
|
||||
}
|
||||
return rule, nil
|
||||
default:
|
||||
return nil, fmt.Errorf("rule name must be a variable: %v", stmt)
|
||||
}
|
||||
} else {
|
||||
panic("unreachable")
|
||||
}
|
||||
}
|
||||
+383
-48
@@ -22,30 +22,41 @@ func TestScalarTerms(t *testing.T) {
|
||||
assertParseOneTerm(t, "exponent", "6e7", NumberTerm(6e7))
|
||||
assertParseOneTerm(t, "string", "\"a string\"", StringTerm("a string"))
|
||||
assertParseOneTerm(t, "string", "\"a string u6abc7def8abc0def with unicode\"", StringTerm("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")
|
||||
assertParseError(t, "hex", "6abc")
|
||||
assertParseError(t, "non-terminated", "\"foo")
|
||||
assertParseError(t, "non-string", "'a string'")
|
||||
assertParseError(t, "non-number", "6zxy")
|
||||
assertParseError(t, "non-number2", "6d7")
|
||||
assertParseError(t, "non-number3", "6\"foo\"")
|
||||
assertParseError(t, "non-number4", "6true")
|
||||
assertParseError(t, "non-number5", "6false")
|
||||
assertParseError(t, "non-number6", "6[null, null]")
|
||||
assertParseError(t, "non-number7", "6{\"foo\": \"bar\"}")
|
||||
assertParseError(t, "out-of-range", "1e1000")
|
||||
}
|
||||
|
||||
func TestVarTerms(t *testing.T) {
|
||||
assertParseOneTerm(t, "var", "foo", VarTerm("foo"))
|
||||
assertParseOneTerm(t, "var", "foo_bar", VarTerm("foo_bar"))
|
||||
assertParseOneTerm(t, "var", "foo0", VarTerm("foo0"))
|
||||
assertParseOneTermFail(t, "non-var", "foo-bar")
|
||||
assertParseOneTermFail(t, "non-var2", "foo-7")
|
||||
assertParseError(t, "non-var", "foo-bar")
|
||||
assertParseError(t, "non-var2", "foo-7")
|
||||
for _, v := range Keywords {
|
||||
assertParseError(t, "keyword", v)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRefTerms(t *testing.T) {
|
||||
assertParseOneTerm(t, "constants", "foo.bar.baz", RefTerm(VarTerm("foo"), StringTerm("bar"), StringTerm("baz")))
|
||||
assertParseOneTerm(t, "constants 2", "foo.bar[0].baz", RefTerm(VarTerm("foo"), StringTerm("bar"), NumberTerm(0), StringTerm("baz")))
|
||||
assertParseOneTerm(t, "variables", "foo.bar[0].baz[i]", RefTerm(VarTerm("foo"), StringTerm("bar"), NumberTerm(0), StringTerm("baz"), VarTerm("i")))
|
||||
assertParseOneTerm(t, "spaces", "foo[\"white space\"].bar", RefTerm(VarTerm("foo"), StringTerm("white space"), StringTerm("bar")))
|
||||
assertParseError(t, "missing component 1", "foo.")
|
||||
assertParseError(t, "missing component 2", "foo[].bar")
|
||||
assertParseError(t, "composite operand 1", "foo[[1,2,3]].bar")
|
||||
assertParseError(t, "composite operand 2", "foo[{1: 2}].bar")
|
||||
// TODO(tsandall): this may be allowed some day
|
||||
assertParseError(t, "nested refs", "foo[baz.qux].bar")
|
||||
}
|
||||
|
||||
func TestObjectWithScalars(t *testing.T) {
|
||||
@@ -63,6 +74,15 @@ func TestObjectWithVars(t *testing.T) {
|
||||
assertParseOneTerm(t, "nested var keys", "{baz: {foo: \"bar\", bar: qux}}", ObjectTerm(Item(VarTerm("baz"), ObjectTerm(Item(VarTerm("foo"), StringTerm("bar")), Item(VarTerm("bar"), VarTerm("qux"))))))
|
||||
}
|
||||
|
||||
func TestObjectFail(t *testing.T) {
|
||||
assertParseError(t, "non-terminated 1", "{foo: bar, baz: [], qux: corge")
|
||||
assertParseError(t, "non-terminated 2", "{foo: bar, baz: [], qux: ")
|
||||
assertParseError(t, "non-terminated 3", "{foo: bar, baz: [], qux ")
|
||||
assertParseError(t, "non-terminated 4", "{foo: bar, baz: [], ")
|
||||
assertParseError(t, "missing separator", "{foo: bar baz: []}")
|
||||
assertParseError(t, "missing start", "foo: bar, baz: [], qux: corge}")
|
||||
}
|
||||
|
||||
func TestArrayWithScalars(t *testing.T) {
|
||||
assertParseOneTerm(t, "number", "[1,2,3,4.5]", ArrayTerm(NumberTerm(1), NumberTerm(2), NumberTerm(3), NumberTerm(4.5)))
|
||||
assertParseOneTerm(t, "bool", "[true, false, true]", ArrayTerm(BooleanTerm(true), BooleanTerm(false), BooleanTerm(true)))
|
||||
@@ -75,6 +95,14 @@ func TestArrayWithVars(t *testing.T) {
|
||||
assertParseOneTerm(t, "nested var elements", "[[foo, true], [null, bar], 42]", ArrayTerm(ArrayTerm(VarTerm("foo"), BooleanTerm(true)), ArrayTerm(NullTerm(), VarTerm("bar")), NumberTerm(42)))
|
||||
}
|
||||
|
||||
func TestArrayFail(t *testing.T) {
|
||||
assertParseError(t, "non-terminated 1", "[foo, bar")
|
||||
assertParseError(t, "non-terminated 2", "[foo, bar, ")
|
||||
assertParseError(t, "missing element", "[foo, bar, ]")
|
||||
assertParseError(t, "missing separator", "[foo bar]")
|
||||
assertParseError(t, "missing start", "foo, bar, baz]")
|
||||
}
|
||||
|
||||
func TestEmptyComposites(t *testing.T) {
|
||||
assertParseOneTerm(t, "empty object", "{}", ObjectTerm())
|
||||
assertParseOneTerm(t, "emtpy array", "[]", ArrayTerm())
|
||||
@@ -91,48 +119,355 @@ func TestCompositesWithRefs(t *testing.T) {
|
||||
assertParseOneTerm(t, "ref values", "[{8: a[i].b, f: c[0][\"d\"].e[j]}]", ArrayTerm(ObjectTerm(Item(NumberTerm(8), ref1), Item(VarTerm("f"), ref2))))
|
||||
}
|
||||
|
||||
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 TestInfixExpr(t *testing.T) {
|
||||
assertParseOneExpr(t, "scalars 1", "true = false", NewBuiltinExpr(VarTerm("="), BooleanTerm(true), BooleanTerm(false)))
|
||||
assertParseOneExpr(t, "scalars 2", "3.14 = null", NewBuiltinExpr(VarTerm("="), NumberTerm(3.14), NullTerm()))
|
||||
assertParseOneExpr(t, "scalars 3", "42 = \"hello world\"", NewBuiltinExpr(VarTerm("="), NumberTerm(42), StringTerm("hello world")))
|
||||
assertParseOneExpr(t, "vars 1", "hello = world", NewBuiltinExpr(VarTerm("="), VarTerm("hello"), VarTerm("world")))
|
||||
assertParseOneExpr(t, "vars 2", "42 = hello", NewBuiltinExpr(VarTerm("="), NumberTerm(42), VarTerm("hello")))
|
||||
|
||||
ref1 := RefTerm(VarTerm("foo"), NumberTerm(0), StringTerm("bar"), VarTerm("x"))
|
||||
ref2 := RefTerm(VarTerm("baz"), BooleanTerm(false), StringTerm("qux"), StringTerm("hello"))
|
||||
assertParseOneExpr(t, "refs 1", "foo[0].bar[x] = baz[false].qux[\"hello\"]", NewBuiltinExpr(VarTerm("="), ref1, ref2))
|
||||
|
||||
left1 := ObjectTerm(Item(VarTerm("a"), ArrayTerm(ref1)))
|
||||
right1 := ArrayTerm(ObjectTerm(Item(NumberTerm(42), BooleanTerm(true))))
|
||||
assertParseOneExpr(t, "composites", "{a: [foo[0].bar[x]]} = [{42: true}]", NewBuiltinExpr(VarTerm("="), left1, right1))
|
||||
|
||||
assertParseOneExpr(t, "ne", "100 != 200", NewBuiltinExpr(VarTerm("!="), NumberTerm(100), NumberTerm(200)))
|
||||
assertParseOneExpr(t, "gt", "17.4 > \"hello\"", NewBuiltinExpr(VarTerm(">"), NumberTerm(17.4), StringTerm("hello")))
|
||||
assertParseOneExpr(t, "lt", "17.4 < \"hello\"", NewBuiltinExpr(VarTerm("<"), NumberTerm(17.4), StringTerm("hello")))
|
||||
assertParseOneExpr(t, "gte", "17.4 >= \"hello\"", NewBuiltinExpr(VarTerm(">="), NumberTerm(17.4), StringTerm("hello")))
|
||||
assertParseOneExpr(t, "lte", "17.4 <= \"hello\"", NewBuiltinExpr(VarTerm("<="), NumberTerm(17.4), StringTerm("hello")))
|
||||
|
||||
left2 := ArrayTerm(ObjectTerm(Item(NumberTerm(14.2), BooleanTerm(true)), Item(StringTerm("a"), NullTerm())))
|
||||
right2 := ObjectTerm(Item(VarTerm("foo"), ObjectTerm(Item(RefTerm(VarTerm("a"), StringTerm("b"), NumberTerm(0)), ArrayTerm(NumberTerm(10))))))
|
||||
assertParseOneExpr(t, "composites", "[{14.2: true, \"a\": null}] != {foo: {a.b[0]: [10]}}", NewBuiltinExpr(VarTerm("!="), left2, right2))
|
||||
}
|
||||
|
||||
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 TestMiscBuiltinExpr(t *testing.T) {
|
||||
xyz := VarTerm("xyz")
|
||||
assertParseOneExpr(t, "empty", "xyz()", NewBuiltinExpr(xyz))
|
||||
assertParseOneExpr(t, "single", "xyz(abc)", NewBuiltinExpr(xyz, VarTerm("abc")))
|
||||
assertParseOneExpr(t, "multiple", "xyz(abc, {\"one\": [1,2,3]})", NewBuiltinExpr(xyz, VarTerm("abc"), ObjectTerm(Item(StringTerm("one"), ArrayTerm(NumberTerm(1), NumberTerm(2), NumberTerm(3))))))
|
||||
}
|
||||
|
||||
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))
|
||||
func TestNegatedExpr(t *testing.T) {
|
||||
assertParseOneTermNegated(t, "scalars 1", "not true", BooleanTerm(true))
|
||||
assertParseOneTermNegated(t, "scalars 2", "not \"hello\"", StringTerm("hello"))
|
||||
assertParseOneTermNegated(t, "scalars 3", "not 100", NumberTerm(100))
|
||||
assertParseOneTermNegated(t, "scalars 4", "not null", NullTerm())
|
||||
assertParseOneTermNegated(t, "var", "not x", VarTerm("x"))
|
||||
assertParseOneTermNegated(t, "ref", "not x[y].z", RefTerm(VarTerm("x"), VarTerm("y"), StringTerm("z")))
|
||||
assertParseOneExprNegated(t, "vars", "not x = y", NewBuiltinExpr(VarTerm("="), VarTerm("x"), VarTerm("y")))
|
||||
|
||||
ref1 := RefTerm(VarTerm("x"), VarTerm("y"), StringTerm("z"), VarTerm("a"))
|
||||
|
||||
assertParseOneExprNegated(t, "membership", "not x[y].z[a] = \"b\"", NewBuiltinExpr(VarTerm("="), ref1, StringTerm("b")))
|
||||
assertParseOneExprNegated(t, "misc. builtin", "not sorted(x[y].z[a])", NewBuiltinExpr(VarTerm("sorted"), ref1))
|
||||
}
|
||||
|
||||
func TestPackage(t *testing.T) {
|
||||
ref1 := RefTerm(DefaultRootDocument, StringTerm("foo"))
|
||||
assertParsePackage(t, "single", "package foo", &Package{Path: ref1.Value.(Ref)})
|
||||
ref2 := RefTerm(DefaultRootDocument, StringTerm("f00"), StringTerm("bar_baz"), StringTerm("qux"))
|
||||
assertParsePackage(t, "multiple", "package f00.bar_baz.qux", &Package{Path: ref2.Value.(Ref)})
|
||||
ref3 := RefTerm(DefaultRootDocument, StringTerm("foo"), StringTerm("bar baz"))
|
||||
assertParsePackage(t, "space", "package foo[\"bar baz\"]", &Package{Path: ref3.Value.(Ref)})
|
||||
assertParseError(t, "non-ground ref", "package foo[x]")
|
||||
assertParseError(t, "non-string value", "package foo.bar[42].baz")
|
||||
}
|
||||
|
||||
func TestImport(t *testing.T) {
|
||||
assertParseImport(t, "single", "import foo", &Import{Path: VarTerm("foo").Value})
|
||||
ref := RefTerm(VarTerm("foo"), StringTerm("bar"), StringTerm("baz")).Value
|
||||
assertParseImport(t, "multiple", "import foo.bar.baz", &Import{Path: ref})
|
||||
assertParseImport(t, "single alias", "import foo as bar", &Import{Path: VarTerm("foo").Value, Alias: Var("bar")})
|
||||
assertParseImport(t, "multiple alias", "import foo.bar.baz as qux", &Import{Path: ref, Alias: Var("qux")})
|
||||
ref2 := RefTerm(VarTerm("foo"), StringTerm("bar"), StringTerm("white space")).Value
|
||||
assertParseImport(t, "white space", "import foo.bar[\"white space\"]", &Import{Path: ref2})
|
||||
assertParseError(t, "non-ground ref", "import foo[x]")
|
||||
}
|
||||
|
||||
func TestRule(t *testing.T) {
|
||||
|
||||
assertParseRule(t, "identity", "p = true :- true", &Rule{
|
||||
Name: Var("p"),
|
||||
Value: BooleanTerm(true),
|
||||
Body: []*Expr{
|
||||
&Expr{Terms: BooleanTerm(true)},
|
||||
},
|
||||
})
|
||||
|
||||
assertParseRule(t, "set", "p[x] :- x = 42", &Rule{
|
||||
Name: Var("p"),
|
||||
Key: VarTerm("x"),
|
||||
Body: []*Expr{
|
||||
NewBuiltinExpr(VarTerm("="), VarTerm("x"), NumberTerm(42)),
|
||||
},
|
||||
})
|
||||
|
||||
assertParseRule(t, "object", "p[x] = y :- x = 42, y = \"hello\"", &Rule{
|
||||
Name: Var("p"),
|
||||
Key: VarTerm("x"),
|
||||
Value: VarTerm("y"),
|
||||
Body: []*Expr{
|
||||
NewBuiltinExpr(VarTerm("="), VarTerm("x"), NumberTerm(42)),
|
||||
NewBuiltinExpr(VarTerm("="), VarTerm("y"), StringTerm("hello")),
|
||||
},
|
||||
})
|
||||
|
||||
assertParseRule(t, "constant composite", "p = [{\"foo\": [1,2,3,4]}] :- true", &Rule{
|
||||
Name: Var("p"),
|
||||
Value: ArrayTerm(
|
||||
ObjectTerm(Item(StringTerm("foo"), ArrayTerm(NumberTerm(1), NumberTerm(2), NumberTerm(3), NumberTerm(4)))),
|
||||
),
|
||||
Body: []*Expr{
|
||||
&Expr{Terms: BooleanTerm(true)},
|
||||
},
|
||||
})
|
||||
|
||||
assertParseError(t, "missing key and/or value", "p :- true")
|
||||
assertParseError(t, "constant key", "p[100] :- true")
|
||||
assertParseError(t, "composite key", "p[[1,2,x]] :- x = true")
|
||||
assertParseError(t, "dangling comma", "p :- true, false,")
|
||||
}
|
||||
|
||||
func TestEmptyModule(t *testing.T) {
|
||||
r, err := ParseModule(" ")
|
||||
if err != nil {
|
||||
t.Errorf("Expected nil for empty module: %s", err)
|
||||
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)
|
||||
if r != nil {
|
||||
t.Errorf("Expected nil for empty module: %v", r)
|
||||
}
|
||||
}
|
||||
|
||||
func TestComments(t *testing.T) {
|
||||
|
||||
testModule := `
|
||||
package a.b.c
|
||||
|
||||
import e.f as g # end of line
|
||||
import h
|
||||
|
||||
# by itself
|
||||
|
||||
p[x] = y :- y = "foo",
|
||||
# inside a rule
|
||||
x = "bar",
|
||||
x != y,
|
||||
q[x]
|
||||
|
||||
import xyz.abc
|
||||
|
||||
q # interruptting
|
||||
[a] # the head of a rule
|
||||
:- m = [1,2,
|
||||
3],
|
||||
a = m[i]
|
||||
`
|
||||
|
||||
assertParseModule(t, "module comments", testModule, &Module{
|
||||
Package: MustParseStatement("package a.b.c").(*Package),
|
||||
Imports: []*Import{
|
||||
MustParseStatement("import e.f as g").(*Import),
|
||||
MustParseStatement("import h").(*Import),
|
||||
MustParseStatement("import xyz.abc").(*Import),
|
||||
},
|
||||
Rules: []*Rule{
|
||||
MustParseStatement("p[x] = y :- y = \"foo\", x = \"bar\", x != y, q[x]").(*Rule),
|
||||
MustParseStatement("q[a] :- m = [1,2,3], a = m[i]").(*Rule),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func TestExample(t *testing.T) {
|
||||
testModule := `
|
||||
package opa.examples # this policy belongs the opa.examples package
|
||||
|
||||
import data.servers # import the data.servers document to refer to it as "servers" instead of "data.servers"
|
||||
import data.networks # same but for data.networks
|
||||
import data.ports # same but for data.ports
|
||||
|
||||
violations[server] :- # a server exists in the violations set if:
|
||||
server = servers[_], # the server exists in the servers collection
|
||||
server.protocols[_] = "http", # and the server has http in its protocols collection
|
||||
public_servers[server] # and the server exists in the public_servers set
|
||||
|
||||
public_servers[server] :- # a server exists in the public_servers set if:
|
||||
server = servers[_], # the server exists in the servers collection
|
||||
server.ports[_] = ports[i].id, # and the server is connected to a port in the ports collection
|
||||
ports[i].networks[_] = networks[j].id, # and the port is connected to a network in the networks collection
|
||||
networks[j].public = true # and the network is public
|
||||
`
|
||||
|
||||
assertParseModule(t, "example module", testModule, &Module{
|
||||
Package: MustParseStatement("package opa.examples").(*Package),
|
||||
Imports: []*Import{
|
||||
MustParseStatement("import data.servers").(*Import),
|
||||
MustParseStatement("import data.networks").(*Import),
|
||||
MustParseStatement("import data.ports").(*Import),
|
||||
},
|
||||
Rules: []*Rule{
|
||||
MustParseStatement(`violations[server] :-
|
||||
server = servers[_],
|
||||
server.protocols[_] = "http",
|
||||
public_servers[server]`).(*Rule),
|
||||
MustParseStatement(`public_servers[server] :-
|
||||
server = servers[_],
|
||||
server.ports[_] = ports[i].id,
|
||||
ports[i].networks[_] = networks[j].id,
|
||||
networks[j].public = true`).(*Rule),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func TestConstantRules(t *testing.T) {
|
||||
testModule := `
|
||||
package a.b.c
|
||||
|
||||
pi = 3.14159
|
||||
|
||||
# intersperse a regular rule
|
||||
p[x] :- x = 1
|
||||
|
||||
greeting = "hello"
|
||||
|
||||
cores = [{0: 1}, {1: 2}]
|
||||
`
|
||||
|
||||
assertParseModule(t, "constant rules", testModule, &Module{
|
||||
Package: MustParseStatement("package a.b.c").(*Package),
|
||||
Rules: []*Rule{
|
||||
MustParseStatement("pi = 3.14159 :- true").(*Rule),
|
||||
MustParseStatement("p[x] :- x = 1").(*Rule),
|
||||
MustParseStatement("greeting = \"hello\" :- true").(*Rule),
|
||||
MustParseStatement("cores = [{0: 1}, {1: 2}] :- true").(*Rule),
|
||||
},
|
||||
})
|
||||
|
||||
multipleExprs := `
|
||||
package a.b.c
|
||||
|
||||
pi = 3.14159, pi > 3
|
||||
`
|
||||
|
||||
nonEquality := `
|
||||
package a.b.c
|
||||
|
||||
pi > 3
|
||||
`
|
||||
|
||||
nonVarName := `
|
||||
package a.b.c
|
||||
|
||||
"pi" = 3
|
||||
`
|
||||
|
||||
ungroundValue := `
|
||||
package a.b.c
|
||||
|
||||
pi = [3, 1, 4, x, y, z]
|
||||
`
|
||||
|
||||
assertParseError(t, "multiple expressions", multipleExprs)
|
||||
assertParseError(t, "non-equality", nonEquality)
|
||||
assertParseError(t, "non-var name", nonVarName)
|
||||
assertParseError(t, "unground value", ungroundValue)
|
||||
}
|
||||
|
||||
func assertParse(t *testing.T, msg string, input string, correct func([]interface{})) {
|
||||
p, err := ParseStatements(input)
|
||||
if err != nil {
|
||||
t.Errorf("Error on test %s: parse error on %s: %s", msg, input, err)
|
||||
return
|
||||
}
|
||||
correct(p)
|
||||
}
|
||||
|
||||
// TODO(tsandall): add assertions to check that error message is as expected
|
||||
func assertParseError(t *testing.T, msg string, input string) {
|
||||
p, err := ParseStatement(input)
|
||||
if err == nil {
|
||||
t.Errorf("Error on test %s: expected parse error: %v (parsed)", msg, p)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func assertParseImport(t *testing.T, msg string, input string, correct *Import) {
|
||||
assertParseOne(t, msg, input, func(parsed interface{}) {
|
||||
imp := parsed.(*Import)
|
||||
if !imp.Equal(correct) {
|
||||
t.Errorf("Error on test %s: imports not equal: %v (parsed), %v (correct)", msg, imp, correct)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func assertParseModule(t *testing.T, msg string, input string, correct *Module) {
|
||||
|
||||
m, err := ParseModule(input)
|
||||
if err != nil {
|
||||
t.Errorf("Error on test %s: parse error on %s: %s", msg, input, err)
|
||||
return
|
||||
}
|
||||
|
||||
if !m.Equal(correct) {
|
||||
t.Errorf("Error on test %s: modules not equal: %v (parsed), %v (correct)", msg, m, correct)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func assertParsePackage(t *testing.T, msg string, input string, correct *Package) {
|
||||
assertParseOne(t, msg, input, func(parsed interface{}) {
|
||||
pkg := parsed.(*Package)
|
||||
if !pkg.Equal(correct) {
|
||||
t.Errorf("Error on test %s: packages not equal: %v (parsed), %v (correct)", msg, pkg, correct)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func assertParseOne(t *testing.T, msg string, input string, correct func(interface{})) {
|
||||
p, err := ParseStatement(input)
|
||||
if err != nil {
|
||||
t.Errorf("Error on test %s: parse error on %s: %s", msg, input, err)
|
||||
}
|
||||
correct(p)
|
||||
}
|
||||
|
||||
func assertParseOneExpr(t *testing.T, msg string, input string, correct *Expr) {
|
||||
assertParseOne(t, msg, input, func(parsed interface{}) {
|
||||
body := parsed.(Body)
|
||||
if len(body) != 1 {
|
||||
t.Errorf("Error on test %s: parser returned multiple expressions: %v", msg, body)
|
||||
return
|
||||
}
|
||||
expr := body[0]
|
||||
if !expr.Equal(correct) {
|
||||
t.Errorf("Error on test %s: expressions not equal: %v (parsed), %v (correct)", msg, expr, correct)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func assertParseOneExprNegated(t *testing.T, msg string, input string, correct *Expr) {
|
||||
correct.Negated = true
|
||||
assertParseOneExpr(t, msg, input, correct)
|
||||
}
|
||||
|
||||
func assertParseOneTerm(t *testing.T, msg string, input string, correct *Term) {
|
||||
assertParseOneExpr(t, msg, input, &Expr{Terms: correct})
|
||||
}
|
||||
|
||||
func assertParseOneTermNegated(t *testing.T, msg string, input string, correct *Term) {
|
||||
assertParseOneExprNegated(t, msg, input, &Expr{Terms: correct})
|
||||
}
|
||||
|
||||
func assertParseRule(t *testing.T, msg string, input string, correct *Rule) {
|
||||
assertParseOne(t, msg, input, func(parsed interface{}) {
|
||||
rule := parsed.(*Rule)
|
||||
if !rule.Equal(correct) {
|
||||
t.Errorf("Error on test %s: rules not equal: %v (parsed), %v (correct)", msg, rule, correct)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -0,0 +1,239 @@
|
||||
// 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"
|
||||
import "strings"
|
||||
|
||||
// DefaultRootDocument is the default root document.
|
||||
// All package directives inside source files are implicitly
|
||||
// prefixed with the DefaultRootDocument value.
|
||||
var DefaultRootDocument = VarTerm("data")
|
||||
|
||||
// Keywords is an array of reserved keywords in the language.
|
||||
// These are reserved names that cannot be used for variables.
|
||||
var Keywords = [...]string{
|
||||
"package", "import", "not",
|
||||
}
|
||||
|
||||
type (
|
||||
// Module represents a collection of policies (defined by rules)
|
||||
// within a namespace (defined by the package) and optional
|
||||
// dependencies on external documents (defined by imports).
|
||||
Module struct {
|
||||
Package *Package
|
||||
Imports []*Import
|
||||
Rules []*Rule
|
||||
}
|
||||
|
||||
// Package represents the namespace of the documents produced
|
||||
// by rules inside the module.
|
||||
Package struct {
|
||||
Location *Location
|
||||
Path Ref
|
||||
}
|
||||
|
||||
// Import represents a dependency on a document outside of the policy
|
||||
// namespace. Imports are optional.
|
||||
Import struct {
|
||||
Location *Location
|
||||
Path Value
|
||||
Alias Var
|
||||
}
|
||||
|
||||
// Rule represents a rule as defined by Opalog. Rules define the
|
||||
// content of documents that represent policy decisions.
|
||||
Rule struct {
|
||||
Location *Location
|
||||
Name Var
|
||||
Key *Term
|
||||
Value *Term
|
||||
Body Body
|
||||
}
|
||||
|
||||
// Body represents one or more expressios contained inside a rule.
|
||||
Body []*Expr
|
||||
|
||||
// Expr represents a single expression contained inside the body of a rule.
|
||||
Expr struct {
|
||||
Location *Location
|
||||
Negated bool
|
||||
Terms interface{}
|
||||
}
|
||||
)
|
||||
|
||||
// Equal returns true if this Module equals the other Module.
|
||||
// Two modules are equal if they contain the same package,
|
||||
// ordered imports, and ordered rules.
|
||||
func (mod *Module) Equal(other *Module) bool {
|
||||
if !mod.Package.Equal(other.Package) {
|
||||
return false
|
||||
}
|
||||
if len(mod.Imports) != len(other.Imports) {
|
||||
return false
|
||||
}
|
||||
for i := range mod.Imports {
|
||||
if !mod.Imports[i].Equal(other.Imports[i]) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
if len(mod.Rules) != len(other.Rules) {
|
||||
return false
|
||||
}
|
||||
for i := range mod.Rules {
|
||||
if !mod.Rules[i].Equal(other.Rules[i]) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// Equal returns true if this Package has the same path as the other Package.
|
||||
func (pkg *Package) Equal(other *Package) bool {
|
||||
return pkg.Path.Equal(other.Path)
|
||||
}
|
||||
|
||||
func (pkg *Package) String() string {
|
||||
return fmt.Sprintf("package %v", pkg.Path)
|
||||
}
|
||||
|
||||
// Equal returns true if this Import has the same path and alias as the other Import.
|
||||
func (imp *Import) Equal(other *Import) bool {
|
||||
return imp.Alias.Equal(other.Alias) && imp.Path.Equal(other.Path)
|
||||
}
|
||||
|
||||
func (imp *Import) String() string {
|
||||
buf := []string{"import", imp.Path.String()}
|
||||
if len(imp.Alias) > 0 {
|
||||
buf = append(buf, "as "+imp.Alias.String())
|
||||
}
|
||||
return strings.Join(buf, " ")
|
||||
}
|
||||
|
||||
// Equal returns true if this Rule has the same name, arguments, and body as the other Rule.
|
||||
func (rule *Rule) Equal(other *Rule) bool {
|
||||
if !rule.Name.Equal(other.Name) {
|
||||
return false
|
||||
}
|
||||
if !rule.Key.Equal(other.Key) {
|
||||
return false
|
||||
}
|
||||
if !rule.Value.Equal(other.Value) {
|
||||
return false
|
||||
}
|
||||
return rule.Body.Equal(other.Body)
|
||||
}
|
||||
|
||||
func (rule *Rule) String() string {
|
||||
var buf []string
|
||||
if rule.Key != nil {
|
||||
buf = append(buf, rule.Name.String()+"["+rule.Key.String()+"]")
|
||||
} else {
|
||||
buf = append(buf, rule.Name.String())
|
||||
}
|
||||
if rule.Value != nil {
|
||||
buf = append(buf, "=")
|
||||
buf = append(buf, rule.Value.String())
|
||||
}
|
||||
if len(rule.Body) >= 0 {
|
||||
buf = append(buf, ":-")
|
||||
buf = append(buf, rule.Body.String())
|
||||
}
|
||||
return strings.Join(buf, " ")
|
||||
}
|
||||
|
||||
// Equal returns true if this Body is equal to the other Body.
|
||||
// Two bodies are equal if consist of equal, ordered expressions.
|
||||
func (body Body) Equal(other Body) bool {
|
||||
if len(body) != len(other) {
|
||||
return false
|
||||
}
|
||||
for i := range body {
|
||||
if !body[i].Equal(other[i]) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (body Body) String() string {
|
||||
var buf []string
|
||||
for _, v := range body {
|
||||
buf = append(buf, v.String())
|
||||
}
|
||||
return strings.Join(buf, ", ")
|
||||
}
|
||||
|
||||
// Equal returns true if this Expr equals the other Expr.
|
||||
// Two expressions are considered equal if both expressions are negated (or not),
|
||||
// are built-ins (or not), and have the same ordered terms.
|
||||
func (expr *Expr) Equal(other *Expr) bool {
|
||||
if expr.Negated != other.Negated {
|
||||
return false
|
||||
}
|
||||
switch t := expr.Terms.(type) {
|
||||
case *Term:
|
||||
switch u := other.Terms.(type) {
|
||||
case *Term:
|
||||
return t.Equal(u)
|
||||
}
|
||||
case []*Term:
|
||||
switch u := other.Terms.(type) {
|
||||
case []*Term:
|
||||
return termSliceEqual(t, u)
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// IsEquality returns true if this is an equality expression.
|
||||
func (expr *Expr) IsEquality() bool {
|
||||
terms, ok := expr.Terms.([]*Term)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
if len(terms) != 3 {
|
||||
return false
|
||||
}
|
||||
return terms[0].Equal(VarTerm("="))
|
||||
}
|
||||
|
||||
var builtinNames = map[string]string{
|
||||
"=": "eq",
|
||||
"<": "lt",
|
||||
">": "gt",
|
||||
"<=": "lte",
|
||||
">=": "gte",
|
||||
"!=": "ne",
|
||||
}
|
||||
|
||||
func (expr *Expr) String() string {
|
||||
var buf []string
|
||||
if expr.Negated {
|
||||
buf = append(buf, "not")
|
||||
}
|
||||
switch t := expr.Terms.(type) {
|
||||
case []*Term:
|
||||
var args []string
|
||||
for _, v := range t[1:] {
|
||||
args = append(args, v.String())
|
||||
}
|
||||
name, ok := builtinNames[string(t[0].Value.(Var))]
|
||||
if !ok {
|
||||
name = t[0].String()
|
||||
}
|
||||
builtinStr := fmt.Sprintf("%s(%s)", name, strings.Join(args, ", "))
|
||||
buf = append(buf, builtinStr)
|
||||
case *Term:
|
||||
buf = append(buf, t.String())
|
||||
}
|
||||
return strings.Join(buf, " ")
|
||||
}
|
||||
|
||||
// NewBuiltinExpr creates a new Expr object with the supplied terms.
|
||||
// The builtin operator must be the first term.
|
||||
func NewBuiltinExpr(terms ...*Term) *Expr {
|
||||
return &Expr{Terms: terms}
|
||||
}
|
||||
@@ -0,0 +1,272 @@
|
||||
// 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 TestPackageEquals(t *testing.T) {
|
||||
pkg1 := &Package{Path: RefTerm(VarTerm("foo"), StringTerm("bar"), StringTerm("baz")).Value.(Ref)}
|
||||
pkg2 := &Package{Path: RefTerm(VarTerm("foo"), StringTerm("bar"), StringTerm("baz")).Value.(Ref)}
|
||||
pkg3 := &Package{Path: RefTerm(VarTerm("foo"), StringTerm("qux"), StringTerm("baz")).Value.(Ref)}
|
||||
assertPackagesEqual(t, pkg1, pkg1)
|
||||
assertPackagesEqual(t, pkg1, pkg2)
|
||||
assertPackagesNotEqual(t, pkg1, pkg3)
|
||||
assertPackagesNotEqual(t, pkg2, pkg3)
|
||||
}
|
||||
|
||||
func TestPackageString(t *testing.T) {
|
||||
pkg1 := &Package{Path: RefTerm(VarTerm("foo"), StringTerm("bar"), StringTerm("baz")).Value.(Ref)}
|
||||
result1 := pkg1.String()
|
||||
expected1 := "package foo.bar.baz"
|
||||
if result1 != expected1 {
|
||||
t.Errorf("Expected %v but got %v", expected1, result1)
|
||||
}
|
||||
}
|
||||
|
||||
func TestImportEquals(t *testing.T) {
|
||||
imp1 := &Import{Path: Var("foo"), Alias: Var("bar")}
|
||||
imp11 := &Import{Path: Var("foo"), Alias: Var("bar")}
|
||||
imp2 := &Import{Path: Var("foo")}
|
||||
imp3 := &Import{Path: RefTerm(VarTerm("bar"), VarTerm("baz"), VarTerm("qux")).Value, Alias: Var("corge")}
|
||||
imp33 := &Import{Path: RefTerm(VarTerm("bar"), VarTerm("baz"), VarTerm("qux")).Value, Alias: Var("corge")}
|
||||
imp4 := &Import{Path: RefTerm(VarTerm("bar"), VarTerm("baz"), VarTerm("qux")).Value}
|
||||
assertImportsEqual(t, imp1, imp1)
|
||||
assertImportsEqual(t, imp1, imp11)
|
||||
assertImportsEqual(t, imp3, imp3)
|
||||
assertImportsEqual(t, imp3, imp33)
|
||||
imps := []*Import{imp1, imp2, imp3, imp4}
|
||||
for i := range imps {
|
||||
for j := range imps {
|
||||
if i != j {
|
||||
assertImportsNotEqual(t, imps[i], imps[j])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestImportString(t *testing.T) {
|
||||
imp1 := &Import{Path: Var("foo"), Alias: Var("bar")}
|
||||
imp2 := &Import{Path: Var("foo")}
|
||||
imp3 := &Import{Path: RefTerm(VarTerm("bar"), StringTerm("baz"), StringTerm("qux")).Value, Alias: Var("corge")}
|
||||
imp4 := &Import{Path: RefTerm(VarTerm("bar"), StringTerm("baz"), StringTerm("qux")).Value}
|
||||
assertImportToString(t, imp1, "import foo as bar")
|
||||
assertImportToString(t, imp2, "import foo")
|
||||
assertImportToString(t, imp3, "import bar.baz.qux as corge")
|
||||
assertImportToString(t, imp4, "import bar.baz.qux")
|
||||
}
|
||||
|
||||
func TestExprEquals(t *testing.T) {
|
||||
|
||||
// Scalars
|
||||
expr1 := &Expr{Terms: BooleanTerm(true)}
|
||||
expr2 := &Expr{Terms: BooleanTerm(true)}
|
||||
expr3 := &Expr{Terms: StringTerm("true")}
|
||||
assertExprEqual(t, expr1, expr2)
|
||||
assertExprNotEqual(t, expr1, expr3)
|
||||
|
||||
// Vars, refs, and composites
|
||||
ref1 := RefTerm(VarTerm("foo"), StringTerm("bar"), VarTerm("i"))
|
||||
ref2 := RefTerm(VarTerm("foo"), StringTerm("bar"), VarTerm("i"))
|
||||
obj1 := ObjectTerm(Item(ref1, ArrayTerm(NumberTerm(1), NullTerm())))
|
||||
obj2 := ObjectTerm(Item(ref2, ArrayTerm(NumberTerm(1), NullTerm())))
|
||||
obj3 := ObjectTerm(Item(ref2, ArrayTerm(StringTerm("1"), NullTerm())))
|
||||
expr10 := &Expr{Terms: obj1}
|
||||
expr11 := &Expr{Terms: obj2}
|
||||
expr12 := &Expr{Terms: obj3}
|
||||
assertExprEqual(t, expr10, expr11)
|
||||
assertExprNotEqual(t, expr10, expr12)
|
||||
|
||||
// Builtins and negation
|
||||
expr20 := &Expr{
|
||||
Negated: true,
|
||||
Terms: []*Term{VarTerm("="), VarTerm("x"), ref1},
|
||||
}
|
||||
expr21 := &Expr{
|
||||
Negated: true,
|
||||
Terms: []*Term{VarTerm("="), VarTerm("x"), ref1},
|
||||
}
|
||||
expr22 := &Expr{
|
||||
Negated: false,
|
||||
Terms: []*Term{VarTerm("="), VarTerm("x"), ref1},
|
||||
}
|
||||
expr23 := &Expr{
|
||||
Negated: true,
|
||||
Terms: []*Term{VarTerm("="), VarTerm("y"), ref1},
|
||||
}
|
||||
assertExprEqual(t, expr20, expr21)
|
||||
assertExprNotEqual(t, expr20, expr22)
|
||||
assertExprNotEqual(t, expr20, expr23)
|
||||
}
|
||||
|
||||
func TextExprString(t *testing.T) {
|
||||
expr1 := &Expr{
|
||||
Terms: RefTerm(VarTerm("q"), StringTerm("r"), VarTerm("x")),
|
||||
}
|
||||
expr2 := &Expr{
|
||||
Negated: true,
|
||||
Terms: RefTerm(VarTerm("q"), StringTerm("r"), VarTerm("x")),
|
||||
}
|
||||
expr3 := &Expr{
|
||||
Terms: []*Term{VarTerm("="), StringTerm("a"), NumberTerm(17.1)},
|
||||
}
|
||||
expr4 := &Expr{
|
||||
Terms: []*Term{
|
||||
VarTerm("!="),
|
||||
ObjectTerm(Item(VarTerm("foo"), ArrayTerm(
|
||||
NumberTerm(1), RefTerm(VarTerm("a"), StringTerm("b")),
|
||||
))),
|
||||
BooleanTerm(false),
|
||||
},
|
||||
}
|
||||
assertExprString(t, expr1, "q.r[x]")
|
||||
assertExprString(t, expr2, "not q.r[x]")
|
||||
assertExprString(t, expr3, "eq(\"a\", 17.1)")
|
||||
assertExprString(t, expr4, "ne({foo: [1, a.b]}, false)")
|
||||
}
|
||||
|
||||
func TestRuleHeadEquals(t *testing.T) {
|
||||
assertRulesEqual(t, &Rule{}, &Rule{})
|
||||
|
||||
// Same name/key/value
|
||||
assertRulesEqual(t, &Rule{Name: Var("p")}, &Rule{Name: Var("p")})
|
||||
assertRulesEqual(t, &Rule{Key: VarTerm("x")}, &Rule{Key: VarTerm("x")})
|
||||
assertRulesEqual(t, &Rule{Value: VarTerm("x")}, &Rule{Value: VarTerm("x")})
|
||||
|
||||
// Different name/key/value
|
||||
assertRulesNotEqual(t, &Rule{Name: Var("p")}, &Rule{Name: Var("q")})
|
||||
assertRulesNotEqual(t, &Rule{Key: VarTerm("x")}, &Rule{Key: VarTerm("y")})
|
||||
assertRulesNotEqual(t, &Rule{Value: VarTerm("x")}, &Rule{Value: VarTerm("y")})
|
||||
}
|
||||
|
||||
func TestRuleBodyEquals(t *testing.T) {
|
||||
|
||||
true1 := &Expr{Terms: []*Term{BooleanTerm(true)}}
|
||||
true2 := &Expr{Terms: []*Term{BooleanTerm(true)}}
|
||||
false1 := &Expr{Terms: []*Term{BooleanTerm(false)}}
|
||||
|
||||
ruleTrue1 := &Rule{Body: []*Expr{true1}}
|
||||
ruleTrue12 := &Rule{Body: []*Expr{true1, true2}}
|
||||
ruleTrue2 := &Rule{Body: []*Expr{true2}}
|
||||
ruleTrue12_2 := &Rule{Body: []*Expr{true1, true2}}
|
||||
ruleFalse1 := &Rule{Body: []*Expr{false1}}
|
||||
ruleTrueFalse := &Rule{Body: []*Expr{true1, false1}}
|
||||
ruleFalseTrue := &Rule{Body: []*Expr{false1, true1}}
|
||||
|
||||
// Same expressions
|
||||
assertRulesEqual(t, ruleTrue1, ruleTrue2)
|
||||
assertRulesEqual(t, ruleTrue12, ruleTrue12_2)
|
||||
|
||||
// Different expressions/different order
|
||||
assertRulesNotEqual(t, ruleTrue1, ruleFalse1)
|
||||
assertRulesNotEqual(t, ruleTrueFalse, ruleFalseTrue)
|
||||
}
|
||||
|
||||
func TestRuleString(t *testing.T) {
|
||||
|
||||
rule1 := &Rule{
|
||||
Name: Var("p"),
|
||||
Body: []*Expr{
|
||||
&Expr{
|
||||
Terms: []*Term{
|
||||
VarTerm("="), StringTerm("foo"), StringTerm("bar"),
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
rule2 := &Rule{
|
||||
Name: Var("p"),
|
||||
Key: VarTerm("x"),
|
||||
Value: VarTerm("y"),
|
||||
Body: []*Expr{
|
||||
&Expr{
|
||||
Terms: []*Term{
|
||||
VarTerm("="), StringTerm("foo"), VarTerm("x"),
|
||||
},
|
||||
},
|
||||
&Expr{
|
||||
Negated: true,
|
||||
Terms: RefTerm(VarTerm("a"), StringTerm("b"), VarTerm("x")),
|
||||
},
|
||||
&Expr{
|
||||
Terms: []*Term{
|
||||
VarTerm("="), StringTerm("b"), VarTerm("y"),
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
assertRuleString(t, rule1, "p :- eq(\"foo\", \"bar\")")
|
||||
assertRuleString(t, rule2, "p[x] = y :- eq(\"foo\", x), not a.b[x], eq(\"b\", y)")
|
||||
}
|
||||
|
||||
func assertExprEqual(t *testing.T, a, b *Expr) {
|
||||
if !a.Equal(b) {
|
||||
t.Errorf("Expressions are not equal (expected equal): a=%v b=%v", a, b)
|
||||
}
|
||||
}
|
||||
|
||||
func assertExprNotEqual(t *testing.T, a, b *Expr) {
|
||||
if a.Equal(b) {
|
||||
t.Errorf("Expressions are equal (expected not equal): a=%v b=%v", a, b)
|
||||
}
|
||||
}
|
||||
|
||||
func assertExprString(t *testing.T, expr *Expr, expected string) {
|
||||
result := expr.String()
|
||||
if result != expected {
|
||||
t.Errorf("Expected %v but got %v", expected, result)
|
||||
}
|
||||
}
|
||||
|
||||
func assertImportsEqual(t *testing.T, a, b *Import) {
|
||||
if !a.Equal(b) {
|
||||
t.Errorf("Imports are not equal (expected equal): a=%v b=%v", a, b)
|
||||
}
|
||||
}
|
||||
|
||||
func assertImportsNotEqual(t *testing.T, a, b *Import) {
|
||||
if a.Equal(b) {
|
||||
t.Errorf("Imports are equal (expected not equal): a=%v b=%v", a, b)
|
||||
}
|
||||
}
|
||||
|
||||
func assertImportToString(t *testing.T, imp *Import, expected string) {
|
||||
result := imp.String()
|
||||
if result != expected {
|
||||
t.Errorf("Expected %v but got %v", expected, result)
|
||||
}
|
||||
}
|
||||
|
||||
func assertPackagesEqual(t *testing.T, a, b *Package) {
|
||||
if !a.Equal(b) {
|
||||
t.Errorf("Packages are not equal (expected equal): a=%v b=%v", a, b)
|
||||
}
|
||||
}
|
||||
|
||||
func assertPackagesNotEqual(t *testing.T, a, b *Package) {
|
||||
if a.Equal(b) {
|
||||
t.Errorf("Packages are not equal (expected not equal): a=%v b=%v", a, b)
|
||||
}
|
||||
}
|
||||
|
||||
func assertRulesEqual(t *testing.T, a, b *Rule) {
|
||||
if !a.Equal(b) {
|
||||
t.Errorf("Rules are not equal (expected equal): a=%v b=%v", a, b)
|
||||
}
|
||||
}
|
||||
|
||||
func assertRulesNotEqual(t *testing.T, a, b *Rule) {
|
||||
if a.Equal(b) {
|
||||
t.Errorf("Rules are equal (expected not equal): a=%v b=%v", a, b)
|
||||
}
|
||||
}
|
||||
|
||||
func assertRuleString(t *testing.T, rule *Rule, expected string) {
|
||||
result := rule.String()
|
||||
if result != expected {
|
||||
t.Errorf("Expected %v but got %v", expected, result)
|
||||
}
|
||||
}
|
||||
+87
-58
@@ -34,6 +34,9 @@ type Value interface {
|
||||
// Equal returns true if this value equals the other value.
|
||||
Equal(other Value) bool
|
||||
|
||||
// IsGround returns true if this value is not a variable or contains no variables.
|
||||
IsGround() bool
|
||||
|
||||
// String returns a human readable string representation of the value.
|
||||
String() string
|
||||
}
|
||||
@@ -47,12 +50,23 @@ type Term struct {
|
||||
// Equal returns true if this term equals the other term. Equality is
|
||||
// defined for each kind of term.
|
||||
func (term *Term) Equal(other *Term) bool {
|
||||
if term == nil && other != nil {
|
||||
return false
|
||||
}
|
||||
if term != nil && other == nil {
|
||||
return false
|
||||
}
|
||||
if term == other {
|
||||
return true
|
||||
}
|
||||
return term.Value.Equal(other.Value)
|
||||
}
|
||||
|
||||
// IsGround returns true if this terms' Value is ground.
|
||||
func (term *Term) IsGround() bool {
|
||||
return term.Value.IsGround()
|
||||
}
|
||||
|
||||
func (term *Term) String() string {
|
||||
return term.Value.String()
|
||||
}
|
||||
@@ -65,11 +79,6 @@ func NullTerm() *Term {
|
||||
return &Term{Value: Null{}}
|
||||
}
|
||||
|
||||
// NullTermWithLoc creates a new Term with a Null value and a Location.
|
||||
func NullTermWithLoc(loc *Location) *Term {
|
||||
return &Term{Value: Null{}, Location: loc}
|
||||
}
|
||||
|
||||
// Equal returns true if the other term Value is also Null.
|
||||
func (null Null) Equal(other Value) bool {
|
||||
switch other.(type) {
|
||||
@@ -80,6 +89,11 @@ func (null Null) Equal(other Value) bool {
|
||||
}
|
||||
}
|
||||
|
||||
// IsGround always returns true.
|
||||
func (null Null) IsGround() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (null Null) String() string {
|
||||
return "null"
|
||||
}
|
||||
@@ -92,11 +106,6 @@ func BooleanTerm(b bool) *Term {
|
||||
return &Term{Value: Boolean(b)}
|
||||
}
|
||||
|
||||
// BooleanTermWithLoc creates a new Term with a Boolean value and a Location.
|
||||
func BooleanTermWithLoc(b bool, loc *Location) *Term {
|
||||
return &Term{Value: Boolean(b), Location: loc}
|
||||
}
|
||||
|
||||
// Equal returns true if the other Value is a Boolean and is equal.
|
||||
func (bol Boolean) Equal(other Value) bool {
|
||||
switch other := other.(type) {
|
||||
@@ -107,6 +116,11 @@ func (bol Boolean) Equal(other Value) bool {
|
||||
}
|
||||
}
|
||||
|
||||
// IsGround always returns true.
|
||||
func (bol Boolean) IsGround() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (bol Boolean) String() string {
|
||||
return strconv.FormatBool(bool(bol))
|
||||
}
|
||||
@@ -119,11 +133,6 @@ func NumberTerm(n float64) *Term {
|
||||
return &Term{Value: Number(n)}
|
||||
}
|
||||
|
||||
// NumberTermWithLoc creates a new Term with a Number value and a Location.
|
||||
func NumberTermWithLoc(n float64, loc *Location) *Term {
|
||||
return &Term{Value: Number(n), Location: loc}
|
||||
}
|
||||
|
||||
// Equal returns true if the other Value is a Number and is equal.
|
||||
func (num Number) Equal(other Value) bool {
|
||||
switch other := other.(type) {
|
||||
@@ -134,6 +143,11 @@ func (num Number) Equal(other Value) bool {
|
||||
}
|
||||
}
|
||||
|
||||
// IsGround always returns true.
|
||||
func (num Number) IsGround() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (num Number) String() string {
|
||||
return strconv.FormatFloat(float64(num), 'G', -1, 64)
|
||||
}
|
||||
@@ -146,11 +160,6 @@ func StringTerm(s string) *Term {
|
||||
return &Term{Value: String(s)}
|
||||
}
|
||||
|
||||
// StringTermWithLoc creates a new Term with a String value and a Location.
|
||||
func StringTermWithLoc(s string, loc *Location) *Term {
|
||||
return &Term{Value: String(s), Location: loc}
|
||||
}
|
||||
|
||||
// Equal returns true if the other Value is a String and is equal.
|
||||
func (str String) Equal(other Value) bool {
|
||||
switch other := other.(type) {
|
||||
@@ -161,6 +170,11 @@ func (str String) Equal(other Value) bool {
|
||||
}
|
||||
}
|
||||
|
||||
// IsGround always returns true.
|
||||
func (str String) IsGround() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (str String) String() string {
|
||||
return strconv.Quote(string(str))
|
||||
}
|
||||
@@ -173,11 +187,6 @@ func VarTerm(v string) *Term {
|
||||
return &Term{Value: Var(v)}
|
||||
}
|
||||
|
||||
// VarTermWithLoc creates a new Term with a Variable value and a Location.
|
||||
func VarTermWithLoc(v string, loc *Location) *Term {
|
||||
return &Term{Value: Var(v), Location: loc}
|
||||
}
|
||||
|
||||
// Equal returns true if the other Value is a Variable and has the same value
|
||||
// (name).
|
||||
func (variable Var) Equal(other Value) bool {
|
||||
@@ -189,11 +198,16 @@ func (variable Var) Equal(other Value) bool {
|
||||
}
|
||||
}
|
||||
|
||||
// IsGround always returns false.
|
||||
func (variable Var) IsGround() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (variable Var) String() string {
|
||||
return string(variable)
|
||||
}
|
||||
|
||||
// Ref represents a variable as defined by Opalog.
|
||||
// Ref represents a reference as defined by Opalog.
|
||||
type Ref []*Term
|
||||
|
||||
// RefTerm creates a new Term with a Ref value.
|
||||
@@ -201,29 +215,22 @@ func RefTerm(r ...*Term) *Term {
|
||||
return &Term{Value: Ref(r)}
|
||||
}
|
||||
|
||||
// RefTermWithLoc creates a new Term with a Ref value and a Location.
|
||||
func RefTermWithLoc(r []*Term, loc *Location) *Term {
|
||||
return &Term{Value: Ref(r), Location: loc}
|
||||
}
|
||||
|
||||
// Equal returns true if the other Value is a Ref and the elements of the
|
||||
// other Ref are equal to the this Ref.
|
||||
func (ref Ref) Equal(other Value) bool {
|
||||
switch other := other.(type) {
|
||||
case Ref:
|
||||
if len(ref) == len(other) {
|
||||
for i := range ref {
|
||||
if !ref[i].Equal(other[i]) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
return termSliceEqual(ref, other)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
var varRegexp = regexp.MustCompile("[[:alpha:]_][[:alpha:][:digit:]_]+")
|
||||
// IsGround returns true if all of the parts of the Ref are ground.
|
||||
func (ref Ref) IsGround() bool {
|
||||
return termSliceIsGround(ref)
|
||||
}
|
||||
|
||||
var varRegexp = regexp.MustCompile("^[[:alpha:]_][[:alpha:][:digit:]_]*$")
|
||||
|
||||
func (ref Ref) String() string {
|
||||
buf := []string{string(ref[0].Value.(Var))}
|
||||
@@ -253,29 +260,22 @@ func ArrayTerm(a ...*Term) *Term {
|
||||
return &Term{Value: Array(a)}
|
||||
}
|
||||
|
||||
// ArrayTermWithLoc creates a new Term with an Array value and a Location.
|
||||
func ArrayTermWithLoc(a []*Term, loc *Location) *Term {
|
||||
return &Term{Value: Array(a), Location: loc}
|
||||
}
|
||||
|
||||
// Equal returns true if the other Value is an Array and the elements of the
|
||||
// other Array are equal to the elements of this Array. The elements are
|
||||
// ordered.
|
||||
func (arr Array) Equal(other Value) bool {
|
||||
switch other := other.(type) {
|
||||
case Array:
|
||||
if len(arr) == len(other) {
|
||||
for i := range arr {
|
||||
if !arr[i].Equal(other[i]) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
return termSliceEqual(arr, other)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// IsGround returns true if all of the Array elements are ground.
|
||||
func (arr Array) IsGround() bool {
|
||||
return termSliceIsGround(arr)
|
||||
}
|
||||
|
||||
func (arr Array) String() string {
|
||||
var buf []string
|
||||
for _, e := range arr {
|
||||
@@ -300,11 +300,6 @@ func ObjectTerm(o ...[2]*Term) *Term {
|
||||
return &Term{Value: Object(o)}
|
||||
}
|
||||
|
||||
// ObjectTermWithLoc creates a new Term with an Object value and a Location.
|
||||
func ObjectTermWithLoc(o [][2]*Term, loc *Location) *Term {
|
||||
return &Term{Value: Object(o), Location: loc}
|
||||
}
|
||||
|
||||
// Equal returns true if the other Value is an Object and the key/value pairs
|
||||
// of the Other object are equal to the key/value pairs of this Object. The
|
||||
// key/value pairs are ordered.
|
||||
@@ -326,6 +321,19 @@ func (obj Object) Equal(other Value) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// IsGround returns true if all of the Object key/value pairs are ground.
|
||||
func (obj Object) IsGround() bool {
|
||||
for i := range obj {
|
||||
if !obj[i][0].IsGround() {
|
||||
return false
|
||||
}
|
||||
if !obj[i][1].IsGround() {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (obj Object) String() string {
|
||||
var buf []string
|
||||
for _, p := range obj {
|
||||
@@ -333,3 +341,24 @@ func (obj Object) String() string {
|
||||
}
|
||||
return "{" + strings.Join(buf, ", ") + "}"
|
||||
}
|
||||
|
||||
func termSliceEqual(a, b []*Term) bool {
|
||||
if len(a) == len(b) {
|
||||
for i := range a {
|
||||
if !a[i].Equal(b[i]) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func termSliceIsGround(a []*Term) bool {
|
||||
for _, v := range a {
|
||||
if !v.IsGround() {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -51,6 +51,18 @@ func TestTermsToString(t *testing.T) {
|
||||
assertToString(t, ArrayTerm(ObjectTerm(Item(VarTerm("foo"), ArrayTerm(RefTerm(VarTerm("bar"), VarTerm("i"))))), StringTerm("foo"), BooleanTerm(true), NullTerm(), NumberTerm(42.1)).Value, "[{foo: [bar[i]]}, \"foo\", true, null, 42.1]")
|
||||
}
|
||||
|
||||
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 assertToString(t *testing.T, val Value, expected string) {
|
||||
result := val.String()
|
||||
if result != expected {
|
||||
|
||||
Reference in New Issue
Block a user