d9a8ca6fb8
Enable App.JSON and add -json on golang-cli v1.4.0, mirroring evpnc so
the two CLIs' JSON contract is identical: show/query -> data, set/action
-> {}, failure -> {"error": "..."}.
- show/query commands branch on cli.IsJSON() -> emit the protobuf via
cli.EmitJSON; text keeps the tabwriter painters (which robot tests
parse via show, not via setter output).
- action commands (pause/resume/enable/disable, set weight, sync,
config reload) are now silent on success in text too — "we did what
you asked" needs no confirmation — and print "{}" in JSON via wrapJSON.
- config check stays informative (it is a query): text "config ok" /
error, JSON the CheckConfig report.
- errors: formatError returns {"error": "..."} in JSON mode.
- watch streams its own JSON events (no trailing {}).
Robot tests assert backend state via `show`, not setter stdout, so the
dropped confirmations don't affect them. Builds on linux and openbsd.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
87 lines
2.6 KiB
Go
87 lines
2.6 KiB
Go
// SPDX-License-Identifier: Apache-2.0
|
|
|
|
package main
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
|
|
"google.golang.org/protobuf/encoding/protojson"
|
|
"google.golang.org/protobuf/proto"
|
|
|
|
cli "git.ipng.ch/ipng/golang-cli"
|
|
"git.ipng.ch/ipng/vpp-maglev/internal/grpcapi"
|
|
)
|
|
|
|
// JSON output (-json). The protobuf response is the model: in JSON mode each
|
|
// command emits it via the shared cli.EmitJSON (protojson field names/enums); in
|
|
// text mode it keeps its curated tabwriter painter (which robot tests parse, and
|
|
// which carries column alignment JSON does not need). A few commands print only
|
|
// a confirmation in text mode — those emit the returned proto in JSON, or, when
|
|
// there is nothing to return, rely on wrapJSON to print "{}". Errors render as
|
|
// {"error": "..."} (see color.go).
|
|
|
|
// jsonEmitted records whether the current command produced JSON output of its
|
|
// own. Commands run serially, so a package var is safe. wrapJSON resets it
|
|
// before each command and appends "{}" for a successful command that produced
|
|
// nothing.
|
|
var jsonEmitted bool
|
|
|
|
// emitProto emits one proto message as JSON via the shared renderer.
|
|
func emitProto(m proto.Message) error {
|
|
raw, err := protoField(m)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
jsonEmitted = true
|
|
return cli.EmitJSON(raw)
|
|
}
|
|
|
|
// emitJSON emits a composite value (maps/structs) as JSON.
|
|
func emitJSON(v any) error {
|
|
jsonEmitted = true
|
|
return cli.EmitJSON(v)
|
|
}
|
|
|
|
// protoField marshals a proto message to compact JSON (proto field names and
|
|
// enum spellings).
|
|
func protoField(m proto.Message) (json.RawMessage, error) {
|
|
b, err := protojson.Marshal(m)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("marshal json: %w", err)
|
|
}
|
|
return json.RawMessage(b), nil
|
|
}
|
|
|
|
// wrapJSON decorates every runnable node so that, in -json mode, a command that
|
|
// succeeds without emitting output of its own (e.g. sync) still produces "{}",
|
|
// giving every command a uniform JSON contract. Commands that show or return
|
|
// data emit it (setting jsonEmitted); errors propagate to App.FormatError, which
|
|
// renders them as {"error": "..."}. In text mode the wrapper is a no-op.
|
|
func wrapJSON(root *node) {
|
|
seen := map[*node]bool{}
|
|
var walk func(*node)
|
|
walk = func(n *node) {
|
|
if n == nil || seen[n] {
|
|
return
|
|
}
|
|
seen[n] = true
|
|
if n.Run != nil {
|
|
orig := n.Run
|
|
n.Run = func(ctx context.Context, c grpcapi.MaglevClient, a []string) error {
|
|
jsonEmitted = false
|
|
err := orig(ctx, c, a)
|
|
if err == nil && cli.IsJSON() && !jsonEmitted {
|
|
return emitJSON(struct{}{})
|
|
}
|
|
return err
|
|
}
|
|
}
|
|
for _, ch := range n.Children {
|
|
walk(ch)
|
|
}
|
|
}
|
|
walk(root)
|
|
}
|