mirror of
https://github.com/danog/liquid.git
synced 2024-11-27 03:24:39 +01:00
Expression evaluator tests; fix revealed bugs
This commit is contained in:
parent
555991ca1d
commit
1f805d5a24
@ -60,3 +60,7 @@ ragel -Z scanner.rl && go test -run TestExpressionParser
|
||||
## Attribution
|
||||
|
||||
Michael Hamrah's [Lexing with Ragel and Parsing with Yacc using Go](https://medium.com/@mhamrah/lexing-with-ragel-and-parsing-with-yacc-using-go-81e50475f88f) was essential to understanding `go yacc`.
|
||||
|
||||
## License
|
||||
|
||||
MIT License
|
||||
|
16
evaluator.go
Normal file
16
evaluator.go
Normal file
@ -0,0 +1,16 @@
|
||||
package main
|
||||
|
||||
import "fmt"
|
||||
|
||||
type Expression struct {
|
||||
value func(Context) (interface{}, error)
|
||||
}
|
||||
|
||||
func EvaluateExpr(expr string, ctx Context) (interface{}, error) {
|
||||
lexer := newLexer([]byte(expr + ";"))
|
||||
n := yyParse(lexer)
|
||||
if n != 0 {
|
||||
return nil, fmt.Errorf("parse error in %s", expr)
|
||||
}
|
||||
return lexer.val(ctx), nil
|
||||
}
|
45
evaluator_test.go
Normal file
45
evaluator_test.go
Normal file
@ -0,0 +1,45 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
var evaluatorTestContext = Context{map[string]interface{}{
|
||||
"n": 123,
|
||||
"ar": []string{"first", "second", "third"},
|
||||
"obj": map[string]interface{}{
|
||||
"a": "first",
|
||||
"b": map[string]interface{}{"c": "d"},
|
||||
"c": []string{"r", "g", "b"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
var evaluatorTests = []struct {
|
||||
in string
|
||||
expected interface{}
|
||||
}{
|
||||
{"12", 12},
|
||||
{"n", 123},
|
||||
{"obj.a", "first"},
|
||||
{"obj.b.c", "d"},
|
||||
{"obj.x", nil},
|
||||
{"ar[1]", "second"},
|
||||
{"ar[-1]", nil},
|
||||
{"ar[100]", nil},
|
||||
{"obj[1]", nil},
|
||||
{"obj.c[0]", "r"},
|
||||
}
|
||||
|
||||
func TestEvaluator(t *testing.T) {
|
||||
for i, test := range evaluatorTests {
|
||||
t.Run(fmt.Sprint(i), func(t *testing.T) {
|
||||
val, err := EvaluateExpr(test.in, evaluatorTestContext)
|
||||
require.NoErrorf(t, err, test.in)
|
||||
require.Equalf(t, test.expected, val, test.in)
|
||||
})
|
||||
}
|
||||
}
|
@ -1,16 +1,21 @@
|
||||
%{
|
||||
package main
|
||||
import (
|
||||
_ "fmt"
|
||||
"fmt"
|
||||
"reflect"
|
||||
)
|
||||
|
||||
func init() {
|
||||
_ = fmt.Sprint("")
|
||||
}
|
||||
|
||||
%}
|
||||
%union {
|
||||
name string
|
||||
val interface{}
|
||||
f func(Context) interface{}
|
||||
}
|
||||
%type <f> expr expr2
|
||||
%type <f> expr
|
||||
%token <val> LITERAL
|
||||
%token <name> IDENTIFIER RELATION
|
||||
%left '.'
|
||||
@ -23,17 +28,19 @@ expr:
|
||||
| expr '.' IDENTIFIER {
|
||||
e, attr := $1, $3
|
||||
$$ = func(ctx Context) interface{} {
|
||||
input := e(ctx)
|
||||
ref := reflect.ValueOf(input)
|
||||
obj := e(ctx)
|
||||
ref := reflect.ValueOf(obj)
|
||||
switch ref.Kind() {
|
||||
case reflect.Map:
|
||||
return ref.MapIndex(reflect.ValueOf(attr)).Interface()
|
||||
default:
|
||||
return nil
|
||||
val := ref.MapIndex(reflect.ValueOf(attr))
|
||||
if val.Kind()!= reflect.Invalid {
|
||||
return val.Interface()
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
| expr '[' expr2 ']' {
|
||||
| expr '[' expr ']' {
|
||||
e, i := $1, $3
|
||||
$$ = func(ctx Context) interface{} {
|
||||
ref := reflect.ValueOf(e(ctx))
|
||||
@ -47,15 +54,9 @@ expr:
|
||||
return ref.Index(n).Interface()
|
||||
}
|
||||
}
|
||||
return nil
|
||||
case reflect.Map:
|
||||
return ref.MapIndex(reflect.ValueOf(index)).Interface()
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
;
|
||||
|
||||
expr2: expr
|
||||
|
||||
|
@ -33,18 +33,3 @@ func TestExpressionScanner(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
require.Len(t, tokens, 3)
|
||||
}
|
||||
|
||||
func TestExpressionParser(t *testing.T) {
|
||||
ctx := Context{map[string]interface{}{
|
||||
"abc": 123,
|
||||
}}
|
||||
lexer := newLexer([]byte(`12;`))
|
||||
n := yyParse(lexer)
|
||||
require.Zero(t, n)
|
||||
require.Equal(t, float64(12), lexer.val(ctx))
|
||||
|
||||
lexer = newLexer([]byte(`abc;`))
|
||||
n = yyParse(lexer)
|
||||
require.Zero(t, n)
|
||||
require.Equal(t, 123, lexer.val(ctx))
|
||||
}
|
||||
|
@ -28,15 +28,6 @@ func (n *ASTText) Render(w io.Writer, _ Context) error {
|
||||
return err
|
||||
}
|
||||
|
||||
func EvaluateExpr(expr string, ctx Context) (interface{}, error) {
|
||||
lexer := newLexer([]byte(expr + ";"))
|
||||
n := yyParse(lexer)
|
||||
if n != 0 {
|
||||
return nil, fmt.Errorf("parse error in %s", expr)
|
||||
}
|
||||
return lexer.val(ctx), nil
|
||||
}
|
||||
|
||||
func writeASTs(w io.Writer, seq []AST, ctx Context) error {
|
||||
for _, n := range seq {
|
||||
if err := n.Render(w, ctx); err != nil {
|
||||
|
@ -303,7 +303,7 @@ _eof_trans:
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
out.val = n
|
||||
out.val = int(n)
|
||||
( lex.p)++; goto _out
|
||||
|
||||
}
|
||||
|
@ -53,7 +53,7 @@ func (lex *lexer) Lex(out *yySymType) int {
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
out.val = n
|
||||
out.val = int(n)
|
||||
fbreak;
|
||||
}
|
||||
action Float {
|
||||
|
58
y.go
58
y.go
@ -5,11 +5,15 @@ import __yyfmt__ "fmt"
|
||||
|
||||
//line expression_parser.y:2
|
||||
import (
|
||||
_ "fmt"
|
||||
"fmt"
|
||||
"reflect"
|
||||
)
|
||||
|
||||
//line expression_parser.y:8
|
||||
func init() {
|
||||
_ = fmt.Sprint("")
|
||||
}
|
||||
|
||||
//line expression_parser.y:13
|
||||
type yySymType struct {
|
||||
yys int
|
||||
name string
|
||||
@ -48,39 +52,39 @@ var yyExca = [...]int{
|
||||
|
||||
const yyPrivate = 57344
|
||||
|
||||
const yyLast = 15
|
||||
const yyLast = 12
|
||||
|
||||
var yyAct = [...]int{
|
||||
|
||||
6, 5, 7, 6, 11, 7, 2, 3, 4, 8,
|
||||
1, 9, 0, 0, 10,
|
||||
6, 2, 7, 10, 6, 5, 7, 3, 4, 9,
|
||||
8, 1,
|
||||
}
|
||||
var yyPact = [...]int{
|
||||
|
||||
3, -1000, -7, -1000, -1000, -1000, 4, 3, -1000, -6,
|
||||
-4, -1000,
|
||||
3, -1000, -3, -1000, -1000, -1000, 5, 3, -1000, -7,
|
||||
-1000,
|
||||
}
|
||||
var yyPgo = [...]int{
|
||||
|
||||
0, 6, 11, 10,
|
||||
0, 1, 11,
|
||||
}
|
||||
var yyR1 = [...]int{
|
||||
|
||||
0, 3, 1, 1, 1, 1, 2,
|
||||
0, 2, 1, 1, 1, 1,
|
||||
}
|
||||
var yyR2 = [...]int{
|
||||
|
||||
0, 2, 1, 1, 3, 4, 1,
|
||||
0, 2, 1, 1, 3, 4,
|
||||
}
|
||||
var yyChk = [...]int{
|
||||
|
||||
-1000, -3, -1, 4, 5, 8, 7, 9, 5, -2,
|
||||
-1, 10,
|
||||
-1000, -2, -1, 4, 5, 8, 7, 9, 5, -1,
|
||||
10,
|
||||
}
|
||||
var yyDef = [...]int{
|
||||
|
||||
0, -2, 0, 2, 3, 1, 0, 0, 4, 0,
|
||||
6, 5,
|
||||
5,
|
||||
}
|
||||
var yyTok1 = [...]int{
|
||||
|
||||
@ -442,43 +446,45 @@ yydefault:
|
||||
|
||||
case 1:
|
||||
yyDollar = yyS[yypt-2 : yypt+1]
|
||||
//line expression_parser.y:18
|
||||
//line expression_parser.y:23
|
||||
{
|
||||
yylex.(*lexer).val = yyDollar[1].f
|
||||
}
|
||||
case 2:
|
||||
yyDollar = yyS[yypt-1 : yypt+1]
|
||||
//line expression_parser.y:21
|
||||
//line expression_parser.y:26
|
||||
{
|
||||
val := yyDollar[1].val
|
||||
yyVAL.f = func(_ Context) interface{} { return val }
|
||||
}
|
||||
case 3:
|
||||
yyDollar = yyS[yypt-1 : yypt+1]
|
||||
//line expression_parser.y:22
|
||||
//line expression_parser.y:27
|
||||
{
|
||||
name := yyDollar[1].name
|
||||
yyVAL.f = func(ctx Context) interface{} { return ctx.Variables[name] }
|
||||
}
|
||||
case 4:
|
||||
yyDollar = yyS[yypt-3 : yypt+1]
|
||||
//line expression_parser.y:23
|
||||
//line expression_parser.y:28
|
||||
{
|
||||
e, attr := yyDollar[1].f, yyDollar[3].name
|
||||
yyVAL.f = func(ctx Context) interface{} {
|
||||
input := e(ctx)
|
||||
ref := reflect.ValueOf(input)
|
||||
obj := e(ctx)
|
||||
ref := reflect.ValueOf(obj)
|
||||
switch ref.Kind() {
|
||||
case reflect.Map:
|
||||
return ref.MapIndex(reflect.ValueOf(attr)).Interface()
|
||||
default:
|
||||
return nil
|
||||
val := ref.MapIndex(reflect.ValueOf(attr))
|
||||
if val.Kind() != reflect.Invalid {
|
||||
return val.Interface()
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
case 5:
|
||||
yyDollar = yyS[yypt-4 : yypt+1]
|
||||
//line expression_parser.y:36
|
||||
//line expression_parser.y:43
|
||||
{
|
||||
e, i := yyDollar[1].f, yyDollar[3].f
|
||||
yyVAL.f = func(ctx Context) interface{} {
|
||||
@ -493,12 +499,8 @@ yydefault:
|
||||
return ref.Index(n).Interface()
|
||||
}
|
||||
}
|
||||
return nil
|
||||
case reflect.Map:
|
||||
return ref.MapIndex(reflect.ValueOf(index)).Interface()
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user