summaryrefslogtreecommitdiff
path: root/internal/api/http.go
diff options
context:
space:
mode:
authorPeter Stone <thepeterstone@gmail.com>2026-01-23 21:37:18 -1000
committerPeter Stone <thepeterstone@gmail.com>2026-01-23 21:37:18 -1000
commit465093343ddd398ce5f6377fc9c472d8251c618b (patch)
treed333a2f1c8879f7b114817e929c95e9fcf5f4c3b /internal/api/http.go
parente23c85577cbb0eac8b847dd989072698ff4e7a30 (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.go143
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
+}