Refactored match to use function chaining

This commit is contained in:
eanderton
2014-09-07 16:32:29 -04:00
parent a98788e0d7
commit c81f1892c2
4 changed files with 232 additions and 145 deletions
+119 -38
View File
@@ -4,14 +4,84 @@ import (
. "github.com/pelletier/go-toml" . "github.com/pelletier/go-toml"
) )
type PathFn interface{ // result set for storage of results
Call(context interface{}, next QueryPath) type pathResult struct {
Values []interface{}
} }
type QueryPath []PathFn func newPathResult() *pathResult {
return &pathResult {
Values: []interface{}{},
}
}
func (path QueryPath) Fn(context interface{}) { func (r *pathResult) Append(value interface{}) {
path[0].Call(context, path[1:]) r.Values = append(r.Values, value)
}
// generic path functor interface
type PathFn interface{
SetNext(next PathFn)
Call(context interface{}, results *pathResult)
}
// contains a functor chain
type QueryPath struct {
root PathFn
tail PathFn
}
func newQueryPath() *QueryPath {
return &QueryPath {
root: nil,
tail: nil,
}
}
func (path *QueryPath) Append(next PathFn) {
if path.root == nil {
path.root = next
} else {
path.tail.SetNext(next)
}
path.tail = next
next.SetNext(newTerminatingFn()) // init the next functor
}
func (path *QueryPath) Call(context interface{}) []interface{} {
results := newPathResult()
if path.root == nil {
results.Append(context) // identity query for no predicates
} else {
path.root.Call(context, results)
}
return results.Values
}
// base match
type matchBase struct {
next PathFn
}
func (f *matchBase) SetNext(next PathFn) {
f.next = next
}
// terminating functor - gathers results
type terminatingFn struct {
// empty
}
func newTerminatingFn() *terminatingFn {
return &terminatingFn{}
}
func (f *terminatingFn) SetNext(next PathFn) {
// do nothing
}
func (f *terminatingFn) Call(context interface{}, results *pathResult) {
results.Append(context)
} }
// shim to ease functor writing // shim to ease functor writing
@@ -21,37 +91,52 @@ func treeValue(tree *TomlTree, key string) interface{} {
// match single key // match single key
type matchKeyFn struct { type matchKeyFn struct {
matchBase
Name string Name string
} }
func (f *matchKeyFn) Call(context interface{}, next QueryPath) { func newMatchKeyFn(name string) *matchKeyFn {
return &matchKeyFn{ Name: name }
}
func (f *matchKeyFn) Call(context interface{}, results *pathResult) {
if tree, ok := context.(*TomlTree); ok { if tree, ok := context.(*TomlTree); ok {
item := treeValue(tree, f.Name) item := treeValue(tree, f.Name)
if item != nil { if item != nil {
next.Fn(item) f.next.Call(item, results)
} }
} }
} }
// match single index // match single index
type matchIndexFn struct { type matchIndexFn struct {
matchBase
Idx int Idx int
} }
func (f *matchIndexFn) Call(context interface{}, next QueryPath) { func newMatchIndexFn(idx int) *matchIndexFn {
return &matchIndexFn{ Idx: idx }
}
func (f *matchIndexFn) Call(context interface{}, results *pathResult) {
if arr, ok := context.([]interface{}); ok { if arr, ok := context.([]interface{}); ok {
if f.Idx < len(arr) && f.Idx >= 0 { if f.Idx < len(arr) && f.Idx >= 0 {
next.Fn(arr[f.Idx]) f.next.Call(arr[f.Idx], results)
} }
} }
} }
// filter by slicing // filter by slicing
type matchSliceFn struct { type matchSliceFn struct {
matchBase
Start, End, Step int Start, End, Step int
} }
func (f *matchSliceFn) Call(context interface{}, next QueryPath) { func newMatchSliceFn(start, end, step int) *matchSliceFn {
return &matchSliceFn{ Start: start, End: end, Step: step }
}
func (f *matchSliceFn) Call(context interface{}, results *pathResult) {
if arr, ok := context.([]interface{}); ok { if arr, ok := context.([]interface{}); ok {
// adjust indexes for negative values, reverse ordering // adjust indexes for negative values, reverse ordering
realStart, realEnd := f.Start, f.End realStart, realEnd := f.Start, f.End
@@ -66,48 +151,63 @@ func (f *matchSliceFn) Call(context interface{}, next QueryPath) {
} }
// loop and gather // loop and gather
for idx := realStart; idx < realEnd; idx += f.Step { for idx := realStart; idx < realEnd; idx += f.Step {
next.Fn(arr[idx]) f.next.Call(arr[idx], results)
} }
} }
} }
// match anything // match anything
type matchAnyFn struct { type matchAnyFn struct {
matchBase
// empty // empty
} }
func (f *matchAnyFn) Call(context interface{}, next QueryPath) { func newMatchAnyFn() *matchAnyFn {
return &matchAnyFn{}
}
func (f *matchAnyFn) Call(context interface{}, results *pathResult) {
if tree, ok := context.(*TomlTree); ok { if tree, ok := context.(*TomlTree); ok {
for _, key := range tree.Keys() { for _, key := range tree.Keys() {
item := treeValue(tree, key) item := treeValue(tree, key)
next.Fn(item) f.next.Call(item, results)
} }
} }
} }
// filter through union // filter through union
type matchUnionFn struct { type matchUnionFn struct {
Union QueryPath Union []PathFn
} }
func (f *matchUnionFn) Call(context interface{}, next QueryPath) { func (f *matchUnionFn) SetNext(next PathFn) {
for _, fn := range f.Union { for _, fn := range f.Union {
fn.Call(context, next) fn.SetNext(next)
}
}
func (f *matchUnionFn) Call(context interface{}, results *pathResult) {
for _, fn := range f.Union {
fn.Call(context, results)
} }
} }
// match every single last node in the tree // match every single last node in the tree
type matchRecursiveFn struct { type matchRecursiveFn struct {
// empty matchBase
} }
func (f *matchRecursiveFn) Call(context interface{}, next QueryPath) { func newMatchRecursiveFn() *matchRecursiveFn{
return &matchRecursiveFn{}
}
func (f *matchRecursiveFn) Call(context interface{}, results *pathResult) {
if tree, ok := context.(*TomlTree); ok { if tree, ok := context.(*TomlTree); ok {
var visit func(tree *TomlTree) var visit func(tree *TomlTree)
visit = func(tree *TomlTree) { visit = func(tree *TomlTree) {
for _, key := range tree.Keys() { for _, key := range tree.Keys() {
item := treeValue(tree, key) item := treeValue(tree, key)
next.Fn(item) f.next.Call(item, results)
switch node := item.(type) { switch node := item.(type) {
case *TomlTree: case *TomlTree:
visit(node) visit(node)
@@ -121,22 +221,3 @@ func (f *matchRecursiveFn) Call(context interface{}, next QueryPath) {
visit(tree) visit(tree)
} }
} }
// terminating expression
type matchEndFn struct {
Result []interface{}
}
func (f *matchEndFn) Call(context interface{}, next QueryPath) {
f.Result = append(f.Result, context)
}
func processPath(path QueryPath, context interface{}) []interface{} {
// terminate the path with a collection funciton
endFn := &matchEndFn{ []interface{}{} }
newPath := append(path, endFn)
// execute the path
newPath.Fn(context)
return endFn.Result
}
+91 -88
View File
@@ -6,181 +6,184 @@ import (
"testing" "testing"
) )
func pathString(path QueryPath) string { // dump path tree to a string
result := "[" func pathString(root PathFn) string {
for _, v := range path { result := fmt.Sprintf("%T:")
result += fmt.Sprintf("%T:%v, ", v, v) switch fn := root.(type) {
case *terminatingFn:
result += "{}"
case *matchKeyFn:
result += fmt.Sprintf("{%s}", fn.Name)
result += pathString(fn.next)
case *matchIndexFn:
result += fmt.Sprintf("{%d}", fn.Idx)
result += pathString(fn.next)
case *matchSliceFn:
result += fmt.Sprintf("{%d:%d:%d}",
fn.Start, fn.End, fn.Step)
result += pathString(fn.next)
case *matchAnyFn:
result += "{}"
result += pathString(fn.next)
case *matchUnionFn:
result += "{["
for _, v := range fn.Union {
result += pathString(v)
}
result += "]}"
case *matchRecursiveFn:
result += "{}"
result += pathString(fn.next)
} }
return result + "]" return result
} }
func assertPathMatch(t *testing.T, path, ref QueryPath) bool { func assertPathMatch(t *testing.T, path, ref *QueryPath) bool {
if len(path) != len(ref) { pathStr := pathString(path.root)
t.Errorf("lengths do not match: %v vs %v", refStr := pathString(ref.root)
pathString(path), pathString(ref)) if pathStr != refStr {
t.Errorf("paths do not match: %v vs %v")
t.Log("test:", pathStr)
t.Log("ref: ", refStr)
return false return false
} else {
for i, v := range ref {
pass := false
node := path[i]
// compare by value
switch refNode := v.(type) {
case *matchKeyFn:
castNode, ok := node.(*matchKeyFn)
pass = ok && (*refNode == *castNode)
case *matchIndexFn:
castNode, ok := node.(*matchIndexFn)
pass = ok && (*refNode == *castNode)
case *matchSliceFn:
castNode, ok := node.(*matchSliceFn)
pass = ok && (*refNode == *castNode)
case *matchAnyFn:
castNode, ok := node.(*matchAnyFn)
pass = ok && (*refNode == *castNode)
case *matchUnionFn:
castNode, ok := node.(*matchUnionFn)
// special case - comapre all contents
pass = ok && assertPathMatch(t, castNode.Union, refNode.Union)
case *matchRecursiveFn:
castNode, ok := node.(*matchRecursiveFn)
pass = ok && (*refNode == *castNode)
}
if !pass {
t.Errorf("paths do not match at index %d: %v vs %v",
i, pathString(path), pathString(ref))
return false
}
}
} }
return true return true
} }
func assertPath(t *testing.T, query string, ref QueryPath) { func assertPath(t *testing.T, query string, ref *QueryPath) {
_, flow := lex(query) _, flow := lex(query)
path := parse(flow) path := parse(flow)
assertPathMatch(t, path, ref) assertPathMatch(t, path, ref)
} }
func buildPath(parts... PathFn) *QueryPath {
path := newQueryPath()
for _, v := range parts {
path.Append(v)
}
return path
}
func TestPathRoot(t *testing.T) { func TestPathRoot(t *testing.T) {
assertPath(t, assertPath(t,
"$", "$",
QueryPath{ buildPath(
// empty // empty
}) ))
} }
func TestPathKey(t *testing.T) { func TestPathKey(t *testing.T) {
assertPath(t, assertPath(t,
"$.foo", "$.foo",
QueryPath{ buildPath(
&matchKeyFn{ "foo" }, newMatchKeyFn("foo"),
}) ))
} }
func TestPathBracketKey(t *testing.T) { func TestPathBracketKey(t *testing.T) {
assertPath(t, assertPath(t,
"$[foo]", "$[foo]",
QueryPath{ buildPath(
&matchKeyFn{ "foo" }, newMatchKeyFn("foo"),
}) ))
} }
func TestPathBracketStringKey(t *testing.T) { func TestPathBracketStringKey(t *testing.T) {
assertPath(t, assertPath(t,
"$['foo']", "$['foo']",
QueryPath{ buildPath(
&matchKeyFn{ "foo" }, newMatchKeyFn("foo"),
}) ))
} }
func TestPathIndex(t *testing.T) { func TestPathIndex(t *testing.T) {
assertPath(t, assertPath(t,
"$[123]", "$[123]",
QueryPath{ buildPath(
&matchIndexFn{ 123 }, newMatchIndexFn(123),
}) ))
} }
func TestPathSliceStart(t *testing.T) { func TestPathSliceStart(t *testing.T) {
assertPath(t, assertPath(t,
"$[123:]", "$[123:]",
QueryPath{ buildPath(
&matchSliceFn{ 123, math.MaxInt64, 1 }, newMatchSliceFn(123, math.MaxInt64, 1),
}) ))
} }
func TestPathSliceStartEnd(t *testing.T) { func TestPathSliceStartEnd(t *testing.T) {
assertPath(t, assertPath(t,
"$[123:456]", "$[123:456]",
QueryPath{ buildPath(
&matchSliceFn{ 123, 456, 1 }, newMatchSliceFn(123, 456, 1),
}) ))
} }
func TestPathSliceStartEndColon(t *testing.T) { func TestPathSliceStartEndColon(t *testing.T) {
assertPath(t, assertPath(t,
"$[123:456:]", "$[123:456:]",
QueryPath{ buildPath(
&matchSliceFn{ 123, 456, 1 }, newMatchSliceFn(123, 456, 1),
}) ))
} }
func TestPathSliceStartStep(t *testing.T) { func TestPathSliceStartStep(t *testing.T) {
assertPath(t, assertPath(t,
"$[123::7]", "$[123::7]",
QueryPath{ buildPath(
&matchSliceFn{ 123, math.MaxInt64, 7 }, newMatchSliceFn(123, math.MaxInt64, 7),
}) ))
} }
func TestPathSliceEndStep(t *testing.T) { func TestPathSliceEndStep(t *testing.T) {
assertPath(t, assertPath(t,
"$[:456:7]", "$[:456:7]",
QueryPath{ buildPath(
&matchSliceFn{ 0, 456, 7 }, newMatchSliceFn(0, 456, 7),
}) ))
} }
func TestPathSliceStep(t *testing.T) { func TestPathSliceStep(t *testing.T) {
assertPath(t, assertPath(t,
"$[::7]", "$[::7]",
QueryPath{ buildPath(
&matchSliceFn{ 0, math.MaxInt64, 7 }, newMatchSliceFn(0, math.MaxInt64, 7),
}) ))
} }
func TestPathSliceAll(t *testing.T) { func TestPathSliceAll(t *testing.T) {
assertPath(t, assertPath(t,
"$[123:456:7]", "$[123:456:7]",
QueryPath{ buildPath(
&matchSliceFn{ 123, 456, 7 }, newMatchSliceFn(123, 456, 7),
}) ))
} }
func TestPathAny(t *testing.T) { func TestPathAny(t *testing.T) {
assertPath(t, assertPath(t,
"$.*", "$.*",
QueryPath{ buildPath(
&matchAnyFn{}, newMatchAnyFn(),
}) ))
} }
func TestPathUnion(t *testing.T) { func TestPathUnion(t *testing.T) {
assertPath(t, assertPath(t,
"$[foo, bar, baz]", "$[foo, bar, baz]",
QueryPath{ buildPath(
&matchUnionFn{ []PathFn { &matchUnionFn{ []PathFn {
&matchKeyFn{ "foo" }, newMatchKeyFn("foo"),
&matchKeyFn{ "bar" }, newMatchKeyFn("bar"),
&matchKeyFn{ "baz" }, newMatchKeyFn("baz"),
}}, }},
}) ))
} }
func TestPathRecurse(t *testing.T) { func TestPathRecurse(t *testing.T) {
assertPath(t, assertPath(t,
"$..*", "$..*",
QueryPath{ buildPath(
&matchRecursiveFn{}, newMatchRecursiveFn(),
}) ))
} }
+21 -18
View File
@@ -1,3 +1,10 @@
/*
Based on the "jsonpath" spec/concept.
http://goessner.net/articles/JsonPath/
https://code.google.com/p/json-path/
*/
package jpath package jpath
import ( import (
@@ -8,7 +15,7 @@ import (
type parser struct { type parser struct {
flow chan token flow chan token
tokensBuffer []token tokensBuffer []token
path []PathFn path *QueryPath
union []PathFn union []PathFn
} }
@@ -76,10 +83,6 @@ func (p *parser) getToken() *token {
return &tok return &tok
} }
func (p *parser) appendPath(fn PathFn) {
p.path = append(p.path, fn)
}
func parseStart(p *parser) parserStateFn { func parseStart(p *parser) parserStateFn {
tok := p.getToken() tok := p.getToken()
@@ -99,12 +102,12 @@ func parseMatchExpr(p *parser) parserStateFn {
tok := p.getToken() tok := p.getToken()
switch tok.typ { switch tok.typ {
case tokenDotDot: case tokenDotDot:
p.appendPath(&matchRecursiveFn{}) p.path.Append(&matchRecursiveFn{})
// nested parse for '..' // nested parse for '..'
tok := p.getToken() tok := p.getToken()
switch tok.typ { switch tok.typ {
case tokenKey: case tokenKey:
p.appendPath(&matchKeyFn{tok.val}) p.path.Append(newMatchKeyFn(tok.val))
return parseMatchExpr return parseMatchExpr
case tokenLBracket: case tokenLBracket:
return parseBracketExpr return parseBracketExpr
@@ -118,10 +121,10 @@ func parseMatchExpr(p *parser) parserStateFn {
tok := p.getToken() tok := p.getToken()
switch tok.typ { switch tok.typ {
case tokenKey: case tokenKey:
p.appendPath(&matchKeyFn{tok.val}) p.path.Append(newMatchKeyFn(tok.val))
return parseMatchExpr return parseMatchExpr
case tokenStar: case tokenStar:
p.appendPath(&matchAnyFn{}) p.path.Append(&matchAnyFn{})
return parseMatchExpr return parseMatchExpr
} }
@@ -158,11 +161,11 @@ loop: // labeled loop for easy breaking
tok := p.getToken() tok := p.getToken()
switch tok.typ { switch tok.typ {
case tokenInteger: case tokenInteger:
p.union = append(p.union, &matchIndexFn{tok.Int()}) p.union = append(p.union, newMatchIndexFn(tok.Int()))
case tokenKey: case tokenKey:
p.union = append(p.union, &matchKeyFn{tok.val}) p.union = append(p.union, newMatchKeyFn(tok.val))
case tokenString: case tokenString:
p.union = append(p.union, &matchKeyFn{tok.val}) p.union = append(p.union, newMatchKeyFn(tok.val))
case tokenQuestion: case tokenQuestion:
return parseFilterExpr return parseFilterExpr
case tokenLParen: case tokenLParen:
@@ -184,9 +187,9 @@ loop: // labeled loop for easy breaking
// if there is only one sub-expression, use that instead // if there is only one sub-expression, use that instead
if len(p.union) == 1 { if len(p.union) == 1 {
p.appendPath(p.union[0]) p.path.Append(p.union[0])
}else { }else {
p.appendPath(&matchUnionFn{p.union}) p.path.Append(&matchUnionFn{p.union})
} }
p.union = nil // clear out state p.union = nil // clear out state
@@ -214,7 +217,7 @@ func parseSliceExpr(p *parser) parserStateFn {
tok = p.getToken() tok = p.getToken()
} }
if tok.typ == tokenRBracket { if tok.typ == tokenRBracket {
p.appendPath(&matchSliceFn{start, end, step}) p.path.Append(newMatchSliceFn(start, end, step))
return parseMatchExpr return parseMatchExpr
} }
if tok.typ != tokenColon { if tok.typ != tokenColon {
@@ -234,7 +237,7 @@ func parseSliceExpr(p *parser) parserStateFn {
p.raiseError(tok, "expected ']'") p.raiseError(tok, "expected ']'")
} }
p.appendPath(&matchSliceFn{start, end, step}) p.path.Append(newMatchSliceFn(start, end, step))
return parseMatchExpr return parseMatchExpr
} }
@@ -248,11 +251,11 @@ func parseScriptExpr(p *parser) parserStateFn {
return nil return nil
} }
func parse(flow chan token) []PathFn { func parse(flow chan token) *QueryPath {
parser := &parser{ parser := &parser{
flow: flow, flow: flow,
tokensBuffer: []token{}, tokensBuffer: []token{},
path: []PathFn{}, path: newQueryPath(),
} }
parser.run() parser.run()
return parser.path return parser.path
+1 -1
View File
@@ -18,7 +18,7 @@ func assertQuery(t *testing.T, toml, query string, ref []interface{}) {
return return
} }
path := parse(flow) path := parse(flow)
result := processPath(path, tree) result := path.Call(tree)
assertValue(t, result, ref, "((" + query + ")) -> ") assertValue(t, result, ref, "((" + query + ")) -> ")
} }