internal/gitea:API 用戶端、認證與輸出基礎(#5)
- internal/gitea:僅標準庫的 HTTP 用戶端;ListAll 以 limit=50 逐頁抓滿 - 認證選序 --token → TEAI_TOKEN → tea 組態(--config/TEA_CONFIG/預設路徑); token 不入輸出、日誌與錯誤訊息 - 共用輸出 --output json|table(預設 json)、--timeout(預設 30s) - CLI 結束碼 0/2/3:用法錯誤 → 2、API 錯誤(*gitea.ErrAPI)→ 3 - 單元測試:認證選序、tea 組態解析、分頁、逾時/401/5xx 錯誤映射、輸出 實作 issue #5
This commit is contained in:
@@ -0,0 +1,228 @@
|
||||
// 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
|
||||
}
|
||||
Reference in New Issue
Block a user