8000 Resolve aliases by sagikazarmark · Pull Request #271 · spf13/cast · GitHub
[go: up one dir, main page]
More Web Proxy on the site http://driver.im/
Skip to content

Resolve aliases #271

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Jun 1, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ jobs:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
go: ["1.20", "1.21", "1.22", "1.23", "1.24"]
go: ["1.21", "1.22", "1.23", "1.24"]

steps:
- name: Checkout repository
Expand Down
69 changes: 69 additions & 0 deletions alias.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
// Copyright © 2014 Steve Francia <spf@spf13.com>.
//
// Use of this source code is governed by an MIT-style
// license that can be found in the LICENSE file.
package cast

import (
"reflect"
"slices"
)

var kindNames = []string{
reflect.String: "string",
reflect.Bool: "bool",
reflect.Int: "int",
reflect.Int8: "int8",
reflect.Int16: "int16",
reflect.Int32: "int32",
reflect.Int64: "int64",
reflect.Uint: "uint",
reflect.Uint8: "uint8",
reflect.Uint16: "uint16",
reflect.Uint32: "uint32",
reflect.Uint64: "uint64",
reflect.Float32: "float32",
reflect.Float64: "float64",
}

var kinds = map[reflect.Kind]func(reflect.Value) any{
reflect.String: func(v reflect.Value) any { return v.String() },
reflect.Bool: func(v reflect.Value) any { return v.Bool() },
reflect.Int: func(v reflect.Value) any { return int(v.Int()) },
reflect.Int8: func(v reflect.Value) any { return int8(v.Int()) },
reflect.Int16: func(v reflect.Value) any { return int16(v.Int()) },
reflect.Int32: func(v reflect.Value) any { return int32(v.Int()) },
reflect.Int64: func(v reflect.Value) any { return v.Int() },
reflect.Uint: func(v reflect.Value) any { return uint(v.Uint()) },
reflect.Uint8: func(v reflect.Value) any { return uint8(v.Uint()) },
reflect.Uint16: func(v reflect.Value) any { return uint16(v.Uint()) },
reflect.Uint32: func(v reflect.Value) any { return uint32(v.Uint()) },
reflect.Uint64: func(v reflect.Value) any { return v.Uint() },
reflect.Float32: func(v reflect.Value) any { return float32(v.Float()) },
reflect.Float64: func(v reflect.Value) any { return v.Float() },
}

// resolveAlias attempts to resolve a named type to its underlying basic type (if possible).
//
// Pointers are expected to be indirected by this point.
func resolveAlias(i any) (any, bool) {
if i == nil {
return nil, false
}

t := reflect.TypeOf(i)

// Not a named type
if t.Name() == "" || slices.Contains(kindNames, t.Name()) {
return i, false
}

resolve, ok := kinds[t.Kind()]
if !ok { // Not a supported kind
return i, false
}

v := reflect.ValueOf(i)

return resolve(v), true
}
87 changes: 87 additions & 0 deletions alias_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
// Copyright © 2014 Steve Francia <spf@spf13.com>.
//
// Use of this source code is governed by an MIT-style
// license that can be found in the LICENSE file.
package cast

import (
"testing"

qt "github.com/frankban/quicktest"
)

