Skip to content
14 changes: 14 additions & 0 deletions basic.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,13 @@ func ToBoolE(i any) (bool, error) {
return ToBoolE(i)
}

// Try custom conversion interface
if setter, ok := i.(ValueSetter); ok {
var result bool
err := setter.SetValue(&result)
return result, err
}

return false, fmt.Errorf(errorMsg, i, i, false)
}
}
Expand Down Expand Up @@ -126,6 +133,13 @@ func ToStringE(i any) (string, error) {
return ToStringE(i)
}

// Try custom conversion interface
if setter, ok := i.(ValueSetter); ok {
var result string
err := setter.SetValue(&result)
return result, err
}

return "", fmt.Errorf(errorMsg, i, i, "")
}
}
31 changes: 21 additions & 10 deletions cast.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,10 @@
// Package cast provides easy and safe casting in Go.
package cast

import "time"
import (
"fmt"
"time"
)

const errorMsg = "unable to cast %#v of type %T to %T"
const errorMsgWith = "unable to cast %#v of type %T to %T: %w"
Expand Down Expand Up @@ -58,6 +61,8 @@ func ToE[T Basic](i any) (T, error) {
v, err = ToTimeE(i)
case time.Duration:
v, err = ToDurationE(i)
default:
return t, fmt.Errorf("unknown basic type: %T", t)
}

if err != nil {
Expand All @@ -67,18 +72,24 @@ func ToE[T Basic](i any) (T, error) {
return v.(T), nil
}

// Must is a helper that wraps a call to a cast function and panics if the error is non-nil.
func Must[T any](i any, err error) T {
if err != nil {
panic(err)
}

return i.(T)
}

// To casts any value to a [Basic] type.
func To[T Basic](i any) T {
v, _ := ToE[T](i)

return v
}

// Must panics if there is an error, otherwise returns the value.
func Must[T any](v T, err error) T {
if err != nil {
panic(err)
}
return v
}

// ValueSetter is an interface for types that can provide custom conversion logic.
// When a conversion function encounters a type it cannot handle in its default case,
// it will check if the type implements ValueSetter and use it for custom conversion.
type ValueSetter interface {
SetValue(any) error
}
120 changes: 120 additions & 0 deletions example_valuesetter_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
package cast

import (
"fmt"
"time"
)

// CustomBool demonstrates a custom type that implements ValueSetter
type CustomBool struct {
value string
}

func (c CustomBool) SetValue(target any) error {
switch t := target.(type) {
case *bool:
*t = c.value == "yes" || c.value == "true" || c.value == "1"
return nil
case *string:
if c.value == "yes" || c.value == "true" || c.value == "1" {
*t = "true"
} else {
*t = "false"
}
return nil
default:
return fmt.Errorf("unsupported target type: %T", target)
}
}

// CustomTime demonstrates a custom time type
type CustomTime struct {
timestamp int64
}

func (c CustomTime) SetValue(target any) error {
switch t := target.(type) {
case *time.Time:
*t = time.Unix(c.timestamp, 0)
return nil
case *string:
*t = time.Unix(c.timestamp, 0).Format(time.RFC3339)
return nil
default:
return fmt.Errorf("unsupported target type: %T", target)
}
}

// CustomInt demonstrates a custom integer type
type CustomInt struct {
hexValue string
}

func (c CustomInt) SetValue(target any) error {
switch t := target.(type) {
case *int:
if c.hexValue == "0xFF" {
*t = 255
} else {
*t = 0
}
return nil
case *string:
*t = c.hexValue
return nil
default:
return fmt.Errorf("unsupported target type: %T", target)
}
}

func ExampleValueSetter_bool() {
custom := CustomBool{value: "yes"}

result, err := ToBoolE(custom)
if err != nil {
fmt.Printf("Error: %v\n", err)
return
}

fmt.Printf("Bool result: %v\n", result)
// Output: Bool result: true
}

func ExampleValueSetter_string() {
custom := CustomBool{value: "yes"}

result, err := ToStringE(custom)
if err != nil {
fmt.Printf("Error: %v\n", err)
return
}

fmt.Printf("String result: %s\n", result)
// Output: String result: true
}

func ExampleValueSetter_time() {
custom := CustomTime{timestamp: 1609459200} // 2021-01-01 00:00:00 UTC

result, err := ToTimeE(custom)
if err != nil {
fmt.Printf("Error: %v\n", err)
return
}

fmt.Printf("Time result: %s\n", result.Format("2006-01-02"))
// Output: Time result: 2021-01-01
}

func ExampleValueSetter_int() {
custom := CustomInt{hexValue: "0xFF"}

result, err := ToIntE(custom)
if err != nil {
fmt.Printf("Error: %v\n", err)
return
}

fmt.Printf("Int result: %d\n", result)
// Output: Int result: 255
}
7 changes: 7 additions & 0 deletions number.go
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,13 @@ func toNumberE[T Number](i any, parseFn func(string) (T, error)) (T, error) {
return toNumberE(i, parseFn)
}

// Try custom conversion interface
if setter, ok := i.(ValueSetter); ok {
var result T
err := setter.SetValue(&result)
return result, err
}

return 0, fmt.Errorf(errorMsg, i, i, n)
}
}
Expand Down
7 changes: 7 additions & 0 deletions slice.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,13 @@ func ToSliceE(i any) ([]any, error) {

return s, nil
default:
// Try custom conversion interface
if setter, ok := i.(ValueSetter); ok {
var result []any
err := setter.SetValue(&result)
return result, err
}

return s, fmt.Errorf(errorMsg, i, i, s)
}
}
Expand Down
14 changes: 14 additions & 0 deletions time.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,13 @@ func ToTimeInDefaultLocationE(i any, location *time.Location) (tim time.Time, er
case nil:
return time.Time{}, nil
default:
// Try custom conversion interface
if setter, ok := i.(ValueSetter); ok {
var result time.Time
err := setter.SetValue(&result)
return result, err
}

return time.Time{}, fmt.Errorf(errorMsg, i, i, time.Time{})
}
}
Expand Down Expand Up @@ -96,6 +103,13 @@ func ToDurationE(i any) (time.Duration, error) {
return ToDurationE(i)
}

