Skip to content

Commit

Permalink
Merge pull request #1 from skuid/feature-PLIN-1851
Browse files Browse the repository at this point in the history
Feature plin 1851
  • Loading branch information
Brian Newton authored Nov 5, 2018
2 parents 1fbf809 + 6086c23 commit f279342
Show file tree
Hide file tree
Showing 13 changed files with 1,067 additions and 275 deletions.
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ redis
coverage.out

# Build artifacts
warden
condparse

# build/test files
env.sh
Expand Down
78 changes: 77 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1 +1,77 @@
#condparse
# condparse

This is mostly intended to be used as a library for parsing, manipulating, and serializing a condition logic string.

For example, given a condition logic string like so `1 OR 2 AND (3 AND 4)`, this will allow you to remove any leaves by value and still get a valid condition logic string.

# Usage

## Parse

To parse an existing condition logic string, call `Parse(string) error`

```go
import {
"github.com/skuid/condparse/condparse"
}

logic := "1 OR 2 AND (3 OR 4)"

tree, err := condparse.Parse(logic)

if err != nil {
fmt.Print("Error: %v", err)
}

fmt.Printf("Built a binary expression tree that looks like this: %v", tree)
```

## Node.Eval

This will take a tree and write it to any `io.Writer`

```go
var b strings.Builder

err := tree.Eval(&b)

if err != nil {
fmt.Print("Error: %v", err)
}

fmt.Printf("Serialized the tree into condition logic: %s", b.String())
```

## Node.Remove

This can be called multiple times to remove leafs from the tree by value. It will hoist any remaining expressions where needed and ignore any leafs it does not contain.

```go
logic := "1 OR (5 AND (1 OR 1)) AND (1 AND 2 OR (56 AND 1)) OR 4"

tree, _ := condparse.Parse(logic)

n := tree.Remove(1)
n = tree.Remove(8)

var b strings.Builder
n.Eval(&b)
fmt.Println(b.String())
tree.Remove(8)

var b strings.Builder
tree.Eval(&b)
fmt.Println(b.String())
// 5 AND (2 OR 56 OR 4)
```

# Errors

`Parse` will throw `ParseError` errors mostly. These errors contain:
- **Position**: The location in the logic string the error occurred
- **Logic**: The logic string that the parser was tryign to parse
- **Reason**: The reason it failed to parse the logic at that location

`Eval` will throw `SerializeError` errors. They contain:
- **Op**: The operation that failed to serialize. Leaf values are unlikely to fail
- **Reason**: The reason it could not serialize that operation, which is typically due to missing nodes.
Binary file removed condparse
Binary file not shown.
153 changes: 153 additions & 0 deletions pkg/condparse_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
package condparse

import (
"fmt"
"strings"
"testing"

"github.com/stretchr/testify/assert"
)

type testCase struct {
desc string
exprTree Node
logic string
}

var cases []testCase

func init() {
cases = []testCase{
{
"Should serialize a single leaf",
&Leaf{1},
"1",
},
{
"Should parse a very simple tree with just one operation and two leafs",
&Op{
Left: &Leaf{1},
Val: "AND",
Right: &Leaf{2},
},
"1 AND 2",
},
{
"Should parse a more complex tree with left only operations (no parens)",
&Op{
Left: &Op{
Left: &Leaf{1},
Val: "OR",
Right: &Leaf{2},
},
Val: "AND",
Right: &Leaf{3},
},
"1 OR 2 AND 3",
},
{
"Should parse a more complex tree with left and right operations, including parens",
&Op{
Left: &Op{
Left: &Leaf{1},
Val: "OR",
Right: &Leaf{2},
},
Val: "AND",
Right: &Op{
Left: &Leaf{3},
Val: "OR",
Right: &Leaf{4},
},
},
"1 OR 2 AND (3 OR 4)",
},
{
"Should complex trees with more depth",
&Op{
Left: &Op{
Left: &Leaf{1},
Val: "OR",
Right: &Op{
Left: &Leaf{5},
Val: "AND",
Right: &Op{
Left: &Leaf{7},
Val: "OR",
Right: &Leaf{8},
},
},
},
Val: "AND",
Right: &Op{
Left: &Op{
Left: &Op{
Left: &Leaf{3},
Val: "AND",
Right: &Leaf{2},
},
Val: "OR",
Right: &Op{
Left: &Leaf{56},
Val: "AND",
Right: &Leaf{1000},
},
},
Val: "OR",
Right: &Leaf{4},
},
},
"1 OR (5 AND (7 OR 8)) AND (3 AND 2 OR (56 AND 1000) OR 4)",
},
}
}

func TestParse(t *testing.T) {
for _, c := range cases {
t.Run(c.desc, func(t *testing.T) {
assert := assert.New(t)
actual, err := Parse(c.logic)

assert.NoError(err, "Should not have an error")
deepEql(assert, c.exprTree, actual)
})
}
}

func TestSerialize(t *testing.T) {
for _, c := range cases {
t.Run(c.desc, func(t *testing.T) {
assert := assert.New(t)
var b strings.Builder
c.exprTree.Eval(&b)

actual := b.String()
assert.Equal(c.logic, actual)
})
}
}

func deepEql(assert *assert.Assertions, expected Node, actual Node) {
switch e := expected.(type) {
case *Leaf:
a, isLeaf := actual.(*Leaf)
assert.True(isLeaf, "Expected was a leaf, actual was not! expected %v, actual %v", expected, actual)
if e != nil && a != nil {
assert.Equal(e.Val, a.Val, "Expected leaf values to match")
} else if e != nil || a != nil {
assert.Fail(fmt.Sprintf("One was nil and the other had a value. Expected %v, Actual %v", e, a))
}
case *Op:
a, isOp := actual.(*Op)
assert.True(isOp, "Expected was an Operation, actual was not! expected %v, actual %v", expected, actual)
if e != nil && a != nil {
assert.Equal(e.Val, a.Val, "Expected operation to be the same")
deepEql(assert, e.Left, a.Left)
deepEql(assert, e.Right, a.Right)
} else if e != nil || a != nil {
assert.Fail(fmt.Sprintf("One was nil and the other had a value. Expected %v, Actual %v", e, a))
}
default:
assert.Fail("Node was neither a leaf or an op.")
}
}
Loading

0 comments on commit f279342

Please sign in to comment.