Files
dingtalk-workspace-cli/internal/output/ndjson.go
2026-07-15 11:41:30 +08:00

90 lines
2.7 KiB
Go

// Copyright 2026 Alibaba Group
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package output
import (
"bufio"
"encoding/json"
"io"
)
var encodeNDJSON = (*json.Encoder).Encode
// writeNDJSON renders payload as newline-delimited JSON (https://ndjson.org):
// - a top-level array → one element per line
// - an object that wraps a → one element of that list per line
// well-known list key (items / results / data / records / value / ...)
// - anything else → a single line containing the whole value
//
// This is the streaming-friendly counterpart to `-f json`: each line is an
// independent, compact JSON document so consumers can `jq -c`, `while read`,
// or pipe into log pipelines without buffering the whole response.
//
// TODO(#252): consider honouring --fields per-line projection here too (today
// WriteFiltered already applies SelectFields before Write is reached, so this
// works, but a dedicated test would be good). Also decide whether non-list
// payloads should error under `-f ndjson` instead of degrading to one line.
func writeNDJSON(w io.Writer, payload any) error {
normalized, err := roundTripJSON(payload)
if err != nil {
return err
}
bw := bufio.NewWriter(w)
enc := json.NewEncoder(bw)
enc.SetEscapeHTML(false)
// json.Encoder.Encode already appends a trailing newline per call.
switch v := normalized.(type) {
case []any:
for _, item := range v {
if err := encodeNDJSON(enc, item); err != nil {
return err
}
}
case map[string]any:
if loc := findDataList(v); loc != nil {
for _, item := range loc.list {
if err := encodeNDJSON(enc, item); err != nil {
return err
}
}
} else {
if err := encodeNDJSON(enc, v); err != nil {
return err
}
}
default:
if err := encodeNDJSON(enc, v); err != nil {
return err
}
}
return bw.Flush()
}
// roundTripJSON normalizes an arbitrary Go value into the
// map[string]any / []any / scalar shape used by the rest of this package by
// marshalling and unmarshalling it through encoding/json.
func roundTripJSON(payload any) (any, error) {
raw, err := marshalJSON(payload)
if err != nil {
return nil, err
}
var out any
if err := unmarshalJSON(raw, &out); err != nil {
return nil, err
}
return out, nil
}