Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement Chaining function #181

Merged
merged 3 commits into from
Apr 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 6 additions & 5 deletions ast/call_expression.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{}
Expand Down
21 changes: 20 additions & 1 deletion compiler.go
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down Expand Up @@ -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{}
Expand Down Expand Up @@ -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
}

Expand Down
15 changes: 15 additions & 0 deletions parser/parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand Down Expand Up @@ -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
Expand Down
95 changes: 95 additions & 0 deletions struct_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package plush_test
import (
"strings"
"testing"
"time"

"github.com/gobuffalo/plush/v4"
"github.com/stretchr/testify/require"
Expand Down Expand Up @@ -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)
}
Loading