fix(output): --format table 支持资源包裹列表响应(此前 list 命令表格从不生效) #343
|
|
@ -0,0 +1,25 @@
|
|||
# `--format table` 支持资源包裹列表响应
|
||||
|
||||
## 动机
|
||||
|
||||
平台所有分页列表端点的响应形状都是资源包裹 map:
|
||||
`{"total_count": N, "<资源名>": [...]}`。此前 `printTable` 遇到含嵌套
|
||||
结构的 map 一律回落 JSON,导致 **所有 list 命令的 `--format table`
|
||||
实际上从不渲染表格**,与 flag 文案承诺不符。
|
||||
|
||||
## 行为
|
||||
|
||||
- 检测「恰好一个数组值 + 其余全是标量」的 map(列表响应形状):
|
||||
先打印标量摘要行(如 `total_count: 7`,键排序),再把包裹数组
|
||||
渲染为表格。
|
||||
- 含嵌套对象的 map(如 commit 详情)与既有行为一致回落 JSON。
|
||||
|
||||
## 生产实测
|
||||
|
||||
`branch +list --format table`(forgeplus):输出 `total_count: 7`
|
||||
摘要行 + 7 行分支表格(此前输出整段 JSON)。
|
||||
|
||||
## 测试
|
||||
|
||||
formatter_test.go 新增 2 个单测:资源包裹列表渲染表格(含摘要行、
|
||||
不回落 JSON);嵌套对象 map 仍回落 JSON。
|
||||
|
|
@ -6,6 +6,7 @@ import (
|
|||
"io"
|
||||
"os"
|
||||
"reflect"
|
||||
"sort"
|
||||
"strings"
|
||||
"text/tabwriter"
|
||||
|
||||
|
|
@ -71,6 +72,11 @@ func printTable(w io.Writer, envelope *Envelope) error {
|
|||
case []interface{}:
|
||||
return printSliceTable(w, data)
|
||||
case map[string]interface{}:
|
||||
// Resource-wrapped list responses ({"total_count": N, "<resource>": [...]})
|
||||
// render the wrapped array as a table with the scalar fields as a summary.
|
||||
if items, ok := unwrapListMap(w, data); ok {
|
||||
return printSliceTable(w, items)
|
||||
}
|
||||
// For maps with nested structures, prefer JSON
|
||||
if hasComplexValues(data) {
|
||||
return printJSON(w, envelope)
|
||||
|
|
@ -82,6 +88,47 @@ func printTable(w io.Writer, envelope *Envelope) error {
|
|||
}
|
||||
}
|
||||
|
||||
// unwrapListMap detects a map containing exactly one array value while every
|
||||
// other value is a scalar (the shape of the platform's paginated list
|
||||
// responses). It prints the scalar fields as a summary line and returns the
|
||||
// wrapped array for table rendering.
|
||||
func unwrapListMap(w io.Writer, m map[string]interface{}) ([]interface{}, bool) {
|
||||
var items []interface{}
|
||||
arrays := 0
|
||||
for _, v := range m {
|
||||
switch value := v.(type) {
|
||||
case []interface{}:
|
||||
arrays++
|
||||
items = value
|
||||
case map[string]interface{}:
|
||||
return nil, false
|
||||
}
|
||||
}
|
||||
if arrays != 1 {
|
||||
return nil, false
|
||||
}
|
||||
scalars := make([]string, 0, len(m))
|
||||
for _, k := range sortedKeys(m) {
|
||||
if _, ok := m[k].([]interface{}); ok {
|
||||
continue
|
||||
}
|
||||
scalars = append(scalars, fmt.Sprintf("%s: %s", k, formatValue(m[k])))
|
||||
}
|
||||
if len(scalars) > 0 {
|
||||
fmt.Fprintln(w, strings.Join(scalars, " "))
|
||||
}
|
||||
return items, true
|
||||
}
|
||||
|
||||
func sortedKeys(m map[string]interface{}) []string {
|
||||
keys := make([]string, 0, len(m))
|
||||
for k := range m {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
return keys
|
||||
}
|
||||
|
||||
func hasComplexValues(m map[string]interface{}) bool {
|
||||
for _, v := range m {
|
||||
switch v.(type) {
|
||||
|
|
|
|||
|
|
@ -244,3 +244,41 @@ func TestFormatValue(t *testing.T) {
|
|||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestPrintTableUnwrapsResourceWrappedList(t *testing.T) {
|
||||
env := SuccessEnvelope(map[string]interface{}{
|
||||
"total_count": 2,
|
||||
"tags": []interface{}{
|
||||
map[string]interface{}{"name": "v1.0.0", "id": 1},
|
||||
map[string]interface{}{"name": "v1.1.0", "id": 2},
|
||||
},
|
||||
}, nil)
|
||||
var buf bytes.Buffer
|
||||
if err := PrintTo(&buf, env, "table"); err != nil {
|
||||
t.Fatalf("PrintTo returned error: %v", err)
|
||||
}
|
||||
out := buf.String()
|
||||
if !strings.Contains(out, "total_count: 2") {
|
||||
t.Fatalf("missing summary line: %s", out)
|
||||
}
|
||||
if !strings.Contains(out, "v1.0.0") || !strings.Contains(out, "v1.1.0") {
|
||||
t.Fatalf("missing table rows: %s", out)
|
||||
}
|
||||
if strings.Contains(out, "\"ok\"") {
|
||||
t.Fatalf("should not fall back to JSON: %s", out)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPrintTableKeepsJSONForNestedObjects(t *testing.T) {
|
||||
env := SuccessEnvelope(map[string]interface{}{
|
||||
"commit": map[string]interface{}{"sha": "abc"},
|
||||
"files": []interface{}{},
|
||||
}, nil)
|
||||
var buf bytes.Buffer
|
||||
if err := PrintTo(&buf, env, "table"); err != nil {
|
||||
t.Fatalf("PrintTo returned error: %v", err)
|
||||
}
|
||||
if !strings.Contains(buf.String(), "\"ok\"") {
|
||||
t.Fatalf("nested object map should fall back to JSON: %s", buf.String())
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue