From 4a8ad5a24cbb0d83268fed6683bec85b3da51350 Mon Sep 17 00:00:00 2001 From: Ryan Chou <88779759+ryanchou1994@users.noreply.github.com> Date: Mon, 21 Sep 2026 11:29:31 +0800 Subject: [PATCH] fix: report map string conversion errors --- map.go | 16 ++++++- map_test.go | 124 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 139 insertions(+), 1 deletion(-) diff --git a/map.go b/map.go index 858d4ee4..0ac7308b 100644 --- a/map.go +++ b/map.go @@ -58,7 +58,21 @@ func toStringMapE[T any](i any, fn func(any) T) (map[string]T, error) { // ToStringMapStringE casts any value to a map[string]string type. func ToStringMapStringE(i any) (map[string]string, error) { - return toStringMapE(i, ToString) + var firstErr error + toString := func(value any) string { + result, err := ToStringE(value) + if firstErr == nil { + firstErr = err + } + return result + } + + result, err := toMapE(i, toString, toString) + if err != nil { + return result, err + } + + return result, firstErr } // ToStringMapStringSliceE casts any value to a map[string][]string type. diff --git a/map_test.go b/map_test.go index a66c27ae..a31848bc 100644 --- a/map_test.go +++ b/map_test.go @@ -288,3 +288,127 @@ func TestStringMapString(t *testing.T) { runMapTests(t, testCases, cast.ToStringMapString, cast.ToStringMapStringE) } + +func TestStringMapStringConversionErrors(t *testing.T) { + type namedMap map[string]any + + testCases := []struct { + name string + input any + expected map[string]string + expectError bool + }{ + { + name: "string keys with multiple invalid values", + input: map[string]any{"slice": []any{1}, "struct": struct{}{}, "number": 123, "nil": nil}, + expected: map[string]string{"slice": "", "struct": "", "number": "123", "nil": ""}, + expectError: true, + }, + { + name: "interface keys with multiple invalid values", + input: map[any]any{"slice": []any{1}, "struct": struct{}{}, 123: true, "nil": nil}, + expected: map[string]string{"slice": "", "struct": "", "123": "true", "nil": ""}, + expectError: true, + }, + { + name: "invalid interface key with string values", + input: map[any]string{struct{}{}: "value", "good": "kept"}, + expected: map[string]string{"": "value", "good": "kept"}, + expectError: true, + }, + { + name: "invalid interface key and value", + input: map[any]any{struct{}{}: "value", "bad": []any{1}, "good": 123}, + expected: map[string]string{"": "value", "bad": "", "good": "123"}, + expectError: true, + }, + { + name: "successful scalars", + input: map[string]any{"string": "value", "bool": true, "int": 123, "float": 1.5, "bytes": []byte("bytes"), "nil": nil}, + expected: map[string]string{"string": "value", "bool": "true", "int": "123", "float": "1.5", "bytes": "bytes", "nil": ""}, + }, + { + name: "string map", + input: map[string]string{"key": "value"}, + expected: map[string]string{"key": "value"}, + }, + { + name: "nil string map", + input: map[string]string(nil), + }, + { + name: "nil interface map", + input: map[string]any(nil), + expected: map[string]string{}, + }, + { + name: "JSON string", + input: `{"key":"value"}`, + expected: map[string]string{"key": "value"}, + }, + { + name: "JSON null", + input: "null", + }, + { + name: "nil input", + expected: map[string]string{}, + expectError: true, + }, + { + name: "invalid input", + input: struct{}{}, + expected: map[string]string{}, + expectError: true, + }, + { + name: "invalid JSON", + input: `{"key":"value"`, + expected: map[string]string{}, + expectError: true, + }, + { + name: "unsupported named map", + input: namedMap{"key": "value"}, + expected: map[string]string{}, + expectError: true, + }, + } + + for _, tc := range testCases { + tc := tc + + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + c := qt.New(t) + result, err := cast.ToStringMapStringE(tc.input) + c.Assert(result, qt.DeepEquals, tc.expected) + c.Assert(cast.ToStringMapString(tc.input), qt.DeepEquals, tc.expected) + c.Assert(err != nil, qt.Equals, tc.expectError) + }) + } +} + +type countingMapStringer struct { + value string + calls int +} + +func (s *countingMapStringer) String() string { + s.calls++ + return s.value +} + +func TestStringMapStringConvertsOnce(t *testing.T) { + c := qt.New(t) + key := &countingMapStringer{value: "key"} + value := &countingMapStringer{value: "value"} + input := map[any]any{key: value, "invalid": []any{1}, "good": "kept"} + + result, err := cast.ToStringMapStringE(input) + c.Assert(result, qt.DeepEquals, map[string]string{"key": "value", "invalid": "", "good": "kept"}) + c.Assert(err, qt.IsNotNil) + c.Assert(key.calls, qt.Equals, 1) + c.Assert(value.calls, qt.Equals, 1) +}