290 lines
6.1 KiB
Go
290 lines
6.1 KiB
Go
/*
|
|
Based on the "jsonpath" spec/concept.
|
|
|
|
http://goessner.net/articles/JsonPath/
|
|
https://code.google.com/p/json-path/
|
|
*/
|
|
|
|
package jpath
|
|
|
|
import (
|
|
"fmt"
|
|
"math"
|
|
)
|
|
|
|
type parser struct {
|
|
flow chan token
|
|
tokensBuffer []token
|
|
path *Query
|
|
union []PathFn
|
|
}
|
|
|
|
type parserStateFn func(*parser) parserStateFn
|
|
|
|
// Formats and panics an error message based on a token
|
|
func (p *parser) raiseError(tok *token, msg string, args ...interface{}) {
|
|
panic(tok.Position.String() + ": " + fmt.Sprintf(msg, args...))
|
|
}
|
|
|
|
func (p *parser) run() {
|
|
for state := parseStart; state != nil; {
|
|
state = state(p)
|
|
}
|
|
}
|
|
|
|
func (p *parser) backup(tok *token) {
|
|
p.tokensBuffer = append(p.tokensBuffer, *tok)
|
|
}
|
|
|
|
func (p *parser) peek() *token {
|
|
if len(p.tokensBuffer) != 0 {
|
|
return &(p.tokensBuffer[0])
|
|
}
|
|
|
|
tok, ok := <-p.flow
|
|
if !ok {
|
|
return nil
|
|
}
|
|
p.backup(&tok)
|
|
return &tok
|
|
}
|
|
|
|
func (p *parser) lookahead(types ...tokenType) bool {
|
|
result := true
|
|
buffer := []token{}
|
|
|
|
for _, typ := range types {
|
|
tok := p.getToken()
|
|
if tok == nil {
|
|
result = false
|
|
break
|
|
}
|
|
buffer = append(buffer, *tok)
|
|
if tok.typ != typ {
|
|
result = false
|
|
break
|
|
}
|
|
}
|
|
// add the tokens back to the buffer, and return
|
|
p.tokensBuffer = append(p.tokensBuffer, buffer...)
|
|
return result
|
|
}
|
|
|
|
func (p *parser) getToken() *token {
|
|
if len(p.tokensBuffer) != 0 {
|
|
tok := p.tokensBuffer[0]
|
|
p.tokensBuffer = p.tokensBuffer[1:]
|
|
return &tok
|
|
}
|
|
tok, ok := <-p.flow
|
|
if !ok {
|
|
return nil
|
|
}
|
|
return &tok
|
|
}
|
|
|
|
func parseStart(p *parser) parserStateFn {
|
|
tok := p.getToken()
|
|
|
|
if tok == nil || tok.typ == tokenEOF {
|
|
return nil
|
|
}
|
|
|
|
if tok.typ != tokenDollar {
|
|
p.raiseError(tok, "Expected '$' at start of expression")
|
|
}
|
|
|
|
return parseMatchExpr
|
|
}
|
|
|
|
// handle '.' prefix, '[]', and '..'
|
|
func parseMatchExpr(p *parser) parserStateFn {
|
|
tok := p.getToken()
|
|
switch tok.typ {
|
|
case tokenDotDot:
|
|
p.path.appendPath(&matchRecursiveFn{})
|
|
// nested parse for '..'
|
|
tok := p.getToken()
|
|
switch tok.typ {
|
|
case tokenKey:
|
|
p.path.appendPath(newMatchKeyFn(tok.val))
|
|
return parseMatchExpr
|
|
case tokenLBracket:
|
|
return parseBracketExpr
|
|
case tokenStar:
|
|
// do nothing - the recursive predicate is enough
|
|
return parseMatchExpr
|
|
}
|
|
|
|
case tokenDot:
|
|
// nested parse for '.'
|
|
tok := p.getToken()
|
|
switch tok.typ {
|
|
case tokenKey:
|
|
p.path.appendPath(newMatchKeyFn(tok.val))
|
|
return parseMatchExpr
|
|
case tokenStar:
|
|
p.path.appendPath(&matchAnyFn{})
|
|
return parseMatchExpr
|
|
}
|
|
|
|
case tokenLBracket:
|
|
return parseBracketExpr
|
|
|
|
case tokenEOF:
|
|
return nil // allow EOF at this stage
|
|
}
|
|
p.raiseError(tok, "expected match expression")
|
|
return nil
|
|
}
|
|
|
|
func parseBracketExpr(p *parser) parserStateFn {
|
|
if p.lookahead(tokenInteger, tokenColon) {
|
|
return parseSliceExpr
|
|
}
|
|
if p.peek().typ == tokenColon {
|
|
return parseSliceExpr
|
|
}
|
|
return parseUnionExpr
|
|
}
|
|
|
|
func parseUnionExpr(p *parser) parserStateFn {
|
|
var tok *token
|
|
|
|
// this state can be traversed after some sub-expressions
|
|
// so be careful when setting up state in the parser
|
|
if p.union == nil {
|
|
p.union = []PathFn{}
|
|
}
|
|
|
|
loop: // labeled loop for easy breaking
|
|
for {
|
|
if len(p.union) > 0 {
|
|
// parse delimiter or terminator
|
|
tok = p.getToken()
|
|
switch tok.typ {
|
|
case tokenComma:
|
|
// do nothing
|
|
case tokenRBracket:
|
|
break loop
|
|
default:
|
|
p.raiseError(tok, "expected ',' or ']', not '%s'", tok.val)
|
|
}
|
|
}
|
|
|
|
// parse sub expression
|
|
tok = p.getToken()
|
|
switch tok.typ {
|
|
case tokenInteger:
|
|
p.union = append(p.union, newMatchIndexFn(tok.Int()))
|
|
case tokenKey:
|
|
p.union = append(p.union, newMatchKeyFn(tok.val))
|
|
case tokenString:
|
|
p.union = append(p.union, newMatchKeyFn(tok.val))
|
|
case tokenQuestion:
|
|
return parseFilterExpr
|
|
case tokenLParen:
|
|
return parseScriptExpr
|
|
default:
|
|
p.raiseError(tok, "expected union sub expression, not '%s', %d", tok.val, len(p.union))
|
|
}
|
|
}
|
|
|
|
// if there is only one sub-expression, use that instead
|
|
if len(p.union) == 1 {
|
|
p.path.appendPath(p.union[0])
|
|
} else {
|
|
p.path.appendPath(&matchUnionFn{p.union})
|
|
}
|
|
|
|
p.union = nil // clear out state
|
|
return parseMatchExpr
|
|
}
|
|
|
|
func parseSliceExpr(p *parser) parserStateFn {
|
|
// init slice to grab all elements
|
|
start, end, step := 0, math.MaxInt64, 1
|
|
|
|
// parse optional start
|
|
tok := p.getToken()
|
|
if tok.typ == tokenInteger {
|
|
start = tok.Int()
|
|
tok = p.getToken()
|
|
}
|
|
if tok.typ != tokenColon {
|
|
p.raiseError(tok, "expected ':'")
|
|
}
|
|
|
|
// parse optional end
|
|
tok = p.getToken()
|
|
if tok.typ == tokenInteger {
|
|
end = tok.Int()
|
|
tok = p.getToken()
|
|
}
|
|
if tok.typ == tokenRBracket {
|
|
p.path.appendPath(newMatchSliceFn(start, end, step))
|
|
return parseMatchExpr
|
|
}
|
|
if tok.typ != tokenColon {
|
|
p.raiseError(tok, "expected ']' or ':'")
|
|
}
|
|
|
|
// parse optional step
|
|
tok = p.getToken()
|
|
if tok.typ == tokenInteger {
|
|
step = tok.Int()
|
|
if step < 0 {
|
|
p.raiseError(tok, "step must be a positive value")
|
|
}
|
|
tok = p.getToken()
|
|
}
|
|
if tok.typ != tokenRBracket {
|
|
p.raiseError(tok, "expected ']'")
|
|
}
|
|
|
|
p.path.appendPath(newMatchSliceFn(start, end, step))
|
|
return parseMatchExpr
|
|
}
|
|
|
|
func parseFilterExpr(p *parser) parserStateFn {
|
|
tok := p.getToken()
|
|
if tok.typ != tokenLParen {
|
|
p.raiseError(tok, "expected left-parenthesis for filter expression")
|
|
}
|
|
tok = p.getToken()
|
|
if tok.typ != tokenKey && tok.typ != tokenString {
|
|
p.raiseError(tok, "expected key or string for filter funciton name")
|
|
}
|
|
name := tok.val
|
|
tok = p.getToken()
|
|
if tok.typ != tokenRParen {
|
|
p.raiseError(tok, "expected right-parenthesis for filter expression")
|
|
}
|
|
p.union = append(p.union, newMatchFilterFn(name, tok.Position))
|
|
return parseUnionExpr
|
|
}
|
|
|
|
func parseScriptExpr(p *parser) parserStateFn {
|
|
tok := p.getToken()
|
|
if tok.typ != tokenKey && tok.typ != tokenString {
|
|
p.raiseError(tok, "expected key or string for script funciton name")
|
|
}
|
|
name := tok.val
|
|
tok = p.getToken()
|
|
if tok.typ != tokenRParen {
|
|
p.raiseError(tok, "expected right-parenthesis for script expression")
|
|
}
|
|
p.union = append(p.union, newMatchScriptFn(name, tok.Position))
|
|
return parseUnionExpr
|
|
}
|
|
|
|
func parse(flow chan token) *Query {
|
|
parser := &parser{
|
|
flow: flow,
|
|
tokensBuffer: []token{},
|
|
path: newQuery(),
|
|
}
|
|
parser.run()
|
|
return parser.path
|
|
}
|