Refactored match to use function chaining
diff --git a/jpath/match.go b/jpath/match.go index 9a9ef0b..ffde749 100644 --- a/jpath/match.go +++ b/jpath/match.go
@@ -4,14 +4,84 @@ . "github.com/pelletier/go-toml" ) -type PathFn interface{ - Call(context interface{}, next QueryPath) +// result set for storage of results +type pathResult struct { + Values []interface{} } -type QueryPath []PathFn +func newPathResult() *pathResult { + return &pathResult { + Values: []interface{}{}, + } +} -func (path QueryPath) Fn(context interface{}) { - path[0].Call(context, path[1:]) +func (r *pathResult) Append(value interface{}) { + 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 @@ -21,37 +91,52 @@ // match single key type matchKeyFn struct { + matchBase 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 { item := treeValue(tree, f.Name) if item != nil { - next.Fn(item) + f.next.Call(item, results) } } } // match single index type matchIndexFn struct { + matchBase 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 f.Idx < len(arr) && f.Idx >= 0 { - next.Fn(arr[f.Idx]) + f.next.Call(arr[f.Idx], results) } } } // filter by slicing type matchSliceFn struct { + matchBase 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 { // adjust indexes for negative values, reverse ordering realStart, realEnd := f.Start, f.End @@ -66,48 +151,63 @@ } // loop and gather for idx := realStart; idx < realEnd; idx += f.Step { - next.Fn(arr[idx]) + f.next.Call(arr[idx], results) } } } // match anything type matchAnyFn struct { + matchBase // 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 { for _, key := range tree.Keys() { item := treeValue(tree, key) - next.Fn(item) + f.next.Call(item, results) } } } // filter through union 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 { - 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 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 { var visit func(tree *TomlTree) visit = func(tree *TomlTree) { for _, key := range tree.Keys() { item := treeValue(tree, key) - next.Fn(item) + f.next.Call(item, results) switch node := item.(type) { case *TomlTree: visit(node) @@ -121,22 +221,3 @@ 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 -}
diff --git a/jpath/match_test.go b/jpath/match_test.go index 5e7f73c..514962b 100644 --- a/jpath/match_test.go +++ b/jpath/match_test.go
@@ -6,181 +6,184 @@ "testing" ) -func pathString(path QueryPath) string { - result := "[" - for _, v := range path { - result += fmt.Sprintf("%T:%v, ", v, v) +// dump path tree to a string +func pathString(root PathFn) string { + result := fmt.Sprintf("%T:") + 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 { - if len(path) != len(ref) { - t.Errorf("lengths do not match: %v vs %v", - pathString(path), pathString(ref)) +func assertPathMatch(t *testing.T, path, ref *QueryPath) bool { + pathStr := pathString(path.root) + refStr := pathString(ref.root) + if pathStr != refStr { + t.Errorf("paths do not match: %v vs %v") + t.Log("test:", pathStr) + t.Log("ref: ", refStr) 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 } -func assertPath(t *testing.T, query string, ref QueryPath) { +func assertPath(t *testing.T, query string, ref *QueryPath) { _, flow := lex(query) path := parse(flow) 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) { assertPath(t, "$", - QueryPath{ + buildPath( // empty - }) + )) } func TestPathKey(t *testing.T) { assertPath(t, "$.foo", - QueryPath{ - &matchKeyFn{ "foo" }, - }) + buildPath( + newMatchKeyFn("foo"), + )) } func TestPathBracketKey(t *testing.T) { assertPath(t, "$[foo]", - QueryPath{ - &matchKeyFn{ "foo" }, - }) + buildPath( + newMatchKeyFn("foo"), + )) } func TestPathBracketStringKey(t *testing.T) { assertPath(t, "$['foo']", - QueryPath{ - &matchKeyFn{ "foo" }, - }) + buildPath( + newMatchKeyFn("foo"), + )) } func TestPathIndex(t *testing.T) { assertPath(t, "$[123]", - QueryPath{ - &matchIndexFn{ 123 }, - }) + buildPath( + newMatchIndexFn(123), + )) } func TestPathSliceStart(t *testing.T) { assertPath(t, "$[123:]", - QueryPath{ - &matchSliceFn{ 123, math.MaxInt64, 1 }, - }) + buildPath( + newMatchSliceFn(123, math.MaxInt64, 1), + )) } func TestPathSliceStartEnd(t *testing.T) { assertPath(t, "$[123:456]", - QueryPath{ - &matchSliceFn{ 123, 456, 1 }, - }) + buildPath( + newMatchSliceFn(123, 456, 1), + )) } func TestPathSliceStartEndColon(t *testing.T) { assertPath(t, "$[123:456:]", - QueryPath{ - &matchSliceFn{ 123, 456, 1 }, - }) + buildPath( + newMatchSliceFn(123, 456, 1), + )) } func TestPathSliceStartStep(t *testing.T) { assertPath(t, "$[123::7]", - QueryPath{ - &matchSliceFn{ 123, math.MaxInt64, 7 }, - }) + buildPath( + newMatchSliceFn(123, math.MaxInt64, 7), + )) } func TestPathSliceEndStep(t *testing.T) { assertPath(t, "$[:456:7]", - QueryPath{ - &matchSliceFn{ 0, 456, 7 }, - }) + buildPath( + newMatchSliceFn(0, 456, 7), + )) } func TestPathSliceStep(t *testing.T) { assertPath(t, "$[::7]", - QueryPath{ - &matchSliceFn{ 0, math.MaxInt64, 7 }, - }) + buildPath( + newMatchSliceFn(0, math.MaxInt64, 7), + )) } func TestPathSliceAll(t *testing.T) { assertPath(t, "$[123:456:7]", - QueryPath{ - &matchSliceFn{ 123, 456, 7 }, - }) + buildPath( + newMatchSliceFn(123, 456, 7), + )) } func TestPathAny(t *testing.T) { assertPath(t, "$.*", - QueryPath{ - &matchAnyFn{}, - }) + buildPath( + newMatchAnyFn(), + )) } func TestPathUnion(t *testing.T) { assertPath(t, "$[foo, bar, baz]", - QueryPath{ + buildPath( &matchUnionFn{ []PathFn { - &matchKeyFn{ "foo" }, - &matchKeyFn{ "bar" }, - &matchKeyFn{ "baz" }, + newMatchKeyFn("foo"), + newMatchKeyFn("bar"), + newMatchKeyFn("baz"), }}, - }) + )) } func TestPathRecurse(t *testing.T) { assertPath(t, "$..*", - QueryPath{ - &matchRecursiveFn{}, - }) + buildPath( + newMatchRecursiveFn(), + )) }
diff --git a/jpath/parser.go b/jpath/parser.go index ce37081..7b842a4 100644 --- a/jpath/parser.go +++ b/jpath/parser.go
@@ -1,3 +1,10 @@ +/* + Based on the "jsonpath" spec/concept. + + http://goessner.net/articles/JsonPath/ + https://code.google.com/p/json-path/ +*/ + package jpath import ( @@ -8,7 +15,7 @@ type parser struct { flow chan token tokensBuffer []token - path []PathFn + path *QueryPath union []PathFn } @@ -76,10 +83,6 @@ return &tok } -func (p *parser) appendPath(fn PathFn) { - p.path = append(p.path, fn) -} - func parseStart(p *parser) parserStateFn { tok := p.getToken() @@ -99,12 +102,12 @@ tok := p.getToken() switch tok.typ { case tokenDotDot: - p.appendPath(&matchRecursiveFn{}) + p.path.Append(&matchRecursiveFn{}) // nested parse for '..' tok := p.getToken() switch tok.typ { case tokenKey: - p.appendPath(&matchKeyFn{tok.val}) + p.path.Append(newMatchKeyFn(tok.val)) return parseMatchExpr case tokenLBracket: return parseBracketExpr @@ -118,10 +121,10 @@ tok := p.getToken() switch tok.typ { case tokenKey: - p.appendPath(&matchKeyFn{tok.val}) + p.path.Append(newMatchKeyFn(tok.val)) return parseMatchExpr case tokenStar: - p.appendPath(&matchAnyFn{}) + p.path.Append(&matchAnyFn{}) return parseMatchExpr } @@ -158,11 +161,11 @@ tok := p.getToken() switch tok.typ { case tokenInteger: - p.union = append(p.union, &matchIndexFn{tok.Int()}) + p.union = append(p.union, newMatchIndexFn(tok.Int())) case tokenKey: - p.union = append(p.union, &matchKeyFn{tok.val}) + p.union = append(p.union, newMatchKeyFn(tok.val)) case tokenString: - p.union = append(p.union, &matchKeyFn{tok.val}) + p.union = append(p.union, newMatchKeyFn(tok.val)) case tokenQuestion: return parseFilterExpr case tokenLParen: @@ -184,9 +187,9 @@ // if there is only one sub-expression, use that instead if len(p.union) == 1 { - p.appendPath(p.union[0]) + p.path.Append(p.union[0]) }else { - p.appendPath(&matchUnionFn{p.union}) + p.path.Append(&matchUnionFn{p.union}) } p.union = nil // clear out state @@ -214,7 +217,7 @@ tok = p.getToken() } if tok.typ == tokenRBracket { - p.appendPath(&matchSliceFn{start, end, step}) + p.path.Append(newMatchSliceFn(start, end, step)) return parseMatchExpr } if tok.typ != tokenColon { @@ -234,7 +237,7 @@ p.raiseError(tok, "expected ']'") } - p.appendPath(&matchSliceFn{start, end, step}) + p.path.Append(newMatchSliceFn(start, end, step)) return parseMatchExpr } @@ -248,11 +251,11 @@ return nil } -func parse(flow chan token) []PathFn { +func parse(flow chan token) *QueryPath { parser := &parser{ flow: flow, tokensBuffer: []token{}, - path: []PathFn{}, + path: newQueryPath(), } parser.run() return parser.path
diff --git a/jpath/parser_test.go b/jpath/parser_test.go index 6daba44..3103970 100644 --- a/jpath/parser_test.go +++ b/jpath/parser_test.go
@@ -18,7 +18,7 @@ return } path := parse(flow) - result := processPath(path, tree) + result := path.Call(tree) assertValue(t, result, ref, "((" + query + ")) -> ") }