Skip to content

Commit 4dff437

Browse files
authored
Merge pull request #27 from rusq/26
fix 406 error and migrate to new Auckland API address
2 parents 46d58ba + 44a1655 commit 4dff437

File tree

7 files changed

+252
-45
lines changed

7 files changed

+252
-45
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,4 @@ aklrubbish
66

77
# binary files
88
aklapi
9+
.env

AGENTS.md

Lines changed: 215 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,215 @@
1-
# AGENTS.md
2-
3-
## Project Snapshot
4-
- Module: `github.com/rusq/aklapi`
5-
- Language: Go
6-
- Purpose: unofficial Auckland Council API wrapper/service for address lookup and rubbish/recycling collection dates.
7-
8-
## Repository Map
9-
- `cmd/`: executable entrypoints.
10-
- `addr.go`: address lookup logic.
11-
- `rubbish.go`: rubbish/recycling API logic and response shaping.
12-
- `caches.go`: cache helpers.
13-
- `time.go`: date/time helpers.
14-
- `*_test.go`: unit tests.
15-
- `test_assets/`: fixtures used by tests.
16-
17-
## Development Commands
18-
- Run tests: `go test ./...`
19-
- Run focused tests: `go test ./... -run <Name>`
20-
- Tidy dependencies: `go mod tidy`
21-
- Build all packages: `go build ./...`
22-
23-
## Working Conventions
24-
- Prefer small, targeted changes over broad refactors.
25-
- Keep API behavior backward compatible unless explicitly requested.
26-
- Add or update tests for behavioral changes.
27-
- Keep exported identifiers and package-level docs concise.
28-
29-
## Validation Checklist
30-
Before finishing a code change, run:
31-
1. `go test ./...`
32-
2. `go build ./...`
33-
34-
If a change only affects docs or comments, note that tests/build were not required.
1+
# AGENTS.md — Coding Agent Instructions for `aklapi`
2+
3+
This document provides guidance for agentic coding assistants operating in this repository.
4+
5+
---
6+
7+
## Project Overview
8+
9+
`aklapi` is a Go library and HTTP server that exposes Auckland Council APIs
10+
(rubbish collection schedules, property address lookup) as a simple REST service.
11+
12+
- **Module:** `github.com/rusq/aklapi` (`go 1.25`)
13+
- **Library package:** root (`aklapi`)
14+
- **Binary:** `cmd/aklapi/` — standard HTTP server on port 8080
15+
- **Language:** Go only — no TypeScript, JavaScript, or Node tooling
16+
17+
---
18+
19+
## Build, Run & Test Commands
20+
21+
```sh
22+
# Build the server binary
23+
go build -o server ./cmd/aklapi
24+
25+
# Run the server (port defaults to 8080)
26+
./server
27+
28+
# Run all tests
29+
go test ./...
30+
31+
# Run all tests with verbose output
32+
go test -v ./...
33+
34+
# Run a single test by name (supports regex)
35+
go test -v -run TestFunctionName ./...
36+
37+
# Run a single test in a specific package
38+
go test -v -run TestCollectionDayDetail ./cmd/aklapi/
39+
40+
# Run tests with race detector
41+
go test -race ./...
42+
43+
# Build all packages (verify compilation)
44+
go build -v ./...
45+
46+
# Format code (use goimports, not gofmt)
47+
goimports -w .
48+
49+
# Lint (golangci-lint with default config)
50+
golangci-lint run ./...
51+
52+
# Docker build
53+
docker build -t aklapi .
54+
55+
# Make targets
56+
make server # go build -o server ./cmd/aklapi
57+
make test # go test ./... -race
58+
make docker # docker build -t aklapi .
59+
```
60+
61+
> **To run a single test:** use `go test -v -run <TestName> <./package/path>`
62+
> Example: `go test -v -run TestNextRubbish .`
63+
64+
---
65+
66+
## Code Style Guidelines
67+
68+
### Formatting
69+
70+
- Use **`goimports`** (not plain `gofmt`) — it manages imports automatically.
71+
- Indentation: **tabs** (Go standard).
72+
- VS Code devcontainer is configured with `"editor.formatOnSave": true` using `goimports`.
73+
- No trailing whitespace; no blank lines at end of file.
74+
75+
### Imports
76+
77+
Group imports in two blocks separated by a blank line:
78+
1. Standard library
79+
2. Third-party packages
80+
81+
```go
82+
import (
83+
"context"
84+
"encoding/json"
85+
"net/http"
86+
87+
"github.com/PuerkitoBio/goquery"
88+
)
89+
```
90+
91+
- Use blank imports only where required: `_ "time/tzdata"`, `_ "embed"`.
92+
- Never use dot imports (`.`).
93+
- Alias imports only when disambiguation is genuinely needed.
94+
95+
### Naming Conventions
96+
97+
| Element | Convention | Example |
98+
|---|---|---|
99+
| Exported types | PascalCase | `AddrRequest`, `RubbishCollection` |
100+
| Unexported types | camelCase | `refuseParser`, `lruCache` |
101+
| Exported functions | PascalCase | `AddressLookup`, `CollectionDayDetail` |
102+
| Unexported functions | camelCase | `fetchandparse`, `oneAddress` |
103+
| Receiver names | Short (1–2 chars) | `(r *RubbishCollection)`, `(c *lruCache[K,V])` |
104+
| Package-level vars | camelCase | `addrCache`, `defaultLoc` |
105+
| Unexported constants | camelCase | `defCacheSz`, `dateLayout` |
106+
| Acronyms | Go convention | `addrURI` (not `addrUrl`), `ID` (not `Id`) |
107+
108+
### Types & Structs
109+
110+
- Add JSON struct tags to all exported response types: `json:"field,omitempty"`.
111+
- Prefer pointer receivers for types that may mutate state or are large.
112+
- Use **generics** for reusable containers (see `lruCache[K comparable, V any]`).
113+
- Use a stateful parser type (struct with fields for state, error, and results) when
114+
parsing multi-step data (see `refuseParser`).
115+
116+
### Error Handling
117+
118+
- Always check errors: `if err != nil { return nil, err }`.
119+
- No `panic` in production code.
120+
- Use `errors.New("...")` for static error messages.
121+
- Use string concatenation (not `fmt.Sprintf`) for simple dynamic error strings:
122+
```go
123+
errors.New("address API returned status code: " + strconv.Itoa(resp.StatusCode))
124+
```
125+
- Prefer `fmt.Errorf("context: %w", err)` for wrapping errors that need context.
126+
- Use package-level sentinel errors for flow control:
127+
```go
128+
var errSkip = errors.New("skip this date")
129+
```
130+
- Use `errors.Is` for sentinel error comparisons.
131+
- HTTP handlers: use `http.Error(w, msg, code)` or a typed `respond(w, body, code)` helper.
132+
133+
### HTTP & Networking
134+
135+
- Use **standard library `net/http` only** — no external router (no Gin, Echo, Chi).
136+
- Register routes with `http.HandleFunc` on the default mux.
137+
- Always pass context to outgoing HTTP requests:
138+
```go
139+
req, err := http.NewRequestWithContext(ctx, http.MethodGet, uri, nil)
140+
```
141+
- Always `defer resp.Body.Close()` immediately after a successful response.
142+
- Decode JSON responses with `json.NewDecoder(resp.Body).Decode(&v)`.
143+
144+
### Logging
145+
146+
- Use **`log/slog`** for all logging — not `log.Printf`, `fmt.Println`, etc.
147+
- Prefer context-aware variants: `slog.DebugContext(ctx, ...)`, `slog.InfoContext(ctx, ...)`.
148+
- Add structured key-value pairs for observability:
149+
```go
150+
start := time.Now()
151+
// ... operation ...
152+
slog.DebugContext(ctx, "fetched addresses", "count", len(results), "duration", time.Since(start))
153+
```
154+
155+
### Dependency Injection & Testability
156+
157+
- Declare external URLs as **package-level `var`** (not `const`) so tests can override them:
158+
```go
159+
var addrURI = `https://example.com/api/addresses`
160+
```
161+
- Inject time via a replaceable variable: `var now = time.Now`.
162+
- Injectable function-type variables enable handler testing without real upstream calls:
163+
```go
164+
var addressLookup = aklapi.AddressLookup
165+
```
166+
- Restore overridden vars with `defer`:
167+
```go
168+
old := addrURI
169+
addrURI = ts.URL
170+
defer func() { addrURI = old }()
171+
```
172+
173+
---
174+
175+
## Testing Guidelines
176+
177+
### Style
178+
179+
- Use **table-driven tests** for all non-trivial functions.
180+
- Table entry struct fields: `name string`, `args`, `want`, `wantErr bool`.
181+
- Field names may be omitted for the `name` field in composite literals.
182+
- Prefer `github.com/stretchr/testify/assert` for assertions in new tests (avoid raw
183+
`reflect.DeepEqual` + `t.Errorf` patterns from older tests).
184+
- Use `t.Context()` (Go 1.24+) for context in subtests.
185+
- Use `t.Cleanup(func() {...})` for teardown instead of `defer` in the test function body
186+
when working with subtests.
187+
188+
### HTTP Testing
189+
190+
- Use `net/http/httptest.NewServer` to mock upstream APIs.
191+
- Use `httptest.NewRequest` + `httptest.NewRecorder` for handler unit tests.
192+
193+
### Test Fixtures
194+
195+
- Embed HTML fixture files with `//go:embed`:
196+
```go
197+
//go:embed test_assets/some-page.html
198+
var fixtureHTML []byte
199+
```
200+
- Fixtures are refreshed by `//go:generate` directives that `curl` the live page.
201+
202+
### Subtests
203+
204+
- Always run subtests with `t.Run(tt.name, func(t *testing.T) { ... })`.
205+
- Use `t.Helper()` in assertion helper functions.
206+
207+
---
208+
209+
## Repository Conventions
210+
211+
- **One concern per file:** `addr.go`, `rubbish.go`, `caches.go`, `time.go`.
212+
- **Library in root, binary in `cmd/`:** follows standard Go project layout.
213+
- CI runs on `push` and `pull_request` to `master` (see `.github/workflows/go.yml`):
214+
`go build -v ./...` then `go test -v ./...`.
215+
- Docker images are published to `ffffuuu/aklapi` on GitHub Release events.

addr.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,9 @@ import (
1212

1313
var (
1414
// defined as a variable so it can be overridden in tests.
15-
addrURI = `https://www.aucklandcouncil.govt.nz/nextapi/property`
15+
addrURI = `https://experience.aucklandcouncil.govt.nz/nextapi/property`
1616
// defined as a variable so tests can replace it.
17-
addrHTTPClient = &http.Client{Timeout: 15 * time.Second}
17+
addrHTTPClient = &http.Client{Timeout: 15 * time.Second, Transport: &browserTransport{wrapped: http.DefaultTransport}}
1818
)
1919

2020
// AddrRequest is the address request.
@@ -25,8 +25,8 @@ type AddrRequest struct {
2525

2626
// Address is the address and its unique identifier (rate account key).
2727
type Address struct {
28-
ID string `json:"ID"`
29-
Address string `json:"Address"`
28+
ID string `json:"id"`
29+
Address string `json:"address"`
3030
}
3131

3232
// AddrResponse is the address response.

aklapi.go

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,33 @@
11
package aklapi
22

33
import (
4+
"net/http"
45
"time"
56
)
67

78
var (
89
defaultLoc, _ = time.LoadLocation("Pacific/Auckland") // Auckland is in NZ.
910
)
11+
12+
// userAgent is sent with all outgoing HTTP requests. The Auckland Council
13+
// website CDN (Fastly) returns 406 for requests that identify as Go's default
14+
// http client, so we send a browser-compatible value instead.
15+
const userAgent = "Mozilla/5.0 (compatible; aklapi/1.0)"
16+
17+
// browserTransport is an http.RoundTripper that adds browser-like headers to
18+
// every request before forwarding it to the underlying transport.
19+
type browserTransport struct {
20+
wrapped http.RoundTripper
21+
}
22+
23+
func (t *browserTransport) RoundTrip(req *http.Request) (*http.Response, error) {
24+
// Clone the request so we don't mutate the caller's copy.
25+
r := req.Clone(req.Context())
26+
if r.Header.Get("User-Agent") == "" {
27+
r.Header.Set("User-Agent", userAgent)
28+
}
29+
if r.Header.Get("Accept") == "" {
30+
r.Header.Set("Accept", "application/json, text/html, */*")
31+
}
32+
return t.wrapped.RoundTrip(r)
33+
}

cmd/aklapi/handlers.go

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,14 @@ import (
1010
"github.com/rusq/aklapi"
1111
)
1212

13-
const dttmLayout = "2006-01-02"
14-
13+
// injectable for testing
1514
var (
1615
addressLookup = aklapi.AddressLookup
1716
collectionDayDetail = aklapi.CollectionDayDetail
1817
)
1918

19+
const dttmLayout = "2006-01-02"
20+
2021
type rrResponse struct {
2122
Rubbish string `json:"rubbish,omitempty"`
2223
Recycle string `json:"recycle,omitempty"`
@@ -53,7 +54,7 @@ func addrHandler(w http.ResponseWriter, r *http.Request) {
5354
resp, err := addressLookup(r.Context(), addr)
5455
if err != nil {
5556
slog.Error("address lookup failed", "error", err)
56-
http.Error(w, http.StatusText(http.StatusBadGateway), http.StatusBadGateway)
57+
http.Error(w, err.Error(), http.StatusBadGateway)
5758
return
5859
}
5960
respond(w, resp, http.StatusOK)

rubbish.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,9 @@ const (
2222

2323
var (
2424
// defined as a variable so it can be overridden in tests.
25-
collectionDayURI = `https://new.aucklandcouncil.govt.nz/en/rubbish-recycling/rubbish-recycling-collections/rubbish-recycling-collection-days/%s.html`
25+
collectionDayURI = `https://www.aucklandcouncil.govt.nz/en/rubbish-recycling/rubbish-recycling-collections/rubbish-recycling-collection-days/%s.html`
2626
// defined as a variable so tests can replace it.
27-
collectionHTTPClient = &http.Client{Timeout: 15 * time.Second}
27+
collectionHTTPClient = &http.Client{Timeout: 15 * time.Second, Transport: &browserTransport{wrapped: http.DefaultTransport}}
2828
)
2929

3030
var errSkip = errors.New("skip this date")

rubbish_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,8 @@ import (
1414
"github.com/stretchr/testify/assert"
1515
)
1616

17-
//go:generate curl -L https://new.aucklandcouncil.govt.nz/en/rubbish-recycling/rubbish-recycling-collections/rubbish-recycling-collection-days/12342478585.html -o test_assets/500-queen-street.html
18-
//go:generate curl -L https://new.aucklandcouncil.govt.nz/en/rubbish-recycling/rubbish-recycling-collections/rubbish-recycling-collection-days/12341511281.html -o test_assets/1-luanda-drive.html
17+
//go:generate curl -L https://www.aucklandcouncil.govt.nz/en/rubbish-recycling/rubbish-recycling-collections/rubbish-recycling-collection-days/12342478585.html -o test_assets/500-queen-street.html
18+
//go:generate curl -L https://www.aucklandcouncil.govt.nz/en/rubbish-recycling/rubbish-recycling-collections/rubbish-recycling-collection-days/12341511281.html -o test_assets/1-luanda-drive.html
1919

2020
// Test data, run go:generate to update, then update dates in tests
2121
// accordingly.

0 commit comments

Comments
 (0)