forked from Gitlink/gitlink-cli
239 lines
5.5 KiB
Go
239 lines
5.5 KiB
Go
package output
|
||
|
||
import (
|
||
"encoding/json"
|
||
"fmt"
|
||
"io"
|
||
"os"
|
||
"reflect"
|
||
"strings"
|
||
"text/tabwriter"
|
||
|
||
"gopkg.in/yaml.v3"
|
||
)
|
||
|
||
func Print(envelope *Envelope, format string) error {
|
||
if format == "" {
|
||
format = "json"
|
||
}
|
||
return PrintTo(os.Stdout, envelope, format)
|
||
}
|
||
|
||
func PrintTo(w io.Writer, envelope *Envelope, format string) error {
|
||
switch format {
|
||
case "json":
|
||
return printJSON(w, envelope)
|
||
case "yaml":
|
||
return printYAML(w, envelope)
|
||
case "table":
|
||
return printTable(w, envelope)
|
||
default:
|
||
return printJSON(w, envelope)
|
||
}
|
||
}
|
||
|
||
func printJSON(w io.Writer, envelope *Envelope) error {
|
||
data, err := json.MarshalIndent(envelope, "", " ")
|
||
if err != nil {
|
||
return err
|
||
}
|
||
_, err = fmt.Fprintln(w, string(data))
|
||
return err
|
||
}
|
||
|
||
func printYAML(w io.Writer, envelope *Envelope) error {
|
||
data, err := yaml.Marshal(envelope)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
_, err = fmt.Fprint(w, string(data))
|
||
return err
|
||
}
|
||
|
||
func printTable(w io.Writer, envelope *Envelope) error {
|
||
if !envelope.OK {
|
||
if envelope.Error != nil {
|
||
fmt.Fprintf(w, "Error: %s\n", envelope.Error.Message)
|
||
if envelope.Error.Suggestion != "" {
|
||
fmt.Fprintf(w, "Suggestion: %s\n", envelope.Error.Suggestion)
|
||
}
|
||
}
|
||
return nil
|
||
}
|
||
|
||
if envelope.Data == nil {
|
||
fmt.Fprintln(w, "No data")
|
||
return nil
|
||
}
|
||
|
||
// Try to render as table if data is a slice of maps
|
||
switch data := envelope.Data.(type) {
|
||
case []interface{}:
|
||
return printSliceTable(w, data)
|
||
case map[string]interface{}:
|
||
// GitLink API 经常返回 {"count":N, "items":[...]} 这种"包装列表"
|
||
// 优先解包内层 slice of maps,按列表渲染
|
||
if unwrapped := unwrapSingleListField(data); unwrapped != nil {
|
||
return printSliceTable(w, unwrapped)
|
||
}
|
||
// For maps with nested structures, prefer JSON
|
||
if hasComplexValues(data) {
|
||
return printJSON(w, envelope)
|
||
}
|
||
return printMapTable(w, data)
|
||
default:
|
||
// Fallback to JSON
|
||
return printJSON(w, envelope)
|
||
}
|
||
}
|
||
|
||
// unwrapSingleListField 检测 map 是否为"包装列表"结构:
|
||
// 至少包含一个 []interface{}(元素为 map,或为空数组)字段。
|
||
// 若是,返回该 slice(用于按列表渲染表格);否则返回 nil。
|
||
//
|
||
// 优先选择已知列表字段名(projects/webhooks/issues 等),
|
||
// 若 map 中只有一个 slice of maps 字段,也直接使用。
|
||
func unwrapSingleListField(m map[string]interface{}) []interface{} {
|
||
knownListFields := []string{
|
||
"projects", "webhooks", "issues", "users", "pull_requests",
|
||
"builds", "releases", "branches", "teams", "members",
|
||
"orgs", "items", "records", "results", "wikis", "search",
|
||
}
|
||
|
||
// 1. 优先选择已知字段名(即使数组为空,也接受 — 空数组也是合法列表)
|
||
for _, name := range knownListFields {
|
||
if s, ok := m[name].([]interface{}); ok {
|
||
if isSliceOfMaps(s) {
|
||
return s
|
||
}
|
||
}
|
||
}
|
||
|
||
// 2. 兜底:检测是否只有一个 slice of maps 字段
|
||
var listField string
|
||
var listValue []interface{}
|
||
for k, v := range m {
|
||
s, ok := v.([]interface{})
|
||
if !ok {
|
||
continue
|
||
}
|
||
if !isSliceOfMaps(s) {
|
||
continue
|
||
}
|
||
if listField != "" {
|
||
// 多个列表字段,无法自动解包
|
||
return nil
|
||
}
|
||
listField = k
|
||
listValue = s
|
||
}
|
||
return listValue
|
||
}
|
||
|
||
// isSliceOfMaps 判断 slice 是否为 map 元素的列表
|
||
// 空 slice 也算合法列表(用于在 printSliceTable 中触发 "No results" 输出)
|
||
func isSliceOfMaps(s []interface{}) bool {
|
||
if len(s) == 0 {
|
||
return true // 空数组视为列表,printSliceTable 会输出 "No results"
|
||
}
|
||
_, ok := s[0].(map[string]interface{})
|
||
return ok
|
||
}
|
||
|
||
func hasComplexValues(m map[string]interface{}) bool {
|
||
for _, v := range m {
|
||
switch v.(type) {
|
||
case map[string]interface{}, []interface{}:
|
||
return true
|
||
}
|
||
}
|
||
return false
|
||
}
|
||
|
||
func printSliceTable(w io.Writer, items []interface{}) error {
|
||
if len(items) == 0 {
|
||
fmt.Fprintln(w, "No results")
|
||
return nil
|
||
}
|
||
|
||
// Collect headers from first item
|
||
first, ok := items[0].(map[string]interface{})
|
||
if !ok {
|
||
data, _ := json.MarshalIndent(items, "", " ")
|
||
fmt.Fprintln(w, string(data))
|
||
return nil
|
||
}
|
||
|
||
headers := collectKeys(first)
|
||
tw := tabwriter.NewWriter(w, 0, 4, 2, ' ', 0)
|
||
|
||
// Print headers
|
||
fmt.Fprintln(tw, strings.Join(headers, "\t"))
|
||
dashes := make([]string, len(headers))
|
||
for i, h := range headers {
|
||
dashes[i] = strings.Repeat("-", len(h))
|
||
}
|
||
fmt.Fprintln(tw, strings.Join(dashes, "\t"))
|
||
|
||
// Print rows
|
||
for _, item := range items {
|
||
m, ok := item.(map[string]interface{})
|
||
if !ok {
|
||
continue
|
||
}
|
||
vals := make([]string, len(headers))
|
||
for i, h := range headers {
|
||
vals[i] = formatValue(m[h])
|
||
}
|
||
fmt.Fprintln(tw, strings.Join(vals, "\t"))
|
||
}
|
||
return tw.Flush()
|
||
}
|
||
|
||
func printMapTable(w io.Writer, m map[string]interface{}) error {
|
||
tw := tabwriter.NewWriter(w, 0, 4, 2, ' ', 0)
|
||
fmt.Fprintln(tw, "KEY\tVALUE")
|
||
fmt.Fprintln(tw, "---\t-----")
|
||
for k, v := range m {
|
||
fmt.Fprintf(tw, "%s\t%s\n", k, formatValue(v))
|
||
}
|
||
return tw.Flush()
|
||
}
|
||
|
||
func collectKeys(m map[string]interface{}) []string {
|
||
keys := make([]string, 0, len(m))
|
||
// Prefer common keys first
|
||
priority := []string{"id", "name", "login", "title", "status", "state", "created_at", "updated_at"}
|
||
seen := map[string]bool{}
|
||
for _, k := range priority {
|
||
if _, ok := m[k]; ok {
|
||
keys = append(keys, k)
|
||
seen[k] = true
|
||
}
|
||
}
|
||
for k := range m {
|
||
if !seen[k] {
|
||
keys = append(keys, k)
|
||
}
|
||
}
|
||
return keys
|
||
}
|
||
|
||
func formatValue(v interface{}) string {
|
||
if v == nil {
|
||
return ""
|
||
}
|
||
rv := reflect.ValueOf(v)
|
||
switch rv.Kind() {
|
||
case reflect.Map, reflect.Slice:
|
||
data, _ := json.Marshal(v)
|
||
s := string(data)
|
||
if len(s) > 60 {
|
||
return s[:57] + "..."
|
||
}
|
||
return s
|
||
default:
|
||
return fmt.Sprintf("%v", v)
|
||
}
|
||
}
|