Skip to content

Commit 613aaab

Browse files
authored
Merge pull request #1 from cdr/init
Init
2 parents 92de874 + 9e8d0f1 commit 613aaab

20 files changed

+1571
-0
lines changed

cmp_test.go

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
package slog
2+
3+
import (
4+
"github.com/google/go-cmp/cmp"
5+
"reflect"
6+
)
7+
8+
// https://github.com/google/go-cmp/issues/40#issuecomment-328615283
9+
// Copied from https://github.com/nhooyr/websocket/blob/1b874731eab56c69c8bb3ebf8a029020c7863fc9/cmp_test.go
10+
func cmpDiff(exp, act interface{}) string {
11+
return cmp.Diff(exp, act, deepAllowUnexported(exp, act))
12+
}
13+
14+
func deepAllowUnexported(vs ...interface{}) cmp.Option {
15+
m := make(map[reflect.Type]struct{})
16+
for _, v := range vs {
17+
structTypes(reflect.ValueOf(v), m)
18+
}
19+
var typs []interface{}
20+
for t := range m {
21+
typs = append(typs, reflect.New(t).Elem().Interface())
22+
}
23+
return cmp.AllowUnexported(typs...)
24+
}
25+
26+
func structTypes(v reflect.Value, m map[reflect.Type]struct{}) {
27+
if !v.IsValid() {
28+
return
29+
}
30+
switch v.Kind() {
31+
case reflect.Ptr:
32+
if !v.IsNil() {
33+
structTypes(v.Elem(), m)
34+
}
35+
case reflect.Interface:
36+
if !v.IsNil() {
37+
structTypes(v.Elem(), m)
38+
}
39+
case reflect.Slice, reflect.Array:
40+
for i := 0; i < v.Len(); i++ {
41+
structTypes(v.Index(i), m)
42+
}
43+
case reflect.Map:
44+
for _, k := range v.MapKeys() {
45+
structTypes(v.MapIndex(k), m)
46+
}
47+
case reflect.Struct:
48+
m[v.Type()] = struct{}{}
49+
for i := 0; i < v.NumField(); i++ {
50+
structTypes(v.Field(i), m)
51+
}
52+
}
53+
}

console.go

Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
package slog
2+
3+
import (
4+
"strconv"
5+
"strings"
6+
)
7+
8+
// consoleMarshaller marshals a fieldValue into a human readable format.
9+
type consoleMarshaller struct {
10+
out strings.Builder
11+
indentstr string
12+
}
13+
14+
func marshalFields(v fieldMap) string {
15+
var y consoleMarshaller
16+
y.marshal(v)
17+
return y.out.String()
18+
}
19+
20+
func (y *consoleMarshaller) line() {
21+
y.out.WriteString("\n" + y.indentstr)
22+
}
23+
24+
func (y *consoleMarshaller) s(s string) {
25+
y.out.WriteString(s)
26+
}
27+
28+
func (y *consoleMarshaller) indent() {
29+
// 2 works really well for yaml because of arrays.
30+
// E.g. if you have a map inside of an list it looks like:
31+
//
32+
// - field: val
33+
// field: val
34+
//
35+
// See how the second field automatically gets indented to the correct level?
36+
// We can use \t later and special case but for now this is simpler.
37+
y.indentstr += " "
38+
}
39+
40+
func (y *consoleMarshaller) unindent() {
41+
y.indentstr = y.indentstr[:len(y.indentstr)-2]
42+
}
43+
44+
func (y *consoleMarshaller) marshal(v fieldValue) {
45+
switch v := v.(type) {
46+
case fieldString:
47+
// Ensures indentation.
48+
y.indent()
49+
// Replaces every newline with a newline plus the correct indentation.
50+
y.s(strings.ReplaceAll(string(v), "\n", "\n"+y.indentstr))
51+
y.unindent()
52+
case fieldBool:
53+
y.s(strconv.FormatBool(bool(v)))
54+
case fieldFloat:
55+
y.s(strconv.FormatFloat(float64(v), 'f', -1, 64))
56+
case fieldInt:
57+
y.s(strconv.FormatInt(int64(v), 10))
58+
case fieldUint:
59+
y.s(strconv.FormatUint(uint64(v), 10))
60+
case fieldMap:
61+
for i, f := range v {
62+
if i > 0 {
63+
// Add newline before every field except first.
64+
y.line()
65+
}
66+
67+
y.s(quote(f.name) + ":")
68+
69+
y.marshalSub(f.value, true)
70+
}
71+
case fieldList:
72+
y.indent()
73+
for _, v := range v {
74+
y.line()
75+
y.s("-")
76+
77+
if _, ok := v.(fieldList); !ok {
78+
// Non list values begin with the -.
79+
y.s(" ")
80+
}
81+
y.marshalSub(v, false)
82+
}
83+
y.unindent()
84+
case nil:
85+
y.s("null")
86+
default:
87+
panicf("unknown fieldValue kind of type %T and value %#v", y, y)
88+
}
89+
}
90+
91+
func (y *consoleMarshaller) marshalSub(v fieldValue, isParentMap bool) {
92+
switch v := v.(type) {
93+
case fieldMap:
94+
if isParentMap && len(v) == 0 {
95+
// Nothing to output for this field. Without this line, we get additional newlines due to below code as
96+
// the map field is expected to start on the next line given it is in a parentMap.
97+
// See the emptyStruct test.
98+
return
99+
}
100+
101+
y.indent()
102+
103+
if isParentMap {
104+
// If we are not inside a list and the value is a map, we need a newline.
105+
// In other words, if we are inside a list and the value is a map, we want to start
106+
// it with the `-` of the list.
107+
y.line()
108+
}
109+
case fieldList:
110+
default:
111+
if isParentMap {
112+
// Non map and non list values in structs begin on the same line with a space between the key and value.
113+
y.s(" ")
114+
}
115+
}
116+
117+
y.marshal(v)
118+
119+
if _, ok := v.(fieldMap); ok {
120+
y.unindent()
121+
}
122+
}
123+
124+
// quotes quotes a string so that it is suitable
125+
// as a key for a map or in general some output that
126+
// cannot span multiple lines or have weird characters.
127+
func quote(key string) string {
128+
// strconv.Quote does not quote an empty string so we need this.
129+
if key == "" {
130+
return `""`
131+
}
132+
133+
// Replace spaces in the map keys with underscores.
134+
key = strings.ReplaceAll(key, " ", "_")
135+
136+
quoted := strconv.Quote(key)
137+
// If the key doesn't need to be quoted, don't quote it.
138+
// We do not use strconv.CanBackquote because it doesn't
139+
// account tabs.
140+
if quoted[1:len(quoted)-1] == key {
141+
return key
142+
}
143+
return quoted
144+
}

