- internal/gitea:僅標準庫的 HTTP 用戶端;ListAll 以 limit=50 逐頁抓滿 - 認證選序 --token → TEAI_TOKEN → tea 組態(--config/TEA_CONFIG/預設路徑); token 不入輸出、日誌與錯err誤訊息 - 共用輸出 --output json|table(預設 json)、--timeout(預設 30s) - CLI 結束碼 0/2/3:用法錯誤 → 2、API 錯誤(*gitea.ErrAPI)→ 3 - 單元測試:認證選序、tea 組態解析、分颁頁、逾時/401/5xx 錯誤映射、輸出 實作 issue #5
229 lines
5.3 KiB
Go
229 lines
5.3 KiB
Go
// output.go — 共用輸出:JSON(預設)與 table(人工抽查)。
|
|
//
|
|
// 命令把結果包成 Values(欄位順序即輸出順序),再由 output 依格式輸出。
|
|
// 這個設計讓同一份結果可以同時餵 JSON 與 table,不需要各自重組。
|
|
package gitea
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"strings"
|
|
)
|
|
|
|
// Values 是一組已排序的欄位值(依 Field 順序)。
|
|
type Values []any
|
|
|
|
// Format 是輸出格式。
|
|
type Format int
|
|
|
|
const (
|
|
// FormatJSON 輸出 JSON(預設)。
|
|
FormatJSON Format = iota
|
|
// FormatTable 輸出對齊的文字表格。
|
|
FormatTable
|
|
)
|
|
|
|
// ParseFormat 解析 --output 的值;不認得的值回傳錯誤。
|
|
func ParseFormat(s string) (Format, error) {
|
|
switch strings.ToLower(strings.TrimSpace(s)) {
|
|
case "", "json":
|
|
return FormatJSON, nil
|
|
case "table":
|
|
return FormatTable, nil
|
|
default:
|
|
return FormatJSON, fmt.Errorf("invalid output format %q (want json or table)", s)
|
|
}
|
|
}
|
|
|
|
// Field 描述 table 輸出的一欄。
|
|
type Field struct {
|
|
// Name 是欄位名(JSON 鍵與 table 表頭)。
|
|
Name string
|
|
// ValueFmt 是值的格式化函式;nil 時用預設(fmt.Sprint + JSON 引號字串)。
|
|
ValueFmt func(v any) string
|
|
}
|
|
|
|
// Printer 依格式輸出結果。
|
|
type Printer struct {
|
|
// Format 是輸出格式。
|
|
Format Format
|
|
// Out 是輸出目標。
|
|
Out io.Writer
|
|
}
|
|
|
|
// PrintList 輸出清單。fields 定義欄位;rows 是每一列的欄位值。
|
|
// JSON 模式輸出物件陣列;table 模式輸出對齊表格。
|
|
func (p Printer) PrintList(fields []Field, rows []Values) error {
|
|
if p.Format == FormatTable {
|
|
return p.printTable(fields, rows)
|
|
}
|
|
w := p.Out
|
|
if len(rows) == 0 {
|
|
_, err := fmt.Fprintln(w, "[]")
|
|
return err
|
|
}
|
|
if _, err := fmt.Fprint(w, "["); err != nil {
|
|
return err
|
|
}
|
|
for i, row := range rows {
|
|
if i > 0 {
|
|
if _, err := fmt.Fprint(w, ","); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
b, err := json.Marshal(mapOf(fields, row))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if _, err := w.Write(b); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
_, err := fmt.Fprintln(w, "]")
|
|
return err
|
|
}
|
|
|
|
// PrintOne 輸出單一結果(物件);v 已是可 JSON 化的值。
|
|
// table 模式輸出 key: value 行。
|
|
func (p Printer) PrintOne(fields []Field, row Values) error {
|
|
if p.Format == FormatTable {
|
|
return p.printTable(fields, []Values{row})
|
|
}
|
|
b, err := json.Marshal(mapOf(fields, row))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
_, err = fmt.Fprintln(p.Out, string(b))
|
|
return err
|
|
}
|
|
|
|
// PrintNull 輸出「沒有結果」的 JSON null(table 模式輸出提示行)。
|
|
func (p Printer) PrintNull() error {
|
|
if p.Format == FormatTable {
|
|
_, err := fmt.Fprintln(p.Out, "(none)")
|
|
return err
|
|
}
|
|
_, err := fmt.Fprintln(p.Out, "null")
|
|
return err
|
|
}
|
|
|
|
// printTable 印出對齊表格:表頭、分隔線、資料列。
|
|
func (p Printer) printTable(fields []Field, rows []Values) error {
|
|
headers := make([]string, len(fields))
|
|
cells := make([][]string, len(rows))
|
|
for i, f := range fields {
|
|
headers[i] = f.Name
|
|
}
|
|
for r, row := range rows {
|
|
cells[r] = make([]string, len(fields))
|
|
for i := range fields {
|
|
cells[r][i] = formatValue(fields[i], row[i])
|
|
}
|
|
}
|
|
widths := make([]int, len(fields))
|
|
for i, h := range headers {
|
|
widths[i] = runeLen(h)
|
|
}
|
|
for _, row := range cells {
|
|
for i, c := range row {
|
|
if l := runeLen(c); l > widths[i] {
|
|
widths[i] = l
|
|
}
|
|
}
|
|
}
|
|
var b strings.Builder
|
|
for i, h := range headers {
|
|
fmt.Fprintf(&b, "%-*s", widths[i], h)
|
|
if i < len(headers)-1 {
|
|
b.WriteString(" ")
|
|
}
|
|
}
|
|
b.WriteString("\n")
|
|
for i := range fields {
|
|
fmt.Fprintf(&b, "%s", strings.Repeat("─", widths[i]))
|
|
if i < len(fields)-1 {
|
|
b.WriteString(" ")
|
|
}
|
|
}
|
|
b.WriteString("\n")
|
|
for _, row := range cells {
|
|
for i, c := range row {
|
|
fmt.Fprintf(&b, "%-*s", widths[i], c)
|
|
if i < len(fields)-1 {
|
|
b.WriteString(" ")
|
|
_ = i
|
|
}
|
|
}
|
|
b.WriteString("\n")
|
|
}
|
|
_, err := fmt.Fprint(p.Out, b.String())
|
|
return err
|
|
}
|
|
|
|
// mapOf 把欄位與值組成 JSON 物件用的 map(保留欄位順序由 encoding/json
|
|
// 處理:map 會排序鍵,因此改用自組 slice 結構以保順序——見 marshalObject)。
|
|
func mapOf(fields []Field, row Values) any {
|
|
if len(row) != len(fields) {
|
|
return nil
|
|
}
|
|
return orderedObject{fields, row}
|
|
}
|
|
|
|
// orderedObject 以欄位順序輸出 JSON 物件(避免 map 排序破壞欄位序)。
|
|
type orderedObject struct {
|
|
fields []Field
|
|
row Values
|
|
}
|
|
|
|
// MarshalJSON 按欄位順序輸出。
|
|
func (o orderedObject) MarshalJSON() ([]byte, error) {
|
|
var b strings.Builder
|
|
b.WriteByte('{')
|
|
for i, f := range o.fields {
|
|
if i > 0 {
|
|
b.WriteByte(',')
|
|
}
|
|
k, err := json.Marshal(f.Name)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
b.Write(k)
|
|
b.WriteByte(':')
|
|
v, err := json.Marshal(o.row[i])
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
b.Write(v)
|
|
}
|
|
b.WriteByte('}')
|
|
return []byte(b.String()), nil
|
|
}
|
|
|
|
// formatValue 把欄位值格式化為 table 用字串;nil → 空字串。
|
|
func formatValue(f Field, v any) string {
|
|
if v == nil {
|
|
return ""
|
|
}
|
|
if f.ValueFmt != nil {
|
|
return f.ValueFmt(v)
|
|
}
|
|
switch x := v.(type) {
|
|
case string:
|
|
return x
|
|
case fmt.Stringer:
|
|
return x.String()
|
|
default:
|
|
return fmt.Sprint(x)
|
|
}
|
|
}
|
|
|
|
// runeLen 回傳字串的 rune 數(表格寬度用,CJK 後續可再調)。
|
|
func runeLen(s string) int {
|
|
n := 0
|
|
for range s {
|
|
n++
|
|
}
|
|
return n
|
|
}
|