Skip to content

Commit 8f73140

Browse files
committed
tpl/collections: Simplify the reflect usage
* Use helper funcs in hreflect package when possible. * Use hreflect.ConvertIfPossible to handle conversions when possible.
1 parent 524b986 commit 8f73140

File tree

10 files changed

+447
-229
lines changed

10 files changed

+447
-229
lines changed

common/hreflect/convert.go

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
// Copyright 2025 The Hugo Authors. All rights reserved.
2+
// Some functions in this file (see comments) is based on the Go source code,
3+
// copyright The Go Authors and governed by a BSD-style license.
4+
//
5+
// Licensed under the Apache License, Version 2.0 (the "License");
6+
// you may not use this file except in compliance with the License.
7+
// You may obtain a copy of the License at
8+
// http://www.apache.org/licenses/LICENSE-2.0
9+
//
10+
// Unless required by applicable law or agreed to in writing, software
11+
// distributed under the License is distributed on an "AS IS" BASIS,
12+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
// See the License for the specific language governing permissions and
14+
// limitations under the License.
15+
16+
package hreflect
17+
18+
import (
19+
"fmt"
20+
"reflect"
21+
)
22+
23+
var (
24+
typeInt64 = reflect.TypeFor[int64]()
25+
typeFloat64 = reflect.TypeFor[float64]()
26+
typeString = reflect.TypeFor[string]()
27+
)
28+
29+
// ToInt64 converts v to int64 if possible, returning an error if not.
30+
func ToInt64E(v reflect.Value) (int64, error) {
31+
if v, ok := ConvertIfPossible(v, typeInt64); ok {
32+
return v.Int(), nil
33+
}
34+
return 0, errConvert(v, "int64")
35+
}
36+
37+
// ToInt64 converts v to int64 if possible, panicking if not.
38+
func ToInt64(v reflect.Value) int64 {
39+
vv, err := ToInt64E(v)
40+
if err != nil {
41+
panic(err)
42+
}
43+
return vv
44+
}
45+
46+
// ToIntFloat64 converts v to float64 if possible, returning an error if not.
47+
func ToFloat64E(v reflect.Value) (float64, error) {
48+
if v, ok := ConvertIfPossible(v, typeFloat64); ok {
49+
return v.Float(), nil
50+
}
51+
return 0, errConvert(v, "float64")
52+
}
53+
54+
// ToFloat64 converts v to float64 if possible, panicking if not.
55+
func ToFloat64(v reflect.Value) float64 {
56+
vv, err := ToFloat64E(v)
57+
if err != nil {
58+
panic(err)
59+
}
60+
return vv
61+
}
62+
63+
// ToStringE converts v to string if possible, returning an error if not.
64+
func ToStringE(v reflect.Value) (string, error) {
65+
vv, err := ToStringValueE(v)
66+
if err != nil {
67+
return "", err
68+
}
69+
return vv.String(), nil
70+
}
71+
72+
func ToStringValueE(v reflect.Value) (reflect.Value, error) {
73+
if v, ok := ConvertIfPossible(v, typeString); ok {
74+
return v, nil
75+
}
76+
return reflect.Value{}, errConvert(v, "string")
77+
}
78+
79+
// ToString converts v to string if possible, panicking if not.
80+
func ToString(v reflect.Value) string {
81+
vv, err := ToStringE(v)
82+
if err != nil {
83+
panic(err)
84+
}
85+
return vv
86+
}
87+
88+
func errConvert(v reflect.Value, s string) error {
89+
return fmt.Errorf("unable to convert value of type %q to %q", v.Type().String(), s)
90+
}

common/hreflect/convert_test.go

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
// Copyright 2025 The Hugo Authors. All rights reserved.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
// http://www.apache.org/licenses/LICENSE-2.0
7+
//
8+
// Unless required by applicable law or agreed to in writing, software
9+
// distributed under the License is distributed on an "AS IS" BASIS,
10+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11+
// See the License for the specific language governing permissions and
12+
// limitations under the License.
13+
14+
package hreflect
15+
16+
import (
17+
"reflect"
18+
"testing"
19+
20+
qt "github.com/frankban/quicktest"
21+
"github.com/gohugoio/hugo/htesting/hqt"
22+
)
23+
24+
func TestToFuncs(t *testing.T) {
25+
c := qt.New(t)
26+
27+
c.Assert(ToInt64(reflect.ValueOf(int(42))), qt.Equals, int64(42))
28+
c.Assert(ToFloat64(reflect.ValueOf(float32(3.14))), hqt.IsSameFloat64, float64(3.14))
29+
c.Assert(ToString(reflect.ValueOf("hello")), qt.Equals, "hello")
30+
}
31+
32+
func BenchmarkToInt64(b *testing.B) {
33+
v := reflect.ValueOf(int(42))
34+
for i := 0; i < b.N; i++ {
35+
ToInt64(v)
36+
}
37+
}

common/hreflect/helpers.go

