diff --git a/ast/call_expression.go b/ast/call_expression.go index 76c0eae..c940d0b 100644 --- a/ast/call_expression.go +++ b/ast/call_expression.go @@ -7,11 +7,12 @@ import ( type CallExpression struct { TokenAble - Callee Expression - Function Expression - Arguments []Expression - Block *BlockStatement - ElseBlock *BlockStatement + Callee Expression + ChainCallee Expression + Function Expression + Arguments []Expression + Block *BlockStatement + ElseBlock *BlockStatement } var _ Comparable = &CallExpression{} diff --git a/compiler.go b/compiler.go index bec2b57..e1167ad 100644 --- a/compiler.go +++ b/compiler.go @@ -623,6 +623,9 @@ func (c *compiler) evalCallExpression(node *ast.CallExpression) (interface{}, er ptr := reflect.New(reflect.TypeOf(c)) ptr.Elem().Set(rc) rv = ptr.MethodByName(mname) + if !rv.IsValid() { + return nil, fmt.Errorf("'%s' does not have a method named '%s' (%s.%s)", node.Callee.String(), mname, node.Callee.String(), mname) + } } if !rv.IsValid() { @@ -660,7 +663,6 @@ func (c *compiler) evalCallExpression(node *ast.CallExpression) (interface{}, er if rt.Kind() != reflect.Func { return nil, fmt.Errorf("%+v (%T) is an invalid function", node.String(), rt) } - rtNumIn := rt.NumIn() isVariadic := rt.IsVariadic() args := []reflect.Value{} @@ -800,6 +802,23 @@ func (c *compiler) evalCallExpression(node *ast.CallExpression) (interface{}, er if e, ok := res[len(res)-1].Interface().(error); ok { return nil, fmt.Errorf("could not call %s function: %w", node.Function, e) } + if node.ChainCallee != nil { + octx := c.ctx.(*Context) + defer func() { + c.ctx = octx + }() + + c.ctx = octx.New() + for k, v := range octx.data { + c.ctx.Set(k, v) + } + c.ctx.Set(node.Function.String(), res[0].Interface()) + vvs, err := c.evalExpression(node.ChainCallee) + if err != nil { + return nil, err + } + return vvs, err + } return res[0].Interface(), nil } diff --git a/parser/parser.go b/parser/parser.go index 066b5aa..b72051e 100644 --- a/parser/parser.go +++ b/parser/parser.go @@ -670,6 +670,18 @@ func (p *parser) parseCallExpression(function ast.Expression) ast.Expression { exp.Block = p.parseBlockStatement() } + if p.peekTokenIs(token.DOT) { + calleeIdent := &ast.Identifier{Value: exp.Function.String()} + p.nextToken() + p.nextToken() + parseExp := p.parseExpression(LOWEST) + + exp.ChainCallee = p.assignCallee(parseExp, calleeIdent) + if exp.ChainCallee == nil { + return nil + } + } + return exp } @@ -749,6 +761,9 @@ func (p *parser) assignCallee(exp ast.Expression, calleeIdent *ast.Identifier) ( msg := fmt.Sprintf("line %d: syntax error: invalid nested index access, expected an identifier %v", p.curToken.LineNumber, ss) p.errors = append(p.errors, msg) } + case *ast.CallExpression: + ss.Callee = calleeIdent + assignedCallee = ss case *ast.Identifier: ss.OriginalCallee.Callee = calleeIdent assignedCallee = ss diff --git a/struct_test.go b/struct_test.go index b29e207..a85ff1e 100644 --- a/struct_test.go +++ b/struct_test.go @@ -3,6 +3,7 @@ package plush_test import ( "strings" "testing" + "time" "github.com/gobuffalo/plush/v4" "github.com/stretchr/testify/require" @@ -461,3 +462,97 @@ func Test_Render_Struct_Nested_Map_Access(t *testing.T) { r.NoError(err) r.Equal("John Dolittle", res) } + +type person struct { + likes []string + hates []string + born time.Time +} + +func (a person) GetAge() time.Duration { + return time.Since(a.born) +} + +func (a person) GetBorn() time.Time { + return a.born +} + +func (a person) Hates() []string { + return a.hates +} +func (a person) Likes() []string { + return a.likes +} + +func Test_Render_Struct_With_ChainingFunction_ArrayAccess(t *testing.T) { + r := require.New(t) + + tt := person{likes: []string{"pringles", "galaxy", "carrot cake", "world pendant", "gold braclet"}, + hates: []string{"boiled eggs", "coconut"}} + input := `<%= nour.Likes()[0] %>` + ctx := plush.NewContext() + ctx.Set("nour", tt) + res, err := plush.Render(input, ctx) + r.NoError(err) + r.Equal("pringles", res) +} + +func Test_Render_Struct_With_ChainingFunction_ArrayAccess_Outofbound(t *testing.T) { + r := require.New(t) + + tt := person{likes: []string{"pringles", "galaxy", "carrot cake", "world pendant", "gold bracelet"}, + hates: []string{"boiled eggs", "coconut"}} + input := `<%= nour.Hates()[30] %>` + ctx := plush.NewContext() + ctx.Set("nour", tt) + _, err := plush.Render(input, ctx) + r.Error(err) +} + +func Test_Render_Struct_With_ChainingFunction_FunctionCall(t *testing.T) { + r := require.New(t) + + tt := person{born: time.Date(2024, time.January, 11, 0, 0, 0, 0, time.UTC).AddDate(-31, 0, 0)} + input := `<%= nour.GetBorn().Format("Jan 2, 2006") %>` + ctx := plush.NewContext() + ctx.Set("nour", tt) + res, err := plush.Render(input, ctx) + r.NoError(err) + r.Equal("Jan 11, 1993", res) +} + +func Test_Render_Struct_With_ChainingFunction_UndefinedStructProperty(t *testing.T) { + r := require.New(t) + + tt := person{born: time.Now()} + input := `<%= nour.GetBorn().TEST %>` + ctx := plush.NewContext() + ctx.Set("nour", tt) + _, err := plush.Render(input, ctx) + r.Error(err) + +} + +func Test_Render_Struct_With_ChainingFunction_InvalidFunctionCall(t *testing.T) { + r := require.New(t) + + tt := person{born: time.Now()} + input := `<%= nour.GetBorn().TEST("Jan 2, 2006") %>` + ctx := plush.NewContext() + ctx.Set("nour", tt) + _, err := plush.Render(input, ctx) + r.Error(err) + r.Contains(err.Error(), "'nour.GetBorn' does not have a method named 'TEST' (nour.GetBorn.TEST)") +} + +func Test_Render_Function_on_Invalid_Function_Struct(t *testing.T) { + r := require.New(t) + ctx := plush.NewContext() + bender := Robot{ + Avatar: Avatar("bender.jpg"), + } + ctx.Set("robot", bender) + input := `<%= robot.Avatar.URL2() %>` + _, err := plush.Render(input, ctx) + r.Error(err) +}