Files
teai/internal/gitea/output.go
T
max acf8cce68c internal/gitea:API 用戶端、認證與輸出基礎(#5)
- 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
2026-09-10 08:20:02 +08:00

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
}