diff --git a/ast/map.go b/ast/map.go new file mode 100644 index 0000000000..66cf3a9b9b --- /dev/null +++ b/ast/map.go @@ -0,0 +1,89 @@ +// 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 ast + +import ( + "github.com/open-policy-agent/opa/util" +) + +// ValueMap represents a key/value map between AST term values. Any type of term +// can be used as a key in the map. +type ValueMap struct { + hashMap *util.HashMap +} + +// NewValueMap returns a new ValueMap. +func NewValueMap() *ValueMap { + vs := &ValueMap{ + hashMap: util.NewHashMap(valueEq, valueHash), + } + return vs +} + +// Copy returns a shallow copy of the ValueMap. +func (vs *ValueMap) Copy() *ValueMap { + cpy := NewValueMap() + cpy.hashMap = vs.hashMap.Copy() + return cpy +} + +// Equal returns true if this ValueMap equals the other. +func (vs *ValueMap) Equal(other *ValueMap) bool { + return vs.hashMap.Equal(other.hashMap) +} + +// Get returns the value in the map for k. +func (vs *ValueMap) Get(k Value) Value { + if v, ok := vs.hashMap.Get(k); ok { + return v.(Value) + } + return nil +} + +// Hash returns a hash code for this ValueMap. +func (vs *ValueMap) Hash() int { + return vs.hashMap.Hash() +} + +// Iter calls the iter function for each key/value pair in the map. If the iter +// function returns true, iteration stops. +func (vs *ValueMap) Iter(iter func(Value, Value) bool) bool { + return vs.hashMap.Iter(func(kt, vt util.T) bool { + k := kt.(Value) + v := vt.(Value) + return iter(k, v) + }) +} + +// Put inserts a key k into the map with value v. +func (vs *ValueMap) Put(k, v Value) { + vs.hashMap.Put(k, v) +} + +// Delete removes a key k from the map. +func (vs *ValueMap) Delete(k Value) { + vs.hashMap.Delete(k) +} + +// Update returns a new ValueMap that contains the union of this ValueMap and +// the other. Overlapping keys are replaced with values from the other. +func (vs *ValueMap) Update(other *ValueMap) *ValueMap { + new := vs.hashMap.Update(other.hashMap) + return &ValueMap{new} +} + +func (vs *ValueMap) String() string { + return vs.hashMap.String() +} + +func valueHash(v util.T) int { + return v.(Value).Hash() +} + +func valueEq(a, b util.T) bool { + av := a.(Value) + bv := b.(Value) + return av.Equal(bv) +} diff --git a/ast/map_test.go b/ast/map_test.go new file mode 100644 index 0000000000..97e4a30d03 --- /dev/null +++ b/ast/map_test.go @@ -0,0 +1,97 @@ +// 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 ast + +import ( + "reflect" + "sort" + "testing" +) + +func TestValueMapOverwrite(t *testing.T) { + + a := NewValueMap() + b := NewValueMap() + a.Put(String("x"), String("foo")) + a.Put(String("x"), String("bar")) + if a.Get(String("x")) != String("bar") { + t.Fatalf("Expected a['x'] = 'bar' but got: %v", a.Get(String("x"))) + } + + a.Put(String("z"), String("corge")) + b.Put(String("y"), String("baz")) + b.Put(String("x"), String("qux")) + + c := a.Update(b) + + if c.Get(String("x")) != String("qux") { + t.Fatalf("Expected c['x'] = 'qux' but got: %v", c.Get(String("x"))) + } +} + +func TestValueMapIter(t *testing.T) { + a := NewValueMap() + a.Put(String("x"), String("foo")) + a.Put(String("y"), String("bar")) + a.Put(String("z"), String("baz")) + values := []string{} + a.Iter(func(k, v Value) bool { + values = append(values, string(v.(String))) + return false + }) + sort.Strings(values) + expected := []string{"bar", "baz", "foo"} + if !reflect.DeepEqual(values, expected) { + t.Fatalf("Unexpected value from iteration: %v", values) + } +} + +func TestValueMapCopy(t *testing.T) { + a := NewValueMap() + a.Put(String("x"), String("foo")) + a.Put(String("y"), String("bar")) + b := a.Copy() + b.Delete(String("y")) + if a.Get(String("y")) != String("bar") { + t.Fatalf("Unexpected a['y'] value: %v", a.Get(String("y"))) + } +} + +func TestValueMapEqual(t *testing.T) { + a := NewValueMap() + a.Put(String("x"), String("foo")) + a.Put(String("y"), String("bar")) + b := a.Copy() + if !a.Equal(b) { + t.Fatalf("Expected a == b but not for: %v / %v", a, b) + } + if a.Hash() != b.Hash() { + t.Fatalf("Expected a.Hash() == b.Hash() but not for: %v / %v", a, b) + } + a.Delete(String("x")) + if a.Equal(b) { + t.Fatalf("Expected a != b but not for: %v / %v", a, b) + } +} + +func TestValueMapGetMissing(t *testing.T) { + a := NewValueMap() + a.Put(String("x"), String("foo")) + a.Put(String("y"), String("bar")) + if a.Get(String("z")) != nil { + t.Fatalf("Expected a['z'] = nil but got: %v", a.Get(String("z"))) + } +} + +func TestValueMapString(t *testing.T) { + a := NewValueMap() + a.Put(MustParseRef("a.b.c[x]"), String("foo")) + a.Put(Var("x"), Number(1)) + expected := `{a.b.c[x]: "foo", x: 1}` + result := a.String() + if expected != result { + t.Fatalf("Expected %v but got: %v", expected, result) + } +} diff --git a/server/server.go b/server/server.go index 999231337e..15f66f59b8 100644 --- a/server/server.go +++ b/server/server.go @@ -722,8 +722,8 @@ func getPretty(p []string) bool { return false } -func parseGlobals(g []string) (*storage.Bindings, error) { - globals := storage.NewBindings() +func parseGlobals(g []string) (*ast.ValueMap, error) { + globals := ast.NewValueMap() for _, g := range g { vs := strings.SplitN(g, ":", 2) k, err := ast.ParseTerm(vs[0]) diff --git a/server/server_test.go b/server/server_test.go index 162c5f3d33..8b7b8efe7e 100644 --- a/server/server_test.go +++ b/server/server_test.go @@ -525,7 +525,7 @@ func TestGlobalParsing(t *testing.T) { t.Errorf("%v (#%d): Unexpected error: %v", tc.note, i+1, err) continue } - exp := storage.NewBindings() + exp := ast.NewValueMap() for _, i := range ast.MustParseTerm(e).Value.(ast.Object) { exp.Put(i[0].Value, i[1].Value) } diff --git a/storage/bindings.go b/storage/bindings.go deleted file mode 100644 index 9e3eebb0db..0000000000 --- a/storage/bindings.go +++ /dev/null @@ -1,93 +0,0 @@ -// 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 storage - -import ( - "github.com/open-policy-agent/opa/ast" - "github.com/open-policy-agent/opa/util" -) - -// Bindings represents a mapping between key/value pairs. -// The key/value pairs are AST values contained in expressions. -// Insertion of a key/value pair represents unification of the -// two values. -// -// TODO(tsandall): rename to ValueMap and move into ast package. -type Bindings struct { - hashMap *util.HashMap -} - -// NewBindings returns a new empty set of bindings. -func NewBindings() *Bindings { - b := &Bindings{ - hashMap: util.NewHashMap(bindingsEq, bindingsHash), - } - return b -} - -// Copy returns a shallow copy of these bindings. -func (b *Bindings) Copy() *Bindings { - cpy := NewBindings() - cpy.hashMap = b.hashMap.Copy() - return cpy -} - -// Equal returns true if these bindings equal the other bindings. -// Two bindings are equal if they contain the same key/value pairs. -func (b *Bindings) Equal(other *Bindings) bool { - return b.hashMap.Equal(other.hashMap) -} - -// Get returns the binding for the given key. -func (b *Bindings) Get(k ast.Value) ast.Value { - if v, ok := b.hashMap.Get(k); ok { - return v.(ast.Value) - } - return nil -} - -// Hash returns the hash code for the bindings. -func (b *Bindings) Hash() int { - return b.hashMap.Hash() -} - -// Iter iterates the bindings and calls the "iter" function for each key/value pair. -func (b *Bindings) Iter(iter func(ast.Value, ast.Value) bool) bool { - return b.hashMap.Iter(func(kt, vt util.T) bool { - k := kt.(ast.Value) - v := vt.(ast.Value) - return iter(k, v) - }) -} - -// Put inserts a key/value pair. -func (b *Bindings) Put(k, v ast.Value) { - b.hashMap.Put(k, v) -} - -// Delete removes a key/value pair. -func (b *Bindings) Delete(k ast.Value) { - b.hashMap.Delete(k) -} - -// Update returns new bindings that are the union of these bindings and the other bindings. -func (b *Bindings) Update(other *Bindings) *Bindings { - new := b.hashMap.Update(other.hashMap) - return &Bindings{new} -} - -func (b *Bindings) String() string { - return b.hashMap.String() -} - -func bindingsHash(v util.T) int { - return v.(ast.Value).Hash() -} - -func bindingsEq(a, b util.T) bool { - av := a.(ast.Value) - bv := b.(ast.Value) - return av.Equal(bv) -} diff --git a/storage/index.go b/storage/index.go index 8ecb5f9438..6fe7e3e034 100644 --- a/storage/index.go +++ b/storage/index.go @@ -53,7 +53,7 @@ func newIndices() *indices { func (ind *indices) Build(store Store, txn Transaction, ref ast.Ref) error { index := newBindingIndex() ind.registerTriggers(store) - err := iterStorage(store, txn, ref, ast.EmptyRef(), NewBindings(), func(bindings *Bindings, val interface{}) { + err := iterStorage(store, txn, ref, ast.EmptyRef(), ast.NewValueMap(), func(bindings *ast.ValueMap, val interface{}) { index.Add(val, bindings) }) if err != nil { @@ -163,7 +163,7 @@ func newBindingIndex() *bindingIndex { // Add updates the index to include new bindings for the value. // If the bindings already exist for the value, no change is made. -func (ind *bindingIndex) Add(val interface{}, bindings *Bindings) { +func (ind *bindingIndex) Add(val interface{}, bindings *ast.ValueMap) { node := ind.getNode(val) if node != nil { @@ -185,7 +185,7 @@ func (ind *bindingIndex) Add(val interface{}, bindings *Bindings) { } // Iter calls the iter function for each set of bindings for the value. -func (ind *bindingIndex) Iter(val interface{}, iter func(*Bindings) error) error { +func (ind *bindingIndex) Iter(val interface{}, iter func(*ast.ValueMap) error) error { node := ind.getNode(val) if node == nil { return nil @@ -219,7 +219,7 @@ func (ind *bindingIndex) String() string { } type bindingSetNode struct { - val *Bindings + val *ast.ValueMap next *bindingSetNode } @@ -233,7 +233,7 @@ func newBindingSet() *bindingSet { } } -func (set *bindingSet) Add(val *Bindings) { +func (set *bindingSet) Add(val *ast.ValueMap) { node := set.getNode(val) if node != nil { return @@ -243,7 +243,7 @@ func (set *bindingSet) Add(val *Bindings) { set.table[hashCode] = &bindingSetNode{val, head} } -func (set *bindingSet) Iter(iter func(*Bindings) error) error { +func (set *bindingSet) Iter(iter func(*ast.ValueMap) error) error { for _, head := range set.table { for entry := head; entry != nil; entry = entry.next { if err := iter(entry.val); err != nil { @@ -256,14 +256,14 @@ func (set *bindingSet) Iter(iter func(*Bindings) error) error { func (set *bindingSet) String() string { buf := []string{} - set.Iter(func(bindings *Bindings) error { + set.Iter(func(bindings *ast.ValueMap) error { buf = append(buf, bindings.String()) return nil }) return "{" + strings.Join(buf, ", ") + "}" } -func (set *bindingSet) getNode(val *Bindings) *bindingSetNode { +func (set *bindingSet) getNode(val *ast.ValueMap) *bindingSetNode { hashCode := val.Hash() for entry := set.table[hashCode]; entry != nil; entry = entry.next { if entry.val.Equal(val) { @@ -304,7 +304,7 @@ func hash(v interface{}) int { panic(fmt.Sprintf("illegal argument: %v (%T)", v, v)) } -func iterStorage(store Store, txn Transaction, ref ast.Ref, path ast.Ref, bindings *Bindings, iter func(*Bindings, interface{})) error { +func iterStorage(store Store, txn Transaction, ref ast.Ref, path ast.Ref, bindings *ast.ValueMap, iter func(*ast.ValueMap, interface{})) error { if len(ref) == 0 { node, err := store.Read(txn, path) diff --git a/storage/index_test.go b/storage/index_test.go index 65353bb79f..94942850a1 100644 --- a/storage/index_test.go +++ b/storage/index_test.go @@ -105,7 +105,7 @@ func assertBindingsEqual(t *testing.T, note string, index *bindingIndex, value i expected := loadExpectedBindings(expectedStr) - err := index.Iter(value, func(bindings *Bindings) error { + err := index.Iter(value, func(bindings *ast.ValueMap) error { for j := range expected { if expected[j].Equal(bindings) { tmp := expected[:j] @@ -127,14 +127,14 @@ func assertBindingsEqual(t *testing.T, note string, index *bindingIndex, value i } } -func loadExpectedBindings(input string) []*Bindings { +func loadExpectedBindings(input string) []*ast.ValueMap { var data []map[string]interface{} if err := json.Unmarshal([]byte(input), &data); err != nil { panic(err) } - var expected []*Bindings + var expected []*ast.ValueMap for _, bindings := range data { - buf := NewBindings() + buf := ast.NewValueMap() for k, v := range bindings { switch v := v.(type) { case string: diff --git a/storage/storage.go b/storage/storage.go index 607b77b077..6124f2b687 100644 --- a/storage/storage.go +++ b/storage/storage.go @@ -303,7 +303,7 @@ func (s *Storage) IndexExists(ref ast.Ref) bool { // Index invokes the iterator with bindings for each variable in the reference // that if plugged into the reference, would locate a document with a matching // value. -func (s *Storage) Index(txn Transaction, ref ast.Ref, value interface{}, iter func(*Bindings) error) error { +func (s *Storage) Index(txn Transaction, ref ast.Ref, value interface{}, iter func(*ast.ValueMap) error) error { idx := s.indices.Get(ref) if idx == nil { diff --git a/test/scheduler/scheduler_bench_test.go b/test/scheduler/scheduler_bench_test.go index c086e964c1..acce939a99 100644 --- a/test/scheduler/scheduler_bench_test.go +++ b/test/scheduler/scheduler_bench_test.go @@ -65,7 +65,7 @@ func setupBenchmark(nodes int, pods int) *topdown.QueryParams { store := storage.New(storage.InMemoryConfig()) // parameter setup - globals := storage.NewBindings() + globals := ast.NewValueMap() req := ast.MustParseTerm(requestedPod).Value globals.Put(ast.Var("requested_pod"), req) path := []interface{}{"opa", "test", "scheduler", "fit"} diff --git a/test/scheduler/scheduler_test.go b/test/scheduler/scheduler_test.go index 5a25fb2c2b..236c8a1143 100644 --- a/test/scheduler/scheduler_test.go +++ b/test/scheduler/scheduler_test.go @@ -54,7 +54,7 @@ func setup(t *testing.T, filename string) *topdown.QueryParams { }) // parameter setup - globals := storage.NewBindings() + globals := ast.NewValueMap() req := ast.MustParseTerm(requestedPod).Value globals.Put(ast.Var("requested_pod"), req) path := []interface{}{"opa", "test", "scheduler", "fit"} diff --git a/topdown/example_test.go b/topdown/example_test.go index 2355a133f2..691b02b3b9 100644 --- a/topdown/example_test.go +++ b/topdown/example_test.go @@ -104,7 +104,7 @@ func ExampleQuery() { // Prepare the query parameters. Queries execute against the policy engine's storage and can // accept additional documents (which are referred to as "globals"). In this case we have no // additional documents. - globals := storage.NewBindings() + globals := ast.NewValueMap() params := topdown.NewQueryParams(compiler, store, txn, globals, []interface{}{"opa", "example", "p"}) // Execute the query against "p". diff --git a/topdown/topdown.go b/topdown/topdown.go index a90614414a..c070ed346c 100644 --- a/topdown/topdown.go +++ b/topdown/topdown.go @@ -17,8 +17,8 @@ import ( type Context struct { Query ast.Body Compiler *ast.Compiler - Globals *storage.Bindings - Locals *storage.Bindings + Globals *ast.ValueMap + Locals *ast.ValueMap Index int Previous *Context Store *storage.Storage @@ -37,8 +37,8 @@ func NewContext(query ast.Body, compiler *ast.Compiler, store *storage.Storage, return &Context{ Query: query, Compiler: compiler, - Globals: storage.NewBindings(), - Locals: storage.NewBindings(), + Globals: ast.NewValueMap(), + Locals: ast.NewValueMap(), Store: store, txn: txn, cache: newContextCache(), @@ -85,7 +85,7 @@ func (ctx *Context) Unbind(undo *Undo) { } // Child returns a new context to evaluate a query that was referenced by this context. -func (ctx *Context) Child(query ast.Body, locals *storage.Bindings) *Context { +func (ctx *Context) Child(query ast.Body, locals *ast.ValueMap) *Context { cpy := *ctx cpy.Query = query cpy.Locals = locals @@ -352,13 +352,13 @@ type QueryParams struct { Compiler *ast.Compiler Store *storage.Storage Transaction storage.Transaction - Globals *storage.Bindings + Globals *ast.ValueMap Tracer Tracer Path []interface{} } // NewQueryParams returns a new QueryParams. -func NewQueryParams(compiler *ast.Compiler, store *storage.Storage, txn storage.Transaction, globals *storage.Bindings, path []interface{}) *QueryParams { +func NewQueryParams(compiler *ast.Compiler, store *storage.Storage, txn storage.Transaction, globals *ast.ValueMap, path []interface{}) *QueryParams { return &QueryParams{ Compiler: compiler, Store: store, @@ -374,7 +374,7 @@ func (q *QueryParams) NewContext(body ast.Body) *Context { Query: body, Compiler: q.Compiler, Globals: q.Globals, - Locals: storage.NewBindings(), + Locals: ast.NewValueMap(), Store: q.Store, Tracer: q.Tracer, txn: q.Transaction, @@ -991,7 +991,7 @@ func evalRefRuleCompleteDoc(ctx *Context, ref ast.Ref, suffix ast.Ref, rules []* for _, rule := range rules { - bindings := storage.NewBindings() + bindings := ast.NewValueMap() child := ctx.Child(rule.Body, bindings) err := Eval(child, func(child *Context) error { @@ -1039,7 +1039,7 @@ func evalRefRulePartialObjectDoc(ctx *Context, ref ast.Ref, path ast.Ref, rule * // a recursive binding if we unified "key" with "rule.Key.Value". If unification // is improved to handle namespacing, this can be revisited. if !key.IsGround() { - child := ctx.Child(rule.Body, storage.NewBindings()) + child := ctx.Child(rule.Body, ast.NewValueMap()) return Eval(child, func(child *Context) error { key := PlugValue(rule.Key.Value, child) @@ -1093,7 +1093,7 @@ func evalRefRulePartialObjectDoc(ctx *Context, ref ast.Ref, path ast.Ref, rule * } } - child := ctx.Child(rule.Body, storage.NewBindings()) + child := ctx.Child(rule.Body, ast.NewValueMap()) _, err := evalEqUnify(child, key, rule.Key.Value, nil, func(child *Context) error { return Eval(child, func(child *Context) error { value := PlugValue(rule.Value.Value, child) @@ -1143,7 +1143,7 @@ func evalRefRulePartialObjectDocFull(ctx *Context, ref ast.Ref, rules []*ast.Rul for _, rule := range rules { - bindings := storage.NewBindings() + bindings := ast.NewValueMap() child := ctx.Child(rule.Body, bindings) err := Eval(child, func(child *Context) error { @@ -1188,7 +1188,7 @@ func evalRefRulePartialSetDoc(ctx *Context, ref ast.Ref, path ast.Ref, rule *ast // See comment in evalRefRulePartialObjectDoc about the two branches below. if !key.IsGround() { - child := ctx.Child(rule.Body, storage.NewBindings()) + child := ctx.Child(rule.Body, ast.NewValueMap()) // TODO(tsandall): Currently this evaluates the child query without any // bindings from the current context. In cases where the key is partially @@ -1214,7 +1214,7 @@ func evalRefRulePartialSetDoc(ctx *Context, ref ast.Ref, path ast.Ref, rule *ast }) } - child := ctx.Child(rule.Body, storage.NewBindings()) + child := ctx.Child(rule.Body, ast.NewValueMap()) _, err := evalEqUnify(child, key, rule.Key.Value, nil, func(child *Context) error { return Eval(child, func(child *Context) error { @@ -1412,7 +1412,7 @@ func evalTermsIndexed(ctx *Context, iter Iterator, indexed ast.Ref, nonIndexed * // Iterate the bindings for the indexed term that when applied to the reference // would locate the non-indexed value obtained above. - return ctx.Store.Index(ctx.txn, indexed, nonIndexedValue, func(bindings *storage.Bindings) error { + return ctx.Store.Index(ctx.txn, indexed, nonIndexedValue, func(bindings *ast.ValueMap) error { var prev *Undo bindings.Iter(func(k, v ast.Value) bool { prev = ctx.Bind(k, v, prev) diff --git a/topdown/topdown_test.go b/topdown/topdown_test.go index 3798a1a78c..0837786dc4 100644 --- a/topdown/topdown_test.go +++ b/topdown/topdown_test.go @@ -1251,14 +1251,14 @@ func compileRules(imports []string, input []string) *ast.Compiler { return c } -func loadExpectedBindings(input string) []*storage.Bindings { +func loadExpectedBindings(input string) []*ast.ValueMap { var data []map[string]interface{} if err := json.Unmarshal([]byte(input), &data); err != nil { panic(err) } - var expected []*storage.Bindings + var expected []*ast.ValueMap for _, bindings := range data { - buf := storage.NewBindings() + buf := ast.NewValueMap() for k, v := range bindings { switch v := v.(type) { case string: @@ -1380,7 +1380,7 @@ func runTopDownTestCase(t *testing.T, data map[string]interface{}, note string, func assertTopDown(t *testing.T, compiler *ast.Compiler, store *storage.Storage, note string, path []string, globals string, expected interface{}) { - g := storage.NewBindings() + g := ast.NewValueMap() for _, i := range ast.MustParseTerm(globals).Value.(ast.Object) { g.Put(i[0].Value, i[1].Value) } diff --git a/topdown/tracer_test.go b/topdown/tracer_test.go index 6c18f7b2ad..2fa4ec4309 100644 --- a/topdown/tracer_test.go +++ b/topdown/tracer_test.go @@ -8,6 +8,7 @@ import ( "fmt" "testing" + "github.com/open-policy-agent/opa/ast" "github.com/open-policy-agent/opa/storage" "github.com/open-policy-agent/opa/util" ) @@ -35,7 +36,7 @@ func TestTracer(t *testing.T) { tracer := &mockTracer{[]string{}} - params := NewQueryParams(compiler, store, txn, storage.NewBindings(), []interface{}{"p"}) + params := NewQueryParams(compiler, store, txn, ast.NewValueMap(), []interface{}{"p"}) params.Tracer = tracer result, err := Query(params)