diff options
Diffstat (limited to 'common/collections')
-rw-r--r-- | common/collections/append.go | 113 | ||||
-rw-r--r-- | common/collections/append_test.go | 75 | ||||
-rw-r--r-- | common/collections/collections.go | 21 | ||||
-rw-r--r-- | common/collections/slice.go | 66 | ||||
-rw-r--r-- | common/collections/slice_test.go | 124 |
5 files changed, 399 insertions, 0 deletions
diff --git a/common/collections/append.go b/common/collections/append.go new file mode 100644 index 000000000..b56455bc9 --- /dev/null +++ b/common/collections/append.go @@ -0,0 +1,113 @@ +// Copyright 2019 The Hugo Authors. All rights reserved. +// +// 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 collections + +import ( + "fmt" + "reflect" +) + +// Append appends from to a slice to and returns the resulting slice. +// If length of from is one and the only element is a slice of same type as to, +// it will be appended. +func Append(to interface{}, from ...interface{}) (interface{}, error) { + tov, toIsNil := indirect(reflect.ValueOf(to)) + + toIsNil = toIsNil || to == nil + var tot reflect.Type + + if !toIsNil { + if tov.Kind() != reflect.Slice { + return nil, fmt.Errorf("expected a slice, got %T", to) + } + + tot = tov.Type().Elem() + toIsNil = tov.Len() == 0 + + if len(from) == 1 { + fromv := reflect.ValueOf(from[0]) + if fromv.Kind() == reflect.Slice { + if toIsNil { + // If we get nil []string, we just return the []string + return from[0], nil + } + + fromt := reflect.TypeOf(from[0]).Elem() + + // If we get []string []string, we append the from slice to to + if tot == fromt { + return reflect.AppendSlice(tov, fromv).Interface(), nil + } else if !fromt.AssignableTo(tot) { + // Fall back to a []interface{} slice. + return appendToInterfaceSliceFromValues(tov, fromv) + + } + } + } + } + + if toIsNil { + return Slice(from...), nil + } + + for _, f := range from { + fv := reflect.ValueOf(f) + if !fv.Type().AssignableTo(tot) { + // Fall back to a []interface{} slice. + tov, _ := indirect(reflect.ValueOf(to)) + return appendToInterfaceSlice(tov, from...) + } + tov = reflect.Append(tov, fv) + } + + return tov.Interface(), nil +} + +func appendToInterfaceSliceFromValues(slice1, slice2 reflect.Value) ([]interface{}, error) { + var tos []interface{} + + for _, slice := range []reflect.Value{slice1, slice2} { + for i := 0; i < slice.Len(); i++ { + tos = append(tos, slice.Index(i).Interface()) + } + } + + return tos, nil +} + +func appendToInterfaceSlice(tov reflect.Value, from ...interface{}) ([]interface{}, error) { + var tos []interface{} + + for i := 0; i < tov.Len(); i++ { + tos = append(tos, tov.Index(i).Interface()) + } + + tos = append(tos, from...) + + return tos, nil +} + +// indirect is borrowed from the Go stdlib: 'text/template/exec.go' +// TODO(bep) consolidate +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 + } + if v.Kind() == reflect.Interface && v.NumMethod() > 0 { + break + } + } + return v, false +} diff --git a/common/collections/append_test.go b/common/collections/append_test.go new file mode 100644 index 000000000..4086570b8 --- /dev/null +++ b/common/collections/append_test.go @@ -0,0 +1,75 @@ +// Copyright 2018 The Hugo Authors. All rights reserved. +// +// 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 collections + +import ( + "html/template" + "testing" + + qt "github.com/frankban/quicktest" +) + +func TestAppend(t *testing.T) { + t.Parallel() + c := qt.New(t) + + for _, test := range []struct { + start interface{} + addend []interface{} + expected interface{} + }{ + {[]string{"a", "b"}, []interface{}{"c"}, []string{"a", "b", "c"}}, + {[]string{"a", "b"}, []interface{}{"c", "d", "e"}, []string{"a", "b", "c", "d", "e"}}, + {[]string{"a", "b"}, []interface{}{[]string{"c", "d", "e"}}, []string{"a", "b", "c", "d", "e"}}, + {[]string{"a"}, []interface{}{"b", template.HTML("c")}, []interface{}{"a", "b", template.HTML("c")}}, + {nil, []interface{}{"a", "b"}, []string{"a", "b"}}, + {nil, []interface{}{nil}, []interface{}{nil}}, + {[]interface{}{}, []interface{}{[]string{"c", "d", "e"}}, []string{"c", "d", "e"}}, + {tstSlicers{&tstSlicer{"a"}, &tstSlicer{"b"}}, + []interface{}{&tstSlicer{"c"}}, + tstSlicers{&tstSlicer{"a"}, &tstSlicer{"b"}, &tstSlicer{"c"}}}, + {&tstSlicers{&tstSlicer{"a"}, &tstSlicer{"b"}}, + []interface{}{&tstSlicer{"c"}}, + tstSlicers{&tstSlicer{"a"}, + &tstSlicer{"b"}, + &tstSlicer{"c"}}}, + {testSlicerInterfaces{&tstSlicerIn1{"a"}, &tstSlicerIn1{"b"}}, + []interface{}{&tstSlicerIn1{"c"}}, + testSlicerInterfaces{&tstSlicerIn1{"a"}, &tstSlicerIn1{"b"}, &tstSlicerIn1{"c"}}}, + //https://github.com/gohugoio/hugo/issues/5361 + {[]string{"a", "b"}, []interface{}{tstSlicers{&tstSlicer{"a"}, &tstSlicer{"b"}}}, + []interface{}{"a", "b", &tstSlicer{"a"}, &tstSlicer{"b"}}}, + {[]string{"a", "b"}, []interface{}{&tstSlicer{"a"}}, + []interface{}{"a", "b", &tstSlicer{"a"}}}, + // Errors + {"", []interface{}{[]string{"a", "b"}}, false}, + // No string concatenation. + {"ab", + []interface{}{"c"}, + false}, + } { + + result, err := Append(test.start, test.addend...) + + if b, ok := test.expected.(bool); ok && !b { + + c.Assert(err, qt.Not(qt.IsNil)) + continue + } + + c.Assert(err, qt.IsNil) + c.Assert(result, qt.DeepEquals, test.expected) + } + +} diff --git a/common/collections/collections.go b/common/collections/collections.go new file mode 100644 index 000000000..bb47c8acc --- /dev/null +++ b/common/collections/collections.go @@ -0,0 +1,21 @@ +// Copyright 2018 The Hugo Authors. All rights reserved. +// +// 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 collections contains common Hugo functionality related to collection +// handling. +package collections + +// Grouper defines a very generic way to group items by a given key. +type Grouper interface { + Group(key interface{}, items interface{}) (interface{}, error) +} diff --git a/common/collections/slice.go b/common/collections/slice.go new file mode 100644 index 000000000..38ca86b08 --- /dev/null +++ b/common/collections/slice.go @@ -0,0 +1,66 @@ +// Copyright 2018 The Hugo Authors. All rights reserved. +// +// 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 collections + +import ( + "reflect" +) + +// Slicer defines a very generic way to create a typed slice. This is used +// in collections.Slice template func to get types such as Pages, PageGroups etc. +// instead of the less useful []interface{}. +type Slicer interface { + Slice(items interface{}) (interface{}, error) +} + +// Slice returns a slice of all passed arguments. +func Slice(args ...interface{}) interface{} { + if len(args) == 0 { + return args + } + + first := args[0] + firstType := reflect.TypeOf(first) + + if firstType == nil { + return args + } + + if g, ok := first.(Slicer); ok { + v, err := g.Slice(args) + if err == nil { + return v + } + + // If Slice fails, the items are not of the same type and + // []interface{} is the best we can do. + return args + } + + if len(args) > 1 { + // This can be a mix of types. + for i := 1; i < len(args); i++ { + if firstType != reflect.TypeOf(args[i]) { + // []interface{} is the best we can do + return args + } + } + } + + slice := reflect.MakeSlice(reflect.SliceOf(firstType), len(args), len(args)) + for i, arg := range args { + slice.Index(i).Set(reflect.ValueOf(arg)) + } + return slice.Interface() +} diff --git a/common/collections/slice_test.go b/common/collections/slice_test.go new file mode 100644 index 000000000..3ebfe6d11 --- /dev/null +++ b/common/collections/slice_test.go @@ -0,0 +1,124 @@ +// Copyright 2019 The Hugo Authors. All rights reserved. +// +// 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 collections + +import ( + "errors" + "testing" + + qt "github.com/frankban/quicktest" +) + +var _ Slicer = (*tstSlicer)(nil) +var _ Slicer = (*tstSlicerIn1)(nil) +var _ Slicer = (*tstSlicerIn2)(nil) +var _ testSlicerInterface = (*tstSlicerIn1)(nil) +var _ testSlicerInterface = (*tstSlicerIn1)(nil) + +type testSlicerInterface interface { + Name() string +} + +type testSlicerInterfaces []testSlicerInterface + +type tstSlicerIn1 struct { + TheName string +} + +type tstSlicerIn2 struct { + TheName string +} + +type tstSlicer struct { + TheName string +} + +func (p *tstSlicerIn1) Slice(in interface{}) (interface{}, error) { + items := in.([]interface{}) + result := make(testSlicerInterfaces, len(items)) + for i, v := range items { + switch vv := v.(type) { + case testSlicerInterface: + result[i] = vv + default: + return nil, errors.New("invalid type") + } + + } + return result, nil +} + +func (p *tstSlicerIn2) Slice(in interface{}) (interface{}, error) { + items := in.([]interface{}) + result := make(testSlicerInterfaces, len(items)) + for i, v := range items { + switch vv := v.(type) { + case testSlicerInterface: + result[i] = vv + default: + return nil, errors.New("invalid type") + } + } + return result, nil +} + +func (p *tstSlicerIn1) Name() string { + return p.TheName +} + +func (p *tstSlicerIn2) Name() string { + return p.TheName +} + +func (p *tstSlicer) Slice(in interface{}) (interface{}, error) { + items := in.([]interface{}) + result := make(tstSlicers, len(items)) + for i, v := range items { + switch vv := v.(type) { + case *tstSlicer: + result[i] = vv + default: + return nil, errors.New("invalid type") + } + } + return result, nil +} + +type tstSlicers []*tstSlicer + +func TestSlice(t *testing.T) { + t.Parallel() + c := qt.New(t) + + for i, test := range []struct { + args []interface{} + expected interface{} + }{ + {[]interface{}{"a", "b"}, []string{"a", "b"}}, + {[]interface{}{&tstSlicer{"a"}, &tstSlicer{"b"}}, tstSlicers{&tstSlicer{"a"}, &tstSlicer{"b"}}}, + {[]interface{}{&tstSlicer{"a"}, "b"}, []interface{}{&tstSlicer{"a"}, "b"}}, + {[]interface{}{}, []interface{}{}}, + {[]interface{}{nil}, []interface{}{nil}}, + {[]interface{}{5, "b"}, []interface{}{5, "b"}}, + {[]interface{}{&tstSlicerIn1{"a"}, &tstSlicerIn2{"b"}}, testSlicerInterfaces{&tstSlicerIn1{"a"}, &tstSlicerIn2{"b"}}}, + {[]interface{}{&tstSlicerIn1{"a"}, &tstSlicer{"b"}}, []interface{}{&tstSlicerIn1{"a"}, &tstSlicer{"b"}}}, + } { + errMsg := qt.Commentf("[%d] %v", i, test.args) + + result := Slice(test.args...) + + c.Assert(test.expected, qt.DeepEquals, result, errMsg) + } + +} |