// Package tools provides a shared HTTP client for external API calls. package tools import ( "encoding/json" "fmt" "io" "net/http" "net/url" "strings" "time" ) // APIClient is a reusable HTTP client for external APIs. // All outbound calls go through this — no hidden network access. type APIClient struct { client *http.Client baseURL string apiKey string } // NewAPIClient creates a client for an external API. func NewAPIClient(baseURL, apiKey string) *APIClient { return &APIClient{ client: &http.Client{ Timeout: 10 * time.Second, }, baseURL: strings.TrimRight(baseURL, "/"), apiKey: apiKey, } } // Get performs a GET request with query parameters. func (c *APIClient) Get(path string, params map[string]string) ([]byte, error) { u, err := url.Parse(c.baseURL + path) if err != nil { return nil, err } q := u.Query() for k, v := range params { q.Set(k, v) } if c.apiKey != "" { q.Set("key", c.apiKey) } u.RawQuery = q.Encode() resp, err := c.client.Get(u.String()) if err != nil { return nil, fmt.Errorf("api: GET %s: %w", path, err) } defer resp.Body.Close() body, err := io.ReadAll(resp.Body) if err != nil { return nil, err } if resp.StatusCode != http.StatusOK { return nil, fmt.Errorf("api: GET %s returned %d: %s", path, resp.StatusCode, string(body[:min(200, len(body))])) } return body, nil } // GetJSON performs a GET and decodes the response as JSON. func (c *APIClient) GetJSON(path string, params map[string]string, result interface{}) error { body, err := c.Get(path, params) if err != nil { return err } return json.Unmarshal(body, result) } // PostJSON performs a POST with JSON body and decodes the response. func (c *APIClient) PostJSON(path string, params map[string]string, reqBody, result interface{}) error { u, err := url.Parse(c.baseURL + path) if err != nil { return err } q := u.Query() for k, v := range params { q.Set(k, v) } if c.apiKey != "" { q.Set("key", c.apiKey) } u.RawQuery = q.Encode() bodyBytes, err := json.Marshal(reqBody) if err != nil { return err } resp, err := c.client.Post(u.String(), "application/json", strings.NewReader(string(bodyBytes))) if err != nil { return fmt.Errorf("api: POST %s: %w", path, err) } defer resp.Body.Close() respBody, err := io.ReadAll(resp.Body) if err != nil { return err } if resp.StatusCode != http.StatusOK { return fmt.Errorf("api: POST %s returned %d: %s", path, resp.StatusCode, string(respBody[:min(200, len(respBody))])) } return json.Unmarshal(respBody, result) } func min(a, b int) int { if a < b { return a } return b }