Browse Source

Indent JSON in nebula ctl itself instead of through a localapi helper

Pretty had one caller and three tests of json.Indent's own behaviour. The
client indents the bytes in place and prints them as they came when they
will not indent; the key-order and precision point moves to its test.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Matt Richardson 1 ngày trước cách đây
mục cha
commit
7d5d8c2166
5 tập tin đã thay đổi với 19 bổ sung và 84 xóa
  1. 8 4
      cmd/internal/ctl/ctl.go
  2. 10 3
      cmd/internal/ctl/ctl_test.go
  3. 0 23
      localapi/client.go
  4. 0 53
      localapi/client_test.go
  5. 1 1
      localapi/protocol.go

+ 8 - 4
cmd/internal/ctl/ctl.go

@@ -1,6 +1,8 @@
 package ctl
 
 import (
+	"bytes"
+	"encoding/json"
 	"errors"
 	"flag"
 	"fmt"
@@ -184,16 +186,18 @@ func Run(args []string) int {
 // JSON request is answered in Result with the value itself, printed as it came, since a
 // value that happens to be a string is still JSON to the script reading it. Indentation
 // is the client's business: whitespace does not survive a JSON envelope, and the daemon
-// has no reason to care how its answer is laid out.
+// has no reason to care how its answer is laid out. The bytes are re-indented rather
+// than decoded and encoded again, which would sort the keys and round the numbers.
 func output(res *localapi.Response, format commands.Format) string {
 	switch format {
 	case commands.FormatJSON:
 		return string(res.Result)
 	case commands.FormatPrettyJSON:
-		if indented, ok := localapi.Pretty(res.Result); ok {
-			return indented
+		var buf bytes.Buffer
+		if err := json.Indent(&buf, res.Result, "", "    "); err != nil {
+			return string(res.Result)
 		}
-		return string(res.Result)
+		return buf.String()
 	default:
 		return string(res.Text)
 	}

+ 10 - 3
cmd/internal/ctl/ctl_test.go

@@ -191,9 +191,16 @@ func TestOutputRendersForTheFormatAsked(t *testing.T) {
 	assert.Equal(t, `"some string"`, output(str, commands.FormatJSON))
 	assert.Equal(t, `"some string"`, output(str, commands.FormatPrettyJSON))
 
-	obj := &localapi.Response{Result: json.RawMessage(`{"a":1}`)}
-	assert.Equal(t, `{"a":1}`, output(obj, commands.FormatJSON))
-	assert.Equal(t, "{\n    \"a\": 1\n}", output(obj, commands.FormatPrettyJSON))
+	// Keys stay in the order the daemon wrote them and a uint64 past 2^53 is not rounded,
+	// which decoding into `any` and encoding again would do.
+	obj := &localapi.Response{Result: json.RawMessage(`{"z":18446744073709551615,"a":{"y":[1,2],"b":true}}`)}
+	//nolint:testifylint // the layout is what is under test, which JSONEq would not see
+	assert.Equal(t, `{"z":18446744073709551615,"a":{"y":[1,2],"b":true}}`, output(obj, commands.FormatJSON))
+	//nolint:testifylint // as above
+	assert.Equal(t, "{\n    \"z\": 18446744073709551615,\n    \"a\": {\n        \"y\": [\n            1,\n            2\n        ],\n        \"b\": true\n    }\n}", output(obj, commands.FormatPrettyJSON))
+
+	bad := &localapi.Response{Result: json.RawMessage(`{"a":`)}
+	assert.Equal(t, `{"a":`, output(bad, commands.FormatPrettyJSON), "what cannot be indented is printed as it came")
 
 	none := &localapi.Response{}
 	assert.Empty(t, output(none, commands.FormatJSON))

+ 0 - 23
localapi/client.go

@@ -1,7 +1,6 @@
 package localapi
 
 import (
-	"bytes"
 	"encoding/json"
 	"errors"
 	"fmt"
@@ -162,25 +161,3 @@ func readResponse(conn net.Conn) (*Response, error) {
 
 	return res, nil
 }
-
-// Pretty renders a JSON Result indented for a human reader, reporting false when the
-// result is empty or is not JSON and there is nothing to indent. A value that is a JSON
-// string has nothing to indent either, and comes back as it is, quotes and all: the
-// caller asked for JSON and that is what the value is.
-//
-// The bytes are re-indented rather than decoded and re-encoded: decoding into `any`
-// sorts object keys and routes every number through float64, so the layout would
-// differ from what the daemon wrote and from the ssh -pretty path.
-func Pretty(raw json.RawMessage) (string, bool) {
-	trimmed := bytes.TrimSpace(raw)
-	if len(trimmed) == 0 {
-		return "", false
-	}
-
-	var buf bytes.Buffer
-	if err := json.Indent(&buf, trimmed, "", "    "); err != nil {
-		return "", false
-	}
-
-	return buf.String(), true
-}

+ 0 - 53
localapi/client_test.go

@@ -1,53 +0,0 @@
-package localapi
-
-import (
-	"encoding/json"
-	"testing"
-
-	"github.com/stretchr/testify/assert"
-)
-
-func TestPrettyPreservesKeyOrderAndPrecision(t *testing.T) {
-	// Keys arrive in the order the daemon wrote them and a uint64 past 2^53 must not be
-	// rounded, so the output has to come from re-indenting the bytes rather than from
-	// decoding into `any` and encoding again.
-	raw := json.RawMessage(`{"z":18446744073709551615,"a":{"y":[1,2],"b":true},"m":"text"}`)
-
-	out, ok := Pretty(raw)
-	assert.True(t, ok)
-	//nolint:testifylint // the layout is what is under test, which JSONEq would not see
-	assert.Equal(t, `{
-    "z": 18446744073709551615,
-    "a": {
-        "y": [
-            1,
-            2
-        ],
-        "b": true
-    },
-    "m": "text"
-}`, out)
-}
-
-func TestPrettyKeepsAStringValueQuoted(t *testing.T) {
-	// A value that is a JSON string is still JSON to whoever asked for it: it has nothing
-	// to indent and comes back as it is, never unquoted.
-	out, ok := Pretty(json.RawMessage(`"some string"`))
-	assert.True(t, ok)
-	assert.Equal(t, `"some string"`, out)
-
-	out, ok = Pretty(json.RawMessage(`  "padded"`))
-	assert.True(t, ok)
-	assert.Equal(t, `"padded"`, out)
-}
-
-func TestPrettyRejectsEmptyAndInvalid(t *testing.T) {
-	_, ok := Pretty(nil)
-	assert.False(t, ok)
-
-	_, ok = Pretty(json.RawMessage("   "))
-	assert.False(t, ok)
-
-	_, ok = Pretty(json.RawMessage(`{"a":`))
-	assert.False(t, ok)
-}

+ 1 - 1
localapi/protocol.go

@@ -77,7 +77,7 @@ type Response struct {
 	//
 	// Whitespace inside a JSON result is not preserved: an envelope normalises it, and
 	// in an API it carries no meaning anyway. Formatting a result for a human to read is
-	// the client's job, which is what Pretty is for.
+	// the client's job.
 	Result json.RawMessage `json:"result,omitempty"`
 
 	// Text is what a request for text produced: the command's own human rendering, the