gitlink-cli/internal/client/client.go

207 lines
5.4 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package client
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"strings"
"github.com/gitlink-org/gitlink-cli/internal/auth"
"github.com/gitlink-org/gitlink-cli/internal/config"
"github.com/gitlink-org/gitlink-cli/internal/output"
)
type Client struct {
HTTP *http.Client
BaseURL string
Debug bool
}
type APIError struct {
StatusCode int
Code interface{}
Message string
Method string
Path string
}
func (e *APIError) Error() string {
if e.Method != "" && e.Path != "" {
return fmt.Sprintf("%s %s: [%v] %s", e.Method, e.Path, e.Code, e.Message)
}
return fmt.Sprintf("[%v] %s", e.Code, e.Message)
}
func New() (*Client, error) {
cfg, err := config.Load()
if err != nil {
return nil, err
}
return &Client{
HTTP: auth.NewHTTPClient(),
BaseURL: cfg.BaseURL,
}, nil
}
func (c *Client) Do(method, path string, body interface{}, query url.Values) (*output.Envelope, error) {
// Append .json suffix if not already present (GitLink API convention)
// Handle paths that may already contain query strings (e.g., /path?key=val)
if idx := strings.Index(path, "?"); idx != -1 {
basePath := path[:idx]
queryStr := path[idx:]
if !strings.HasSuffix(basePath, ".json") {
path = basePath + ".json" + queryStr
}
} else if !strings.HasSuffix(path, ".json") {
path += ".json"
}
fullURL := c.BaseURL + path
if query != nil && len(query) > 0 {
sep := "?"
if strings.Contains(fullURL, "?") {
sep = "&"
}
fullURL += sep + query.Encode()
}
// Replace path params
var bodyReader io.Reader
if body != nil {
data, err := json.Marshal(body)
if err != nil {
return nil, err
}
bodyReader = bytes.NewReader(data)
}
req, err := http.NewRequest(method, fullURL, bodyReader)
if err != nil {
return nil, err
}
if c.Debug {
fmt.Printf("→ %s %s\n", method, fullURL)
}
resp, err := c.HTTP.Do(req)
if err != nil {
return nil, fmt.Errorf("request failed: %w", err)
}
defer resp.Body.Close()
respData, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("failed to read response: %w", err)
}
if c.Debug {
fmt.Printf("← %d %s\n", resp.StatusCode, string(respData[:min(len(respData), 200)]))
}
// Check HTTP-level errors
if resp.StatusCode >= 400 {
return nil, &APIError{
StatusCode: resp.StatusCode,
Code: resp.StatusCode,
Message: fmt.Sprintf("HTTP %d: %s", resp.StatusCode, strings.TrimSpace(string(respData))),
Method: method,
Path: path,
}
}
// Parse JSON
var raw map[string]interface{}
if err := json.Unmarshal(respData, &raw); err != nil {
// Not JSON, return as-is
return output.SuccessEnvelope(string(respData), nil), nil
}
// Check GitLink error-in-body pattern
if status, ok := raw["status"]; ok {
var statusCode float64
switch v := status.(type) {
case float64:
statusCode = v
case int:
statusCode = float64(v)
}
if statusCode != 0 && statusCode != 200 && statusCode != 1 {
msg, _ := raw["message"].(string)
// [P4 改进] 增加 HTTP 状态码到友好提示的映射,替换原来的裸错误信息
suggestion := suggestFix(int(statusCode))
return output.ErrorEnvelope(int(statusCode), msg, suggestion), &APIError{
StatusCode: int(statusCode),
Code: int(statusCode),
Message: msg,
Method: method,
Path: path,
}
}
}
// Auto-parse JSON string data (GitLink API quirk: some endpoints return data as JSON string)
if dataStr, ok := raw["data"].(string); ok {
var parsedData interface{}
if err := json.Unmarshal([]byte(dataStr), &parsedData); err == nil {
raw["data"] = json.RawMessage(dataStr)
}
}
// Build meta from pagination info
var meta *output.Meta
if tc, ok := raw["total_count"]; ok {
meta = &output.Meta{}
if v, ok := tc.(float64); ok {
meta.TotalCount = int(v)
}
if v, ok := raw["page"].(float64); ok {
meta.Page = int(v)
}
if v, ok := raw["limit"].(float64); ok {
meta.Limit = int(v)
}
}
return output.SuccessEnvelope(raw, meta), nil
}
func (c *Client) Get(path string, query url.Values) (*output.Envelope, error) {
return c.Do("GET", path, nil, query)
}
func (c *Client) Post(path string, body interface{}) (*output.Envelope, error) {
return c.Do("POST", path, body, nil)
}
func (c *Client) Put(path string, body interface{}) (*output.Envelope, error) {
return c.Do("PUT", path, body, nil)
}
func (c *Client) Delete(path string, query url.Values) (*output.Envelope, error) {
return c.Do("DELETE", path, nil, query)
}
// suggestFix 返回 HTTP 状态码对应的友好修复建议P4 改进)
// 当 API 返回 4xx/5xx 时,在错误信息中附加这些提示,帮助用户快速定位问题
func suggestFix(code int) string {
// HTTP 状态码 → 中文友好提示映射
// 注意:这些提示直接展示给终端用户,使用英文以便国际化兼容
switch code {
case 401:
return "Run 'gitlink auth login' to authenticate" // 未认证
case 403:
return "Permission denied. Check your account permissions or contact the project admin" // 权限不足
case 404:
return "Resource not found. Verify --owner, --repo, and the resource ID" // 资源不存在
case 422:
return "Validation failed. Check the request parameters" // 参数校验失败
case 500:
return "Server error. Try again later or contact the platform admin" // 服务端错误
default:
return "" // 其他状态码不给出具体建议
}
}