From: satotake <doublequotation@gmail.com>
Date: Mon, 9 Mar 2020 12:32:38 +0000 (+0900)
Subject: Support unComparable args of uniq/complement/in
X-Git-Tag: v0.67.0~8
X-Git-Url: http://git.maquefel.me/?a=commitdiff_plain;h=8279d2e2271ee64725133d36a12d1d7e2158bffd;p=brevno-suite%2Fhugo

Support unComparable args of uniq/complement/in

Fixes #6105
---

diff --git a/tpl/collections/collections.go b/tpl/collections/collections.go
index 80f4ccc0..d9046702 100644
--- a/tpl/collections/collections.go
+++ b/tpl/collections/collections.go
@@ -271,18 +271,13 @@ func (ns *Namespace) In(l interface{}, v interface{}) (bool, error) {
 	lv := reflect.ValueOf(l)
 	vv := reflect.ValueOf(v)
 
-	if !vv.Type().Comparable() {
-		return false, errors.Errorf("value to check must be comparable: %T", v)
-	}
-
-	// Normalize numeric types to float64 etc.
 	vvk := normalize(vv)
 
 	switch lv.Kind() {
 	case reflect.Array, reflect.Slice:
 		for i := 0; i < lv.Len(); i++ {
 			lvv, isNil := indirectInterface(lv.Index(i))
-			if isNil || !lvv.Type().Comparable() {
+			if isNil {
 				continue
 			}
 
@@ -713,6 +708,7 @@ func (ns *Namespace) Uniq(seq interface{}) (interface{}, error) {
 	switch v.Kind() {
 	case reflect.Slice:
 		slice = reflect.MakeSlice(v.Type(), 0, 0)
+
 	case reflect.Array:
 		slice = reflect.MakeSlice(reflect.SliceOf(v.Type().Elem()), 0, 0)
 	default:
@@ -720,12 +716,12 @@ func (ns *Namespace) Uniq(seq interface{}) (interface{}, error) {
 	}
 
 	seen := make(map[interface{}]bool)
+
 	for i := 0; i < v.Len(); i++ {
 		ev, _ := indirectInterface(v.Index(i))
-		if !ev.Type().Comparable() {
-			return nil, errors.New("elements must be comparable")
-		}
+
 		key := normalize(ev)
+
 		if _, found := seen[key]; !found {
 			slice = reflect.Append(slice, ev)
 			seen[key] = true
diff --git a/tpl/collections/collections_test.go b/tpl/collections/collections_test.go
index 24d3b051..21c8bfb5 100644
--- a/tpl/collections/collections_test.go
+++ b/tpl/collections/collections_test.go
@@ -348,6 +348,9 @@ func TestIn(t *testing.T) {
 		// template.HTML
 		{template.HTML("this substring should be found"), "substring", true},
 		{template.HTML("this substring should not be found"), "subseastring", false},
+		// Uncomparable, use hashstructure
+		{[]string{"a", "b"}, []string{"a", "b"}, false},
+		{[][]string{{"a", "b"}}, []string{"a", "b"}, true},
 	} {
 
 		errMsg := qt.Commentf("[%d] %v", i, test)
@@ -356,10 +359,6 @@ func TestIn(t *testing.T) {
 		c.Assert(err, qt.IsNil)
 		c.Assert(result, qt.Equals, test.expect, errMsg)
 	}
-
-	// Slices are not comparable
-	_, err := ns.In([]string{"a", "b"}, []string{"a", "b"})
-	c.Assert(err, qt.Not(qt.IsNil))
 }
 
 type testPage struct {
@@ -835,9 +834,14 @@ func TestUniq(t *testing.T) {
 		// Structs
 		{pagesVals{p3v, p2v, p3v, p2v}, pagesVals{p3v, p2v}, false},
 
+		// not Comparable(), use hashstruscture
+		{[]map[string]int{
+			{"K1": 1}, {"K2": 2}, {"K1": 1}, {"K2": 1},
+		}, []map[string]int{
+			{"K1": 1}, {"K2": 2}, {"K2": 1},
+		}, false},
+
 		// should fail
-		// uncomparable types
-		{[]map[string]int{{"K1": 1}}, []map[string]int{{"K2": 2}, {"K2": 2}}, true},
 		{1, 1, true},
 		{"foo", "fo", true},
 	} {
diff --git a/tpl/collections/complement.go b/tpl/collections/complement.go
index a5633f8b..4dc5e3bf 100644
--- a/tpl/collections/complement.go
+++ b/tpl/collections/complement.go
@@ -44,9 +44,6 @@ func (ns *Namespace) Complement(seqs ...interface{}) (interface{}, error) {
 		sl := reflect.MakeSlice(v.Type(), 0, 0)
 		for i := 0; i < v.Len(); i++ {
 			ev, _ := indirectInterface(v.Index(i))
-			if !ev.Type().Comparable() {
-				return nil, errors.New("elements in complement must be comparable")
-			}
 			if _, found := aset[normalize(ev)]; !found {
 				sl = reflect.Append(sl, ev)
 			}
diff --git a/tpl/collections/complement_test.go b/tpl/collections/complement_test.go
index abe572b6..d0e27353 100644
--- a/tpl/collections/complement_test.go
+++ b/tpl/collections/complement_test.go
@@ -65,7 +65,10 @@ func TestComplement(t *testing.T) {
 		{[]string{"a", "b", "c"}, []interface{}{"error"}, false},
 		{"error", []interface{}{[]string{"c", "d"}, []string{"a", "b"}}, false},
 		{[]string{"a", "b", "c"}, []interface{}{[][]string{{"c", "d"}}}, false},
-		{[]interface{}{[][]string{{"c", "d"}}}, []interface{}{[]string{"c", "d"}, []string{"a", "b"}}, false},
+		{
+			[]interface{}{[][]string{{"c", "d"}}}, []interface{}{[]string{"c", "d"}, []string{"a", "b"}},
+			[]interface{}{[][]string{{"c", "d"}}},
+		},
 	} {
 
 		errMsg := qt.Commentf("[%d]", i)
diff --git a/tpl/collections/reflect_helpers.go b/tpl/collections/reflect_helpers.go
index 69425fcb..3d73b70e 100644
--- a/tpl/collections/reflect_helpers.go
+++ b/tpl/collections/reflect_helpers.go
@@ -18,6 +18,7 @@ import (
 	"reflect"
 	"time"
 
+	"github.com/mitchellh/hashstructure"
 	"github.com/pkg/errors"
 )
 
@@ -42,18 +43,25 @@ func numberToFloat(v reflect.Value) (float64, error) {
 	}
 }
 
-// normalizes different numeric types to make them comparable.
+// normalizes different numeric types if isNumber
+// or get the hash values if not Comparable (such as map or struct)
+// to make them comparable
 func normalize(v reflect.Value) interface{} {
 	k := v.Kind()
 
 	switch {
+	case !v.Type().Comparable():
+		h, err := hashstructure.Hash(v.Interface(), nil)
+		if err != nil {
+			panic(err)
+		}
+		return h
 	case isNumber(k):
 		f, err := numberToFloat(v)
 		if err == nil {
 			return f
 		}
 	}
-
 	return v.Interface()
 }
 
diff --git a/tpl/collections/symdiff.go b/tpl/collections/symdiff.go
index 1c58257e..85a2076a 100644
--- a/tpl/collections/symdiff.go
+++ b/tpl/collections/symdiff.go
@@ -48,10 +48,8 @@ func (ns *Namespace) SymDiff(s2, s1 interface{}) (interface{}, error) {
 
 			for i := 0; i < v.Len(); i++ {
 				ev, _ := indirectInterface(v.Index(i))
-				if !ev.Type().Comparable() {
-					return nil, errors.New("symdiff: elements must be comparable")
-				}
 				key := normalize(ev)
+
 				// Append if the key is not in their intersection.
 				if ids1[key] != ids2[key] {
 					v, err := convertValue(ev, sliceElemType)