From 846b31008d956cdbc0aa60385edac84e85eecf07 Mon Sep 17 00:00:00 2001 From: Ville Vesilehto Date: Mon, 27 Jan 2025 21:01:34 +0200 Subject: [PATCH] fix: add pointer-string support to fromJSON & preserve interfaces This commit enables fromJSON to accept both string and *string arguments by performing a runtime check on the provided argument. It also refactors the deref.Type function to preserve interface types, ensuring methods sets are not lost when unwrapping pointers. Changes: - fromJSON now handles `string` and `*string` distinctly, returning an error if the pointer is nil or if an unsupported type is given. - deref.Type preserves the Kind() == reflect.Interface immediately instead of unwrapping further, which prevents losing interface method sets. - Updated unit tests to confirm that interface-wrapped pointers are unwrapped correctly (e.g., reflect.String) and interface types remain interfaces. Signed-off-by: Ville Vesilehto --- builtin/builtin.go | 20 +++++++++++++++----- internal/deref/deref.go | 19 +++++++++++++++++++ internal/deref/deref_test.go | 17 +++++++++++++++++ 3 files changed, 51 insertions(+), 5 deletions(-) diff --git a/builtin/builtin.go b/builtin/builtin.go index 4375f994..b80b7796 100644 --- a/builtin/builtin.go +++ b/builtin/builtin.go @@ -443,17 +443,27 @@ var Builtins = []*Function{ Name: "fromJSON", Func: func(args ...any) (any, error) { var v any - jsonStr := args[0] - if strPtr, ok := jsonStr.(*string); ok { - jsonStr = *strPtr + var jsonStr string + + switch arg := args[0].(type) { + case string: + jsonStr = arg + case *string: + if arg == nil { + return nil, fmt.Errorf("nil string pointer") + } + jsonStr = *arg + default: + return nil, fmt.Errorf("expected string or *string, got %T", args[0]) } - err := json.Unmarshal([]byte(jsonStr.(string)), &v) + + err := json.Unmarshal([]byte(jsonStr), &v) if err != nil { return nil, err } return v, nil }, - Types: types(new(func(string) any)), + Types: types(new(func(string) any), new(func(*string) any)), }, { Name: "toBase64", diff --git a/internal/deref/deref.go b/internal/deref/deref.go index acdc8981..06c0900b 100644 --- a/internal/deref/deref.go +++ b/internal/deref/deref.go @@ -30,9 +30,28 @@ func Type(t reflect.Type) reflect.Type { if t == nil { return nil } + + // Preserve interface types immediately to maintain type information + // This handles both empty (interface{}) and non-empty (e.g., io.Reader) interfaces + if t.Kind() == reflect.Interface { + return t + } + + // Iteratively unwrap pointer types until we reach a non-pointer + // or encounter an interface type that needs preservation for t.Kind() == reflect.Ptr { t = t.Elem() + if t == nil { + return nil + } + // Stop unwrapping if we hit an interface type to preserve its type information + // This ensures interface method sets are not lost + if t.Kind() == reflect.Interface { + return t + } } + + // Return the final unwrapped type, which could be any non-pointer, non-interface type return t } diff --git a/internal/deref/deref_test.go b/internal/deref/deref_test.go index 5f812bee..d9e31da3 100644 --- a/internal/deref/deref_test.go +++ b/internal/deref/deref_test.go @@ -67,6 +67,23 @@ func TestType_nil(t *testing.T) { assert.Nil(t, deref.Type(nil)) } +func TestType_interface_wrapped_pointer(t *testing.T) { + t.Run("one level", func(t *testing.T) { + str := "hello" + var iface any = &str + dt := deref.Type(reflect.TypeOf(iface)) + assert.Equal(t, reflect.String, dt.Kind()) + }) + + t.Run("two levels", func(t *testing.T) { + str := "hello" + strPtr := &str + var iface any = &strPtr + dt := deref.Type(reflect.TypeOf(iface)) + assert.Equal(t, reflect.String, dt.Kind()) + }) +} + func TestValue(t *testing.T) { a := uint(42) b := &a