Lines changed: 110 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Copyright 2024 The Hugo Authors. All rights reserved.
1+
// Copyright 2025 The Hugo Authors. All rights reserved.
22
// Some functions in this file (see comments) is based on the Go source code,
33
// copyright The Go Authors and governed by a BSD-style license.
44
//
@@ -318,37 +318,119 @@ func IsContextType(tp reflect.Type) bool {
318318
// This conversion is lossless.
319319
// See Issue 14079.
320320
func ConvertIfPossible(val reflect.Value, typ reflect.Type) (reflect.Value, bool) {
321+
if val.Kind() == reflect.Interface {
322+
val = val.Elem()
323+
}
324+
325+
if val.Type().AssignableTo(typ) {
326+
// No conversion needed.
327+
return val, true
328+
}
329+
321330
if IsInt(typ.Kind()) {
322-
if IsInt(val.Kind()) {
323-
if typ.OverflowInt(val.Int()) {
324-
return reflect.Value{}, false
325-
}
326-
return val.Convert(typ), true
331+
return convertToIntIfPossible(val, typ)
332+
}
333+
if IsFloat(typ.Kind()) {
334+
return convertToFloatIfPossible(val, typ)
335+
}
336+
if IsUint(typ.Kind()) {
337+
return convertToUintIfPossible(val, typ)
338+
}
339+
return reflect.Value{}, false
340+
}
341+
342+
func convertToUintIfPossible(val reflect.Value, typ reflect.Type) (reflect.Value, bool) {
343+
if IsInt(val.Kind()) {
344+
i := val.Int()
345+
if i < 0 {
346+
return reflect.Value{}, false
327347
}
328-
if IsUint(val.Kind()) {
329-
if val.Uint() > uint64(math.MaxInt64) {
330-
return reflect.Value{}, false
331-
}
332-
if typ.OverflowInt(int64(val.Uint())) {
333-
return reflect.Value{}, false
334-
}
335-
return val.Convert(typ), true
348+
u := uint64(i)
349+
if typ.OverflowUint(u) {
350+
return reflect.Value{}, false
336351
}
337-
if IsFloat(val.Kind()) {
338-
f := val.Float()
339-
if f < float64(math.MinInt64) || f > float64(math.MaxInt64) {
340-
return reflect.Value{}, false
341-
}
342-
i := int64(f)
343-
if typ.OverflowInt(i) {
344-
return reflect.Value{}, false
345-
}
346-
// Check for lossless conversion.
347-
if float64(i) != f {
348-
return reflect.Value{}, false
349-
}
350-
return val.Convert(typ), true
352+
return reflect.ValueOf(u).Convert(typ), true
353+
}
354+
if IsUint(val.Kind()) {
355+
if typ.OverflowUint(val.Uint()) {
356+
return reflect.Value{}, false
351357
}
358+
return val.Convert(typ), true
352359
}
360+
if IsFloat(val.Kind()) {
361+
f := val.Float()
362+
if f < 0 || f > float64(math.MaxUint64) {
363+
return reflect.Value{}, false
364+
}
365+
if f != math.Trunc(f) {
366+
return reflect.Value{}, false
367+
}
368+
u := uint64(f)
369+
if typ.OverflowUint(u) {
370+
return reflect.Value{}, false
371+
}
372+
return reflect.ValueOf(u).Convert(typ), true
373+
}
374+
return reflect.Value{}, false
375+
}
376+
377+
func convertToFloatIfPossible(val reflect.Value, typ reflect.Type) (reflect.Value, bool) {
378+
if IsInt(val.Kind()) {
379+
i := val.Int()
380+
f := float64(i)
381+
if typ.OverflowFloat(f) {
382+
return reflect.Value{}, false
383+
}
384+
return reflect.ValueOf(f).Convert(typ), true
385+
}
386+
if IsUint(val.Kind()) {
387+
u := val.Uint()
388+
f := float64(u)
389+
if typ.OverflowFloat(f) {
390+
return reflect.Value{}, false
391+
}
392+
return reflect.ValueOf(f).Convert(typ), true
393+
}
394+
if IsFloat(val.Kind()) {
395+
if typ.OverflowFloat(val.Float()) {
396+
return reflect.Value{}, false
397+
}
398+
return val.Convert(typ), true
399+
}
400+
401+
return reflect.Value{}, false
402+
}
403+
404+
func convertToIntIfPossible(val reflect.Value, typ reflect.Type) (reflect.Value, bool) {
405+
if IsInt(val.Kind()) {
406+
if typ.OverflowInt(val.Int()) {
407+
return reflect.Value{}, false
408+
}
409+
return val.Convert(typ), true
410+
}
411+
if IsUint(val.Kind()) {
412+
if val.Uint() > uint64(math.MaxInt64) {
413+
return reflect.Value{}, false
414+
}
415+
if typ.OverflowInt(int64(val.Uint())) {
416+
return reflect.Value{}, false
417+
}
418+
return val.Convert(typ), true
419+
}
420+
if IsFloat(val.Kind()) {
421+
f := val.Float()
422+
if f < float64(math.MinInt64) || f > float64(math.MaxInt64) {
423+
return reflect.Value{}, false
424+
}
425+
if f != math.Trunc(f) {
426+
return reflect.Value{}, false
427+
}
428+
if typ.OverflowInt(int64(f)) {
429+
return reflect.Value{}, false
430+
}
431+
return reflect.ValueOf(int64(f)).Convert(typ), true
432+
433+
}
434+
353435
return reflect.Value{}, false
354436
}

0 commit comments

Comments
 (0)