feat(render): default JSON-model renderer + bright-white values (v1.4.0)

Render(v) treats JSON as the model: in -json mode it prints v as JSON;
otherwise it paints it as text — object scalars on one line as key=value
(keys blue, values bright white: a structural key/value distinction, not
semantic color), nested objects indented, arrays one block per element,
field order preserved via an order-keeping JSON decode. EmitJSON(v) is
the JSON-only arm for commands that paint their own text. Operates on
JSON only, so core stays protobuf-free (NFR-4).

Adds White to the palette. The example gains an `inspect` command
demoing Render (text vs -json). design.md FR-4.5/4.6 document the
renderer and the "JSON is always the full record; synopsis-vs-detail is
text-only" principle.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-05 23:13:42 +02:00
parent 496557858d
commit d35e1f2832
5 changed files with 333 additions and 1 deletions
+92
View File
@@ -0,0 +1,92 @@
// SPDX-FileCopyrightText: (C) Copyright 2026 Pim van Pelt <pim@ipng.ch>
// SPDX-License-Identifier: Apache-2.0
package cli
import (
"encoding/json"
"strings"
"testing"
)
// TestRenderTextNoColor checks the plain text shape: object scalars on one line
// as key=value, nested objects indented under "key:", arrays as one block per
// element, and source field order preserved (not sorted).
func TestRenderTextNoColor(t *testing.T) {
SetColor(false)
raw := json.RawMessage(`{
"instanceId": "host1",
"connected": true,
"version": "1.2.3",
"labels": {"site": "ams", "rack": "b3"},
"bvis": [
{"evpnId": "blue", "installed": true},
{"evpnId": "red", "installed": false}
]
}`)
got, err := renderText(raw)
if err != nil {
t.Fatalf("renderText: %v", err)
}
want := "instanceId=host1 connected=true version=1.2.3\n" +
"labels:\n" +
" site=ams rack=b3\n" +
"bvis:\n" +
" evpnId=blue installed=true\n" +
" evpnId=red installed=false\n"
if got != want {
t.Errorf("renderText mismatch:\n--- got ---\n%s\n--- want ---\n%s", got, want)
}
}
// TestRenderTextColor checks keys are blue and values bright white when color is
// on.
func TestRenderTextColor(t *testing.T) {
SetColor(true)
defer SetColor(false)
got, err := renderText(json.RawMessage(`{"a": "x"}`))
if err != nil {
t.Fatalf("renderText: %v", err)
}
want := Blue + "a" + Reset + "=" + White + "x" + Reset + "\n"
if got != want {
t.Errorf("colored render = %q, want %q", got, want)
}
}
// TestRenderPreservesNumberAndNull checks json.Number passes through verbatim
// (no float reformatting) and null renders as "null".
func TestRenderPreservesNumberAndNull(t *testing.T) {
SetColor(false)
got, err := renderText(json.RawMessage(`{"vni": 10000000, "primary": null}`))
if err != nil {
t.Fatalf("renderText: %v", err)
}
if !strings.Contains(got, "vni=10000000") {
t.Errorf("number not verbatim: %q", got)
}
if !strings.Contains(got, "primary=null") {
t.Errorf("null not rendered: %q", got)
}
}
// TestRenderTextFromStruct checks a Go struct (not RawMessage) is accepted and
// keeps struct field order.
func TestRenderTextFromStruct(t *testing.T) {
SetColor(false)
v := struct {
Name string `json:"name"`
Count int `json:"count"`
}{Name: "web1", Count: 3}
raw, err := toRawJSON(v)
if err != nil {
t.Fatalf("toRawJSON: %v", err)
}
got, err := renderText(raw)
if err != nil {
t.Fatalf("renderText: %v", err)
}
if got != "name=web1 count=3\n" {
t.Errorf("struct render = %q", got)
}
}