Skip to content

Commit

Permalink
Dot
Browse files Browse the repository at this point in the history
  • Loading branch information
bep committed Dec 8, 2019
1 parent 0fdaff9 commit 0490afb
Show file tree
Hide file tree
Showing 5 changed files with 273 additions and 6 deletions.
13 changes: 12 additions & 1 deletion common/hreflect/helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ func IsTruthful(in interface{}) bool {
}

var zeroType = reflect.TypeOf((*types.Zeroer)(nil)).Elem()
var zero reflect.Value

// IsTruthfulValue returns whether the given value has a meaningful truth value.
// This is based on template.IsTrue in Go's stdlib, but also considers
Expand Down Expand Up @@ -79,7 +80,8 @@ func IsTruthfulValue(val reflect.Value) (truth bool) {
return
}

// Based on: https://github.com/golang/go/blob/178a2c42254166cffed1b25fb1d3c7a5727cada6/src/text/template/exec.go#L931
// Indirect funcs below borrowed from Go:
// https://github.com/golang/go/blob/178a2c42254166cffed1b25fb1d3c7a5727cada6/src/text/template/exec.go#L931
func indirectInterface(v reflect.Value) reflect.Value {
if v.Kind() != reflect.Interface {
return v
Expand All @@ -89,3 +91,12 @@ func indirectInterface(v reflect.Value) reflect.Value {
}
return v.Elem()
}

func indirect(v reflect.Value) (rv reflect.Value, isNil bool) {
for ; v.Kind() == reflect.Ptr || v.Kind() == reflect.Interface; v = v.Elem() {
if v.IsNil() {
return v, true
}
}
return v, false
}
140 changes: 140 additions & 0 deletions common/hreflect/invoke.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
// Copyright 2019 The Hugo Authors. All rights reserved.
// Some functions in this file (see comments) is based on the Go source code,
// copyright The Go Authors and governed by a BSD-style license.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package hreflect

import (
"fmt"
"reflect"

"github.com/gohugoio/hugo/common/maps"

"github.com/pkg/errors"
)

var (
errorType = reflect.TypeOf((*error)(nil)).Elem()
fmtStringerType = reflect.TypeOf((*fmt.Stringer)(nil)).Elem()
reflectValueType = reflect.TypeOf((*reflect.Value)(nil)).Elem()
)

func Invoke(dot interface{}, path []string, args ...interface{}) (interface{}, error) {
v := reflect.ValueOf(dot)
result, err := invoke(v, path, args)
if err != nil {
return nil, err
}

if !result.IsValid() {
return nil, nil
}

return result.Interface(), nil

}

func argsToValues(args []interface{}) []reflect.Value {
// TODO1 varargs
if len(args) == 0 {
return nil
}
argsv := make([]reflect.Value, len(args))
for i, v := range args {
argsv[i] = reflect.ValueOf(v)
}
return argsv
}

func invoke(receiver reflect.Value, path []string, args []interface{}) (reflect.Value, error) {
if len(path) == 0 {
return receiver, nil
}
name := path[0]
typ := receiver.Type()
receiver, isNil := indirect(receiver)
if receiver.Kind() == reflect.Interface && isNil {
return err("nil pointer evaluating %s.%s", typ, name)
}

ptr := receiver
if ptr.Kind() != reflect.Interface && ptr.Kind() != reflect.Ptr && ptr.CanAddr() {
ptr = ptr.Addr()
}

if m := ptr.MethodByName(name); m.IsValid() {
mt := m.Type()
if !isValidFunc(mt) {
return err("method %s not valid", name)
}

var argsv []reflect.Value
if len(path) == 1 {
numArgs := len(args)
if mt.IsVariadic() {
if numArgs < (mt.NumIn() - 1) {
return err("methods %s expects at leas %d arguments, got %d", name, mt.NumIn()-1, numArgs)
}
} else if numArgs != mt.NumIn() {
return err("methods %s takes %d arguments, got %d", name, mt.NumIn(), numArgs)
}
argsv = argsToValues(args)
}

result := m.Call(argsv)
if mt.NumOut() == 2 {
if !result[1].IsZero() {
return reflect.Value{}, result[1].Interface().(error)
}
}

return invoke(result[0], path[1:], args)
}

switch receiver.Kind() {
case reflect.Struct:
if f := receiver.FieldByName(name); f.IsValid() {
return invoke(f, path[1:], args)
} else {
return err("no method or field with name %s found", name)
}
case reflect.Map:
if p, ok := receiver.Interface().(maps.Params); ok {
// Do case insensitive map lookup
v := p.Get(path...)
return reflect.ValueOf(v), nil

}
v := receiver.MapIndex(reflect.ValueOf(name))
if !v.IsValid() {
return reflect.Value{}, nil
}
return invoke(v, path[1:], args)
}
return receiver, nil
}

func err(s string, args ...interface{}) (reflect.Value, error) {
return reflect.Value{}, errors.Errorf(s, args...)
}

func isValidFunc(typ reflect.Type) bool {
switch {
case typ.NumOut() == 1:
return true
case typ.NumOut() == 2 && typ.Out(1) == errorType:
return true
}
return false
}
105 changes: 105 additions & 0 deletions common/hreflect/invoke_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
// Copyright 2019 The Hugo Authors. All rights reserved.
// Some functions in this file (see comments) is based on the Go source code,
// copyright The Go Authors and governed by a BSD-style license.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package hreflect

import (
"testing"

"github.com/gohugoio/hugo/common/maps"

"github.com/pkg/errors"

qt "github.com/frankban/quicktest"
)

type testStruct struct {
Val string
Struct testStruct2
err error
}

type testStruct2 struct {
Val2 string
err error
}

func (t testStruct) GetStruct() testStruct2 {
return t.Struct
}

func (t testStruct) GetVal() string {
return t.Val
}

func (t testStruct) OneArg(arg string) string {
return arg
}

func (t *testStruct) GetValP() string {
return t.Val
}

func (t testStruct) GetValError() (string, error) {
return t.Val, t.err
}

func (t testStruct2) GetVal2() string {
return t.Val2
}

func (t testStruct2) GetVal2Error() (string, error) {
return t.Val2, t.err
}

func TestInvoke(t *testing.T) {
c := qt.New(t)

hello := testStruct{Val: "hello"}

for _, test := range []struct {
name string
dot interface{}
path []string
args []interface{}
expect interface{}
}{
{"Method", hello, []string{"GetVal"}, nil, "hello"},
{"Method one arg", hello, []string{"OneArg"}, []interface{}{"hello"}, "hello"},
{"Method pointer 1", &testStruct{Val: "hello"}, []string{"GetValP"}, nil, "hello"},
{"Method pointer 2", &testStruct{Val: "hello"}, []string{"GetVal"}, nil, "hello"},
{"Method error", testStruct{Val: "hello", err: errors.New("This failed")}, []string{"GetValError"}, nil, false},
{"Method error nil", hello, []string{"GetValError"}, nil, "hello"},
{"Field", hello, []string{"Val"}, nil, "hello"},
{"Field pointer receiver", &testStruct{Val: "hello"}, []string{"Val"}, nil, "hello"},
{"Method nested", testStruct{Val: "hello", Struct: testStruct2{Val2: "hello2"}}, []string{"GetStruct", "GetVal2"}, nil, "hello2"},
{"Field nested", testStruct{Val: "hello", Struct: testStruct2{Val2: "hello2"}}, []string{"Struct", "Val2"}, nil, "hello2"},
{"Method field nested", testStruct{Val: "hello", Struct: testStruct2{Val2: "hello2"}}, []string{"GetStruct", "Val2"}, nil, "hello2"},
{"Method nested error", testStruct{Val: "hello", Struct: testStruct2{Val2: "hello2", err: errors.New("This failed")}}, []string{"GetStruct", "GetVal2Error"}, nil, false},
{"Map", map[string]string{"hello": "world"}, []string{"hello"}, nil, "world"},
{"Map not found", map[string]string{"hello": "world"}, []string{"Hugo"}, nil, nil}, // TODO1 nil type
{"Map nested", map[string]map[string]string{"hugo": map[string]string{"does": "rock"}}, []string{"hugo", "does"}, nil, "rock"},
{"Params", maps.Params{"hello": "world"}, []string{"Hello"}, nil, "world"},
} {
c.Run(test.name, func(c *qt.C) {
got, err := Invoke(test.dot, test.path, test.args...)
if b, ok := test.expect.(bool); ok && !b {
c.Assert(err, qt.Not(qt.IsNil))
return
}
c.Assert(got, qt.DeepEquals, test.expect)
})
}
}
17 changes: 13 additions & 4 deletions tpl/compare/truth.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,10 @@
package compare

import (
"fmt"
"reflect"
"strings"

"github.com/spf13/cast"

"github.com/gohugoio/hugo/common/hreflect"
)
Expand All @@ -38,9 +40,16 @@ func (*Namespace) getIf(arg reflect.Value) reflect.Value {
return reflect.ValueOf("")
}

func (*Namespace) invokeDot(args ...interface{}) interface{} {
fmt.Println("invokeDot:", args)
return "FOO"
func (*Namespace) invokeDot(in ...interface{}) (interface{}, error) {
dot := in[0]
path := strings.Split(cast.ToString(in[1])[1:], ".")

var args []interface{}
if len(in) > 2 {
args = in[2:]
}

return hreflect.Invoke(dot, path, args...)
}

// And computes the Boolean AND of its arguments, returning
Expand Down
4 changes: 3 additions & 1 deletion tpl/tplimpl/template_ast_transformers.go
Original file line number Diff line number Diff line change
Expand Up @@ -237,10 +237,12 @@ func (c *templateContext) wrapDot(n *parse.CommandNode) bool {
sn.Text = fields

args := wrapper.Args[:3]
if len(n.Args) > 2 {
if len(n.Args) > 1 {
args = append(args, n.Args[1:]...)
}

n.Args = args

return true

}
Expand Down

0 comments on commit 0490afb

Please sign in to comment.