diff --git a/README.md b/README.md index 2cadd9d..d4a26d1 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/evaluator.go b/evaluator.go new file mode 100644 index 0000000..eb96e85 --- /dev/null +++ b/evaluator.go @@ -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 +} diff --git a/evaluator_test.go b/evaluator_test.go new file mode 100644 index 0000000..dc041f4 --- /dev/null +++ b/evaluator_test.go @@ -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) + }) + } +} diff --git a/expression_parser.y b/expression_parser.y index 1542df0..a14da32 100644 --- a/expression_parser.y +++ b/expression_parser.y @@ -1,16 +1,21 @@ %{ package main import ( - _ "fmt" + "fmt" "reflect" ) + +func init() { + _ = fmt.Sprint("") +} + %} %union { name string val interface{} f func(Context) interface{} } -%type expr expr2 +%type expr %token LITERAL %token 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 - diff --git a/expression_parser_test.go b/expression_parser_test.go index 243d87e..2d0aed9 100644 --- a/expression_parser_test.go +++ b/expression_parser_test.go @@ -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)) -} diff --git a/render.go b/render.go index 8876f13..168a20d 100644 --- a/render.go +++ b/render.go @@ -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 { diff --git a/scanner.go b/scanner.go index c7484a8..9c7ca7a 100644 --- a/scanner.go +++ b/scanner.go @@ -303,7 +303,7 @@ _eof_trans: if err != nil { panic(err) } - out.val = n + out.val = int(n) ( lex.p)++; goto _out } diff --git a/scanner.rl b/scanner.rl index adba43b..6f81d22 100644 --- a/scanner.rl +++ b/scanner.rl @@ -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 { diff --git a/y.go b/y.go index ad7c685..d722559 100644 --- a/y.go +++ b/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 } } }