From 75cee19201162ea4730a8d2182a2341bc77b3cc6 Mon Sep 17 00:00:00 2001 From: Torin Sandall Date: Fri, 2 Dec 2016 10:14:49 -0800 Subject: [PATCH] Add new storage.Path type --- storage/path.go | 127 +++++++++++++++++++++++++++++++++++ storage/path_test.go | 153 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 280 insertions(+) create mode 100644 storage/path.go create mode 100644 storage/path_test.go diff --git a/storage/path.go b/storage/path.go new file mode 100644 index 0000000000..106fed0ab1 --- /dev/null +++ b/storage/path.go @@ -0,0 +1,127 @@ +// 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 ( + "fmt" + "strconv" + "strings" + + "github.com/open-policy-agent/opa/ast" +) + +// Path refers to a document in storage. +type Path []string + +// ParsePath returns a new path for the given str. +func ParsePath(str string) (path Path, ok bool) { + if len(str) == 0 { + return nil, false + } + if str[0] != '/' { + return nil, false + } + if len(str) == 1 { + return Path{}, true + } + parts := strings.Split(str[1:], "/") + return parts, true +} + +// NewPathForRef returns a new path for the given ref. +func NewPathForRef(ref ast.Ref) (path Path, err error) { + + if len(ref) == 0 { + return nil, fmt.Errorf("empty reference (indicates error in caller)") + } + + if len(ref) == 1 { + return Path{}, nil + } + + for _, term := range ref[1:] { + switch v := term.Value.(type) { + case ast.String: + path = append(path, string(v)) + case ast.Number: + path = append(path, v.String()) + case ast.Boolean, ast.Null: + return nil, notFoundRefError(ref, doesNotExistMsg) + default: + return nil, fmt.Errorf("unresolved reference (indicates error in caller): %v", ref) + } + } + + return path, nil +} + +// Compare performs lexigraphical comparison on p and other and returns -1 if p +// is less than other, 0 if p is equal to other, or 1 if p is greater than +// other. +func (p Path) Compare(other Path) (cmp int) { + min := len(p) + if len(other) < min { + min = len(other) + } + for i := 0; i < min; i++ { + if cmp := strings.Compare(p[i], other[i]); cmp != 0 { + return cmp + } + } + if len(p) < len(other) { + return -1 + } + if len(p) == len(other) { + return 0 + } + return 1 +} + +// Equal returns true if p is the same as other. +func (p Path) Equal(other Path) bool { + return p.Compare(other) == 0 +} + +// HasPrefix returns true if p starts with other. +func (p Path) HasPrefix(other Path) bool { + if len(other) > len(p) { + return false + } + for i := range other { + if p[i] != other[i] { + return false + } + } + return true +} + +// Ref returns a ref that represents p rooted at head. +func (p Path) Ref(head *ast.Term) (ref ast.Ref) { + ref = make(ast.Ref, len(p)+1) + ref[0] = head + for i := range p { + idx, err := strconv.ParseInt(p[i], 10, 64) + if err == nil { + ref[i+1] = ast.NumberTerm(float64(idx)) + } else { + ref[i+1] = ast.StringTerm(p[i]) + } + } + return ref +} + +func (p Path) String() string { + return "/" + strings.Join(p, "/") +} + +// MustParsePath returns a new Path for s. If s cannot be parsed, this function +// will panic. This is mostly for test purposes. +func MustParsePath(s string) Path { + path, ok := ParsePath(s) + if !ok { + panic(s) + } + return path +} diff --git a/storage/path_test.go b/storage/path_test.go new file mode 100644 index 0000000000..6ae01aef63 --- /dev/null +++ b/storage/path_test.go @@ -0,0 +1,153 @@ +// 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 ( + "reflect" + "testing" + + "fmt" + + "github.com/open-policy-agent/opa/ast" +) + +func TestNewPathForString(t *testing.T) { + + tests := []struct { + input string + result Path + ok bool + }{ + {"", nil, false}, + {"foo", nil, false}, + {"/", Path{}, true}, + {"/", nil, true}, + {"/foo", Path{"foo"}, true}, + {"/foo/bar", Path{"foo", "bar"}, true}, + } + + for _, tc := range tests { + result, ok := ParsePath(tc.input) + if (tc.ok != ok) || !tc.result.Equal(result) { + t.Errorf("For %v wanted (%v, %v) but got (%v, %v)", tc.input, tc.result, tc.ok, result, ok) + } + } +} + +func TestNewPathForRef(t *testing.T) { + tests := []struct { + input ast.Ref + result Path + err error + }{ + {ast.Ref{}, nil, fmt.Errorf("empty reference (indicates error in caller)")}, + {ast.MustParseRef("data.foo[x]"), nil, fmt.Errorf("unresolved reference (indicates error in caller): data.foo[x]")}, + {ast.MustParseRef("data.foo[true]"), nil, notFoundRefError(ast.MustParseRef("data.foo[true]"), doesNotExistMsg)}, + {ast.MustParseRef("data"), Path{}, nil}, + {ast.MustParseRef("data.foo"), Path{"foo"}, nil}, + {ast.MustParseRef("data.foo[1]"), Path{"foo", "1"}, nil}, + {ast.MustParseRef("data.foo.bar"), Path{"foo", "bar"}, nil}, + } + + for _, tc := range tests { + result, err := NewPathForRef(tc.input) + if tc.err != nil && !reflect.DeepEqual(tc.err, err) { + t.Errorf("For %v expected %v but got %v", tc.input, tc.err, err) + } else if !result.Equal(tc.result) { + t.Errorf("For %v expected %v but got %v", tc.input, tc.result, result) + } + } +} + +func TestPathCompare(t *testing.T) { + tests := []struct { + a Path + b Path + result int + }{ + {Path{}, Path{}, 0}, + {Path{}, Path{"x"}, -1}, + {Path{"x"}, Path{}, 1}, + {Path{"x"}, Path{"x"}, 0}, + {Path{"x"}, Path{"y"}, -1}, + {Path{"x"}, Path{"w"}, 1}, + {Path{"x"}, Path{"wz"}, 1}, + {Path{"x"}, Path{"xx"}, -1}, + {Path{"xx"}, Path{"x"}, 1}, + {Path{"xx"}, Path{"xx"}, 0}, + {Path{"xy"}, Path{"xx"}, 1}, + } + for _, tc := range tests { + result := tc.a.Compare(tc.b) + if result != tc.result { + t.Errorf("For %v.Compare(%v) expected %v but got %v", tc.a, tc.b, tc.result, result) + } + } +} + +func TestPathEqual(t *testing.T) { + tests := []struct { + a Path + b Path + result bool + }{ + {Path{}, Path{}, true}, + {Path{}, Path{"foo"}, false}, + {Path{"foo"}, Path{}, false}, + {Path{"foo", "bar"}, Path{"foo"}, false}, + {Path{"foo", "bar"}, Path{"foo", "bar"}, true}, + } + for _, tc := range tests { + result := tc.a.Equal(tc.b) + if result != tc.result { + t.Errorf("For %v.HasPrefix(%v) expected %v but got %v", tc.a, tc.b, tc.result, result) + } + } +} + +func TestPathHasPrefix(t *testing.T) { + tests := []struct { + a Path + b Path + result bool + }{ + {Path{}, Path{}, true}, + {Path{}, Path{"foo"}, false}, + {Path{"foo"}, Path{}, true}, + {Path{"foo"}, Path{"bar"}, false}, + {Path{"bar"}, Path{"foo"}, false}, + {Path{"foo", "bar"}, Path{"foo"}, true}, + {Path{"foo", "bar"}, Path{"foo", "bar"}, true}, + {Path{"foo", "bar"}, Path{"foo", "bar", "baz"}, false}, + {Path{"foo", "bar", "baz"}, Path{}, true}, + } + for _, tc := range tests { + result := tc.a.HasPrefix(tc.b) + if result != tc.result { + t.Errorf("For %v.HasPrefix(%v) expected %v but got %v", tc.a, tc.b, tc.result, result) + } + } +} + +func TestPathRef(t *testing.T) { + tests := []struct { + path string + head string + ref string + }{ + {"/", "data", "data"}, + {"/foo/bar", "data", "data.foo.bar"}, + {"/foo/bar/3", "data", "data.foo.bar[3]"}, + } + for _, tc := range tests { + path := MustParsePath(tc.path) + head := ast.VarTerm(tc.head) + ref := ast.MustParseRef(tc.ref) + result := path.Ref(head) + if !result.Equal(ref) { + t.Errorf("Expected %v but got %v", ref, result) + } + } +}