diff --git a/pkg/printer/formatter.go b/pkg/printer/formatter.go index c0aac10..1db3a2b 100644 --- a/pkg/printer/formatter.go +++ b/pkg/printer/formatter.go @@ -1,10 +1,10 @@ package printer import ( - "encoding/json" "fmt" "net/http" "sort" + "strconv" "strings" "time" @@ -28,7 +28,7 @@ func (f Formatter) RequestLine(line result.RequestLine) string { } func (f Formatter) Json(j map[string]any) string { - return formatJson(j) + return formatJson(j, f.colored) } func formatHeaders(headers http.Header, colorized bool) string { @@ -113,10 +113,128 @@ func formatRequestLine(line result.RequestLine, colorized bool) string { return fmt.Sprintf(fstring, Colorize(line.Method, methodColor), line.Url, line.Protocol) } -func formatJson(j map[string]any) string { - if prettyJson, err := json.MarshalIndent(j, "", " "); err != nil { - return fmt.Sprintf("unable to parse json: %v\n", err) - } else { - return string(prettyJson) +func formatJson(j map[string]any, colorized bool) string { + var f func(inner map[string]any, level int) string + + var formatValue func(value any, level int) string + + f = func(inner map[string]any, level int) string { + sb := strings.Builder{} + sb.WriteString("{") + + keys := []string{} + for k := range inner { + keys = append(keys, k) + } + + sort.Strings(keys) + + padding := strings.Repeat(" ", level+1) + + for i, k := range keys { + sb.WriteString("\n") + + value := inner[k] + isLast := i == len(keys)-1 + + sb.WriteString(padding) + sb.WriteString(formatJsonKey(k, colorized)) + + sb.WriteString(formatValue(value, level)) + + if !isLast { + sb.WriteString(",") + } else { + sb.WriteString("\n") + } + } + + padding = strings.Repeat(" ", level) + if sb.Len() != 1 { + sb.WriteString(padding) + } + + sb.WriteString("}") + + return sb.String() + } + + formatValue = func(value any, level int) string { + switch v := value.(type) { + case string: + return formatJsonStringValue(v, colorized) + case int, float64: + return formatJsonNumberValue(v, colorized) + case bool: + return formatJsonBoolValue(v, colorized) + case nil: + return formatJsonNilValue(colorized) + case map[string]any: + return f(v, level+1) + case []any: + sb := strings.Builder{} + sb.WriteString("[") + + for i, vv := range v { + sb.WriteString(formatValue(vv, level)) + + if i != len(v)-1 { + sb.WriteString(", ") + } + } + + sb.WriteString("]") + + return sb.String() + } + + return "" } + + return f(j, 0) +} + +func formatJsonKey(s string, colorized bool) string { + fstring := `%s: ` + value := strconv.Quote(s) + + if !colorized { + return fmt.Sprintf(fstring, value) + } + + return fmt.Sprintf(fstring, Yellow(value)) +} + +func formatJsonStringValue(s string, colorized bool) string { + if !colorized { + return strconv.Quote(s) + } + + return Green(strconv.Quote(s)) +} + +func formatJsonNumberValue(n any, colorized bool) string { + s := fmt.Sprintf("%v", n) + if !colorized { + return s + } + + return Cyan(s) +} + +func formatJsonBoolValue(b bool, colorized bool) string { + s := fmt.Sprintf("%v", b) + if !colorized { + return s + } + + return Magenta(s) +} + +func formatJsonNilValue(colorized bool) string { + if !colorized { + return "null" + } + + return Red("null") } diff --git a/pkg/printer/formatter_test.go b/pkg/printer/formatter_test.go index c244c1d..a4feee4 100644 --- a/pkg/printer/formatter_test.go +++ b/pkg/printer/formatter_test.go @@ -174,9 +174,10 @@ func Test_formatRequestLine(t *testing.T) { func Test_formatJson(t *testing.T) { tests := []struct { - name string - json map[string]any - want string + name string + json map[string]any + useColor bool + want string }{ { name: "simple", @@ -184,7 +185,8 @@ func Test_formatJson(t *testing.T) { "name": "Alice", "age": 30, }, - want: "{\n \"age\": 30,\n \"name\": \"Alice\"\n}", + useColor: false, + want: "{\n \"age\": 30,\n \"name\": \"Alice\"\n}", }, { name: "nested", @@ -194,25 +196,76 @@ func Test_formatJson(t *testing.T) { "name": "Bob", }, }, - want: "{\n \"user\": {\n \"id\": 1,\n \"name\": \"Bob\"\n }\n}", + useColor: false, + want: "{\n \"user\": {\n \"id\": 1,\n \"name\": \"Bob\"\n }\n}", + }, + { + name: "empty", + json: map[string]any{}, + useColor: false, + want: "{}", + }, + { + name: "colored int", + json: map[string]any{ + "int": 1, + }, + useColor: true, + want: fmt.Sprintf("{\n %v: %v\n}", Yellow(`"int"`), Cyan("1")), + }, + { + name: "colored string", + json: map[string]any{ + "string": "Hello, World", + }, + useColor: true, + want: fmt.Sprintf("{\n %v: %v\n}", Yellow(`"string"`), Green(`"Hello, World"`)), }, { - name: "empty", - json: map[string]any{}, - want: "{}", + name: "colored boolean", + json: map[string]any{ + "boolean": true, + }, + useColor: true, + want: fmt.Sprintf("{\n %v: %v\n}", Yellow(`"boolean"`), Magenta("true")), }, { - name: "unmarshalable value (channel)", + name: "colored float", json: map[string]any{ - "invalid": make(chan int), + "float": 1.23, }, - want: "unable to parse json: json: unsupported type: chan int\n", + useColor: true, + want: fmt.Sprintf("{\n %v: %v\n}", Yellow(`"float"`), Cyan("1.23")), + }, + { + name: "colored nil", + json: map[string]any{ + "nil": nil, + }, + useColor: true, + want: fmt.Sprintf("{\n %v: %v\n}", Yellow(`"nil"`), Red("null")), + }, + { + name: "colored list", + json: map[string]any{ + "list": []any{1, "string", false, nil, map[string]any{"one": 1}}, + }, + useColor: true, + want: fmt.Sprintf("{\n %v: [%v, %v, %v, %v, {\n %v: %v\n }]\n}", + Yellow(`"list"`), + Cyan("1"), + Green(`"string"`), + Magenta("false"), + Red("null"), + Yellow(`"one"`), + Cyan("1"), + ), }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - formatter := Formatter{} + formatter := Formatter{colored: tt.useColor} got := formatter.Json(tt.json) assert.Equal(t, tt.want, got) })