From e92a06bc7658498428f7e98b2d9cb05f79bb59d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=96rjan=20Fors?= Date: Sun, 12 May 2024 22:04:31 +0200 Subject: [PATCH] feat: add color to json output --- json.go | 120 ++++++++++++++++++++++++++++++++++++++------------- json_test.go | 26 +++++------ 2 files changed, 105 insertions(+), 41 deletions(-) diff --git a/json.go b/json.go index f8aaa21..c4fc33b 100644 --- a/json.go +++ b/json.go @@ -5,10 +5,12 @@ import ( "encoding/json" "fmt" "time" + + "github.com/charmbracelet/lipgloss" ) func (l *Logger) jsonFormatter(keyvals ...interface{}) { - jw := &jsonWriter{w: &l.b} + jw := &jsonWriter{w: &l.b, r: l.re, s: l.styles.Separator} jw.start() i := 0 @@ -33,87 +35,114 @@ func (l *Logger) jsonFormatterRoot(jw *jsonWriter, key, value any) { switch key { case TimestampKey: if t, ok := value.(time.Time); ok { - jw.objectItem(TimestampKey, t.Format(l.timeFormat)) + jw.objectItem(l.styles.Key, TimestampKey, l.styles.Timestamp, t.Format(l.timeFormat)) } case LevelKey: if level, ok := value.(Level); ok { - jw.objectItem(LevelKey, level.String()) + ls, ok := l.styles.Levels[level] + if ok { + jw.objectItem(l.styles.Key, LevelKey, ls, level.String()) + } } case CallerKey: if caller, ok := value.(string); ok { - jw.objectItem(CallerKey, caller) + jw.objectItem(l.styles.Key, CallerKey, l.styles.Caller, caller) } case PrefixKey: if prefix, ok := value.(string); ok { - jw.objectItem(PrefixKey, prefix) + jw.objectItem(l.styles.Key, PrefixKey, l.styles.Prefix, prefix) } case MessageKey: if msg := value; msg != nil { - jw.objectItem(MessageKey, fmt.Sprint(msg)) + jw.objectItem(l.styles.Key, MessageKey, l.styles.Message, fmt.Sprint(msg)) } default: - l.jsonFormatterItem(jw, key, value) + l.jsonFormatterItem(jw, 0, l.styles.Key, key, l.styles.Value, value) } } -func (l *Logger) jsonFormatterItem(jw *jsonWriter, key, value any) { - switch k := key.(type) { +func (l *Logger) jsonFormatterItem( + jw *jsonWriter, d int, ks lipgloss.Style, anyKey any, vs lipgloss.Style, value any, +) { + var key string + switch k := anyKey.(type) { case fmt.Stringer: - jw.objectKey(k.String()) + key = k.String() case error: - jw.objectKey(k.Error()) + key = k.Error() default: - jw.objectKey(fmt.Sprint(k)) + key = fmt.Sprint(k) } + + // override styles based on root key + if d == 0 { + if s, ok := l.styles.Keys[key]; ok { + ks = s + } + if s, ok := l.styles.Values[key]; ok { + vs = s + } + } + + jw.objectKey(ks, key) + switch v := value.(type) { case error: - jw.objectValue(v.Error()) + jw.objectValue(vs, v.Error()) case slogLogValuer: - l.writeSlogValue(jw, v.LogValue()) + l.writeSlogValue(jw, d, ks, vs, v.LogValue()) case slogValue: - l.writeSlogValue(jw, v.Resolve()) + l.writeSlogValue(jw, d, ks, vs, v.Resolve()) case fmt.Stringer: - jw.objectValue(v.String()) + jw.objectValue(vs, v.String()) default: - jw.objectValue(v) + jw.objectValue(vs, v) } } -func (l *Logger) writeSlogValue(jw *jsonWriter, v slogValue) { +func (l *Logger) writeSlogValue(jw *jsonWriter, depth int, ks, vs lipgloss.Style, v slogValue) { switch v.Kind() { case slogKindGroup: jw.start() for _, attr := range v.Group() { - l.jsonFormatterItem(jw, attr.Key, attr.Value) + l.jsonFormatterItem(jw, depth+1, ks, attr.Key, vs, attr.Value) } jw.end() default: - jw.objectValue(v.Any()) + jw.objectValue(vs, v.Any()) } } type jsonWriter struct { w *bytes.Buffer + r *lipgloss.Renderer + s lipgloss.Style d int } func (w *jsonWriter) start() { - w.w.WriteRune('{') + objectStart := w.s.Renderer(w.r).Render("{") + w.w.WriteString(objectStart) w.d = 0 } func (w *jsonWriter) end() { - w.w.WriteRune('}') + objectEnd := w.s.Renderer(w.r).Render("}") + w.w.WriteString(objectEnd) } -func (w *jsonWriter) objectItem(key string, value any) { - w.objectKey(key) - w.objectValue(value) +func (w *jsonWriter) objectItem( + ks lipgloss.Style, key string, + vs lipgloss.Style, value any, +) { + w.objectKey(ks, key) + w.objectValue(vs, value) } -func (w *jsonWriter) objectKey(key string) { +func (w *jsonWriter) objectKey(s lipgloss.Style, key string) { if w.d > 0 { - w.w.WriteRune(',') + itemSep := w.s.Renderer(w.r).Render(",") + w.w.WriteString(itemSep) } w.d++ @@ -123,16 +152,49 @@ func (w *jsonWriter) objectKey(key string) { w.w.Truncate(pos) w.w.WriteString(`"invalid key"`) } - w.w.WriteRune(':') + + // re-apply value with style + w.renderStyle(s, pos) + + valSep := w.s.Renderer(w.r).Render(`:`) + w.w.WriteString(valSep) } -func (w *jsonWriter) objectValue(value any) { +func (w *jsonWriter) objectValue(s lipgloss.Style, value any) { pos := w.w.Len() err := w.writeEncoded(value) if err != nil { w.w.Truncate(pos) w.w.WriteString(`"invalid value"`) } + + // re-apply value with style + w.renderStyle(s, pos) +} + +// renderStyle applies the given style to the string at the given position. +func (w *jsonWriter) renderStyle(st lipgloss.Style, pos int) { + s := w.w.String()[pos:] + + // manually apply quotes + sep := "" + if len(s) > 2 && s[0] == '"' && s[len(s)-1] == '"' { + s = s[1 : len(s)-1] // apply style within quotes + sep = w.s.Renderer(w.r).Render(`"`) + } else if st.String() != "" { + sep = w.s.Renderer(w.r).Render(`"`) + } + + // render with style + s = st.Renderer(w.r).Render(s) + + // rewind + w.w.Truncate(pos) + + // re-apply with colors + w.w.WriteString(sep) + w.w.WriteString(s) + w.w.WriteString(sep) } func (w *jsonWriter) writeEncoded(v any) error { diff --git a/json_test.go b/json_test.go index 99fc3fa..a52ffdd 100644 --- a/json_test.go +++ b/json_test.go @@ -8,6 +8,7 @@ import ( "runtime" "testing" + "github.com/charmbracelet/lipgloss" "github.com/stretchr/testify/require" ) @@ -200,6 +201,7 @@ func TestJsonCustomKey(t *testing.T) { } func TestJsonWriter(t *testing.T) { + noStyle := lipgloss.NewStyle() testCases := []struct { name string fn func(w *jsonWriter) @@ -209,7 +211,7 @@ func TestJsonWriter(t *testing.T) { "string", func(w *jsonWriter) { w.start() - w.objectItem("a", "value") + w.objectItem(noStyle, "a", noStyle, "value") w.end() }, `{"a":"value"}`, @@ -218,7 +220,7 @@ func TestJsonWriter(t *testing.T) { "int", func(w *jsonWriter) { w.start() - w.objectItem("a", 123) + w.objectItem(noStyle, "a", noStyle, 123) w.end() }, `{"a":123}`, @@ -227,7 +229,7 @@ func TestJsonWriter(t *testing.T) { "bytes", func(w *jsonWriter) { w.start() - w.objectItem("b", []byte{0x0, 0x1}) + w.objectItem(noStyle, "b", noStyle, []byte{0x0, 0x1}) w.end() }, `{"b":"AAE="}`, @@ -244,8 +246,8 @@ func TestJsonWriter(t *testing.T) { "multiple in asc order", func(w *jsonWriter) { w.start() - w.objectItem("a", "value") - w.objectItem("b", "some-other") + w.objectItem(noStyle, "a", noStyle, "value") + w.objectItem(noStyle, "b", noStyle, "some-other") w.end() }, `{"a":"value","b":"some-other"}`, @@ -254,8 +256,8 @@ func TestJsonWriter(t *testing.T) { "multiple in desc order", func(w *jsonWriter) { w.start() - w.objectItem("b", "some-other") - w.objectItem("a", "value") + w.objectItem(noStyle, "b", noStyle, "some-other") + w.objectItem(noStyle, "a", noStyle, "value") w.end() }, `{"b":"some-other","a":"value"}`, @@ -264,7 +266,7 @@ func TestJsonWriter(t *testing.T) { "depth", func(w *jsonWriter) { w.start() - w.objectItem("a", map[string]int{"b": 123}) + w.objectItem(noStyle, "a", noStyle, map[string]int{"b": 123}) w.end() }, `{"a":{"b":123}}`, @@ -273,7 +275,7 @@ func TestJsonWriter(t *testing.T) { "key contains reserved", func(w *jsonWriter) { w.start() - w.objectItem("a:\"b", "value") + w.objectItem(noStyle, "a:\"b", noStyle, "value") w.end() }, `{"a:\"b":"value"}`, @@ -282,7 +284,7 @@ func TestJsonWriter(t *testing.T) { "pointer", func(w *jsonWriter) { w.start() - w.objectItem("a", ptr("pointer")) + w.objectItem(noStyle, "a", noStyle, ptr("pointer")) w.end() }, `{"a":"pointer"}`, @@ -291,7 +293,7 @@ func TestJsonWriter(t *testing.T) { "double-pointer", func(w *jsonWriter) { w.start() - w.objectItem("a", ptr(ptr("pointer"))) + w.objectItem(noStyle, "a", noStyle, ptr(ptr("pointer"))) w.end() }, `{"a":"pointer"}`, @@ -300,7 +302,7 @@ func TestJsonWriter(t *testing.T) { "invalid", func(w *jsonWriter) { w.start() - w.objectItem("a", invalidJSON{}) + w.objectItem(noStyle, "a", noStyle, invalidJSON{}) w.end() }, `{"a":"invalid value"}`,