From 0490afb2fc54162924ada3b1093b477831d97c18 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B8rn=20Erik=20Pedersen?= Date: Sun, 8 Dec 2019 11:22:17 +0100 Subject: [PATCH] Dot --- common/hreflect/helpers.go | 13 ++- common/hreflect/invoke.go | 140 +++++++++++++++++++++++ common/hreflect/invoke_test.go | 105 +++++++++++++++++ tpl/compare/truth.go | 17 ++- tpl/tplimpl/template_ast_transformers.go | 4 +- 5 files changed, 273 insertions(+), 6 deletions(-) create mode 100644 common/hreflect/invoke.go create mode 100644 common/hreflect/invoke_test.go diff --git a/common/hreflect/helpers.go b/common/hreflect/helpers.go index db7b208b5b6..eace5b3d353 100644 --- a/common/hreflect/helpers.go +++ b/common/hreflect/helpers.go @@ -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 @@ -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 @@ -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 +} diff --git a/common/hreflect/invoke.go b/common/hreflect/invoke.go new file mode 100644 index 00000000000..4bbe0c01476 --- /dev/null +++ b/common/hreflect/invoke.go @@ -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 +} diff --git a/common/hreflect/invoke_test.go b/common/hreflect/invoke_test.go new file mode 100644 index 00000000000..4867691f7c1 --- /dev/null +++ b/common/hreflect/invoke_test.go @@ -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) + }) + } +} diff --git a/tpl/compare/truth.go b/tpl/compare/truth.go index bec78980e2a..2277afbce42 100644 --- a/tpl/compare/truth.go +++ b/tpl/compare/truth.go @@ -17,8 +17,10 @@ package compare import ( - "fmt" "reflect" + "strings" + + "github.com/spf13/cast" "github.com/gohugoio/hugo/common/hreflect" ) @@ -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 diff --git a/tpl/tplimpl/template_ast_transformers.go b/tpl/tplimpl/template_ast_transformers.go index 9d7978d31f8..66ede0cb1d4 100644 --- a/tpl/tplimpl/template_ast_transformers.go +++ b/tpl/tplimpl/template_ast_transformers.go @@ -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 }