Files
rainyun-cli/internal/provider/rainyun/client.go
T

193 lines
4.5 KiB
Go

package rainyun
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"os"
"time"
)
const defaultEndpoint = "https://api.v2.rainyun.com"
type client struct {
httpClient *http.Client
baseURL string
apiKey string
debug bool
}
func newClient(endpoint, apiKey string, debug bool) *client {
if endpoint == "" {
endpoint = defaultEndpoint
}
return &client{
httpClient: &http.Client{Timeout: 30 * time.Second},
baseURL: endpoint,
apiKey: apiKey,
debug: debug,
}
}
type apiResponse struct {
Code int `json:"code"`
Msg string `json:"msg"`
Data json.RawMessage `json:"data"`
}
func (c *client) get(ctx context.Context, path string, result any) error {
return c.do(ctx, "GET", path, nil, result, nil)
}
func (c *client) getWithQuery(ctx context.Context, path string, query map[string]string, result any) error {
return c.do(ctx, "GET", path, nil, result, query)
}
// getList sends a GET request to a Rainyun list endpoint and extracts the "records" field.
func (c *client) getList(ctx context.Context, path string, result any) error {
var wrapper listResponse
if err := c.do(ctx, "GET", path, nil, &wrapper, listOptions()); err != nil {
return err
}
// Unmarshal the records array into the result
recordsJSON, err := json.Marshal(wrapper.Records)
if err != nil {
return fmt.Errorf("marshal records: %w", err)
}
if err := json.Unmarshal(recordsJSON, result); err != nil {
return fmt.Errorf("unmarshal records: %w", err)
}
return nil
}
func (c *client) post(ctx context.Context, path string, body, result any) error {
return c.do(ctx, "POST", path, body, result, nil)
}
func (c *client) patch(ctx context.Context, path string, body, result any) error {
return c.do(ctx, "PATCH", path, body, result, nil)
}
func (c *client) delete(ctx context.Context, path string, result any) error {
return c.do(ctx, "DELETE", path, nil, result, nil)
}
func (c *client) do(ctx context.Context, method, path string, body, result any, query map[string]string) error {
u := c.baseURL + path
if len(query) > 0 {
params := url.Values{}
for k, v := range query {
params.Set(k, v)
}
u += "?" + params.Encode()
}
var bodyReader io.Reader
var bodyBytes []byte
if body != nil {
b, err := json.Marshal(body)
if err != nil {
return fmt.Errorf("marshal request: %w", err)
}
bodyBytes = b
bodyReader = bytes.NewReader(b)
}
if c.debug {
fmt.Fprintf(os.Stderr, "[debug] > %s %s\n", method, u)
if bodyBytes != nil {
fmt.Fprintf(os.Stderr, "[debug] %s\n", bodyBytes)
}
}
req, err := http.NewRequestWithContext(ctx, method, u, bodyReader)
if err != nil {
return fmt.Errorf("create request: %w", err)
}
req.Header.Set("X-Api-Key", c.apiKey)
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Accept", "application/json")
start := time.Now()
resp, err := c.httpClient.Do(req)
if err != nil {
return fmt.Errorf("request failed: %w", err)
}
defer resp.Body.Close()
respBody, err := io.ReadAll(resp.Body)
if err != nil {
return fmt.Errorf("read response: %w", err)
}
if c.debug {
fmt.Fprintf(os.Stderr, "[debug] < %d %s %s\n", resp.StatusCode, http.StatusText(resp.StatusCode), time.Since(start).Round(time.Millisecond))
fmt.Fprintf(os.Stderr, "[debug] %s\n", respBody)
}
var apiResp apiResponse
if err := json.Unmarshal(respBody, &apiResp); err == nil {
// Rainyun uses code:200 and code:0 both as success indicators
isError := resp.StatusCode >= 400 || (apiResp.Code != 0 && apiResp.Code != 200)
if isError {
return &APIError{
StatusCode: resp.StatusCode,
Method: method,
URL: u,
Body: respBody,
APICode: apiResp.Code,
APIMsg: apiResp.Msg,
}
}
} else if resp.StatusCode >= 400 {
return &APIError{
StatusCode: resp.StatusCode,
Method: method,
URL: u,
Body: respBody,
}
}
if result != nil && apiResp.Data != nil {
if err := json.Unmarshal(apiResp.Data, result); err != nil {
return fmt.Errorf("unmarshal data: %w", err)
}
}
return nil
}
type APIError struct {
StatusCode int
Method string
URL string
Body []byte
APICode int
APIMsg string
}
func (e *APIError) Error() string {
msg := e.APIMsg
if msg == "" {
// 非 JSON 响应,截取 body 前 200 字符
body := string(e.Body)
if len(body) > 200 {
body = body[:200] + "..."
}
if body != "" {
msg = body
} else {
msg = http.StatusText(e.StatusCode)
}
}
return msg
}
func (e *APIError) Detail() string {
return fmt.Sprintf("%s %s [%d]", e.Method, e.URL, e.StatusCode)
}