diff options
| author | Peter Stone <thepeterstone@gmail.com> | 2026-01-23 21:37:18 -1000 |
|---|---|---|
| committer | Peter Stone <thepeterstone@gmail.com> | 2026-01-23 21:37:18 -1000 |
| commit | 465093343ddd398ce5f6377fc9c472d8251c618b (patch) | |
| tree | d333a2f1c8879f7b114817e929c95e9fcf5f4c3b /internal/api/http.go | |
| parent | e23c85577cbb0eac8b847dd989072698ff4e7a30 (diff) | |
Refactor: reduce code duplication with shared abstractions
- Add BaseClient HTTP abstraction (internal/api/http.go) to eliminate
duplicated HTTP boilerplate across Todoist, Trello, and PlanToEat clients
- Add response helpers (internal/handlers/response.go) for JSON/HTML responses
- Add generic cache wrapper (internal/handlers/cache.go) using Go generics
- Consolidate HandleCompleteAtom/HandleUncompleteAtom into handleAtomToggle
- Merge TabsHandler into Handler, delete tabs.go
- Extract sortTasksByUrgency and filterAndSortTrelloTasks helpers
- Update tests to work with new BaseClient structure
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Diffstat (limited to 'internal/api/http.go')
| -rw-r--r-- | internal/api/http.go | 143 |
1 files changed, 143 insertions, 0 deletions
diff --git a/internal/api/http.go b/internal/api/http.go new file mode 100644 index 0000000..8854625 --- /dev/null +++ b/internal/api/http.go @@ -0,0 +1,143 @@ +package api + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "io" + "net/http" + "time" +) + +// BaseClient provides common HTTP functionality for API clients +type BaseClient struct { + HTTPClient *http.Client + BaseURL string +} + +// NewBaseClient creates a new BaseClient with default settings +func NewBaseClient(baseURL string) BaseClient { + return BaseClient{ + HTTPClient: &http.Client{Timeout: 15 * time.Second}, + BaseURL: baseURL, + } +} + +// Get performs a GET request and decodes the JSON response +func (c *BaseClient) Get(ctx context.Context, path string, headers map[string]string, result interface{}) error { + req, err := http.NewRequestWithContext(ctx, "GET", c.BaseURL+path, nil) + if err != nil { + return fmt.Errorf("failed to create request: %w", err) + } + + for k, v := range headers { + req.Header.Set(k, v) + } + + return c.doJSON(req, result) +} + +// Post performs a POST request with JSON body and decodes the response +func (c *BaseClient) Post(ctx context.Context, path string, headers map[string]string, body interface{}, result interface{}) error { + var bodyReader io.Reader + if body != nil { + jsonData, err := json.Marshal(body) + if err != nil { + return fmt.Errorf("failed to marshal request body: %w", err) + } + bodyReader = bytes.NewBuffer(jsonData) + } + + req, err := http.NewRequestWithContext(ctx, "POST", c.BaseURL+path, bodyReader) + if err != nil { + return fmt.Errorf("failed to create request: %w", err) + } + + if body != nil { + req.Header.Set("Content-Type", "application/json") + } + + for k, v := range headers { + req.Header.Set(k, v) + } + + return c.doJSON(req, result) +} + +// PostForm performs a POST request with form-encoded body +func (c *BaseClient) PostForm(ctx context.Context, path string, headers map[string]string, formData string, result interface{}) error { + req, err := http.NewRequestWithContext(ctx, "POST", c.BaseURL+path, bytes.NewBufferString(formData)) + if err != nil { + return fmt.Errorf("failed to create request: %w", err) + } + + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + for k, v := range headers { + req.Header.Set(k, v) + } + + return c.doJSON(req, result) +} + +// PostEmpty performs a POST request with no body and expects no response body +func (c *BaseClient) PostEmpty(ctx context.Context, path string, headers map[string]string) error { + req, err := http.NewRequestWithContext(ctx, "POST", c.BaseURL+path, nil) + if err != nil { + return fmt.Errorf("failed to create request: %w", err) + } + + for k, v := range headers { + req.Header.Set(k, v) + } + + resp, err := c.HTTPClient.Do(req) + if err != nil { + return fmt.Errorf("request failed: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode < 200 || resp.StatusCode >= 300 { + body, _ := io.ReadAll(resp.Body) + return fmt.Errorf("API error (status %d): %s", resp.StatusCode, string(body)) + } + + return nil +} + +// Put performs a PUT request with form-encoded body +func (c *BaseClient) Put(ctx context.Context, path string, headers map[string]string, formData string, result interface{}) error { + req, err := http.NewRequestWithContext(ctx, "PUT", c.BaseURL+path, bytes.NewBufferString(formData)) + if err != nil { + return fmt.Errorf("failed to create request: %w", err) + } + + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + for k, v := range headers { + req.Header.Set(k, v) + } + + return c.doJSON(req, result) +} + +// doJSON executes the request and decodes JSON response +func (c *BaseClient) doJSON(req *http.Request, result interface{}) error { + resp, err := c.HTTPClient.Do(req) + if err != nil { + return fmt.Errorf("request failed: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode < 200 || resp.StatusCode >= 300 { + body, _ := io.ReadAll(resp.Body) + return fmt.Errorf("API error (status %d): %s", resp.StatusCode, string(body)) + } + + if result != nil { + if err := json.NewDecoder(resp.Body).Decode(result); err != nil { + return fmt.Errorf("failed to decode response: %w", err) + } + } + + return nil +} |
