Skip to content

Commit

Permalink
Implement Chaining function
Browse files Browse the repository at this point in the history
  • Loading branch information
Mido-sys committed Apr 25, 2024
1 parent 4814270 commit c85e6aa
Show file tree
Hide file tree
Showing 4 changed files with 128 additions and 9 deletions.
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
28 changes: 24 additions & 4 deletions compiler.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package plush
import (
"bytes"
"fmt"
"log"

"github.com/gobuffalo/plush/v4/token"

Expand Down Expand Up @@ -398,7 +399,7 @@ func (c *compiler) evalIdentifier(node *ast.Identifier) (interface{}, error) {
if rv.Kind() == reflect.Ptr {
rv = rv.Elem()
}

log.Println("FUND ME NSNSNS")
if rv.Kind() != reflect.Struct {
return nil, fmt.Errorf("'%s' does not have a field or method named '%s' (%s)", node.Callee.String(), node.Value, node)
}
Expand Down Expand Up @@ -605,13 +606,11 @@ func (c *compiler) stringsOperator(l string, r interface{}, op string) (interfac

func (c *compiler) evalCallExpression(node *ast.CallExpression) (interface{}, error) {
var rv reflect.Value

if node.Callee != nil {
c, err := c.evalExpression(node.Callee)
if err != nil {
return nil, err
}

rc := reflect.ValueOf(c)
mname := node.Function.String()
if i, ok := node.Function.(*ast.Identifier); ok {
Expand All @@ -623,6 +622,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 All @@ -635,6 +637,7 @@ func (c *compiler) evalCallExpression(node *ast.CallExpression) (interface{}, er

return rc.Interface(), nil
}

} else {
f, err := c.evalExpression(node.Function)
if err != nil {
Expand All @@ -660,7 +663,7 @@ 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)
}

log.Println("NONANANA")
rtNumIn := rt.NumIn()
isVariadic := rt.IsVariadic()
args := []reflect.Value{}
Expand Down Expand Up @@ -800,6 +803,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
83 changes: 83 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,85 @@ 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)")
}

0 comments on commit c85e6aa

Please sign in to comment.