1
0
mirror of https://github.com/danog/liquid.git synced 2024-11-27 01:24:40 +01:00

Expression evaluator tests; fix revealed bugs

This commit is contained in:
Oliver Steele 2017-06-26 08:23:50 -04:00
parent 555991ca1d
commit 1f805d5a24
9 changed files with 113 additions and 69 deletions

View File

@ -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
View 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
View 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)
})
}
}

View File

@ -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

View File

@ -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))
}

View File

@ -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 {

View File

@ -303,7 +303,7 @@ _eof_trans:
if err != nil {
panic(err)
}
out.val = n
out.val = int(n)
( lex.p)++; goto _out
}

View File

@ -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
View File

@ -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
}
}
}