Skip to content

Commit f212120

Browse files
committed
Handle structs with unexported fields and all Go values better
1 parent 517dff0 commit f212120

File tree

2 files changed

+129
-38
lines changed

2 files changed

+129
-38
lines changed

map.go

Lines changed: 56 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -27,17 +27,23 @@ var _ json.Marshaler = Map(nil)
2727
//
2828
// Every field value is encoded with the following process:
2929
//
30-
// 1. slog.Value is handled to allow any type to replace its representation for logging.
30+
// 1. slog.Value is checked to allow any type to replace its representation for logging.
31+
//
32+
// 2. json.Marshaller is handled.
3133
//
3234
// 2. xerrors.Formatter is handled.
3335
//
34-
// 3. Protobufs are handled with json.Marshal.
36+
// 3. Protobufs are encoded with json.Marshal.
37+
//
38+
// 4. error and fmt.Stringer are used if possible.
3539
//
36-
// 4. error and fmt.Stringer are handled.
40+
// 5. slices and arrays go through the encode function for every element.
3741
//
38-
// 5. slices and arrays are handled to go through the encode function for every value.
42+
// 6. If the value is a struct without exported fields or a type that
43+
// cannot be encoded with json.Marshal (like channels) then
44+
// fmt.Sprintf("%+v") is used.
3945
//
40-
// 6. json.Marshal is invoked as the default case.
46+
// 8. json.Marshal(v) is used for all other values.
4147
func (m Map) MarshalJSON() ([]byte, error) {
4248
b := &bytes.Buffer{}
4349
b.WriteByte('{')
@@ -56,8 +62,11 @@ func (m Map) MarshalJSON() ([]byte, error) {
5662
return b.Bytes(), nil
5763
}
5864

59-
// ForceJSON ensures the value is logged via json.Marshal even
60-
// if it implements fmt.Stringer or error.
65+
// ForceJSON ensures the value is logged via json.Marshal.
66+
//
67+
// Use it when implementing SlogValue to ensure a structured
68+
// representation of a struct if you know it's capable even
69+
// when it implements fmt.Stringer or error.
6170
func ForceJSON(v interface{}) interface{} {
6271
return jsonVal{v: v}
6372
}
@@ -66,13 +75,6 @@ type jsonVal struct {
6675
v interface{}
6776
}
6877

69-
var _ json.Marshaler = jsonVal{}
70-
71-
// MarshalJSON implements json.Marshaler.
72-
func (v jsonVal) MarshalJSON() ([]byte, error) {
73-
return json.Marshal(v.v)
74-
}
75-
7678
func marshalList(rv reflect.Value) []byte {
7779
b := &bytes.Buffer{}
7880
b.WriteByte('[')
@@ -93,6 +95,10 @@ func encode(v interface{}) []byte {
9395
switch v := v.(type) {
9496
case Value:
9597
return encode(v.SlogValue())
98+
case json.Marshaler:
99+
return encodeJSON(v)
100+
case jsonVal:
101+
return encodeJSON(v.v)
96102
case xerrors.Formatter:
97103
return encode(errorChain(v))
98104
case interface {
@@ -103,28 +109,47 @@ func encode(v interface{}) []byte {
103109
return encode(fmt.Sprint(v))
104110
default:
105111
rv := reflect.Indirect(reflect.ValueOf(v))
106-
if rv.IsValid() {
107-
switch rv.Type().Kind() {
108-
case reflect.Slice:
109-
if rv.IsNil() {
110-
break
111-
}
112-
fallthrough
113-
case reflect.Array:
112+
if !rv.IsValid() {
113+
return encodeJSON(v)
114+
}
115+
116+
switch rv.Type().Kind() {
117+
case reflect.Slice:
118+
if !rv.IsNil() {
114119
return marshalList(rv)
115120
}
116-
}
121+
case reflect.Array:
122+
return marshalList(rv)
123+
case reflect.Struct:
124+
typ := rv.Type()
125+
for i := 0; i < rv.NumField(); i++ {
126+
// Found an exported field.
127+
if typ.Field(i).PkgPath == "" {
128+
return encodeJSON(v)
129+
}
130+
}
117131

118-
b, err := json.Marshal(v)
119-
if err != nil {
120-
return encode(M(
121-
Error(xerrors.Errorf("failed to marshal to JSON: %w", err)),
122-
F("type", reflect.TypeOf(v)),
123-
F("value", fmt.Sprintf("%+v", v)),
124-
))
132+
return encodeJSON(fmt.Sprintf("%+v", v))
133+
case reflect.Chan, reflect.Complex64, reflect.Complex128, reflect.Func:
134+
// These types cannot be directly encoded with json.Marshal.
135+
// See https://golang.org/pkg/encoding/json/#Marshal
136+
return encodeJSON(fmt.Sprintf("%+v", v))
125137
}
126-
return b
138+
139+
return encodeJSON(v)
140+
}
141+
}
142+
143+
func encodeJSON(v interface{}) []byte {
144+
b, err := json.Marshal(v)
145+
if err != nil {
146+
return encode(M(
147+
Error(xerrors.Errorf("failed to marshal to JSON: %w", err)),
148+
F("type", reflect.TypeOf(v)),
149+
F("value", fmt.Sprintf("%+v", v)),
150+
))
127151
}
152+
return b
128153
}
129154

130155
func errorChain(f xerrors.Formatter) []interface{} {

map_test.go

Lines changed: 73 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,11 @@ package slog_test
33
import (
44
"bytes"
55
"encoding/json"
6-
"fmt"
76
"io"
87
"runtime"
98
"strings"
109
"testing"
10+
"time"
1111

1212
"golang.org/x/xerrors"
1313

@@ -86,19 +86,19 @@ func TestMap(t *testing.T) {
8686
mapTestFile := strings.Replace(mapTestFile, "_test", "", 1)
8787

8888
test(t, slog.M(
89-
slog.F("meow", indentJSON),
89+
slog.F("meow", slog.ForceJSON(complex(10, 10))),
9090
), `{
9191
"meow": {
9292
"error": [
9393
{
9494
"msg": "failed to marshal to JSON",
95-
"fun": "cdr.dev/slog.encode",
96-
"loc": "`+mapTestFile+`:121"
95+
"fun": "cdr.dev/slog.encodeJSON",
96+
"loc": "`+mapTestFile+`:147"
9797
},
98-
"json: unsupported type: func(*testing.T, string) string"
98+
"json: unsupported type: complex128"
9999
],
100-
"type": "func(*testing.T, string) string",
101-
"value": "`+fmt.Sprint(interface{}(indentJSON))+`"
100+
"type": "complex128",
101+
"value": "(10+10i)"
102102
}
103103
}`)
104104
})
@@ -145,6 +145,24 @@ func TestMap(t *testing.T) {
145145
}`)
146146
})
147147

148+
t.Run("array", func(t *testing.T) {
149+
t.Parallel()
150+
151+
test(t, slog.M(
152+
slog.F("meow", [3]string{
153+
"1",
154+
"2",
155+
"3",
156+
}),
157+
), `{
158+
"meow": [
159+
"1",
160+
"2",
161+
"3"
162+
]
163+
}`)
164+
})
165+
148166
t.Run("forceJSON", func(t *testing.T) {
149167
t.Parallel()
150168

@@ -174,6 +192,54 @@ func TestMap(t *testing.T) {
174192
"slice": null
175193
}`)
176194
})
195+
196+
t.Run("nil", func(t *testing.T) {
197+
t.Parallel()
198+
199+
test(t, slog.M(
200+
slog.F("val", nil),
201+
), `{
202+
"val": null
203+
}`)
204+
})
205+
206+
t.Run("json.Marshaler", func(t *testing.T) {
207+
t.Parallel()
208+
209+
test(t, slog.M(
210+
slog.F("val", time.Date(2000, 02, 05, 4, 4, 4, 0, time.UTC)),
211+
), `{
212+
"val": "2000-02-05T04:04:04Z"
213+
}`)
214+
})
215+
216+
t.Run("complex", func(t *testing.T) {
217+
t.Parallel()
218+
219+
test(t, slog.M(
220+
slog.F("val", complex(10, 10)),
221+
), `{
222+
"val": "(10+10i)"
223+
}`)
224+
})
225+
226+
t.Run("privateStruct", func(t *testing.T) {
227+
t.Parallel()
228+
229+
test(t, slog.M(
230+
slog.F("val", struct {
231+
meow string
232+
bar int
233+
far uint
234+
}{
235+
meow: "hi",
236+
bar: 23,
237+
far: 600,
238+
}),
239+
), `{
240+
"val": "{meow:hi bar:23 far:600}"
241+
}`)
242+
})
177243
}
178244

179245
type meow struct {

0 commit comments

Comments
 (0)