func TestAlias(t *testing.T) {
type MyStruct struct{}

type MyString string
type MyOtherString MyString
type MyAliasString = MyOtherString

type MyBool bool
type MyOtherBool MyBool
type MyAliasBool = MyOtherBool

type MyInt int
type MyInt8 int8
type MyInt16 int16
type MyInt32 int32
type MyInt64 int64
type MyUint uint
type MyUint8 uint8
type MyUint16 uint16
type MyUint32 uint32
type MyUint64 uint64
type MyFloat32 float32
type MyFloat64 float64

var myStruct *MyStruct

testCases := []struct {
input any
expectedValue any
expectedOk bool
}{
{"string", "string", false}, // Already resolved
{MyStruct{}, MyStruct{}, false}, // Non-resolvable
{nil, nil, false},
{&MyStruct{}, &MyStruct{}, false},
{myStruct, myStruct, false},

{MyString("string"), "string", true},
{MyOtherString("string"), "string", true},
{MyAliasString("string"), "string", true},

{MyBool(true), true, true},
{MyOtherBool(true), true, true},
{MyAliasBool(true), true, true},

{MyInt(1234), int(1234), true},
{MyInt8(123), int8(123), true},
{MyInt16(1234), int16(1234), true},
{MyInt32(1234), int32(1234), true},
{MyInt64(1234), int64(1234), true},

{MyUint(1234), uint(1234), true},
{MyUint8(123), uint8(123), true},
{MyUint16(1234), uint16(1234), true},
{MyUint32(1234), uint32(1234), true},
{MyUint64(1234), uint64(1234), true},

{MyFloat32(1.0), float32(1.0), true},
{MyFloat64(1.0), float64(1.0), true},
}

for _, testCase := range testCases {
// TODO: remove after minimum Go version is >=1.22
testCase := testCase

t.Run("", func(t *testing.T) {
c := qt.New(t)

actualValue, ok := resolveAlias(testCase.input)

c.Assert(actualValue, qt.Equals, testCase.expectedValue)
c.Assert(ok, qt.Equals, testCase.expectedOk)
})
}
}
10 changes: 9 additions & 1 deletion basic.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,10 @@ func ToBoolE(i any) (bool, error) {

return false, fmt.Errorf(errorMsg, i, i, false)
default:
if i, ok := resolveAlias(i); ok {
return ToBoolE(i)
}

return false, fmt.Errorf(errorMsg, i, i, false)
}
}
Expand Down Expand Up @@ -114,7 +118,11 @@ func ToStringE(i any) (string, error) {
case error:
return s.Error(), nil
default:
if i, ok := indirect(s); ok {
if i, ok := indirect(i); ok {
return ToStringE(i)
}

if i, ok := resolveAlias(i); ok {
return ToStringE(i)
}

Expand Down
8 changes: 8 additions & 0 deletions basic_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,10 @@ func TestBool(t *testing.T) {
{int32(-1), true, false},
{int64(-1), true, false},

// Alias
{MyBool(true), true, false},
{MyBool(false), false, false},

// Failure cases
{"test", false, true},
{testing.T{}, false, true},
Expand Down Expand Up @@ -124,6 +128,10 @@ func TestString(t *testing.T) {
{template.CSS("a"), "a", false},
{template.HTMLAttr("a"), "a", false},

// Alias
{MyString("foo"), "foo", false},

// Stringer and error
{foo{val: "bar"}, "bar", false},
{fu{val: "bar"}, "bar", false},

Expand Down
16 changes: 16 additions & 0 deletions cast_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -223,3 +223,19 @@ func BenchmarkCast(b *testing.B) {
}
})
}

// Alias types for alias testing
type MyString string
type MyBool bool
type MyInt int
type MyInt8 int8
type MyInt16 int16
type MyInt32 int32
type MyInt64 int64
type MyUint uint
type MyUint8 uint8
type MyUint16 uint16
type MyUint32 uint32
type MyUint64 uint64
type MyFloat32 float32
type MyFloat64 float64
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
module github.com/spf13/cast

go 1.19
go 1.21.0

require github.com/frankban/quicktest v1.14.6

Expand Down
8 changes: 8 additions & 0 deletions number.go
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,10 @@ func toNumberE[T Number](i any, parseFn func(string) (T, error)) (T, error) {

return T(s.Float64()), nil
default:
if i, ok := resolveAlias(i); ok {
return toNumberE(i, parseFn)
}

return 0, fmt.Errorf(errorMsg, i, i, n)
}
}
Expand Down Expand Up @@ -333,6 +337,10 @@ func toUnsignedNumberE[T Number](i any, parseFn func(string) (T, error)) (T, err

return T(v), nil
default:
if i, ok := resolveAlias(i); ok {
return toUnsignedNumberE(i, parseFn)
}

return 0, fmt.Errorf(errorMsg, i, i, n)
}
}
Expand Down
38 changes: 22 additions & 16 deletions number_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ var numberContexts = map[string]numberContext{
generic: toAny(cast.ToNumber[int]),
specificErr: toAnyErr(cast.ToIntE),
genericErr: toAnyErr(cast.ToNumberE[int]),
samples: []any{int(0), int(1), int(8), int(-8), int(8), int(-8), math.MinInt, math.MaxInt, new(big.Int).Sub(big.NewInt(math.MinInt), big.NewInt(1)).String(), new(big.Int).Add(big.NewInt(math.MaxInt), big.NewInt(1)).String()},
samples: []any{int(0), int(1), int(8), int(-8), int(8), int(-8), MyInt(8), math.MinInt, math.MaxInt, new(big.Int).Sub(big.NewInt(math.MinInt), big.NewInt(1)).String(), new(big.Int).Add(big.NewInt(math.MaxInt), big.NewInt(1)).String()},
},
"int8": {
to: toAny(cast.To[int8]),
Expand All @@ -56,7 +56,7 @@ var numberContexts = map[string]numberContext{
generic: toAny(cast.ToNumber[int8]),
specificErr: toAnyErr(cast.ToInt8E),
genericErr: toAnyErr(cast.ToNumberE[int8]),
samples: []any{int8(0), int8(1), int8(8), int8(-8), int8(8), int8(-8), int8(math.MinInt8), int8(math.MaxInt8), "-129", "128"},
samples: []any{int8(0), int8(1), int8(8), int8(-8), int8(8), int8(-8), MyInt8(8), int8(math.MinInt8), int8(math.MaxInt8), "-129", "128"},
},
"int16": {
to: toAny(cast.To[int16]),
Expand All @@ -65,7 +65,7 @@ var numberContexts = map[string]numberContext{
generic: toAny(cast.ToNumber[int16]),
specificErr: toAnyErr(cast.ToInt16E),
genericErr: toAnyErr(cast.ToNumberE[int16]),
samples: []any{int16(0), int16(1), int16(8), int16(-8), int16(8), int16(-8), int16(math.MinInt16), int16(math.MaxInt16), "-32769", "32768"},
samples: []any{int16(0), int16(1), int16(8), int16(-8), int16(8), int16(-8), MyInt16(8), int16(math.MinInt16), int16(math.MaxInt16), "-32769", "32768"},
},
"int32": {
to: toAny(cast.To[int32]),
Expand All @@ -74,7 +74,7 @@ var numberContexts = map[string]numberContext{
generic: toAny(cast.ToNumber[int32]),
specificErr: toAnyErr(cast.ToInt32E),
genericErr: toAnyErr(cast.ToNumberE[int32]),
samples: []any{int32(0), int32(1), int32(8), int32(-8), int32(8), int32(-8), int32(math.MinInt32), int32(math.MaxInt32), "-2147483649", "2147483648"},
samples: []any{int32(0), int32(1), int32(8), int32(-8), int32(8), int32(-8), MyInt32(8), int32(math.MinInt32), int32(math.MaxInt32), "-2147483649", "2147483648"},
},
"int64": {
to: toAny(cast.To[int64]),
Expand All @@ -83,7 +83,7 @@ var numberContexts = map[string]numberContext{
generic: toAny(cast.ToNumber[int64]),
specificErr: toAnyErr(cast.ToInt64E),
genericErr: toAnyErr(cast.ToNumberE[int64]),
samples: []any{int64(0), int64(1), int64(8), int64(-8), int64(8), int64(-8), int64(math.MinInt64), int64(math.MaxInt64), "-9223372036854775809", "9223372036854775808"},
samples: []any{int64(0), int64(1), int64(8), int64(-8), int64(8), int64(-8), MyInt64(8), int64(math.MinInt64), int64(math.MaxInt64), "-9223372036854775809", "9223372036854775808"},
},
"uint": {
to: toAny(cast.To[uint]),
Expand All @@ -92,7 +92,7 @@ var numberContexts = map[string]numberContext{
generic: toAny(cast.ToNumber[uint]),
specificErr: toAnyErr(cast.ToUintE),
genericErr: toAnyErr(cast.ToNumberE[uint]),
samples: []any{uint(0), uint(1), uint(8), uint(0), uint(8), uint(0), uint(0), uint(math.MaxUint), nil, nil},
samples: []any{uint(0), uint(1), uint(8), uint(0), uint(8), uint(0), MyUint(8), uint(0), uint(math.MaxUint), nil, nil},
},
"uint8": {
to: toAny(cast.To[uint8]),
Expand All @@ -101,7 +101,7 @@ var numberContexts = map[string]numberContext{
generic: toAny(cast.ToNumber[uint8]),
specificErr: toAnyErr(cast.ToUint8E),
genericErr: toAnyErr(cast.ToNumberE[uint8]),
samples: []any{uint8(0), uint8(1), uint8(8), uint8(0), uint8(8), uint8(0), uint8(0), uint8(math.MaxUint8), "-1", "256"},
samples: []any{uint8(0), uint8(1), uint8(8), uint8(0), uint8(8), uint8(0), MyUint8(8), uint8(0), uint8(math.MaxUint8), "-1", "256"},
},
"uint16": {
to: toAny(cast.To[uint16]),
Expand All @@ -110,7 +110,7 @@ var numberContexts = map[string]numberContext{
generic: toAny(cast.ToNumber[uint16]),
specificErr: toAnyErr(cast.ToUint16E),
genericErr: toAnyErr(cast.ToNumberE[uint16]),
samples: []any{uint16(0), uint16(1), uint16(8), uint16(0), uint16(8), uint16(0), uint16(0), uint16(math.MaxUint16), "-1", "65536"},
samples: []any{uint16(0), uint16(1), uint16(8), uint16(0), uint16(8), uint16(0), MyUint16(8), uint16(0), uint16(math.MaxUint16), "-1", "65536"},
},
"uint32": {
to: toAny(cast.To[uint32]),
Expand All @@ -119,7 +119,7 @@ var numberContexts = map[string]numberContext{
generic: toAny(cast.ToNumber[uint32]),
specificErr: toAnyErr(cast.ToUint32E),
genericErr: toAnyErr(cast.ToNumberE[uint32]),
samples: []any{uint32(0), uint32(1), uint32(8), uint32(0), uint32(8), uint32(0), uint32(0), uint32(math.MaxUint32), "-1", "4294967296"},
samples: []any{uint32(0), uint32(1), uint32(8), uint32(0), uint32(8), uint32(0), MyUint32(8), uint32(0), uint32(math.MaxUint32), "-1", "4294967296"},
},
"uint64": {
to: toAny(cast.To[uint64]),
Expand All @@ -128,7 +128,7 @@ var numberContexts = map[string]numberContext{
generic: toAny(cast.ToNumber[uint64]),
specificErr: toAnyErr(cast.ToUint64E),
genericErr: toAnyErr(cast.ToNumberE[uint64]),
samples: []any{uint64(0), uint64(1), uint64(8), uint64(0), uint64(8), uint64(0), uint64(0), uint64(math.MaxUint64), "-1", "18446744073709551616"},
samples: []any{uint64(0), uint64(1), uint64(8), uint64(0), uint64(8), uint64(0), MyUint64(8), uint64(0), uint64(math.MaxUint64), "-1", "18446744073709551616"},
},
"float32": {
to: toAny(cast.To[float32]),
Expand All @@ -137,7 +137,7 @@ var numberContexts = map[string]numberContext{
generic: toAny(cast.ToNumber[float32]),
specificErr: toAnyErr(cast.ToFloat32E),
genericErr: toAnyErr(cast.ToNumberE[float32]),
samples: []any{float32(0), float32(1), float32(8), float32(-8), float32(8.31), float32(-8.31), float32(-math.MaxFloat32), float32(math.MaxFloat32), nil, nil},
samples: []any{float32(0), float32(1), float32(8), float32(-8), float32(8.31), float32(-8.31), MyFloat32(8), float32(-math.MaxFloat32), float32(math.MaxFloat32), nil, nil},
},
"float64": {
to: toAny(cast.To[float64]),
Expand All @@ -146,7 +146,7 @@ var numberContexts = map[string]numberContext{
generic: toAny(cast.ToNumber[float64]),
specificErr: toAnyErr(cast.ToFloat64E),
genericErr: toAnyErr(cast.ToNumberE[float64]),
samples: []any{float64(0), float64(1), float64(8), float64(-8), float64(8.31), float64(-8.31), float64(-math.MaxFloat64), float64(math.MaxFloat64), nil, nil},
samples: []any{float64(0), float64(1), float64(8), float64(-8), float64(8.31), float64(-8.31), MyFloat64(8), float64(-math.MaxFloat64), float64(math.MaxFloat64), nil, nil},
},
}

Expand All @@ -157,10 +157,11 @@ func generateNumberTestCases(samples []any) []testCase {
eightNegative := samples[3]
eightPoint31 := samples[4]
eightPoint31Negative := samples[5]
min := samples[6]
max := samples[7]
underflowString := samples[8]
overflowString := samples[9]
aliasEight := samples[6]
min := samples[7]
max := samples[8]
underflowString := samples[9]
overflowString := samples[10]

_ = min
_ = max
Expand Down Expand Up @@ -222,6 +223,11 @@ func generateNumberTestCases(samples []any) []testCase {
{json.Number("-8.31"), eightPoint31Negative, isUint},
{json.Number(""), zero, false},

// Aliases
{aliasEight, eight, false},
{MyString("8"), eight, false},
{MyBool(true), one, false},

// Failure cases
{"test", zero, true},
{testing.T{}, zero, true},
Expand Down
Loading
0