package provider import ( "bytes" "context" "encoding/json" "fmt" "io" "net/http" ) type apiClient struct { baseURL string httpClient *http.Client } func newAPIClient(baseURL string) *apiClient { return &apiClient{ baseURL: baseURL, httpClient: &http.Client{}, } } func (c *apiClient) get(ctx context.Context, path string, out any) error { return c.do(ctx, http.MethodGet, path, nil, out) } func (c *apiClient) post(ctx context.Context, path string, body, out any) error { return c.do(ctx, http.MethodPost, path, body, out) } func (c *apiClient) put(ctx context.Context, path string, body, out any) error { return c.do(ctx, http.MethodPut, path, body, out) } func (c *apiClient) del(ctx context.Context, path string) error { return c.do(ctx, http.MethodDelete, path, nil, nil) } func (c *apiClient) do(ctx context.Context, method, path string, body, out any) error { var bodyReader io.Reader if body != nil { b, err := json.Marshal(body) if err != nil { return fmt.Errorf("marshal request: %w", err) } bodyReader = bytes.NewReader(b) } req, err := http.NewRequestWithContext(ctx, method, c.baseURL+path, bodyReader) if err != nil { return fmt.Errorf("create request: %w", err) } if body != nil { req.Header.Set("Content-Type", "application/json") } resp, err := c.httpClient.Do(req) if err != nil { return fmt.Errorf("http request: %w", err) } defer resp.Body.Close() if resp.StatusCode == http.StatusNotFound { return ¬FoundError{path: path} } if resp.StatusCode >= 400 { b, _ := io.ReadAll(resp.Body) return fmt.Errorf("api error %d: %s", resp.StatusCode, string(b)) } if out != nil && resp.StatusCode != http.StatusNoContent { if err := json.NewDecoder(resp.Body).Decode(out); err != nil { return fmt.Errorf("decode response: %w", err) } } return nil } type notFoundError struct { path string } func (e *notFoundError) Error() string { return fmt.Sprintf("not found: %s", e.path) } func isNotFound(err error) bool { _, ok := err.(*notFoundError) return ok }