// 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) }