Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Add basic REST API support #33

Merged
merged 9 commits into from
May 17, 2016
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# development environment
.DS_Store
.vscode

# build artifacts
coverage
opa
ast/parser.go
coverage

# runtime artifacts
policies
7 changes: 5 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -37,14 +37,17 @@ generate:
build: generate
$(GO) build -o opa $(LDFLAGS)

install: generate
$(GO) install $(LDFLAGS)

test: generate
$(GO) test -v $(PACKAGES)

COVER_PACKAGES=$(PACKAGES)
$(COVER_PACKAGES):
@mkdir -p coverage/$(shell dirname $@)
go test -covermode=count -coverprofile=coverage/$(shell dirname $@)/coverage.out $@
go tool cover -html=coverage/$(shell dirname $@)/coverage.out || true
$(GO) test -covermode=count -coverprofile=coverage/$(shell dirname $@)/coverage.out $@
$(GO) tool cover -html=coverage/$(shell dirname $@)/coverage.out || true

cover: $(COVER_PACKAGES)

Expand Down
4 changes: 2 additions & 2 deletions ast/compile.go
Original file line number Diff line number Diff line change
Expand Up @@ -196,7 +196,7 @@ func (c *Compiler) setGlobals() {
// Populate globals with imports within this module.
for _, i := range m.Imports {
if len(i.Alias) > 0 {
switch p := i.Path.(type) {
switch p := i.Path.Value.(type) {
case Ref:
globals[i.Alias] = p
case Var:
Expand All @@ -205,7 +205,7 @@ func (c *Compiler) setGlobals() {
c.err("unexpected %T: %v", p, i)
}
} else {
switch p := i.Path.(type) {
switch p := i.Path.Value.(type) {
case Ref:
switch v := p[len(p)-1].Value.(type) {
case String:
Expand Down
8 changes: 4 additions & 4 deletions ast/parser_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -200,12 +200,12 @@ func TestPackage(t *testing.T) {
}

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, "single", "import foo", &Import{Path: VarTerm("foo")})
ref := RefTerm(VarTerm("foo"), StringTerm("bar"), StringTerm("baz"))
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, "single alias", "import foo as bar", &Import{Path: VarTerm("foo"), 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
ref2 := RefTerm(VarTerm("foo"), StringTerm("bar"), StringTerm("white space"))
assertParseImport(t, "white space", "import foo.bar[\"white space\"]", &Import{Path: ref2})
assertParseError(t, "non-ground ref", "import foo[x]")
}
Expand Down
70 changes: 59 additions & 11 deletions ast/policy.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,11 @@

package ast

import "fmt"
import "strings"
import (
"encoding/json"
"fmt"
"strings"
)

// DefaultRootDocument is the default root document.
// All package directives inside source files are implicitly
Expand All @@ -31,25 +34,25 @@ type (
// Package represents the namespace of the documents produced
// by rules inside the module.
Package struct {
Location *Location
Location *Location `json:"-"`
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
Location *Location `json:"-"`
Path *Term
Alias Var `json:",omitempty"`
}

// Rule represents a rule as defined in the language. Rules define the
// content of documents that represent policy decisions.
Rule struct {
Location *Location
Location *Location `json:"-"`
Name Var
Key *Term
Value *Term
Key *Term `json:",omitempty"`
Value *Term `json:",omitempty"`
Body Body
}

Expand All @@ -58,8 +61,8 @@ type (

// Expr represents a single expression contained inside the body of a rule.
Expr struct {
Location *Location
Negated bool
Location *Location `json:"-"`
Negated bool `json:",omitempty"`
Terms interface{}
}
)
Expand Down Expand Up @@ -264,6 +267,51 @@ func (expr *Expr) String() string {
return strings.Join(buf, " ")
}

// UnmarshalJSON parses the byte array and stores the result in expr.
func (expr *Expr) UnmarshalJSON(bs []byte) error {
v := map[string]interface{}{}
if err := json.Unmarshal(bs, &v); err != nil {
return err
}

n, ok := v["Negated"]
if !ok {
expr.Negated = false
} else {
b, ok := n.(bool)
if !ok {
return unmarshalError(n, "bool")
}
expr.Negated = b
}

switch ts := v["Terms"].(type) {
case map[string]interface{}:
v, err := unmarshalValue(ts)
if err != nil {
return err
}
expr.Terms = &Term{Value: v}
case []interface{}:
buf := []*Term{}
for _, v := range ts {
e, ok := v.(map[string]interface{})
if !ok {
return unmarshalError(v, "map[string]interface{}")
}
v, err := unmarshalValue(e)
if err != nil {
return err
}
buf = append(buf, &Term{Value: v})
}
expr.Terms = buf
default:
return unmarshalError(v["Terms"], "Term or []Term")
}
return nil
}

// NewBuiltinExpr creates a new Expr object with the supplied terms.
// The builtin operator must be the first term.
func NewBuiltinExpr(terms ...*Term) *Expr {
Expand Down
95 changes: 84 additions & 11 deletions ast/policy_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,38 @@

package ast

import "testing"
import (
"encoding/json"
"reflect"
"testing"
)

func TestModuleJSONRoundTrip(t *testing.T) {
mod := MustParseModule(`
package a.b.c
import data.x.y as z
import data.u.i
p = [1,2,{"foo":3}] :- r[x] = 1, not q[x]
r[y] = v :- i[1] = y, v = i[2]
q[x] :- a=[true,false,null,{"x":[1,2,3]}], a[i] = x
`)

bs, err := json.Marshal(mod)
if err != nil {
panic(err)
}

roundtrip := &Module{}

err = json.Unmarshal(bs, roundtrip)
if err != nil {
panic(err)
}

if !roundtrip.Equal(mod) {
t.Errorf("Expected roundtripped module to be equal to original:\nExpected:\n\n%v\n\nGot:\n\n%v\n", mod, roundtrip)
}
}

func TestPackageEquals(t *testing.T) {
pkg1 := &Package{Path: RefTerm(VarTerm("foo"), StringTerm("bar"), StringTerm("baz")).Value.(Ref)}
Expand All @@ -26,12 +57,12 @@ func TestPackageString(t *testing.T) {
}

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}
imp1 := &Import{Path: VarTerm("foo"), Alias: Var("bar")}
imp11 := &Import{Path: VarTerm("foo"), Alias: Var("bar")}
imp2 := &Import{Path: VarTerm("foo")}
imp3 := &Import{Path: RefTerm(VarTerm("bar"), VarTerm("baz"), VarTerm("qux")), Alias: Var("corge")}
imp33 := &Import{Path: RefTerm(VarTerm("bar"), VarTerm("baz"), VarTerm("qux")), Alias: Var("corge")}
imp4 := &Import{Path: RefTerm(VarTerm("bar"), VarTerm("baz"), VarTerm("qux"))}
assertImportsEqual(t, imp1, imp1)
assertImportsEqual(t, imp1, imp11)
assertImportsEqual(t, imp3, imp3)
Expand All @@ -47,10 +78,10 @@ func TestImportEquals(t *testing.T) {
}

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}
imp1 := &Import{Path: VarTerm("foo"), Alias: Var("bar")}
imp2 := &Import{Path: VarTerm("foo")}
imp3 := &Import{Path: RefTerm(VarTerm("bar"), StringTerm("baz"), StringTerm("qux")), Alias: Var("corge")}
imp4 := &Import{Path: RefTerm(VarTerm("bar"), StringTerm("baz"), StringTerm("qux"))}
assertImportToString(t, imp1, "import foo as bar")
assertImportToString(t, imp2, "import foo")
assertImportToString(t, imp3, "import bar.baz.qux as corge")
Expand Down Expand Up @@ -126,6 +157,48 @@ func TextExprString(t *testing.T) {
assertExprString(t, expr4, "ne({foo: [1, a.b]}, false)")
}

func TestExprBadJSON(t *testing.T) {

assert := func(js string, exp error) {
expr := Expr{}
err := json.Unmarshal([]byte(js), &expr)
if !reflect.DeepEqual(exp, err) {
t.Errorf("Expected %v but got: %v", exp, err)
}
}

js := `
{
"Negated": 100,
"Terms": {
"Value": "foo",
"Type": "string"
}
}
`

exp := unmarshalError(100.0, "bool")
assert(js, exp)

js = `
{
"Terms": [
"foo"
]
}
`
exp = unmarshalError("foo", "map[string]interface{}")
assert(js, exp)

js = `
{
"Terms": "bad value"
}
`
exp = unmarshalError("bad value", "Term or []Term")
assert(js, exp)
}

func TestRuleHeadEquals(t *testing.T) {
assertRulesEqual(t, &Rule{}, &Rule{})

Expand Down
4 changes: 2 additions & 2 deletions ast/rego.peg
Original file line number Diff line number Diff line change
Expand Up @@ -74,8 +74,8 @@ Package <- "package" ws val:(Ref / Var) {
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) {
imp.Path = path.(*Term)
switch p := imp.Path.Value.(type) {
case Ref:
if !p.IsGround() {
return nil, fmt.Errorf("import cannot contain variables in tail: %v", p)
Expand Down
Loading