package sdk import ( "bytes" "context" "encoding/json" "fmt" "io" "net/http" "strings" "time" ) type Client struct { baseURL string token string accountType string http *http.Client } func New(baseURL, token string, timeout time.Duration) *Client { if timeout <= 0 { timeout = 15 * time.Second } return &Client{ baseURL: strings.TrimRight(baseURL, "/"), token: token, accountType: "stock", http: &http.Client{Timeout: timeout}, } } func (c *Client) SetAccountType(accountType string) *Client { if strings.TrimSpace(accountType) != "" { c.accountType = accountType } return c } func (c *Client) get(ctx context.Context, path string, dest any) error { return c.do(ctx, http.MethodGet, path, nil, dest) } func (c *Client) post(ctx context.Context, path string, body any, dest any) error { if body == nil { body = map[string]any{} } return c.do(ctx, http.MethodPost, path, body, dest) } func (c *Client) do(ctx context.Context, method, path string, body any, dest any) error { var rdr io.Reader if body != nil && method != http.MethodGet { raw, err := json.Marshal(body) if err != nil { return fmt.Errorf("marshal request: %w", err) } rdr = bytes.NewReader(raw) } req, err := http.NewRequestWithContext(ctx, method, c.baseURL+path, rdr) if err != nil { return err } req.Header.Set("X-Token", c.token) req.Header.Set("Accept", "application/json") if rdr != nil { req.Header.Set("Content-Type", "application/json") } resp, err := c.http.Do(req) if err != nil { return err } defer resp.Body.Close() raw, err := io.ReadAll(resp.Body) if err != nil { return err } if resp.StatusCode >= 400 { apiErr := &APIError{StatusCode: resp.StatusCode, Message: strings.TrimSpace(string(raw))} var parsed APIError if json.Unmarshal(raw, &parsed) == nil { if parsed.StatusCode == 0 { parsed.StatusCode = resp.StatusCode } if parsed.Message != "" { apiErr = &parsed } } return apiErr } if dest == nil || len(raw) == 0 { return nil } if err := json.Unmarshal(raw, dest); err != nil { return fmt.Errorf("unmarshal %s: %w; body=%s", path, err, truncate(raw, 512)) } return nil } func (c *Client) getField(ctx context.Context, path, key string) (any, error) { var out map[string]any if err := c.get(ctx, path, &out); err != nil { return nil, err } return out[key], nil } func (c *Client) postField(ctx context.Context, path string, body any, key string) (any, error) { var out map[string]any if err := c.post(ctx, path, body, &out); err != nil { return nil, err } if msg, ok := out["error"].(string); ok && msg != "" { return nil, &BusinessError{Message: msg} } if key == "" { return out, nil } if v, ok := out[key]; ok { return v, nil } return out, nil } func csvJoin(items []string) string { parts := make([]string, 0, len(items)) for _, s := range items { s = strings.TrimSpace(s) if s != "" { parts = append(parts, s) } } return strings.Join(parts, ",") } func truncate(b []byte, n int) string { if len(b) <= n { return string(b) } return string(b[:n]) + "..." }