Skip to content

Commit

Permalink
tpl/collections: Unwrap any interface value in sort and where
Browse files Browse the repository at this point in the history
Hugo `0.55.0` introduced some new interface types for `Page` etc.

This worked great in general, but there were cases where this would fail in `where` and `sort`.

One such example would be sorting by `MenuItem.Page.Date` where `Page` on `MenuItem` was a small subset of the bigger `page.Page` interface.

This commit fixes that by unwrapping such interface values.

Fixes #5989
  • Loading branch information
bep committed Jun 9, 2019
1 parent fad183c commit 8d898ad
Show file tree
Hide file tree
Showing 4 changed files with 153 additions and 22 deletions.
51 changes: 51 additions & 0 deletions hugolib/menu_test.go
Original file line number Original file line Diff line number Diff line change
Expand Up @@ -222,3 +222,54 @@ menu: "main"
b.AssertFileContent("public/index.html", "AMP and HTML|/blog/html-amp/|AMP only|/amp/blog/amp/|HTML only|/blog/html/|Home Sweet Home|/|") b.AssertFileContent("public/index.html", "AMP and HTML|/blog/html-amp/|AMP only|/amp/blog/amp/|HTML only|/blog/html/|Home Sweet Home|/|")
b.AssertFileContent("public/amp/index.html", "AMP and HTML|/amp/blog/html-amp/|AMP only|/amp/blog/amp/|HTML only|/blog/html/|Home Sweet Home|/amp/|") b.AssertFileContent("public/amp/index.html", "AMP and HTML|/amp/blog/html-amp/|AMP only|/amp/blog/amp/|HTML only|/blog/html/|Home Sweet Home|/amp/|")
} }

// https://github.com/gohugoio/hugo/issues/5989
func TestMenuPageSortByDate(t *testing.T) {

b := newTestSitesBuilder(t).WithSimpleConfigFile()

b.WithContent("blog/a.md", `
---
Title: A
date: 2019-01-01
menu:
main:
identifier: "a"
weight: 1
---
`)

b.WithContent("blog/b.md", `
---
Title: B
date: 2018-01-02
menu:
main:
parent: "a"
weight: 100
---
`)

b.WithContent("blog/c.md", `
---
Title: C
date: 2019-01-03
menu:
main:
parent: "a"
weight: 10
---
`)

b.WithTemplatesAdded("index.html", `{{ range .Site.Menus.main }}{{ .Title }}|Children:
{{- $children := sort .Children ".Page.Date" "desc" }}{{ range $children }}{{ .Title }}|{{ end }}{{ end }}
`)

b.Build(BuildCfg{})

b.AssertFileContent("public/index.html", "A|Children:C|B|")
}
31 changes: 31 additions & 0 deletions tpl/collections/collections_test.go
Original file line number Original file line Diff line number Diff line change
Expand Up @@ -819,6 +819,10 @@ func (x TstX) TstRv() string {
return "r" + x.B return "r" + x.B
} }


func (x TstX) TstRv2() string {
return "r" + x.B
}

func (x TstX) unexportedMethod() string { func (x TstX) unexportedMethod() string {
return x.unexported return x.unexported
} }
Expand Down Expand Up @@ -850,6 +854,33 @@ type TstX struct {
unexported string unexported string
} }


type TstXIHolder struct {
XI TstXI
}

// Partially implemented by the TstX struct.
type TstXI interface {
TstRv2() string
}

func ToTstXIs(slice interface{}) []TstXI {
s := reflect.ValueOf(slice)
if s.Kind() != reflect.Slice {
return nil
}
tis := make([]TstXI, s.Len())

for i := 0; i < s.Len(); i++ {
tsti, ok := s.Index(i).Interface().(TstXI)
if !ok {
return nil
}
tis[i] = tsti
}

return tis
}

