From b729841db22d1acf22430feb23d5dbe71c71f406 Mon Sep 17 00:00:00 2001 From: Felipe Gasper Date: Fri, 10 Apr 2026 13:51:13 -0400 Subject: [PATCH] Stringify arrays, slices, and maps in greater detail; quote strings. --- encoding.go | 47 +++++++++++++++++++++- encoding_test.go | 102 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 148 insertions(+), 1 deletion(-) create mode 100644 encoding_test.go diff --git a/encoding.go b/encoding.go index 6088e65..4ec0cc6 100644 --- a/encoding.go +++ b/encoding.go @@ -1,10 +1,15 @@ package console import ( + "cmp" "fmt" "log/slog" "path/filepath" + "reflect" "runtime" + "slices" + "strconv" + "strings" "time" ) @@ -154,10 +159,50 @@ func (e encoder) writeValue(buf *buffer, value slog.Value) { case slog.KindString: fallthrough default: - e.writeColoredString(buf, value.String(), attrValue) + e.writeColoredString(buf, stringifyValue(value), attrValue) } } +func stringifyValue(val slog.Value) string { + refVal := reflect.ValueOf(val.Any()) + + if refVal.Kind() == reflect.Map { + keys := refVal.MapKeys() + + // Sort keys lexicographically by their string representation + slices.SortFunc(keys, func(i, j reflect.Value) int { + return cmp.Compare(i.String(), j.String()) + }) + + pieces := make([]string, len(keys)) + for i, keyVal := range keys { + key := keyVal.String() + mapVal := refVal.MapIndex(keyVal) + pieces[i] = key + "=" + stringifyValue(slog.AnyValue(mapVal.Interface())) + } + + return `{` + strings.Join(pieces, ", ") + `}` + } + + if refVal.Kind() == reflect.Slice || refVal.Kind() == reflect.Array { + pieces := make([]string, refVal.Len()) + for i := 0; i < refVal.Len(); i++ { + pieces[i] = stringifyValue(slog.AnyValue(refVal.Index(i).Interface())) + } + + return `[` + strings.Join(pieces, ", ") + `]` + } + + valStr := val.String() + + // Quote string values to disambiguate spaces + if val.Kind() == slog.KindString { + valStr = strconv.Quote(valStr) + } + + return valStr +} + func (e encoder) writeLevel(buf *buffer, l slog.Level) { var style ANSIMod var str string diff --git a/encoding_test.go b/encoding_test.go new file mode 100644 index 0000000..b3ef906 --- /dev/null +++ b/encoding_test.go @@ -0,0 +1,102 @@ +package console + +import ( + "log/slog" + "strings" + "testing" +) + +func TestStringifyValue_Maps(t *testing.T) { + tests := []struct { + name string + input any + expected string + }{ + { + name: "simple map", + input: map[string]int{"b": 2, "a": 1}, + expected: "a=1, b=2", // Should be sorted by key + }, + { + name: "nested map", + input: map[string]interface{}{"key": map[string]int{"inner": 42}}, + expected: "key={inner=42}", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + val := slog.AnyValue(tt.input) + result := stringifyValue(val) + + if !strings.Contains(result, tt.expected) { + t.Errorf("expected %q to contain %q", result, tt.expected) + } + }) + } +} + +func TestStringifyValue_Slices(t *testing.T) { + tests := []struct { + name string + input any + expected string + }{ + { + name: "int slice", + input: []int{1, 2, 3}, + expected: "[1, 2, 3]", + }, + { + name: "string slice", + input: []string{"a", "b", "c"}, + expected: `["a", "b", "c"]`, + }, + { + name: "empty slice", + input: []int{}, + expected: "[]", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + val := slog.AnyValue(tt.input) + result := stringifyValue(val) + + if result != tt.expected { + t.Errorf("expected %q, got %q", tt.expected, result) + } + }) + } +} + +func TestStringifyValue_Strings(t *testing.T) { + tests := []struct { + name string + input any + expected string + }{ + { + name: "plain string", + input: "hello", + expected: `"hello"`, + }, + { + name: "string with spaces", + input: "hello world", + expected: `"hello world"`, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + val := slog.StringValue(tt.input.(string)) + result := stringifyValue(val) + + if result != tt.expected { + t.Errorf("expected %q, got %q", tt.expected, result) + } + }) + } +}