// Try custom conversion interface
if setter, ok := i.(ValueSetter); ok {
var result time.Duration
err := setter.SetValue(&result)
return result, err
}

return 0, fmt.Errorf(errorMsg, i, i, time.Duration(0))
}
}
Expand Down
85 changes: 85 additions & 0 deletions valuesetter_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
package cast

import (
"testing"
"time"
)

// TestValueSetterIntegration tests that ValueSetter interface works correctly
// with all conversion functions in their default branches
func TestValueSetterIntegration(t *testing.T) {
// Test with CustomBool
customBool := CustomBool{value: "yes"}

// Test ToBoolE
result, err := ToBoolE(customBool)
if err != nil {
t.Errorf("ToBoolE failed: %v", err)
}
if !result {
t.Errorf("Expected true, got %v", result)
}

// Test ToStringE
strResult, err := ToStringE(customBool)
if err != nil {
t.Errorf("ToStringE failed: %v", err)
}
if strResult != "true" {
t.Errorf("Expected 'true', got %v", strResult)
}

// Test with CustomTime
customTime := CustomTime{timestamp: 1609459200} // 2021-01-01 00:00:00 UTC

timeResult, err := ToTimeE(customTime)
if err != nil {
t.Errorf("ToTimeE failed: %v", err)
}
expected := time.Unix(1609459200, 0)
if !timeResult.Equal(expected) {
t.Errorf("Expected %v, got %v", expected, timeResult)
}

// Test with CustomInt
customInt := CustomInt{hexValue: "0xFF"}

intResult, err := ToIntE(customInt)
if err != nil {
t.Errorf("ToIntE failed: %v", err)
}
if intResult != 255 {
t.Errorf("Expected 255, got %v", intResult)
}
}

// TestStandardConversionPriority ensures standard conversions work normally
// and ValueSetter is only used as fallback
func TestStandardConversionPriority(t *testing.T) {
// Test that standard string conversion works normally
result, err := ToStringE("hello")
if err != nil {
t.Errorf("ToStringE failed: %v", err)
}
if result != "hello" {
t.Errorf("Expected 'hello', got %v", result)
}

// Test that standard bool conversion works normally
boolResult, err := ToBoolE(true)
if err != nil {
t.Errorf("ToBoolE failed: %v", err)
}
if !boolResult {
t.Errorf("Expected true, got %v", boolResult)
}

// Test that standard int conversion works normally
intResult, err := ToIntE(42)
if err != nil {
t.Errorf("ToIntE failed: %v", err)
}
if intResult != 42 {
t.Errorf("Expected 42, got %v", intResult)
}
}