func newDeps(cfg config.Provider) *deps.Deps { func newDeps(cfg config.Provider) *deps.Deps {
l := langs.NewLanguage("en", cfg) l := langs.NewLanguage("en", cfg)
l.Set("i18nDir", "i18n") l.Set("i18nDir", "i18n")
Expand Down
10 changes: 9 additions & 1 deletion tpl/collections/where.go
Original file line number Original file line Diff line number Diff line change
Expand Up @@ -280,8 +280,16 @@ func evaluateSubElem(obj reflect.Value, elemName string) (reflect.Value, error)
typ := obj.Type() typ := obj.Type()
obj, isNil := indirect(obj) obj, isNil := indirect(obj)


if obj.Kind() == reflect.Interface {
// If obj is an interface, we need to inspect the value it contains
// to see the full set of methods and fields.
// Indirect returns the value that it points to, which is what's needed
// below to be able to reflect on its fields.
obj = reflect.Indirect(obj.Elem())
}

// first, check whether obj has a method. In this case, obj is // first, check whether obj has a method. In this case, obj is
// an interface, a struct or its pointer. If obj is a struct, // a struct or its pointer. If obj is a struct,
// to check all T and *T method, use obj pointer type Value // to check all T and *T method, use obj pointer type Value
objPtr := obj objPtr := obj
if objPtr.Kind() != reflect.Interface && objPtr.CanAddr() { if objPtr.Kind() != reflect.Interface && objPtr.CanAddr() {
Expand Down
83 changes: 62 additions & 21 deletions tpl/collections/where_test.go
Original file line number Original file line Diff line number Diff line change
Expand Up @@ -38,13 +38,31 @@ func TestWhere(t *testing.T) {
d5 := d4.Add(1 * time.Hour) d5 := d4.Add(1 * time.Hour)
d6 := d5.Add(1 * time.Hour) d6 := d5.Add(1 * time.Hour)


for i, test := range []struct { type testt struct {
seq interface{} seq interface{}
key interface{} key interface{}
op string op string
match interface{} match interface{}
expect interface{} expect interface{}
}{ }

createTestVariants := func(test testt) []testt {
testVariants := []testt{test}
if islice := ToTstXIs(test.seq); islice != nil {
variant := test
variant.seq = islice
expect := ToTstXIs(test.expect)
if expect != nil {
variant.expect = expect
}
testVariants = append(testVariants, variant)
}

return testVariants

}

for i, test := range []testt{
{ {
seq: []map[int]string{ seq: []map[int]string{
{1: "a", 2: "m"}, {1: "c", 2: "d"}, {1: "e", 3: "m"}, {1: "a", 2: "m"}, {1: "c", 2: "d"}, {1: "e", 3: "m"},
Expand Down Expand Up @@ -216,6 +234,24 @@ func TestWhere(t *testing.T) {
{"foo": &TstX{A: "c", B: "d"}}, {"foo": &TstX{A: "c", B: "d"}},
}, },
}, },
{
seq: []TstXIHolder{
{&TstX{A: "a", B: "b"}}, {&TstX{A: "c", B: "d"}}, {&TstX{A: "e", B: "f"}},
},
key: "XI.TstRp", match: "rc",
expect: []TstXIHolder{
{&TstX{A: "c", B: "d"}},
},
},
{
seq: []TstXIHolder{
{&TstX{A: "a", B: "b"}}, {&TstX{A: "c", B: "d"}}, {&TstX{A: "e", B: "f"}},
},
key: "XI.A", match: "e",
expect: []TstXIHolder{
{&TstX{A: "e", B: "f"}},
},
},
{ {
seq: []map[string]Mid{ seq: []map[string]Mid{
{"foo": Mid{Tst: TstX{A: "a", B: "b"}}}, {"foo": Mid{Tst: TstX{A: "c", B: "d"}}}, {"foo": Mid{Tst: TstX{A: "e", B: "f"}}}, {"foo": Mid{Tst: TstX{A: "a", B: "b"}}}, {"foo": Mid{Tst: TstX{A: "c", B: "d"}}}, {"foo": Mid{Tst: TstX{A: "e", B: "f"}}},
Expand Down Expand Up @@ -522,28 +558,33 @@ func TestWhere(t *testing.T) {
}, },
}, },
} { } {
t.Run(fmt.Sprintf("test case %d for key %s", i, test.key), func(t *testing.T) {
var results interface{}
var err error


if len(test.op) > 0 { testVariants := createTestVariants(test)
results, err = ns.Where(test.seq, test.key, test.op, test.match) for j, test := range testVariants {
} else { name := fmt.Sprintf("[%d/%d] %T %s %s", i, j, test.seq, test.op, test.key)
results, err = ns.Where(test.seq, test.key, test.match) t.Run(name, func(t *testing.T) {
} var results interface{}
if b, ok := test.expect.(bool); ok && !b { var err error
if err == nil {
t.Errorf("[%d] Where didn't return an expected error", i) if len(test.op) > 0 {
} results, err = ns.Where(test.seq, test.key, test.op, test.match)
} else { } else {
if err != nil { results, err = ns.Where(test.seq, test.key, test.match)
t.Errorf("[%d] failed: %s", i, err)
} }
if !reflect.DeepEqual(results, test.expect) { if b, ok := test.expect.(bool); ok && !b {
t.Errorf("[%d] Where clause matching %v with %v, got %v but expected %v", i, test.key, test.match, results, test.expect) if err == nil {
t.Fatalf("[%d] Where didn't return an expected error", i)
}
} else {
if err != nil {
t.Fatalf("[%d] failed: %s", i, err)
}
if !reflect.DeepEqual(results, test.expect) {
t.Fatalf("Where clause matching %v with %v in seq %v (%T),\ngot\n%v (%T) but expected\n%v (%T)", test.key, test.match, test.seq, test.seq, results, results, test.expect, test.expect)
}
} }
} })
}) }
} }


var err error var err error
Expand Down

0 comments on commit 8d898ad

Please sign in to comment.