forked from Gitlink/gitlink-cli
207 lines
5.4 KiB
Go
207 lines
5.4 KiB
Go
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 "" // 其他状态码不给出具体建议
|
||
}
|
||
}
|