Commit c752bc6f authored by Brendan Burns's avatar Brendan Burns

Merge pull request #11738 from daizuozhuo/master

JSONPath template language
parents acc27683 949704af
/*
Copyright 2015 The Kubernetes Authors All rights reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
// package jsonpath is a template engine using jsonpath syntax,
// which can be seen at http://goessner.net/articles/JsonPath/.
// In addition, it has {range} {end} function to iterate list and slice.
package jsonpath
/*
Copyright 2015 The Kubernetes Authors All rights reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package jsonpath
import (
"bytes"
"encoding/json"
"testing"
)
type jsonpathTest struct {
name string
template string
input interface{}
expect string
}
func testJSONPath(tests []jsonpathTest, t *testing.T) {
for _, test := range tests {
j := New(test.name)
err := j.Parse(test.template)
if err != nil {
t.Errorf("in %s, parse %s error %v", test.name, test.template, err)
}
buf := new(bytes.Buffer)
err = j.Execute(buf, test.input)
if err != nil {
t.Errorf("in %s, execute error %v", test.name, err)
}
out := buf.String()
if out != test.expect {
t.Errorf(`in %s, expect to get "%s", got "%s"`, test.name, test.expect, out)
}
}
}
func testFailJSONPath(tests []jsonpathTest, t *testing.T) {
for _, test := range tests {
j := New(test.name)
err := j.Parse(test.template)
if err != nil {
t.Errorf("in %s, parse %s error %v", test.name, test.template, err)
}
buf := new(bytes.Buffer)
err = j.Execute(buf, test.input)
var out string
if err == nil {
out = "nil"
} else {
out = err.Error()
}
if out != test.expect {
t.Errorf("in %s, expect to get error %s, got %s", test.name, test.expect, out)
}
}
}
func TestStructInput(t *testing.T) {
type book struct {
Category string
Author string
Title string
Price float32
}
type bicycle struct {
Color string
Price float32
}
type store struct {
Book []book
Bicycle bicycle
Name string
Labels map[string]int
}
storeData := store{
Name: "jsonpath",
Book: []book{
{"reference", "Nigel Rees", "Sayings of the Centurey", 8.95},
{"fiction", "Evelyn Waugh", "Sword of Honour", 12.99},
{"fiction", "Herman Melville", "Moby Dick", 8.99},
},
Bicycle: bicycle{"red", 19.95},
Labels: map[string]int{
"engieer": 10,
"web/html": 15,
"k8s-app": 20,
},
}
storeTests := []jsonpathTest{
{"plain", "hello jsonpath", nil, "hello jsonpath"},
{"recursive", "{..}", []int{1, 2, 3}, "[1, 2, 3]"},
{"filter", "{[?(@<5)]}", []int{2, 6, 3, 7}, "2 3"},
{"quote", `{"{"}`, nil, "{"},
{"union", "{[1,3,4]}", []int{0, 1, 2, 3, 4}, "1 3 4"},
{"array", "{[0:2]}", []string{"Monday", "Tudesday"}, "Monday Tudesday"},
{"variable", "hello {.Name}", storeData, "hello jsonpath"},
{"dict/", "{.Labels.web/html}", storeData, "15"},
{"dict-", "{.Labels.k8s-app}", storeData, "20"},
{"nest", "{.Bicycle.Color}", storeData, "red"},
{"allarray", "{.Book[*].Author}", storeData, "Nigel Rees Evelyn Waugh Herman Melville"},
{"allfileds", "{.Bicycle.*}", storeData, "red 19.95"},
{"recurfileds", "{..Price}", storeData, "8.95 12.99 8.99 19.95"},
{"lastarray", "{.Book[-1:]}", storeData,
"{Category: fiction, Author: Herman Melville, Title: Moby Dick, Price: 8.99}"},
{"recurarray", "{..Book[2]}", storeData,
"{Category: fiction, Author: Herman Melville, Title: Moby Dick, Price: 8.99}"},
}
testJSONPath(storeTests, t)
failStoreTests := []jsonpathTest{
{"invalid identfier", "{hello}", storeData, "unrecongnized identifier hello"},
{"nonexistent field", "{.hello}", storeData, "hello is not found"},
{"invalid array", "{.Labels[0]}", storeData, "<map[string]int Value> is not array or slice"},
{"invalid filter operator", "{.Book[?(@.Price<>10)]}", storeData, "unrecognized filter operator <>"},
{"redundent end", "{range .Labels.*}{@}{end}{end}", storeData, "not in range, nothing to end"},
}
testFailJSONPath(failStoreTests, t)
}
func TestJSONInput(t *testing.T) {
var pointsJSON = []byte(`[
{"id": "i1", "x":4, "y":-5},
{"id": "i2", "x":-2, "y":-5, "z":1},
{"id": "i3", "x": 8, "y": 3 },
{"id": "i4", "x": -6, "y": -1 },
{"id": "i5", "x": 0, "y": 2, "z": 1 },
{"id": "i6", "x": 1, "y": 4 }
]`)
var pointsData interface{}
err := json.Unmarshal(pointsJSON, &pointsData)
if err != nil {
t.Error(err)
}
pointsTests := []jsonpathTest{
{"exists filter", "{[?(@.z)].id}", pointsData, "i2 i5"},
{"bracket key", "{[0]['id']}", pointsData, "i1"},
}
testJSONPath(pointsTests, t)
}
// TestKubenates tests some use cases from kubenates
func TestKubenates(t *testing.T) {
var input = []byte(`{
"kind": "List",
"items":[
{
"kind":"None",
"metadata":{"name":"127.0.0.1"},
"status":{
"capacity":{"cpu":"4"},
"addresses":[{"type": "LegacyHostIP", "address":"127.0.0.1"}]
}
},
{
"kind":"None",
"metadata":{"name":"127.0.0.2"},
"status":{
"capacity":{"cpu":"8"},
"addresses":[
{"type": "LegacyHostIP", "address":"127.0.0.2"},
{"type": "another", "address":"127.0.0.3"}
]
}
}
],
"users":[
{
"name": "myself",
"user": {}
},
{
"name": "e2e",
"user": {"username": "admin", "password": "secret"}
}
]
}`)
var nodesData interface{}
err := json.Unmarshal(input, &nodesData)
if err != nil {
t.Error(err)
}
nodesTests := []jsonpathTest{
{"range item", "{range .items[*]}{.metadata.name}, {end}{.kind}", nodesData, `127.0.0.1, 127.0.0.2, List`},
{"range addresss", "{.items[*].status.addresses[*].address}", nodesData,
`127.0.0.1 127.0.0.2 127.0.0.3`},
{"double range", "{range .items[*]}{range .status.addresses[*]}{.address}, {end}{end}", nodesData,
`127.0.0.1, 127.0.0.2, 127.0.0.3, `},
{"item name", "{.items[*].metadata.name}", nodesData, `127.0.0.1 127.0.0.2`},
{"union nodes capacity", "{.items[*]['metadata.name', 'status.capacity']}", nodesData,
`127.0.0.1 127.0.0.2 {cpu: 4} {cpu: 8}`},
{"range nodes capacity", "{range .items[*]}[{.metadata.name}, {.status.capacity}] {end}", nodesData,
`[127.0.0.1, {cpu: 4}] [127.0.0.2, {cpu: 8}] `},
{"user password", `{.users[?(@.name=="e2e")].user.password}`, nodesData, "secret"},
}
testJSONPath(nodesTests, t)
}
/*
Copyright 2015 The Kubernetes Authors All rights reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package jsonpath
import "fmt"
// NodeType identifies the type of a parse tree node.
type NodeType int
// Type returns itself and provides an easy default implementation
func (t NodeType) Type() NodeType {
return t
}
func (t NodeType) String() string {
return NodeTypeName[t]
}
const (
NodeText NodeType = iota
NodeArray
NodeList
NodeField
NodeIdentifier
NodeFilter
NodeInt
NodeFloat
NodeWildcard
NodeRecursive
NodeUnion
)
var NodeTypeName = map[NodeType]string{
NodeText: "NodeText",
NodeArray: "NodeArray",
NodeList: "NodeList",
NodeField: "NodeField",
NodeIdentifier: "NodeIdentifier",
NodeFilter: "NodeFilter",
NodeInt: "NodeInt",
NodeFloat: "NodeFloat",
NodeWildcard: "NodeWildcard",
NodeRecursive: "NodeRecursive",
NodeUnion: "NodeUnion",
}
type Node interface {
Type() NodeType
String() string
}
// ListNode holds a sequence of nodes.
type ListNode struct {
NodeType
Nodes []Node // The element nodes in lexical order.
}
func newList() *ListNode {
return &ListNode{NodeType: NodeList}
}
func (l *ListNode) append(n Node) {
l.Nodes = append(l.Nodes, n)
}
func (l *ListNode) String() string {
return fmt.Sprintf("%s", l.Type())
}
// TextNode holds plain text.
type TextNode struct {
NodeType
Text []byte // The text; may span newlines.
}
func newText(text string) *TextNode {
return &TextNode{NodeType: NodeText, Text: []byte(text)}
}
func (t *TextNode) String() string {
return fmt.Sprintf("%s: %s", t.Type(), t.Text)
}
// FieldNode holds filed of struct
type FieldNode struct {
NodeType
Value string
}
func newField(value string) *FieldNode {
return &FieldNode{NodeType: NodeField, Value: value}
}
func (f *FieldNode) String() string {
return fmt.Sprintf("%s: %s", f.Type(), f.Value)
}
// IdentifierNode holds an identifier
type IdentifierNode struct {
NodeType
Name string
}
func newIdentifier(value string) *IdentifierNode {
return &IdentifierNode{
NodeType: NodeIdentifier,
Name: value,
}
}
func (f *IdentifierNode) String() string {
return fmt.Sprintf("%s: %s", f.Type(), f.Name)
}
// ParamsEntry holds param information for ArrayNode
type ParamsEntry struct {
Value int
Known bool //whether the value is known when parse it
}
// ArrayNode holds start, end, step information for array index selection
type ArrayNode struct {
NodeType
Params [3]ParamsEntry //start, end, step
}
func newArray(params [3]ParamsEntry) *ArrayNode {
return &ArrayNode{
NodeType: NodeArray,
Params: params,
}
}
func (a *ArrayNode) String() string {
return fmt.Sprintf("%s: %v", a.Type(), a.Params)
}
// FilterNode holds operand and operator information for filter
type FilterNode struct {
NodeType
Left *ListNode
Right *ListNode
Operator string
}
func newFilter(left, right *ListNode, operator string) *FilterNode {
return &FilterNode{
NodeType: NodeFilter,
Left: left,
Right: right,
Operator: operator,
}
}
func (f *FilterNode) String() string {
return fmt.Sprintf("%s: %s %s %s", f.Type(), f.Left, f.Operator, f.Right)
}
// IntNode holds integer value
type IntNode struct {
NodeType
Value int
}
func newInt(num int) *IntNode {
return &IntNode{NodeType: NodeInt, Value: num}
}
func (i *IntNode) String() string {
return fmt.Sprintf("%s: %d", i.Type(), i.Value)
}
// FloatNode holds float value
type FloatNode struct {
NodeType
Value float64
}
func newFloat(num float64) *FloatNode {
return &FloatNode{NodeType: NodeFloat, Value: num}
}
func (i *FloatNode) String() string {
return fmt.Sprintf("%s: %f", i.Type(), i.Value)
}
// WildcardNode means a wildcard
type WildcardNode struct {
NodeType
}
func newWildcard() *WildcardNode {
return &WildcardNode{NodeType: NodeWildcard}
}
func (i *WildcardNode) String() string {
return fmt.Sprintf("%s", i.Type())
}
// RecursiveNode means a recursive descent operator
type RecursiveNode struct {
NodeType
}
func newRecursive() *RecursiveNode {
return &RecursiveNode{NodeType: NodeRecursive}
}
func (r *RecursiveNode) String() string {
return fmt.Sprintf("%s", r.Type())
}
// UnionNode is union of ListNode
type UnionNode struct {
NodeType
Nodes []*ListNode
}
func newUnion(nodes []*ListNode) *UnionNode {
return &UnionNode{NodeType: NodeUnion, Nodes: nodes}
}
func (u *UnionNode) String() string {
return fmt.Sprintf("%s", u.Type())
}
/*
Copyright 2015 The Kubernetes Authors All rights reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package jsonpath
import (
"fmt"
"regexp"
"strconv"
"strings"
"unicode"
"unicode/utf8"
)
const eof = -1
const (
leftDelim = "{"
rightDelim = "}"
)
type Parser struct {
Name string
Root *ListNode
input string
cur *ListNode
pos int
start int
width int
}
// Parse parsed the given text and return a node Parser.
// If an error is encountered, parsing stops and an empty
// Parser is returned with the error
func Parse(name, text string) (*Parser, error) {
p := NewParser(name)
err := p.Parse(text)
if err != nil {
p = nil
}
return p, err
}
func NewParser(name string) *Parser {
return &Parser{
Name: name,
}
}
// parseAction parsed the expression inside delimiter
func parseAction(name, text string) (*Parser, error) {
p, err := Parse(name, fmt.Sprintf("%s%s%s", leftDelim, text, rightDelim))
p.Root = p.Root.Nodes[0].(*ListNode)
return p, err
}
func (p *Parser) Parse(text string) error {
p.input = text
p.Root = newList()
p.pos = 0
return p.parseText(p.Root)
}
// consumeText return the parsed text since last cosumeText
func (p *Parser) consumeText() string {
value := p.input[p.start:p.pos]
p.start = p.pos
return value
}
// next returns the next rune in the input.
func (p *Parser) next() rune {
if int(p.pos) >= len(p.input) {
p.width = 0
return eof
}
r, w := utf8.DecodeRuneInString(p.input[p.pos:])
p.width = w
p.pos += p.width
return r
}
// peek returns but does not consume the next rune in the input.
func (p *Parser) peek() rune {
r := p.next()
p.backup()
return r
}
// backup steps back one rune. Can only be called once per call of next.
func (p *Parser) backup() {
p.pos -= p.width
}
func (p *Parser) parseText(cur *ListNode) error {
for {
if strings.HasPrefix(p.input[p.pos:], leftDelim) {
if p.pos > p.start {
cur.append(newText(p.consumeText()))
}
return p.parseLeftDelim(cur)
}
if p.next() == eof {
break
}
}
// Correctly reached EOF.
if p.pos > p.start {
cur.append(newText(p.consumeText()))
}
return nil
}
// parseLeftDelim scans the left delimiter, which is known to be present.
func (p *Parser) parseLeftDelim(cur *ListNode) error {
p.pos += len(leftDelim)
p.consumeText()
newNode := newList()
cur.append(newNode)
cur = newNode
return p.parseInsideAction(cur)
}
func (p *Parser) parseInsideAction(cur *ListNode) error {
prefixMap := map[string]func(*ListNode) error{
rightDelim: p.parseRightDelim,
"[?(": p.parseFilter,
"..": p.parseRecursive,
}
for prefix, parseFunc := range prefixMap {
if strings.HasPrefix(p.input[p.pos:], prefix) {
return parseFunc(cur)
}
}
switch r := p.next(); {
case r == eof || isEndOfLine(r):
return fmt.Errorf("unclosed action")
case r == ' ':
p.consumeText()
case r == '@': //the current object, just pass it
p.consumeText()
case r == '[':
return p.parseArray(cur)
case r == '"':
return p.parseQuote(cur)
case r == '.':
return p.parseField(cur)
case r == '+' || r == '-' || unicode.IsDigit(r):
p.backup()
return p.parseNumber(cur)
case isAlphaNumeric(r):
p.backup()
return p.parseIdentifier(cur)
default:
return fmt.Errorf("unrecognized charactor in action: %#U", r)
}
return p.parseInsideAction(cur)
}
// parseRightDelim scans the right delimiter, which is known to be present.
func (p *Parser) parseRightDelim(cur *ListNode) error {
p.pos += len(rightDelim)
p.consumeText()
cur = p.Root
return p.parseText(cur)
}
// parseIdentifier scans build-in keywords, like "range" "end"
func (p *Parser) parseIdentifier(cur *ListNode) error {
var r rune
for {
r = p.next()
if isTerminator(r) {
p.backup()
break
}
}
value := p.consumeText()
cur.append(newIdentifier(value))
return p.parseInsideAction(cur)
}
// parseRecursive scans the recursive desent operator ..
func (p *Parser) parseRecursive(cur *ListNode) error {
p.pos += len("..")
p.consumeText()
cur.append(newRecursive())
if r := p.peek(); isAlphaNumeric(r) {
return p.parseField(cur)
}
return p.parseInsideAction(cur)
}
// parseNumber scans number
func (p *Parser) parseNumber(cur *ListNode) error {
r := p.peek()
if r == '+' || r == '-' {
r = p.next()
}
for {
r = p.next()
if r != '.' && !unicode.IsDigit(r) {
p.backup()
break
}
}
value := p.consumeText()
i, err := strconv.Atoi(value)
if err == nil {
cur.append(newInt(i))
return p.parseInsideAction(cur)
}
d, err := strconv.ParseFloat(value, 64)
if err == nil {
cur.append(newFloat(d))
return p.parseInsideAction(cur)
}
return fmt.Errorf("cannot parse number %s", value)
}
// parseArray scans array index selection
func (p *Parser) parseArray(cur *ListNode) error {
Loop:
for {
switch p.next() {
case eof, '\n':
return fmt.Errorf("unterminated array")
case ']':
break Loop
}
}
text := p.consumeText()
text = string(text[1 : len(text)-1])
if text == "*" {
text = ":"
}
//union operator
strs := strings.Split(text, ",")
if len(strs) > 1 {
union := []*ListNode{}
for _, str := range strs {
parser, err := parseAction("union", fmt.Sprintf("[%s]", strings.Trim(str, " ")))
if err != nil {
return err
}
union = append(union, parser.Root)
}
cur.append(newUnion(union))
return p.parseInsideAction(cur)
}
// dict key
reg := regexp.MustCompile(`^'([^']*)'$`)
value := reg.FindStringSubmatch(text)
if value != nil {
parser, err := parseAction("arraydict", fmt.Sprintf(".%s", value[1]))
if err != nil {
return err
}
for _, node := range parser.Root.Nodes {
cur.append(node)
}
return p.parseInsideAction(cur)
}
//slice operator
reg = regexp.MustCompile(`^(-?[\d]*)(:-?[\d]*)?(:[\d]*)?$`)
value = reg.FindStringSubmatch(text)
if value == nil {
return fmt.Errorf("invalid array index %s", text)
}
value = value[1:]
params := [3]ParamsEntry{}
for i := 0; i < 3; i++ {
if value[i] != "" {
if i > 0 {
value[i] = value[i][1:]
}
if i > 0 && value[i] == "" {
params[i].Known = false
} else {
var err error
params[i].Known = true
params[i].Value, err = strconv.Atoi(value[i])
if err != nil {
return fmt.Errorf("array index %s is not a number", params[i].Value)
}
}
} else {
if i == 1 {
params[i].Known = true
params[i].Value = params[0].Value + 1
} else {
params[i].Known = false
params[i].Value = 0
}
}
}
cur.append(newArray(params))
return p.parseInsideAction(cur)
}
// parseFilter scans filter inside array selection
func (p *Parser) parseFilter(cur *ListNode) error {
p.pos += len("[?(")
p.consumeText()
Loop:
for {
switch p.next() {
case eof, '\n':
return fmt.Errorf("unterminated filter")
case ')':
break Loop
}
}
if p.next() != ']' {
return fmt.Errorf("unclosed array expect ]")
}
reg := regexp.MustCompile(`^([^!<>=]+)([!<>=]+)(.+?)$`)
text := p.consumeText()
text = string(text[:len(text)-2])
value := reg.FindStringSubmatch(text)
if value == nil {
parser, err := parseAction("text", text)
if err != nil {
return err
}
cur.append(newFilter(parser.Root, newList(), "exists"))
} else {
leftParser, err := parseAction("left", value[1])
if err != nil {
return err
}
rightParser, err := parseAction("right", value[3])
if err != nil {
return err
}
cur.append(newFilter(leftParser.Root, rightParser.Root, value[2]))
}
return p.parseInsideAction(cur)
}
// parseQuote scans array index selection
func (p *Parser) parseQuote(cur *ListNode) error {
Loop:
for {
switch p.next() {
case eof, '\n':
return fmt.Errorf("unterminated quoted string")
case '"':
break Loop
}
}
value := p.consumeText()
cur.append(newText(value[1 : len(value)-1]))
return p.parseInsideAction(cur)
}
// parseField scans a field until a terminator
func (p *Parser) parseField(cur *ListNode) error {
p.consumeText()
var r rune
for {
r = p.next()
if isTerminator(r) {
p.backup()
break
}
}
value := p.consumeText()
if value == "*" {
cur.append(newWildcard())
} else {
cur.append(newField(value))
}
return p.parseInsideAction(cur)
}
// isTerminator reports whether the input is at valid termination character to appear after an identifier.
func isTerminator(r rune) bool {
if isSpace(r) || isEndOfLine(r) {
return true
}
switch r {
case eof, '.', ',', '[', ']', '$', '@', '{', '}':
return true
}
return false
}
// isSpace reports whether r is a space character.
func isSpace(r rune) bool {
return r == ' ' || r == '\t'
}
// isEndOfLine reports whether r is an end-of-line character.
func isEndOfLine(r rune) bool {
return r == '\r' || r == '\n'
}
// isAlphaNumeric reports whether r is an alphabetic, digit, or underscore.
func isAlphaNumeric(r rune) bool {
return r == '_' || unicode.IsLetter(r) || unicode.IsDigit(r)
}
/*
Copyright 2015 The Kubernetes Authors All rights reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package jsonpath
import (
"testing"
)
type parserTest struct {
name string
text string
nodes []Node
}
var parserTests = []parserTest{
{"plain", `hello jsonpath`, []Node{newText("hello jsonpath")}},
{"variable", `hello {.jsonpath}`,
[]Node{newText("hello "), newList(), newField("jsonpath")}},
{"arrayfiled", `hello {['jsonpath']}`,
[]Node{newText("hello "), newList(), newField("jsonpath")}},
{"quote", `{"{"}`, []Node{newList(), newText("{")}},
{"array", `{[1:3]}`, []Node{newList(),
newArray([3]ParamsEntry{{1, true}, {3, true}, {0, false}})}},
{"allarray", `{.book[*].author}`,
[]Node{newList(), newField("book"),
newArray([3]ParamsEntry{{0, false}, {0, false}, {0, false}}), newField("author")}},
{"wildcard", `{.bicycle.*}`,
[]Node{newList(), newField("bicycle"), newWildcard()}},
{"filter", `{[?(@.price<3)]}`,
[]Node{newList(), newFilter(newList(), newList(), "<"),
newList(), newField("price"), newList(), newInt(3)}},
{"recursive", `{..}`, []Node{newList(), newRecursive()}},
{"recurField", `{..price}`,
[]Node{newList(), newRecursive(), newField("price")}},
{"arraydict", `{['book.price']}`, []Node{newList(),
newField("book"), newField("price"),
}},
{"union", `{['bicycle.price', 3, 'book.price']}`, []Node{newList(), newUnion([]*ListNode{}),
newList(), newField("bicycle"), newField("price"),
newList(), newArray([3]ParamsEntry{{3, true}, {4, true}, {0, false}}),
newList(), newField("book"), newField("price"),
}},
{"range", `{range .items}{.name},{end}`, []Node{
newList(), newIdentifier("range"), newField("items"),
newList(), newField("name"), newText(","),
newList(), newIdentifier("end"),
}},
}
func collectNode(nodes []Node, cur Node) []Node {
nodes = append(nodes, cur)
switch cur.Type() {
case NodeList:
for _, node := range cur.(*ListNode).Nodes {
nodes = collectNode(nodes, node)
}
case NodeFilter:
nodes = collectNode(nodes, cur.(*FilterNode).Left)
nodes = collectNode(nodes, cur.(*FilterNode).Right)
case NodeUnion:
for _, node := range cur.(*UnionNode).Nodes {
nodes = collectNode(nodes, node)
}
}
return nodes
}
func TestParser(t *testing.T) {
for _, test := range parserTests {
parser, err := Parse(test.name, test.text)
if err != nil {
t.Errorf("parse %s error %v", test.name, err)
}
result := collectNode([]Node{}, parser.Root)[1:]
if len(result) != len(test.nodes) {
t.Errorf("in %s, expect to get %d nodes, got %d nodes", test.name, len(test.nodes), len(result))
t.Error(result)
}
for i, expect := range test.nodes {
if result[i].String() != expect.String() {
t.Errorf("in %s, %dth node, expect %v, got %v", test.name, i, expect, result[i])
}
}
}
}
type failParserTest struct {
name string
text string
err string
}
func TestFailParser(t *testing.T) {
failParserTests := []failParserTest{
{"unclosed action", "{.hello", "unclosed action"},
{"unrecognized charactor", "{*}", "unrecognized charactor in action: U+002A '*'"},
{"invalid number", "{+12.3.0}", "cannot parse number +12.3.0"},
{"unterminated array", "{[1}", "unterminated array"},
{"invalid index", "{[::-1]}", "invalid array index ::-1"},
{"unterminated filter", "{[?(.price]}", "unterminated filter"},
}
for _, test := range failParserTests {
_, err := Parse(test.name, test.text)
var out string
if err == nil {
out = "nil"
} else {
out = err.Error()
}
if out != test.err {
t.Errorf("in %s, expect to get error %v, got %v", test.name, test.err, out)
}
}
}
package template
import (
"reflect"
"fmt"
)
var (
errorType = reflect.TypeOf((*error)(nil)).Elem()
fmtStringerType = reflect.TypeOf((*fmt.Stringer)(nil)).Elem()
)
// indirect returns the item at the end of indirection, and a bool to indicate if it's nil.
// We indirect through pointers and empty interfaces (only) because
// non-empty interfaces have methods we might need.
func indirect(v reflect.Value) (rv reflect.Value, isNil bool) {
for ; v.Kind() == reflect.Ptr || v.Kind() == reflect.Interface; v = v.Elem() {
if v.IsNil() {
return v, true
}
if v.Kind() == reflect.Interface && v.NumMethod() > 0 {
break
}
}
return v, false
}
// printableValue returns the, possibly indirected, interface value inside v that
// is best for a call to formatted printer.
func printableValue(v reflect.Value) (interface{}, bool) {
if v.Kind() == reflect.Ptr {
v, _ = indirect(v) // fmt.Fprint handles nil.
}
if !v.IsValid() {
return "<no value>", true
}
if !v.Type().Implements(errorType) && !v.Type().Implements(fmtStringerType) {
if v.CanAddr() && (reflect.PtrTo(v.Type()).Implements(errorType) || reflect.PtrTo(v.Type()).Implements(fmtStringerType)) {
v = v.Addr()
} else {
switch v.Kind() {
case reflect.Chan, reflect.Func:
return nil, false
}
}
}
return v.Interface(), true
}
// canBeNil reports whether an untyped nil can be assigned to the type. See reflect.Zero.
func canBeNil(typ reflect.Type) bool {
switch typ.Kind() {
case reflect.Chan, reflect.Func, reflect.Interface, reflect.Map, reflect.Ptr, reflect.Slice:
return true
}
return false
}
// isTrue reports whether the value is 'true', in the sense of not the zero of its type,
// and whether the value has a meaningful truth value.
func isTrue(val reflect.Value) (truth, ok bool) {
if !val.IsValid() {
// Something like var x interface{}, never set. It's a form of nil.
return false, true
}
switch val.Kind() {
case reflect.Array, reflect.Map, reflect.Slice, reflect.String:
truth = val.Len() > 0
case reflect.Bool:
truth = val.Bool()
case reflect.Complex64, reflect.Complex128:
truth = val.Complex() != 0
case reflect.Chan, reflect.Func, reflect.Ptr, reflect.Interface:
truth = !val.IsNil()
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
truth = val.Int() != 0
case reflect.Float32, reflect.Float64:
truth = val.Float() != 0
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
truth = val.Uint() != 0
case reflect.Struct:
truth = true // Struct values are always true.
default:
return
}
return truth, true
}
\ No newline at end of file
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment