2 Commits

Author SHA1 Message Date
4310375dc9 Replace io.ReadAll + json.Unmarshal with streaming JSON decoder
Refactor the internal HTTP helper from get() returning raw bytes to
getAndDecode() that streams directly into the target struct via
json.NewDecoder. This eliminates the intermediate []byte allocation
from io.ReadAll on every API response.

The new decoder also validates that responses contain exactly one JSON
value by attempting a second Decode after the primary one — any content
beyond the first value (e.g., concatenated objects from a misbehaving
proxy) returns an error instead of silently discarding it.

Changes:
- api/client.go: Replace get() with getAndDecode(), update FetchStores
  and FetchSavings callers to use the new signature
- api/client_test.go: Add TestFetchSavings_TrailingJSONIsRejected and
  TestFetchStores_MalformedJSONReturnsDecodeError covering the new
  decoder error paths

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-23 00:11:24 -05:00
5efe7581ed Add Publix API client for savings and store location endpoints
HTTP client that wraps the Publix services API with two endpoints:
- /api/v4/savings — fetches weekly ad deals for a given store number
- /api/v1/storelocation — finds nearby stores by ZIP code

Includes request types (SavingsResponse, SavingItem, StoreResponse,
Store) mapping directly to the Publix JSON schema. The client sends
a PublixStore header for store-scoped requests and uses a 15-second
timeout. Tests use httptest servers to verify header propagation,
JSON decoding, and error handling for non-200 responses.
2026-02-22 21:41:39 -05:00