console_test.go

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
package slog
2+
3+
import (
4+
"reflect"
5+
"testing"
6+
)
7+
8+
func Test_marshalFields(t *testing.T) {
9+
t.Parallel()
10+
11+
testCases := []struct {
12+
name string
13+
in map[string]interface{}
14+
out string
15+
}{
16+
{
17+
name: "stringWithNewlines",
18+
in: map[string]interface{}{
19+
"a": `hi
20+
two
21+
three`,
22+
},
23+
out: `a: hi
24+
two
25+
three`,
26+
},
27+
{
28+
name: "bool",
29+
in: map[string]interface{}{
30+
"a": false,
31+
},
32+
out: `a: false`,
33+
},
34+
{
35+
name: "float",
36+
in: map[string]interface{}{
37+
"a": 0.3,
38+
},
39+
out: `a: 0.3`,
40+
},
41+
{
42+
name: "int",
43+
in: map[string]interface{}{
44+
"a": -1,
45+
},
46+
out: `a: -1`,
47+
},
48+
{
49+
name: "uint",
50+
in: map[string]interface{}{
51+
"a": uint(3),
52+
},
53+
out: `a: 3`,
54+
},
55+
{
56+
name: "list",
57+
in: map[string]interface{}{
58+
"a": []interface{}{
59+
map[string]interface{}{
60+
"hi": "hello",
61+
"hi3": "hello",
62+
},
63+
"3",
64+
[]string{"a", "b", "c"},
65+
},
66+
},
67+
out: `a:
68+
- hi: hello
69+
hi3: hello
70+
- 3
71+
-
72+
- a
73+
- b
74+
- c`,
75+
},
76+
{
77+
name: "emptyStruct",
78+
in: map[string]interface{}{
79+
"a": struct{}{},
80+
"b": struct{}{},
81+
"c": struct{}{},
82+
},
83+
out: `a:
84+
b:
85+
c:`,
86+
},
87+
{
88+
name: "nestedMap",
89+
in: map[string]interface{}{
90+
"a": map[string]string{
91+
"0": "hi",
92+
"1": "hi",
93+
},
94+
},
95+
out: `a:
96+
0: hi
97+
1: hi`,
98+
},
99+
{
100+
name: "specialCharacterKey",
101+
in: map[string]interface{}{
102+
"nhooyr \tsoftware™️": "hi",
103+
"\rxeow\r": `mdsla
104+
dsamkld`,
105+
},
106+
out: `"\rxeow\r": mdsla
107+
dsamkld
108+
"nhooyr_\tsoftware™️": hi`,
109+
},
110+
}
111+
112+
for _, tc := range testCases {
113+
tc := tc
114+
t.Run(tc.name, func(t *testing.T) {
115+
t.Parallel()
116+
117+
v := reflectFieldValue(reflect.ValueOf(tc.in))
118+
actOut := marshalFields(v.(fieldMap))
119+
t.Logf("yaml:\n%v", actOut)
120+
if diff := cmpDiff(tc.out, actOut); diff != "" {
121+
t.Fatalf("unexpected output: %v", diff)
122+
}
123+
})
124+
}
125+
}

0 commit comments

Comments
 (0)