diff --git a/basic.go b/basic.go index fa330e2..38d6f40 100644 --- a/basic.go +++ b/basic.go @@ -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) } } @@ -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, "") } } diff --git a/cast.go b/cast.go index 8d85539..65c6927 100644 --- a/cast.go +++ b/cast.go @@ -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" @@ -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 { @@ -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 +} diff --git a/example_valuesetter_test.go b/example_valuesetter_test.go new file mode 100644 index 0000000..c1fb748 --- /dev/null +++ b/example_valuesetter_test.go @@ -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 +} diff --git a/number.go b/number.go index a58dc4d..19fc9cc 100644 --- a/number.go +++ b/number.go @@ -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) } } diff --git a/slice.go b/slice.go index e6a8328..a165a1e 100644 --- a/slice.go +++ b/slice.go @@ -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) } } diff --git a/time.go b/time.go index 744cd5a..3bd0a33 100644 --- a/time.go +++ b/time.go @@ -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{}) } } @@ -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)) } } diff --git a/valuesetter_test.go b/valuesetter_test.go new file mode 100644 index 0000000..7551980 --- /dev/null +++ b/valuesetter_test.go @@ -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) + } +}