Merge pull request #51 from tsandall/repl-term-eval-match-modules

REPL Improvements
This commit is contained in:
Torin Sandall
2016-06-23 14:56:54 -07:00
committed by GitHub
9 changed files with 917 additions and 428 deletions
+11
View File
@@ -160,6 +160,17 @@ type Builtin struct {
TargetPos []int
}
// Expr creates a new expression for the built-in with the given terms.
func (b *Builtin) Expr(terms ...*Term) *Expr {
ts := []*Term{VarTerm(string(b.Name))}
for _, t := range terms {
ts = append(ts, t)
}
return &Expr{
Terms: ts,
}
}
// GetPrintableName returns a printable name for the builtin.
// Some built-ins have names that are used for infix operators
// but when printing we want to use something a bit more readable,
+46 -41
View File
@@ -12,6 +12,8 @@ package ast
import (
"fmt"
"io/ioutil"
"os"
"github.com/pkg/errors"
)
@@ -86,6 +88,37 @@ func MustParseTerm(input string) *Term {
return parsed
}
// 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.
func ParseConstantRule(body Body) *Rule {
if len(body) != 1 {
return nil
}
expr := body[0]
if !expr.IsEquality() {
return nil
}
terms := expr.Terms.([]*Term)
a, b := terms[1], terms[2]
if !b.IsGround() {
return nil
}
name, ok := a.Value.(Var)
if !ok {
return nil
}
return &Rule{
Location: expr.Location,
Name: name,
Value: b,
Body: []*Expr{
&Expr{Terms: BooleanTerm(true)},
},
}
}
// ParseModule returns a parsed Module object.
// For details on Module objects and their fields, see policy.go.
// Empty input will return nil, nil.
@@ -99,11 +132,19 @@ func ParseModule(input string) (*Module, error) {
// ParseModuleFile returns a parsed Module object.
func ParseModuleFile(filename string) (*Module, error) {
parsed, err := ParseFile(filename)
f, err := os.Open(filename)
if err != nil {
return nil, err
}
defer f.Close()
bs, err := ioutil.ReadAll(f)
if err != nil {
return nil, err
}
stmts, err := ParseStatements(string(bs))
if err != nil {
return nil, err
}
stmts := parsed.([]interface{})
return parseModule(stmts)
}
@@ -198,42 +239,6 @@ func ParseRef(input string) (Ref, error) {
return ref, 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")
}
}
func parseModule(stmts []interface{}) (*Module, error) {
if len(stmts) == 0 {
@@ -256,9 +261,9 @@ func parseModule(stmts []interface{}) (*Module, error) {
case *Rule:
mod.Rules = append(mod.Rules, stmt)
case Body:
rule, err := parseConstantRule(stmt)
if err != nil {
return nil, err
rule := ParseConstantRule(stmt)
if rule == nil {
return nil, fmt.Errorf("body must be contained inside rule: %v", stmt)
}
mod.Rules = append(mod.Rules, rule)
}
+61 -40
View File
@@ -34,43 +34,63 @@ Steps
opa run
Without any data, you can experiment with simple boolean expressions to get the hang of it:
Without any data, you can experiment with simple expressions to get the hang of it:
> a = 1, b = 2, a != b
+---+---+
| A | B |
+---+---+
| 1 | 2 |
+---+---+
> a = 1, b = 2, a = b
false
> 10 > 9
> true
true
> a = [1,2,3,4], a[i] > 2
+-----------+---+
| A | I |
+-----------+---+
| [1,2,3,4] | 2 |
| [1,2,3,4] | 3 |
+-----------+---+
> 3.14
3.14
> ["hello", "world"]
[
"hello",
"world"
]
When you enter expressions into the REPL, you are effectively running *queries* against OPA. The REPL output shows the values of variables in the expression that make the query **true**. If there is not set of variables that would make the query true, the REPL prints **false**. If there are no variables in the query and the query evaluates successfully, then the REPL just prints **true**.
You can also test simple boolean expressions:
1. In addition to running queries, the REPL also lets you define rules:
> true = false
false
> 3.14 > 3
true
> "hello" != "goodbye"
true
> p[x] :- a = [1,2,3,4], a[x] = _
defined
> p[x]
Most REPLs let you define variables that you can reference later on. OPA allows you to do something similiar. For example, we can define a "pi" constant as follows:
> pi = 3.14
Once "pi" is defined, you query for the value and write expressions in terms of it:
> pi
3.14
> pi > 3
true
One thing to watch out for in the REPL is that = is used both for assigning variables values and for testing the value of variables. For example p = q sometimes assigns p the value of q and sometimes checks if the values of p and q are the same. The REPL decides between assignment and test based on whether p already has a value or not. If p has a value, p = q is a test (returning true or false), and if p has no value p = q is an assignment. To unset a value for a variable, use the 'unset' command. (This ambiguity is only really an issue in the REPL--when writing policy the duality of = is actually beneficial.)
> pi = 3
false
> unset pi
> pi = 3
> pi
3
In addition to running queries, the REPL also lets you define rules:
> p[x] :- a = [1,2,3,4], a[x]
> p[x], x > 1
+---+
| X |
| x |
+---+
| 0 |
| 1 |
| 2 |
| 3 |
+---+
1. Quit out of the REPL by pressing Control-C or typing "exit":
The rule above defines a set of values that are the indices of elements in the array "a".
When you enter expressions into the REPL, you are effectively running *queries* against OPA. The REPL output shows the values of variables in the expression that make the query **true**. If there is no set of variables that would make the query true, the REPL prints **false**. If there are no variables in the query and the query evaluates successfully, then the REPL just prints **true**.
Quit out of the REPL by pressing Control-C or typing "exit":
> exit
Exiting
@@ -120,18 +140,18 @@ Steps
You can now run queries against the various documents:
> data.servers[_].id = id
+------+
| ID |
+------+
| "s1" |
| "s2" |
| "s3" |
| "s4" |
+------+
> data.servers[_].id
+--------------------+
| data.servers[_].id |
+--------------------+
| "s1" |
| "s2" |
| "s3" |
| "s4" |
+--------------------+
> data.opa.example.public_servers[x]
+-------------------------------------------------------------------------------+
| X |
| x |
+-------------------------------------------------------------------------------+
| {"id":"s1","name":"app","ports":["p1","p2","p3"],"protocols":["https","ssh"]} |
| {"id":"s4","name":"dev","ports":["p1","p2"],"protocols":["http"]} |
@@ -142,28 +162,29 @@ Steps
> import data.servers
> servers[i].ports[_] = "p2", servers[i].id = id
+---+------+
| I | ID |
| i | id |
+---+------+
| 3 | "s4" |
| 0 | "s1" |
| 3 | "s4" |
+---+------+
> package opa.example
> public_servers[x], x.protocols[_] = "http"
+-------------------------------------------------------------------+
| X |
| x |
+-------------------------------------------------------------------+
| {"id":"s4","name":"dev","ports":["p1","p2"],"protocols":["http"]} |
+-------------------------------------------------------------------+
1. Finally, we can define a rule to identify servers in violation of our security policy:
> import data.servers
> violations[s] :-
s = servers[_],
s.protocols[_] = "http",
public_servers[s]
> violations[server]
+-------------------------------------------------------------------+
| SERVER |
| server |
+-------------------------------------------------------------------+
| {"id":"s4","name":"dev","ports":["p1","p2"],"protocols":["http"]} |
+-------------------------------------------------------------------+
+96 -132
View File
@@ -25,66 +25,62 @@ Rego is declarative so policy authors can focus on what queries should return ra
## The Basics
This section introduces the main aspsects of Rego.
This section introduces the main aspects of Rego.
The simplest rule is a single expression and is defined in terms of a [Scalar Value](#scalar-values):
```rego
pi = 3.14159 :- true
pi = 3.14159
```
Rules define the content of documents. We can query for the content of the "pi" document generated by the rule above:
```
> pi = x
+---------+
| X |
+---------+
| 3.14159 |
+---------+
> pi
3.14159
```
Rules can also be defined in terms of [Composite Values](#composite-values):
```rego
rect = {"width": 2, "height": 4} :- true
rect = {"width": 2, "height": 4}
```
The result:
```
> rect = x
+------------------------+
| X |
+------------------------+
| {"height":4,"width":2} |
+------------------------+
> rect
{
"height": 4,
"width": 2
}
```
Many expressions are defined in terms of [Equality](#equality). These expressions can be thought of as assertions. The simplest example of a rule containing an equality expression involves two scalar values:
```rego
v :- 42 = "the meaning of life"
v = true :- 42 = "the meaning of life"
```
If we query for the contents of "v" we see the expression has been evaluated:
We can evaluate "v" to check if it is equal to true:
```
> v = true
false
```
The order of operands in an equality expression does not matter. The result is the same:
```
> true = v
false
```
If we evaluate "v" on its own, the REPL prints "undefined" because the body of the rule never evaluates to true. As a result, the document generated by the rule is undefined.
```
> v
false
```
The order of operands in an equality expression does not matter:
```rego
u :- "the meaning of life" = 42
```
The result is the same:
```
> u
false
undefined
```
We can define rules in terms of [Variables](#variables) as well:
@@ -115,10 +111,11 @@ The query result is the same:
true
```
Rego supports [References](#references) to nested documents. For example:
Rego [References](#references) help you refer to nested documents. For example:
```rego
sites = [{"name": "prod"}, {"name": "smoke1"}, {"name": "dev"}] :- true
sites = [{"name": "prod"}, {"name": "smoke1"}, {"name": "dev"}]
r :- sites[i].name = "prod"
```
@@ -134,7 +131,6 @@ true
We can generalize the example above with a rule that defines a set document instead of a boolean document:
```rego
sites = [{"name": "prod"}, {"name": "smoke1"}, {"name": "dev"}] :- true
q[name] :- sites[i].name = name
```
@@ -143,7 +139,7 @@ When we query for "q" we obtain a set of names:
```repl
> q[x]
+----------+
| X |
| x |
+----------+
| "prod" |
| "smoke1" |
@@ -168,7 +164,7 @@ Rules which have arguments can be queried with input values:
```
> q["smoke2"]
false
undefined
> q["dev"]
true
```
@@ -195,36 +191,16 @@ sentinel = null
These documents can be queried like any other:
```
> greeting = x
+---------+
| X |
+---------+
| "Hello" |
+---------+
> max_height = x
+----+
| X |
+----+
| 42 |
+----+
> pi = x
+---------+
| X |
+---------+
| 3.14159 |
+---------+
> allowed = x
+------+
| X |
+------+
| true |
+------+
> sentinel = x
+------+
| X |
+------+
| null |
+------+
> greeting
"Hello"
> max_height
42
> pi
3.14159
> allowed
true
> sentinel
null
```
## <a name="composite-values"></a> Composite Values
@@ -239,16 +215,11 @@ cube = {"width": 3, "height": 4, "depth": 5}
The result:
```
> cube.width = x
+---+
| X |
+---+
| 3 |
+---+
> cube.width
3
```
Composite values can also be defined in terms of [Variables](#variables) or
[References](#references). For example:
Composite values can also be defined in terms of [Variables](#variables) or [References](#references). For example:
```
> a = 42, b = false, c = null, d = {"a": a, "x": [b, c]}
@@ -273,6 +244,7 @@ For example:
```rego
sites = [{"name": "prod"}, {"name": "smoke1"}, {"name": "dev"}] :- true
q[name] :- sites[i].name = name
```
@@ -281,7 +253,7 @@ In this case, we evaluate "q" with a variable "x" (which is not bound to a value
```
> q[x]
+----------+
| X |
| x |
+----------+
| "prod" |
| "smoke1" |
@@ -293,7 +265,7 @@ On the other hand, if we evaluate "q" with an input value for "name" we can dete
```
> q["smoke2"]
false
undefined
> q["dev"]
true
```
@@ -315,23 +287,15 @@ reference returns the hostname of the second server in the first site document
from our example data:
```
> sites[0].servers[1].hostname = hostname
+----------+
| HOSTNAME |
+----------+
| "helium" |
+----------+
> sites[0].servers[1].hostname
"helium"
```
References are typically written using the "dot-access" style. The canonical form does away with "." and closely resembles dictionary lookup in a language such as Python:
```
> sites[0]["servers"][1]["hostname"] = hostname
+----------+
| HOSTNAME |
+----------+
| "helium" |
+----------+
> sites[0]["servers"][1]["hostname"]
"helium"
```
Both forms are valid, however, the "dot-access" style is typically more readable. Note, there are two cases where brackets need to be used:
@@ -356,19 +320,19 @@ The following reference will select the hostnames of all the servers in our
example data:
```
> sites[i].servers[j].hostname = hostname
+------------+---+---+
| HOSTNAME | I | J |
+------------+---+---+
| "hydrogen" | 0 | 0 |
| "helium" | 0 | 1 |
| "lithium" | 0 | 2 |
| "berylium" | 1 | 0 |
| "boron" | 1 | 1 |
| "carbon" | 1 | 2 |
| "nitrogen" | 2 | 0 |
| "oxygen" | 2 | 1 |
+------------+---+---+
> sites[i].servers[j].hostname
+---+---+------------------------------+
| i | j | sites[i].servers[j].hostname |
+---+---+------------------------------+
| 0 | 0 | "hydrogen" |
| 0 | 1 | "helium" |
| 0 | 2 | "lithium" |
| 1 | 0 | "berylium" |
| 1 | 1 | "boron" |
| 1 | 2 | "carbon" |
| 2 | 0 | "nitrogen" |
| 2 | 1 | "oxygen" |
+---+---+------------------------------+
```
Conceptually, this is the same as the following imperative code (Python):
@@ -385,19 +349,19 @@ def hostnames(sites):
In the reference above, we effectively used variables named "i" and "j" to iterate the collections. If the variables are unused outside the reference, we prefer to replace them with an underscore ("_") character. The reference above can be rewritten as:
```
> sites[_].servers[_].hostname = hostname
+------------+
| HOSTNAME |
+------------+
| "hydrogen" |
| "helium" |
| "lithium" |
| "berylium" |
| "boron" |
| "carbon" |
| "nitrogen" |
| "oxygen" |
+------------+
> sites[_].servers[_].hostname
+------------------------------+
| sites[_].servers[_].hostname |
+------------------------------+
| "hydrogen" |
| "helium" |
| "lithium" |
| "berylium" |
| "boron" |
| "carbon" |
| "nitrogen" |
| "oxygen" |
+------------------------------+
```
The underscore is special because it cannot be referred to by other parts of the rule, e.g., the other side of the expression, another expression, etc. The underscore can be thought of as a special iterator. Each time an underscore is specified, a new iterator is instantiated.
@@ -422,7 +386,7 @@ The result:
```
> apps_and_hostnames[x]
+----------------------+
| X |
| x |
+----------------------+
| ["web","hydrogen"] |
| ["web","helium"] |
@@ -461,7 +425,7 @@ The result:
```
> same_site[x]
+-------+
| X |
| x |
+-------+
| "web" |
| "web" |
@@ -481,7 +445,7 @@ The body of a comprehension is able to refer to variables defined in the outer b
```
> region = "west", names = [name | sites[i].region = region, sites[i].name = name]
+-----------------+--------+
| NAMES | REGION |
| names | region |
+-----------------+--------+
| ["smoke","dev"] | "west" |
+-----------------+--------+
@@ -525,7 +489,7 @@ The result:
```
> app_to_hostnames[app] = hostnames
+-----------+-----------------------------------------------------+
| APP | HOSTNAMES |
| app | hostnames |
+-----------+-----------------------------------------------------+
| "web" | ["hydrogen","helium","berylium","boron","nitrogen"] |
| "mysql" | ["lithium","carbon"] |
@@ -557,7 +521,7 @@ When we query for the content of "hostnames" we see the same data as we would if
```
> hostnames[name]
+------------+
| NAME |
| name |
+------------+
| "hydrogen" |
| "helium" |
@@ -609,7 +573,7 @@ The result:
```
> apps_by_hostname["helium"] = app
+-------+
| APP |
| app |
+-------+
| "web" |
+-------+
@@ -643,7 +607,7 @@ The result:
```
> instances[x]
+-----------------------------------------------+
| X |
| x |
+-----------------------------------------------+
| {"address":"hydrogen","name":"web-0"} |
| {"address":"helium","name":"web-1"} |
@@ -689,20 +653,20 @@ For example, we can write a rule that defines a document containing names of
apps not deployed on the "prod" site:
```rego
apps_not_in_prod[name] :-
apps[_].name = name,
not apps_in_prod[name]
apps_in_prod[name] :-
apps[_] = app,
app.servers[_] = server,
app.name = name
prod_servers[server],
prod_servers[name] :-
sites[_] = site,
site.name = "prod",
site.servers[_].name = name
apps_in_prod[name] :-
apps[_] = app,
app.servers[_] = server,
app.name = name,
prod_servers[server]
apps_not_in_prod[name] :-
apps[_].name = name,
not apps_in_prod[name]
```
The result:
@@ -710,7 +674,7 @@ The result:
```
> apps_not_in_prod[name]
+-----------+
| NAME |
| name |
+-----------+
| "mongodb" |
+-----------+
+360 -86
View File
@@ -16,6 +16,8 @@ import (
"github.com/open-policy-agent/opa/ast"
"github.com/open-policy-agent/opa/storage"
"github.com/open-policy-agent/opa/topdown"
"github.com/open-policy-agent/opa/version"
"github.com/peterh/liner"
)
@@ -64,6 +66,11 @@ func (r *REPL) Loop() {
line.SetMultiLineMode(true)
r.loadHistory(line)
fmt.Fprintf(r.output, "OPA %v (commit %v, built at %v)\n", version.Version, version.Vcs, version.Timestamp)
fmt.Fprintf(r.output, "\n")
fmt.Fprintf(r.output, "Run 'help' to see a list of commands.\n")
fmt.Fprintf(r.output, "\n")
for true {
input, err := line.Prompt(r.getPrompt())
@@ -97,23 +104,23 @@ func (r *REPL) OneShot(line string) bool {
}
if len(r.buffer) == 0 {
switch strings.TrimSpace(strings.ToLower(line)) {
case "dump":
return r.cmdDump()
case "json":
return r.cmdFormat("json")
case "pretty":
return r.cmdFormat("pretty")
case "trace":
return r.cmdTrace()
case "?":
fallthrough
case "help":
return r.cmdHelp()
case "quit":
fallthrough
case "exit":
return r.cmdExit()
if cmd := newCommand(line); cmd != nil {
switch cmd.op {
case "dump":
return r.cmdDump()
case "json":
return r.cmdFormat("json")
case "unset":
return r.cmdUnset(cmd.args)
case "pretty":
return r.cmdFormat("pretty")
case "trace":
return r.cmdTrace()
case "help":
return r.cmdHelp()
case "exit":
return r.cmdExit()
}
}
r.buffer = append(r.buffer, line)
return r.evalBufferOne()
@@ -143,31 +150,22 @@ func (r *REPL) cmdFormat(s string) bool {
func (r *REPL) cmdHelp() bool {
commands := []struct {
name string
note string
}{
{"<stmt>", "evaluate the statement"},
{"json", "set output format to JSON"},
{"pretty", "set output format to pretty"},
{"dump", "dump the raw storage content"},
{"trace", "toggle stdout tracing"},
{"help", "print this message (or ?)"},
{"exit", "exit back to shell (or ctrl+c, ctrl+d, quit)"},
{"ctrl+l", "clear the screen"},
}
all := extra[:]
all = append(all, builtin[:]...)
maxLength := 0
for _, command := range commands {
length := len(command.name)
for _, c := range all {
length := len(c.syntax())
if length > maxLength {
maxLength = length
}
}
for _, command := range commands {
f := fmt.Sprintf("%%%dv : %%v\n", maxLength)
fmt.Printf(f, command.name, command.note)
f := fmt.Sprintf("%%%dv : %%v\n", maxLength)
for _, c := range all {
fmt.Printf(f, c.syntax(), c.help)
}
return false
@@ -178,6 +176,59 @@ func (r *REPL) cmdTrace() bool {
return false
}
func (r *REPL) cmdUnset(args []string) bool {
if len(args) != 1 {
fmt.Fprintln(r.output, "error: unset <var>: expects exactly one argument")
return false
}
term, err := ast.ParseTerm(args[0])
if err != nil {
fmt.Fprintln(r.output, "error: argument must identify a rule")
return false
}
v, ok := term.Value.(ast.Var)
if !ok {
fmt.Fprintln(r.output, "error: argument must identify a rule")
return false
}
modules := r.policyStore.List()
mod := modules[r.currentModuleID]
rules := []*ast.Rule{}
for _, r := range mod.Rules {
if !r.Name.Equal(v) {
rules = append(rules, r)
}
}
if len(rules) == len(mod.Rules) {
fmt.Fprintln(r.output, "warning: no matching rules in current module")
return false
}
cpy := *mod
cpy.Rules = rules
modules[r.currentModuleID] = &cpy
c := ast.NewCompiler()
if c.Compile(modules); c.Failed() {
fmt.Fprintln(r.output, "error:", c.FlattenErrors())
return false
}
err = r.policyStore.Add(r.currentModuleID, c.Modules[r.currentModuleID], nil, false)
if err != nil {
fmt.Fprintln(r.output, "error:", err)
return true
}
return false
}
func (r *REPL) compileBody(body ast.Body) (ast.Body, error) {
name := fmt.Sprintf("repl%d", r.nextID)
@@ -276,6 +327,14 @@ func (r *REPL) evalStatement(stmt interface{}) bool {
fmt.Fprintln(r.output, "error:", err)
return false
}
if s := ast.ParseConstantRule(s); s != nil {
mod, err := r.compileRule(s)
if err != nil {
fmt.Fprintln(r.output, "error:", err)
return false
}
return r.evalModule(mod, s)
}
return r.evalBody(s)
case *ast.Rule:
mod, err := r.compileRule(s)
@@ -283,7 +342,7 @@ func (r *REPL) evalStatement(stmt interface{}) bool {
fmt.Fprintln(r.output, "error:", err)
return false
}
return r.evalModule(mod)
return r.evalModule(mod, s)
case *ast.Import:
return r.evalImport(s)
case *ast.Package:
@@ -294,6 +353,19 @@ func (r *REPL) evalStatement(stmt interface{}) bool {
func (r *REPL) evalBody(body ast.Body) bool {
// Special case for positive, single term inputs.
if len(body) == 1 {
expr := body[0]
if !expr.Negated {
if _, ok := expr.Terms.(*ast.Term); ok {
if singleValue(body) {
return r.evalTermSingleValue(body)
}
return r.evalTermMultiValue(body)
}
}
}
ctx := topdown.NewContext(body, r.dataStore)
if r.trace {
ctx.Tracer = &topdown.StdoutTracer{}
@@ -350,7 +422,7 @@ func (r *REPL) evalBody(body ast.Body) bool {
if isTrue {
if len(results) >= 1 {
r.printResults(body, results)
r.printResults(getHeaderForBody(body), results)
} else {
fmt.Fprintln(r.output, "true")
}
@@ -361,7 +433,7 @@ func (r *REPL) evalBody(body ast.Body) bool {
return false
}
func (r *REPL) evalModule(mod *ast.Module) bool {
func (r *REPL) evalModule(mod *ast.Module, stmt *ast.Rule) bool {
err := r.policyStore.Add(r.currentModuleID, mod, nil, false)
if err != nil {
@@ -369,7 +441,6 @@ func (r *REPL) evalModule(mod *ast.Module) bool {
return true
}
fmt.Fprintln(r.output, "defined")
return false
}
@@ -423,6 +494,127 @@ func (r *REPL) evalPackage(p *ast.Package) bool {
return false
}
// evalTermSingleValue evaluates and prints terms in cases where the term evaluates to a
// single value, e.g., "1", true, [1,2,"foo"], [x | x = a[i], a = [1,2,3]], etc. Ground terms
// and comprehensions always evaluate to a single value. To handle references, this function
// still executes the query, except it does so by rewriting the body to assign the term
// to a variable. This allows the REPL to obtain the result even if the term is false.
func (r *REPL) evalTermSingleValue(body ast.Body) bool {
term := body[0].Terms.(*ast.Term)
outputVar := ast.VarTerm("$")
body = ast.Body{ast.Equality.Expr(term, outputVar)}
ctx := topdown.NewContext(body, r.dataStore)
if r.trace {
ctx.Tracer = &topdown.StdoutTracer{}
}
var result interface{}
isTrue := false
err := topdown.Eval(ctx, func(ctx *topdown.Context) error {
p := ctx.Locals.Get(outputVar.Value)
v, err := topdown.ValueToInterface(p, ctx)
if err != nil {
return err
}
result = v
isTrue = true
return nil
})
if err != nil {
fmt.Fprintln(r.output, "error:", err)
} else if isTrue {
r.printJSON(result)
} else {
r.printUndefined()
}
return false
}
// evalTermMultiValue evaluates and prints terms in cases where the term may evaluate to multiple
// ground values, e.g., a[i], [servers[x]], etc.
func (r *REPL) evalTermMultiValue(body ast.Body) bool {
ctx := topdown.NewContext(body, r.dataStore)
if r.trace {
ctx.Tracer = &topdown.StdoutTracer{}
}
term := body[0].Terms.(*ast.Term)
vars := map[string]struct{}{}
results := []map[string]interface{}{}
resultKey := string(term.Location.Text)
// Do not include the value of the input term if the input term was a set reference. E.g.,
// for "p[x]", the value users are interested in is "x" not p[x] which is always defined
// as true.
includeValue := !r.isSetReference(term)
err := topdown.Eval(ctx, func(ctx *topdown.Context) error {
result := map[string]interface{}{}
var err error
ctx.Locals.Iter(func(k, v ast.Value) bool {
if k, ok := k.(ast.Var); ok {
name := string(k)
if strings.HasPrefix(name, ast.WildcardPrefix) {
return false
}
x, e := topdown.ValueToInterface(v, ctx)
if e != nil {
err = e
return true
}
result[name] = x
vars[name] = struct{}{}
}
return false
})
if err != nil {
return err
}
if includeValue {
p := topdown.PlugTerm(term, ctx)
v, err := topdown.ValueToInterface(p.Value, ctx)
if err != nil {
return err
}
result[resultKey] = v
}
results = append(results, result)
return nil
})
if err != nil {
fmt.Fprintln(r.output, "error:", err)
} else if len(results) > 0 {
keys := []string{}
for v := range vars {
keys = append(keys, v)
}
sort.Strings(keys)
if includeValue {
keys = append(keys, resultKey)
}
r.printResults(keys, results)
} else {
r.printUndefined()
}
return false
}
func (r *REPL) getPrompt() string {
if len(r.buffer) > 0 {
return r.bufferPrompt
@@ -459,6 +651,26 @@ func (r *REPL) init() bool {
return false
}
// isSetReference returns true if term is a reference that refers to a set document.
func (r *REPL) isSetReference(term *ast.Term) bool {
ref, ok := term.Value.(ast.Ref)
if !ok {
return false
}
p := ast.Ref{}
for _, x := range ref {
p = append(p, x)
if node, err := r.dataStore.GetRef(p); err == nil {
if rs, ok := node.([]*ast.Rule); ok {
if rs[0].DocKind() == ast.PartialSetDoc {
return true
}
}
}
}
return false
}
func (r *REPL) loadHistory(prompt *liner.State) {
if f, err := os.Open(r.historyPath); err == nil {
prompt.ReadHistory(f)
@@ -466,19 +678,17 @@ func (r *REPL) loadHistory(prompt *liner.State) {
}
}
func (r *REPL) printResults(body ast.Body, results []map[string]interface{}) {
func (r *REPL) printResults(keys []string, results []map[string]interface{}) {
switch r.outputFormat {
case "json":
r.printJSON(results)
default:
r.printPretty(body, results)
r.printPretty(keys, results)
}
}
func (r *REPL) printJSON(results []map[string]interface{}) {
buf, err := json.MarshalIndent(results, "", " ")
func (r *REPL) printJSON(x interface{}) {
buf, err := json.MarshalIndent(x, "", " ")
if err != nil {
fmt.Fprintln(r.output, err)
return
@@ -486,54 +696,18 @@ func (r *REPL) printJSON(results []map[string]interface{}) {
fmt.Fprintln(r.output, string(buf))
}
func (r *REPL) printPretty(body ast.Body, results []map[string]interface{}) {
func (r *REPL) printPretty(keys []string, results []map[string]interface{}) {
table := tablewriter.NewWriter(r.output)
table.SetAlignment(tablewriter.ALIGN_LEFT)
r.printPrettyHeader(table, body)
table.SetAutoFormatHeaders(false)
table.SetHeader(keys)
for _, row := range results {
r.printPrettyRow(table, row)
r.printPrettyRow(table, keys, row)
}
table.Render()
}
func (r *REPL) printPrettyHeader(table *tablewriter.Table, body ast.Body) {
// Build set of fields for the output. The fields are the variables from inside the body.
// If the variable appears multiple times, we only want a single field so store them in a
// map/set.
fields := map[string]struct{}{}
// TODO(tsandall): perhaps we could refactor this to use a "walk" function on the body.
for _, expr := range body {
switch ts := expr.Terms.(type) {
case []*ast.Term:
for _, t := range ts[1:] {
buildHeader(fields, t)
}
case *ast.Term:
buildHeader(fields, ts)
}
}
// Sort/display fields by name.
keys := []string{}
for k := range fields {
keys = append(keys, k)
}
sort.Strings(keys)
table.SetHeader(keys)
}
func (r *REPL) printPrettyRow(table *tablewriter.Table, row map[string]interface{}) {
// Arrange fields in same order as header.
keys := []string{}
for k := range row {
keys = append(keys, k)
}
sort.Strings(keys)
func (r *REPL) printPrettyRow(table *tablewriter.Table, keys []string, row map[string]interface{}) {
buf := []string{}
for _, k := range keys {
@@ -549,6 +723,10 @@ func (r *REPL) printPrettyRow(table *tablewriter.Table, row map[string]interface
table.Append(buf)
}
func (r *REPL) printUndefined() {
fmt.Fprintln(r.output, "undefined")
}
func (r *REPL) saveHistory(prompt *liner.State) {
if f, err := os.Create(r.historyPath); err == nil {
prompt.WriteHistory(f)
@@ -556,6 +734,57 @@ func (r *REPL) saveHistory(prompt *liner.State) {
}
}
type commandDesc struct {
name string
args []string
help string
}
func (c commandDesc) syntax() string {
if len(c.args) > 0 {
return fmt.Sprintf("%v %v", c.name, strings.Join(c.args, " "))
}
return c.name
}
var extra = [...]commandDesc{
{"<stmt>", []string{}, "evaluate the statement"},
{"package", []string{"<term>"}, "change currently active package"},
{"import", []string{"<term>"}, "add import to currently active module"},
}
var builtin = [...]commandDesc{
{"unset", []string{"<var>"}, "undefine rules in currently active module"},
{"json", []string{}, "set output format to JSON"},
{"pretty", []string{}, "set output format to pretty"},
{"dump", []string{}, "dump the raw storage content"},
{"trace", []string{}, "toggle stdout tracing"},
{"help", []string{}, "print this message"},
{"exit", []string{}, "exit back to shell (or ctrl+c, ctrl+d)"},
{"ctrl+l", []string{}, "clear the screen"},
}
type command struct {
op string
args []string
}
func newCommand(line string) *command {
p := strings.Fields(strings.TrimSpace(strings.ToLower(line)))
if len(p) == 0 {
return nil
}
for _, c := range builtin {
if c.name == p[0] {
return &command{
op: c.name,
args: p[1:],
}
}
}
return nil
}
func buildHeader(fields map[string]struct{}, term *ast.Term) {
switch v := term.Value.(type) {
case ast.Ref:
@@ -578,3 +807,48 @@ func buildHeader(fields map[string]struct{}, term *ast.Term) {
}
}
}
func getHeaderForBody(body ast.Body) []string {
// Build set of fields for the output. The fields are the variables from inside the body.
// If the variable appears multiple times, we only want a single field so store them in a
// map/set.
fields := map[string]struct{}{}
// TODO(tsandall): perhaps we could refactor this to use a "walk" function on the body.
for _, expr := range body {
switch ts := expr.Terms.(type) {
case []*ast.Term:
for _, t := range ts[1:] {
buildHeader(fields, t)
}
case *ast.Term:
buildHeader(fields, ts)
}
}
// Sort/display fields by name.
keys := []string{}
for k := range fields {
keys = append(keys, k)
}
sort.Strings(keys)
return keys
}
// singleValue returns true if body can be evaluated to a single term.
func singleValue(body ast.Body) bool {
if len(body) != 1 {
return false
}
term, ok := body[0].Terms.(*ast.Term)
if !ok {
return false
}
switch term.Value.(type) {
case *ast.ArrayComprehension:
return true
default:
return term.IsGround()
}
}
+219 -8
View File
@@ -25,16 +25,86 @@ func TestDump(t *testing.T) {
store := storage.NewDataStoreFromJSONObject(data)
var buffer bytes.Buffer
repl := newRepl(store, &buffer)
repl.cmdDump()
repl.OneShot("dump")
expectOutput(t, buffer.String(), "map[a:[1 2 3 4]]\n")
}
func TestUnset(t *testing.T) {
store := storage.NewDataStore()
var buffer bytes.Buffer
repl := newRepl(store, &buffer)
repl.OneShot("magic = 23")
repl.OneShot("p = 3.14")
repl.OneShot("unset p")
repl.OneShot("p")
result := buffer.String()
if result != "error: 1 error occurred: unsafe variables in repl2: [p]\n" {
t.Errorf("Expected p to be unsafe but got: %v", result)
return
}
buffer.Reset()
repl.OneShot("p = 3.14")
repl.OneShot("p = 3 :- false")
repl.OneShot("unset p")
repl.OneShot("p")
result = buffer.String()
if result != "error: 1 error occurred: unsafe variables in repl4: [p]\n" {
t.Errorf("Expected p to be unsafe but got: %v", result)
return
}
buffer.Reset()
repl.OneShot("unset ")
result = buffer.String()
if result != "error: unset <var>: expects exactly one argument\n" {
t.Errorf("Expected unset error for bad syntax but got: %v", result)
}
buffer.Reset()
repl.OneShot("unset 1=1")
result = buffer.String()
if result != "error: argument must identify a rule\n" {
t.Errorf("Expected unset error for bad syntax but got: %v", result)
}
buffer.Reset()
repl.OneShot(`unset "p"`)
result = buffer.String()
if result != "error: argument must identify a rule\n" {
t.Errorf("Expected unset error for bad syntax but got: %v", result)
}
buffer.Reset()
repl.OneShot(`unset q`)
result = buffer.String()
if result != "warning: no matching rules in current module\n" {
t.Errorf("Expected unset error for missing rule but got: %v", result)
}
buffer.Reset()
repl.OneShot(`magic`)
result = buffer.String()
if result != "23\n" {
t.Errorf("Expected magic to be defined but got: %v", result)
}
buffer.Reset()
repl.OneShot(`package data.other`)
repl.OneShot(`unset magic`)
result = buffer.String()
if result != "warning: no matching rules in current module\n" {
t.Errorf("Expected unset error for bad syntax but got: %v", result)
}
}
func TestOneShotEmptyBufferOneExpr(t *testing.T) {
store := newTestDataStore()
var buffer bytes.Buffer
repl := newRepl(store, &buffer)
repl.OneShot("data.a[i].b.c[j] = 2")
expectOutput(t, buffer.String(), "+---+---+\n| I | J |\n+---+---+\n| 0 | 1 |\n+---+---+\n")
expectOutput(t, buffer.String(), "+---+---+\n| i | j |\n+---+---+\n| 0 | 1 |\n+---+---+\n")
buffer.Reset()
repl.OneShot("data.a[i].b.c[j] = \"deadbeef\"")
expectOutput(t, buffer.String(), "false\n")
@@ -45,7 +115,7 @@ func TestOneShotEmptyBufferOneRule(t *testing.T) {
var buffer bytes.Buffer
repl := newRepl(store, &buffer)
repl.OneShot("p[x] :- data.a[i] = x")
expectOutput(t, buffer.String(), "defined\n")
expectOutput(t, buffer.String(), "")
}
func TestOneShotBufferedExpr(t *testing.T) {
@@ -57,7 +127,7 @@ func TestOneShotBufferedExpr(t *testing.T) {
repl.OneShot("2")
expectOutput(t, buffer.String(), "")
repl.OneShot("")
expectOutput(t, buffer.String(), "+---+---+\n| I | J |\n+---+---+\n| 0 | 1 |\n+---+---+\n")
expectOutput(t, buffer.String(), "+---+---+\n| i | j |\n+---+---+\n| 0 | 1 |\n+---+---+\n")
}
func TestOneShotBufferedRule(t *testing.T) {
@@ -73,7 +143,7 @@ func TestOneShotBufferedRule(t *testing.T) {
repl.OneShot("x")
expectOutput(t, buffer.String(), "")
repl.OneShot("")
expectOutput(t, buffer.String(), "defined\n")
expectOutput(t, buffer.String(), "")
}
func TestOneShotJSON(t *testing.T) {
@@ -126,6 +196,148 @@ func TestOneShotJSON(t *testing.T) {
}
}
func TestEvalFalse(t *testing.T) {
store := newTestDataStore()
var buffer bytes.Buffer
repl := newRepl(store, &buffer)
repl.OneShot("false")
result := buffer.String()
if result != "false\n" {
t.Errorf("Expected result to be false but got: %v", result)
}
}
func TestEvalConstantRule(t *testing.T) {
store := newTestDataStore()
var buffer bytes.Buffer
repl := newRepl(store, &buffer)
repl.OneShot("pi = 3.14")
result := buffer.String()
if result != "" {
t.Errorf("Expected rule to be defined but got: %v", result)
return
}
buffer.Reset()
repl.OneShot("pi")
result = buffer.String()
expected := "3.14\n"
if result != expected {
t.Errorf("Expected pi to evaluate to 3.14 but got: %v", result)
return
}
buffer.Reset()
repl.OneShot("pi.deadbeef")
result = buffer.String()
if result != "undefined\n" {
t.Errorf("Expected pi.deadbeef to be undefined but got: %v", result)
return
}
buffer.Reset()
repl.OneShot("pi > 3")
result = buffer.String()
if result != "true\n" {
t.Errorf("Expected pi > 3 to be true but got: %v", result)
return
}
}
func TestEvalSingleTermMultiValue(t *testing.T) {
store := newTestDataStore()
var buffer bytes.Buffer
repl := newRepl(store, &buffer)
repl.outputFormat = "json"
input := `
[
{
"data.a[i].b.c[_]": true,
"i": 0
},
{
"data.a[i].b.c[_]": 2,
"i": 0
},
{
"data.a[i].b.c[_]": false,
"i": 0
},
{
"data.a[i].b.c[_]": false,
"i": 1
},
{
"data.a[i].b.c[_]": true,
"i": 1
},
{
"data.a[i].b.c[_]": 1,
"i": 1
}
]`
var expected interface{}
if err := json.Unmarshal([]byte(input), &expected); err != nil {
panic(err)
}
repl.OneShot("data.a[i].b.c[_]")
var result interface{}
if err := json.Unmarshal(buffer.Bytes(), &result); err != nil {
t.Errorf("Expected valid JSON document: %v: %v", err, buffer.String())
return
}
if !reflect.DeepEqual(expected, result) {
t.Errorf("Expected %v but got: %v", expected, result)
return
}
buffer.Reset()
repl.OneShot("data.deadbeef[x]")
s := buffer.String()
if s != "undefined\n" {
t.Errorf("Expected undefined from reference but got: %v", s)
return
}
buffer.Reset()
repl.OneShot("p[x] :- a = [1,2,3,4], a[_] = x")
buffer.Reset()
repl.OneShot("p[x]")
input = `
[
{
"x": 1
},
{
"x": 2
},
{
"x": 3
},
{
"x": 4
}
]
`
if err := json.Unmarshal([]byte(input), &expected); err != nil {
panic(err)
}
if err := json.Unmarshal(buffer.Bytes(), &result); err != nil {
t.Errorf("Expected valid JSON document: %v: %v", err, buffer.String())
return
}
if !reflect.DeepEqual(expected, result) {
t.Errorf("Exepcted %v but got: %v", expected, result)
}
}
func TestEvalRuleCompileError(t *testing.T) {
store := newTestDataStore()
var buffer bytes.Buffer
@@ -140,8 +352,7 @@ func TestEvalRuleCompileError(t *testing.T) {
buffer.Reset()
repl.OneShot("p = true :- true")
result = buffer.String()
expected = "defined\n"
if result != expected {
if result != "" {
t.Errorf("Expected valid rule to compile (because state should have been rolled back) but got: %v", result)
return
}
@@ -186,7 +397,7 @@ func TestEvalBodyContainingWildCards(t *testing.T) {
repl.OneShot("data.a[_].b.c[_] = x")
expected := strings.TrimSpace(`
+-------+
| X |
| x |
+-------+
| true |
| 2 |
+2 -2
View File
@@ -55,8 +55,8 @@ func evalEqUnify(ctx *Context, a ast.Value, b ast.Value, iter Iterator) error {
// Plug bindings into both terms because this will be called recursively and there may be
// new bindings that have been made as part of unification.
a = plugValue(a, ctx)
b = plugValue(b, ctx)
a = PlugValue(a, ctx)
b = PlugValue(b, ctx)
switch a := a.(type) {
case ast.Var:
+120 -117
View File
@@ -89,7 +89,7 @@ func (ctx *Context) BindVar(variable ast.Var, value ast.Value) *Context {
// it should be overwritten with the new value, so ignore the old
// value here.
if cpy.Locals.Get(k) == nil {
cpy.Locals.Put(k, plugValue(v, &cpy))
cpy.Locals.Put(k, PlugValue(v, &cpy))
}
return false
})
@@ -203,6 +203,115 @@ func Eval(ctx *Context, iter Iterator) error {
return evalContext(ctx, iter)
}
// PlugExpr returns a copy of expr with bound terms substituted for values in ctx.
func PlugExpr(expr *ast.Expr, ctx *Context) *ast.Expr {
plugged := *expr
switch ts := plugged.Terms.(type) {
case []*ast.Term:
var buf []*ast.Term
buf = append(buf, ts[0])
for _, term := range ts[1:] {
buf = append(buf, PlugTerm(term, ctx))
}
plugged.Terms = buf
case *ast.Term:
plugged.Terms = PlugTerm(ts, ctx)
default:
panic(fmt.Sprintf("illegal argument: %v", ts))
}
return &plugged
}
// PlugTerm returns a copy of term with bound terms substituted for values in ctx.
func PlugTerm(term *ast.Term, ctx *Context) *ast.Term {
switch v := term.Value.(type) {
case ast.Var:
return &ast.Term{Value: PlugValue(v, ctx)}
case ast.Ref:
plugged := *term
plugged.Value = PlugValue(v, ctx)
return &plugged
case ast.Array:
plugged := *term
plugged.Value = PlugValue(v, ctx)
return &plugged
case ast.Object:
plugged := *term
plugged.Value = PlugValue(v, ctx)
return &plugged
case *ast.ArrayComprehension:
plugged := *term
plugged.Value = PlugValue(v, ctx)
return &plugged
default:
if !term.IsGround() {
panic("unreachable")
}
return term
}
}
// PlugValue returns a copy of v with bound terms substituted for values in ctx.
func PlugValue(v ast.Value, ctx *Context) ast.Value {
switch v := v.(type) {
case ast.Var:
b := ctx.Binding(v)
if b == nil {
return v
}
return b
case *ast.ArrayComprehension:
b := ctx.Binding(v)
if b == nil {
return v
}
return b
case ast.Ref:
if b := ctx.Binding(v); b != nil {
return b
}
if v.IsGround() {
return v
}
var buf ast.Ref
buf = append(buf, v[0])
for _, p := range v[1:] {
buf = append(buf, PlugTerm(p, ctx))
}
return buf
case ast.Array:
var buf ast.Array
for _, e := range v {
buf = append(buf, PlugTerm(e, ctx))
}
return buf
case ast.Object:
var buf ast.Object
for _, e := range v {
k := PlugTerm(e[0], ctx)
v := PlugTerm(e[1], ctx)
buf = append(buf, [...]*ast.Term{k, v})
}
return buf
default:
if !v.IsGround() {
panic(fmt.Sprintf("illegal value: %v %v", ctx, v))
}
return v
}
}
// QueryParams defines input parameters for the query interface.
type QueryParams struct {
DataStore *storage.DataStore
@@ -437,7 +546,7 @@ func evalContextNegated(ctx *Context, iter Iterator) error {
}
func evalExpr(ctx *Context, iter Iterator) error {
expr := plugExpr(ctx.Current(), ctx)
expr := PlugExpr(ctx.Current(), ctx)
ctx.traceTry(expr)
switch tt := expr.Terms.(type) {
case []*ast.Term:
@@ -563,7 +672,7 @@ func evalRefRecNonGround(ctx *Context, path, tail ast.Ref, iter Iterator) error
// Check if the variable has a binding.
// If there is a binding, process the rest of the reference normally.
// If there is no binding, enumerate the collection referred to by the path.
plugged := plugTerm(tail[0], ctx)
plugged := PlugTerm(tail[0], ctx)
if plugged.IsGround() {
path = append(path, plugged)
return evalRefRec(ctx, path, tail[1:], iter)
@@ -643,7 +752,7 @@ func evalRefRuleCompleteDoc(ctx *Context, ref ast.Ref, suffix ast.Ref, rules []*
func evalRefRulePartialObjectDoc(ctx *Context, ref ast.Ref, path ast.Ref, rule *ast.Rule, iter Iterator) error {
suffix := ref[len(path):]
key := plugValue(suffix[0].Value, ctx)
key := PlugValue(suffix[0].Value, ctx)
// There are two cases being handled below. The first case is for when
// the object key is ground in the original expression or there is a
@@ -733,7 +842,7 @@ func evalRefRulePartialSetDoc(ctx *Context, ref ast.Ref, path ast.Ref, rule *ast
// See comment in evalRefRulePartialObjectDoc about the two branches below.
// The behaviour is similar for sets.
key := plugValue(suffix[0].Value, ctx)
key := PlugValue(suffix[0].Value, ctx)
if !key.IsGround() {
child := ctx.Child(rule.Body, storage.NewBindings())
@@ -782,7 +891,7 @@ func evalRefRuleResult(ctx *Context, ref ast.Ref, suffix ast.Ref, result ast.Val
binding = append(binding, result...)
binding = append(binding, suffix...)
return evalRefRec(ctx, result, suffix, func(ctx *Context) error {
ctx = ctx.BindValue(ref, plugValue(binding, ctx))
ctx = ctx.BindValue(ref, PlugValue(binding, ctx))
return iter(ctx)
})
@@ -790,7 +899,7 @@ func evalRefRuleResult(ctx *Context, ref ast.Ref, suffix ast.Ref, result ast.Val
if len(suffix) > 0 {
var pluggedSuffix ast.Ref
for _, t := range suffix {
pluggedSuffix = append(pluggedSuffix, plugTerm(t, ctx))
pluggedSuffix = append(pluggedSuffix, PlugTerm(t, ctx))
}
return result.Query(pluggedSuffix, func(keys map[ast.Var]ast.Value, value ast.Value) error {
ctx = ctx.BindValue(ref, value)
@@ -807,7 +916,7 @@ func evalRefRuleResult(ctx *Context, ref ast.Ref, suffix ast.Ref, result ast.Val
if len(suffix) > 0 {
var pluggedSuffix ast.Ref
for _, t := range suffix {
pluggedSuffix = append(pluggedSuffix, plugTerm(t, ctx))
pluggedSuffix = append(pluggedSuffix, PlugTerm(t, ctx))
}
return result.Query(pluggedSuffix, func(keys map[ast.Var]ast.Value, value ast.Value) error {
ctx = ctx.BindValue(ref, value)
@@ -838,7 +947,7 @@ func evalTerms(ctx *Context, iter Iterator) error {
// Check if indexing is available for this expression. Need to
// perform check on the plugged version of the expression, otherwise
// the index will return false positives.
plugged := plugExpr(expr, ctx)
plugged := PlugExpr(expr, ctx)
if indexAvailable(ctx, plugged) {
@@ -887,7 +996,7 @@ func evalTermsComprehension(ctx *Context, comp ast.Value, iter Iterator) error {
r := ast.Array{}
c := ctx.Child(comp.Body, ctx.Locals)
err := Eval(c, func(c *Context) error {
r = append(r, plugTerm(comp.Term, c))
r = append(r, PlugTerm(comp.Term, c))
return nil
})
if err != nil {
@@ -905,7 +1014,7 @@ func evalTermsIndexed(ctx *Context, iter Iterator, indexed ast.Ref, nonIndexed *
iterateIndex := func(ctx *Context) error {
// Evaluate the non-indexed term.
plugged := plugTerm(nonIndexed, ctx)
plugged := PlugTerm(nonIndexed, ctx)
nonIndexedValue, err := ValueToInterface(plugged.Value, ctx)
if err != nil {
return err
@@ -1141,112 +1250,6 @@ func lookupRule(ds *storage.DataStore, ref ast.Ref) ([]*ast.Rule, error) {
}
}
func plugExpr(expr *ast.Expr, ctx *Context) *ast.Expr {
plugged := *expr
switch ts := plugged.Terms.(type) {
case []*ast.Term:
var buf []*ast.Term
buf = append(buf, ts[0])
for _, term := range ts[1:] {
buf = append(buf, plugTerm(term, ctx))
}
plugged.Terms = buf
case *ast.Term:
plugged.Terms = plugTerm(ts, ctx)
default:
panic(fmt.Sprintf("illegal argument: %v", ts))
}
return &plugged
}
func plugTerm(term *ast.Term, ctx *Context) *ast.Term {
switch v := term.Value.(type) {
case ast.Var:
return &ast.Term{Value: plugValue(v, ctx)}
case ast.Ref:
plugged := *term
plugged.Value = plugValue(v, ctx)
return &plugged
case ast.Array:
plugged := *term
plugged.Value = plugValue(v, ctx)
return &plugged
case ast.Object:
plugged := *term
plugged.Value = plugValue(v, ctx)
return &plugged
case *ast.ArrayComprehension:
plugged := *term
plugged.Value = plugValue(v, ctx)
return &plugged
default:
if !term.IsGround() {
panic("unreachable")
}
return term
}
}
func plugValue(v ast.Value, ctx *Context) ast.Value {
switch v := v.(type) {
case ast.Var:
b := ctx.Binding(v)
if b == nil {
return v
}
return b
case *ast.ArrayComprehension:
b := ctx.Binding(v)
if b == nil {
return v
}
return b
case ast.Ref:
if b := ctx.Binding(v); b != nil {
return b
}
if v.IsGround() {
return v
}
var buf ast.Ref
buf = append(buf, v[0])
for _, p := range v[1:] {
buf = append(buf, plugTerm(p, ctx))
}
return buf
case ast.Array:
var buf ast.Array
for _, e := range v {
buf = append(buf, plugTerm(e, ctx))
}
return buf
case ast.Object:
var buf ast.Object
for _, e := range v {
k := plugTerm(e[0], ctx)
v := plugTerm(e[1], ctx)
buf = append(buf, [...]*ast.Term{k, v})
}
return buf
default:
if !v.IsGround() {
panic(fmt.Sprintf("illegal value: %v %v", ctx, v))
}
return v
}
}
func topDownQueryCompleteDoc(params *QueryParams, rules []*ast.Rule) (interface{}, error) {
var result ast.Value
+2 -2
View File
@@ -200,14 +200,14 @@ func TestPlugValue(t *testing.T) {
expected := ast.MustParseTerm(`[{"hello": "world"}]`).Value
r1 := plugValue(a, ctx1)
r1 := PlugValue(a, ctx1)
if !expected.Equal(r1) {
t.Errorf("Expected %v but got %v", expected, r1)
return
}
r2 := plugValue(a, ctx2)
r2 := PlugValue(a, ctx2)
if !expected.Equal(r2) {
t.Errorf("Expected %v but got %v", expected, r2)