diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index 3066f1b..f335dc9 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -19,7 +19,7 @@ jobs: - name: Set up Go uses: actions/setup-go@v5 with: - go-version: 1.24 + go-version: 1.25 - name: Build run: go build -v ./... diff --git a/Dockerfile b/Dockerfile index 59c6948..26dacd3 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM golang:1.24-alpine AS builder +FROM golang:1.25-alpine AS builder LABEL maintainer="github:@rusq" WORKDIR /build diff --git a/addr.go b/addr.go index 5ba0a5d..a51b69d 100644 --- a/addr.go +++ b/addr.go @@ -1,9 +1,10 @@ package aklapi import ( + "context" "encoding/json" "errors" - "log" + "log/slog" "net/http" "strconv" "time" @@ -36,19 +37,19 @@ func (s Address) String() string { } // AddressLookup is a convenience function to get addresses. -func AddressLookup(addr string) (*AddrResponse, error) { - return MatchingPropertyAddresses(&AddrRequest{SearchText: addr, PageSize: 10}) +func AddressLookup(ctx context.Context, addr string) (*AddrResponse, error) { + return MatchingPropertyAddresses(ctx, &AddrRequest{SearchText: addr, PageSize: 10}) } // MatchingPropertyAddresses wrapper around the AKL Council API. -func MatchingPropertyAddresses(addrReq *AddrRequest) (*AddrResponse, error) { +func MatchingPropertyAddresses(ctx context.Context, addrReq *AddrRequest) (*AddrResponse, error) { cachedAr, ok := addrCache.Lookup(addrReq.SearchText) if ok { - log.Printf("cached address result: %q", cachedAr) + slog.DebugContext(ctx, "found cached address result", "addr", cachedAr) return cachedAr, nil } - req, err := http.NewRequest("GET", addrURI, nil) + req, err := http.NewRequestWithContext(ctx, http.MethodGet, addrURI, nil) if err != nil { return nil, err } @@ -66,7 +67,7 @@ func MatchingPropertyAddresses(addrReq *AddrRequest) (*AddrResponse, error) { return nil, err } defer resp.Body.Close() - log.Printf("address call complete in %s", time.Since(start)) + slog.DebugContext(ctx, "address call complete", "duration", time.Since(start)) if resp.StatusCode != http.StatusOK { return nil, errors.New("address API returned status code: " + strconv.Itoa(resp.StatusCode)) @@ -82,8 +83,8 @@ func MatchingPropertyAddresses(addrReq *AddrRequest) (*AddrResponse, error) { return &apiResp, nil } -func oneAddress(addr string) (*Address, error) { - resp, err := AddressLookup(addr) +func oneAddress(ctx context.Context, addr string) (*Address, error) { + resp, err := AddressLookup(ctx, addr) if err != nil { return nil, err } diff --git a/addr_cache.go b/addr_cache.go deleted file mode 100644 index 240ef09..0000000 --- a/addr_cache.go +++ /dev/null @@ -1,17 +0,0 @@ -package aklapi - -type addrResponseCache map[string]*AddrResponse - -var addrCache = make(addrResponseCache) - -func (c addrResponseCache) Lookup(searchText string) (resp *AddrResponse, ok bool) { - if NoCache { - return nil, false - } - resp, ok = c[searchText] - return -} - -func (c addrResponseCache) Add(searchText string, ar *AddrResponse) { - c[searchText] = ar -} diff --git a/addr_cache_test.go b/addr_cache_test.go deleted file mode 100644 index 3b25fe4..0000000 --- a/addr_cache_test.go +++ /dev/null @@ -1,92 +0,0 @@ -package aklapi - -import ( - "reflect" - "testing" - - "github.com/stretchr/testify/assert" -) - -func Test_addrResponseCache_Lookup(t *testing.T) { - defer func() { - NoCache = false - }() - type args struct { - searchText string - } - tests := []struct { - name string - NoCache bool // if true, set NoCache to true before running test - c addrResponseCache - args args - wantResp *AddrResponse - wantOk bool - }{ - {"not in cache", - false, - addrResponseCache{ - "xxx": &AddrResponse{Items: []Address{*testAddr}}, - }, - args{"yyy"}, - nil, - false, - }, - {"cached", - false, - addrResponseCache{ - testAddr.Address: &AddrResponse{Items: []Address{*testAddr}}, - }, - args{testAddr.Address}, - &AddrResponse{Items: []Address{*testAddr}}, - true, - }, - {"cached, no cache mode", - true, - addrResponseCache{ - testAddr.Address: &AddrResponse{Items: []Address{*testAddr}}, - }, - args{testAddr.Address}, - nil, - false, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - NoCache = tt.NoCache - gotResp, gotOk := tt.c.Lookup(tt.args.searchText) - if !reflect.DeepEqual(gotResp, tt.wantResp) { - t.Errorf("addrResponseCache.Lookup() gotResp = %v, want %v", gotResp, tt.wantResp) - } - if gotOk != tt.wantOk { - t.Errorf("addrResponseCache.Lookup() gotOk = %v, want %v", gotOk, tt.wantOk) - } - }) - } -} - -func Test_addrResponseCache_Add(t *testing.T) { - type args struct { - searchText string - ar *AddrResponse - } - tests := []struct { - name string - c addrResponseCache - args args - wantAddrResponseCache addrResponseCache - }{ - {"add", - addrResponseCache{}, - args{testAddr.Address, &AddrResponse{Items: []Address{*testAddr}}}, - addrResponseCache{ - testAddr.Address: &AddrResponse{Items: []Address{*testAddr}}, - }, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - tt.c.Add(tt.args.searchText, tt.args.ar) - assert.Equal(t, tt.wantAddrResponseCache, tt.c) - }) - } -} diff --git a/addr_test.go b/addr_test.go index ca345aa..3999713 100644 --- a/addr_test.go +++ b/addr_test.go @@ -68,7 +68,7 @@ func TestMatchingPropertyAddresses(t *testing.T) { oldURI := addrURI defer func() { addrURI = oldURI }() addrURI = tt.testSrv.URL - got, err := MatchingPropertyAddresses(tt.args.addrReq) + got, err := MatchingPropertyAddresses(t.Context(), tt.args.addrReq) if (err != nil) != tt.wantErr { t.Errorf("MatchingPropertyAddresses() error = %v, wantErr %v", err, tt.wantErr) return @@ -106,7 +106,7 @@ func TestAddress(t *testing.T) { oldURI := addrURI defer func() { addrURI = oldURI }() addrURI = tt.testSrv.URL - got, err := AddressLookup(tt.args.addr) + got, err := AddressLookup(t.Context(), tt.args.addr) if (err != nil) != tt.wantErr { t.Errorf("Address() error = %v, wantErr %v", err, tt.wantErr) return @@ -160,7 +160,7 @@ func Test_oneAddress(t *testing.T) { oldURI := addrURI defer func() { addrURI = oldURI }() addrURI = tt.testSrv.URL - got, err := oneAddress(tt.args.addr) + got, err := oneAddress(t.Context(), tt.args.addr) if (err != nil) != tt.wantErr { t.Errorf("oneAddress() error = %v, wantErr %v", err, tt.wantErr) return diff --git a/aklapi.go b/aklapi.go index c770167..21988ea 100644 --- a/aklapi.go +++ b/aklapi.go @@ -1,11 +1,9 @@ package aklapi import ( - "regexp" "time" ) var ( defaultLoc, _ = time.LoadLocation("Pacific/Auckland") // Auckland is in NZ. - dow = regexp.MustCompile("Monday|Tuesday|Wednesday|Thursday|Friday|Saturday|Sunday") ) diff --git a/caches.go b/caches.go new file mode 100644 index 0000000..d603c38 --- /dev/null +++ b/caches.go @@ -0,0 +1,62 @@ +package aklapi + +import ( + "github.com/phuslu/lru" +) + +const defCacheSz = 100 // seems reasonable. +var ( + addrCache = newLRUCache[string, *AddrResponse](defCacheSz) + rubbishCache = rubbishResultCache{lc: newLRUCache[string, *CollectionDayDetailResult](defCacheSz)} +) + +type lruCache[K comparable, V any] struct { + cache *lru.LRUCache[K, V] +} + +func newLRUCache[K comparable, V any](size int) *lruCache[K, V] { + return &lruCache[K, V]{ + cache: lru.NewLRUCache[K, V](size), + } +} + +func (c *lruCache[K, V]) Lookup(key K) (resp V, ok bool) { + var nothing V + if NoCache { + return nothing, false + } + return c.cache.Get(key) +} + +func (c *lruCache[K, V]) Add(key K, value V) { + c.cache.Set(key, value) +} + +func (c *lruCache[K, V]) Delete(key K) { + c.cache.Delete(key) +} + +type rubbishResultCache struct { + lc *lruCache[string, *CollectionDayDetailResult] +} + +func (c *rubbishResultCache) Lookup(searchText string) (result *CollectionDayDetailResult, ok bool) { + result, ok = c.lc.Lookup(searchText) + if !ok { + return nil, false + } + + today := now() + for _, res := range result.Collections { + if today.After(res.Date) || res.Date.IsZero() { + // invalidate from cache. + c.lc.Delete(searchText) + return nil, false + } + } + return +} + +func (c *rubbishResultCache) Add(searchText string, result *CollectionDayDetailResult) { + c.lc.Add(searchText, result) +} diff --git a/caches_test.go b/caches_test.go new file mode 100644 index 0000000..30b4d65 --- /dev/null +++ b/caches_test.go @@ -0,0 +1,246 @@ +package aklapi + +import ( + "reflect" + "testing" + "time" + + "github.com/stretchr/testify/assert" +) + +func newLRUFromMap[K comparable, V any](m map[K]V) *lruCache[K, V] { + ret := newLRUCache[K, V](defCacheSz) + for k, v := range m { + ret.Add(k, v) + } + return ret +} + +func Test_addrResponseCache_Lookup(t *testing.T) { + defer func() { + NoCache = false + }() + type args struct { + searchText string + } + tests := []struct { + name string + NoCache bool // if true, set NoCache to true before running test + c *lruCache[string, *AddrResponse] + args args + wantResp *AddrResponse + wantOk bool + }{ + {"not in cache", + false, + newLRUFromMap(map[string]*AddrResponse{ + "xxx": &AddrResponse{Items: []Address{*testAddr}}, + }), + args{"yyy"}, + nil, + false, + }, + {"cached", + false, + newLRUFromMap(map[string]*AddrResponse{ + testAddr.Address: &AddrResponse{Items: []Address{*testAddr}}, + }), + args{testAddr.Address}, + &AddrResponse{Items: []Address{*testAddr}}, + true, + }, + {"cached, no cache mode", + true, + newLRUFromMap(map[string]*AddrResponse{ + testAddr.Address: &AddrResponse{Items: []Address{*testAddr}}, + }), + args{testAddr.Address}, + nil, + false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + NoCache = tt.NoCache + gotResp, gotOk := tt.c.Lookup(tt.args.searchText) + if !reflect.DeepEqual(gotResp, tt.wantResp) { + t.Errorf("addrResponseCache.Lookup() gotResp = %v, want %v", gotResp, tt.wantResp) + } + if gotOk != tt.wantOk { + t.Errorf("addrResponseCache.Lookup() gotOk = %v, want %v", gotOk, tt.wantOk) + } + }) + } +} + +func Test_addrResponseCache_Add(t *testing.T) { + type args struct { + searchText string + ar *AddrResponse + } + tests := []struct { + name string + c *lruCache[string, *AddrResponse] + args args + want *AddrResponse + }{ + {"add", + newLRUCache[string, *AddrResponse](10), + args{testAddr.Address, &AddrResponse{Items: []Address{*testAddr}}}, + &AddrResponse{Items: []Address{*testAddr}}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tt.c.Add(tt.args.searchText, tt.args.ar) + + got, ok := tt.c.Lookup(tt.args.searchText) + assert.True(t, ok) + assert.Equal(t, got, tt.want) + }) + } +} + +var ( + tomorrow = &CollectionDayDetailResult{ + Collections: []RubbishCollection{ + {Date: time.Now().Add(24 * time.Hour), Rubbish: true}, + {Date: time.Now().Add(7 * 24 * time.Hour), Rubbish: true}, + }, + Address: testAddr, + } + today = &CollectionDayDetailResult{ + Collections: []RubbishCollection{ + {Date: time.Now().Add(1 * time.Minute), Rubbish: true}, + {Date: time.Now().Add(7 * 24 * time.Hour), Rubbish: true}, + }, + Address: testAddr, + } + yesterday = &CollectionDayDetailResult{ + Collections: []RubbishCollection{ + {Date: time.Now().Add(-24 * time.Hour), Rubbish: true}, + {Date: time.Now().Add(5 * 24 * time.Hour), Rubbish: true}, + }, + Address: testAddr, + } + secondYesterday = &CollectionDayDetailResult{ + Collections: []RubbishCollection{ + {Date: time.Now().Add(24 * time.Hour), Rubbish: true}, + {Date: time.Now().Add(-24 * time.Hour), Rubbish: true}, + }, + Address: testAddr, + } +) + +func newRubbishCacheFromMap(m map[string]*CollectionDayDetailResult) *rubbishResultCache { + var res = rubbishResultCache{ + lc: newLRUCache[string, *CollectionDayDetailResult](defCacheSz), + } + for k, v := range m { + res.Add(k, v) + } + return &res +} + +func Test_rubbishResultCache_Lookup(t *testing.T) { + defer func() { + NoCache = false + }() + type args struct { + searchText string + } + tests := []struct { + name string + NoCache bool // if true, set NoCache to true before running test + c *rubbishResultCache + args args + wantResult *CollectionDayDetailResult + wantOk bool + }{ + {"not in cache (empty)", false, newRubbishCacheFromMap(map[string]*CollectionDayDetailResult{}), args{"blah"}, nil, false}, + {"not in cache", + false, + newRubbishCacheFromMap(map[string]*CollectionDayDetailResult{ + testAddr.Address: tomorrow, + }), + args{"blah"}, + nil, false}, + {"in cache (future)", + false, + newRubbishCacheFromMap(map[string]*CollectionDayDetailResult{ + testAddr.Address: tomorrow, + }), + args{testAddr.Address}, + tomorrow, true}, + {"in cache (today)", + false, + newRubbishCacheFromMap(map[string]*CollectionDayDetailResult{ + testAddr.Address: today, + }), + args{testAddr.Address}, + today, true}, + {"in cache, expired", + false, + newRubbishCacheFromMap(map[string]*CollectionDayDetailResult{ + testAddr.Address: yesterday, + }), + args{testAddr.Address}, + nil, false}, + {"in cache, second entry expired", + false, + newRubbishCacheFromMap(map[string]*CollectionDayDetailResult{ + testAddr.Address: secondYesterday, + }), + args{testAddr.Address}, + nil, false}, + {"no cache", + true, + newRubbishCacheFromMap(map[string]*CollectionDayDetailResult{ + testAddr.Address: secondYesterday, + }), + args{testAddr.Address}, + nil, false}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + NoCache = tt.NoCache + gotResult, gotOk := tt.c.Lookup(tt.args.searchText) + if !reflect.DeepEqual(gotResult, tt.wantResult) { + t.Errorf("rubbishResultCache.Lookup() gotResult = %v, want %v", gotResult, tt.wantResult) + } + if gotOk != tt.wantOk { + t.Errorf("rubbishResultCache.Lookup() gotOk = %v, want %v", gotOk, tt.wantOk) + } + }) + } +} + +func Test_rubbishResultCache_Add(t *testing.T) { + type args struct { + searchText string + ar *CollectionDayDetailResult + } + tests := []struct { + name string + c *rubbishResultCache + args args + want *CollectionDayDetailResult + }{ + {"add", + newRubbishCacheFromMap(map[string]*CollectionDayDetailResult{ + testAddr.Address: tomorrow, + }), + args{testAddr.Address, tomorrow}, + tomorrow, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tt.c.Add(tt.args.searchText, tt.args.ar) + + res, ok := tt.c.Lookup(tt.args.searchText) + assert.True(t, ok) + assert.Equal(t, tt.want, res) + }) + } +} diff --git a/cmd/aklapi/assets.go b/cmd/aklapi/assets.go index 9004621..5355356 100644 --- a/cmd/aklapi/assets.go +++ b/cmd/aklapi/assets.go @@ -17,7 +17,7 @@ const rootHTML = ` -

AT SOME POINT IN TIME, FUTURE WILL BE DIFFERENT FROM THE PRESENT.

+

AT SOME POINT, THE FUTURE WILL BE DIFFERENT FROM THE PRESENT.

` diff --git a/cmd/aklapi/handlers.go b/cmd/aklapi/handlers.go index 871c0fb..d5ebe04 100644 --- a/cmd/aklapi/handlers.go +++ b/cmd/aklapi/handlers.go @@ -3,7 +3,7 @@ package main import ( "encoding/json" "errors" - "log" + "log/slog" "net/http" "time" @@ -20,7 +20,7 @@ type rrResponse struct { Error string `json:"error,omitempty"` } -func respond(w http.ResponseWriter, data interface{}, code int) { +func respond(w http.ResponseWriter, data any, code int) { b, err := json.Marshal(data) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) @@ -35,7 +35,7 @@ func rubbish(r *http.Request) (*aklapi.CollectionDayDetailResult, error) { if addr == "" { return nil, errors.New(http.StatusText(http.StatusBadRequest)) } - return aklapi.CollectionDayDetail(addr) + return aklapi.CollectionDayDetail(r.Context(), addr) } func addrHandler(w http.ResponseWriter, r *http.Request) { @@ -44,9 +44,9 @@ func addrHandler(w http.ResponseWriter, r *http.Request) { http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) return } - resp, err := aklapi.AddressLookup(addr) + resp, err := aklapi.AddressLookup(r.Context(), addr) if err != nil { - log.Println(err) + slog.Error("address lookup failed", "error", err) http.NotFound(w, r) return } @@ -92,7 +92,7 @@ func rootHandler(w http.ResponseWriter, r *http.Request) { CyberdyneLogo: cyberdynePng, } if err := tmpl.ExecuteTemplate(w, "index.html", &page); err != nil { - log.Println(err) + slog.Error("template rendering failed", "err", err) http.NotFound(w, r) return } diff --git a/cmd/aklapi/main.go b/cmd/aklapi/main.go index c0942bb..528d150 100644 --- a/cmd/aklapi/main.go +++ b/cmd/aklapi/main.go @@ -1,10 +1,12 @@ package main import ( + "errors" "flag" "fmt" "html/template" "log" + "log/slog" "net/http" "os" _ "time/tzdata" @@ -42,10 +44,9 @@ var ( var tmpl = template.Must(template.New("index.html").Parse(rootHTML)) func main() { - log.SetFlags(log.LstdFlags | log.Lmicroseconds) flag.Parse() if *port == "" { - log.Printf("no port specified, defaulting to %s", defaultPort) + slog.Info("no port specified, using default", "port", defaultPort) } // Set the global caching flag. @@ -58,9 +59,15 @@ func main() { http.HandleFunc(apiRRExt, rrExtHandler) hostport := fmt.Sprintf("%s:%s", *host, *port) - log.Println(banner) - log.Println("Listening on: ", hostport) - log.Fatal(http.ListenAndServe(hostport, nil)) + slog.Info(banner) + slog.Info("Listening", "addr", hostport) + if err := http.ListenAndServe(hostport, nil); err != nil { + if errors.Is(err, http.ErrServerClosed) { + slog.Info("server closed") + return + } + log.Fatal(err) + } } func usage() { diff --git a/go.mod b/go.mod index 7a2888f..f72012d 100644 --- a/go.mod +++ b/go.mod @@ -1,11 +1,12 @@ module github.com/rusq/aklapi -go 1.23.0 +go 1.24.0 toolchain go1.24.2 require ( - github.com/PuerkitoBio/goquery v1.10.3 + github.com/PuerkitoBio/goquery v1.11.0 + github.com/phuslu/lru v1.0.18 github.com/rusq/osenv/v2 v2.0.1 github.com/stretchr/testify v1.8.1 ) @@ -14,6 +15,6 @@ require ( github.com/andybalholm/cascadia v1.3.3 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect - golang.org/x/net v0.39.0 // indirect + golang.org/x/net v0.48.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index d6de205..e2007ec 100644 --- a/go.sum +++ b/go.sum @@ -1,15 +1,15 @@ -github.com/PuerkitoBio/goquery v1.9.2 h1:4/wZksC3KgkQw7SQgkKotmKljk0M6V8TUvA8Wb4yPeE= -github.com/PuerkitoBio/goquery v1.9.2/go.mod h1:GHPCaP0ODyyxqcNoFGYlAprUFH81NuRPd0GX3Zu2Mvk= github.com/PuerkitoBio/goquery v1.10.3 h1:pFYcNSqHxBD06Fpj/KsbStFRsgRATgnf3LeXiUkhzPo= github.com/PuerkitoBio/goquery v1.10.3/go.mod h1:tMUX0zDMHXYlAQk6p35XxQMqMweEKB7iK7iLNd4RH4Y= -github.com/andybalholm/cascadia v1.3.2 h1:3Xi6Dw5lHF15JtdcmAHD3i1+T8plmv7BQ/nsViSLyss= -github.com/andybalholm/cascadia v1.3.2/go.mod h1:7gtRlve5FxPPgIgX36uWBX58OdBsSS6lUvCFb+h7KvU= +github.com/PuerkitoBio/goquery v1.11.0 h1:jZ7pwMQXIITcUXNH83LLk+txlaEy6NVOfTuP43xxfqw= +github.com/PuerkitoBio/goquery v1.11.0/go.mod h1:wQHgxUOU3JGuj3oD/QFfxUdlzW6xPHfqyHre6VMY4DQ= github.com/andybalholm/cascadia v1.3.3 h1:AG2YHrzJIm4BZ19iwJ/DAua6Btl3IwJX+VI4kktS1LM= github.com/andybalholm/cascadia v1.3.3/go.mod h1:xNd9bqTn98Ln4DwST8/nG+H0yuB8Hmgu1YHNnWw0GeA= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/phuslu/lru v1.0.18 h1:ioKRYLym7nv6UmaKHXSR0Z8s2KCEra+mcWcn9zXQnlM= +github.com/phuslu/lru v1.0.18/go.mod h1:ci5hb8dRIa+2I+KcPl4958OWCg09FxwZCP8InU1L1ME= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rusq/osenv/v2 v2.0.1 h1:1LtNt8VNV/W86wb38Hyu5W3Rwqt/F1JNRGE+8GRu09o= @@ -21,8 +21,6 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= -github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= @@ -39,16 +37,15 @@ golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLL golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= -golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= -golang.org/x/net v0.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE= -golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg= golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4= golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY= golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E= +golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU= +golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -62,7 +59,6 @@ golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= @@ -72,7 +68,6 @@ golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXct golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= -golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY= golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= diff --git a/rubbish.go b/rubbish.go index 8e670a7..c1371b0 100644 --- a/rubbish.go +++ b/rubbish.go @@ -1,10 +1,11 @@ package aklapi import ( + "context" "errors" "fmt" "io" - "log" + "log/slog" "net/http" "strings" "time" @@ -91,29 +92,33 @@ func (res *CollectionDayDetailResult) NextFoodScraps() time.Time { // CollectionDayDetail returns a collection day details for the specified // address as reported by the Auckland Council Website. -func CollectionDayDetail(addr string) (*CollectionDayDetailResult, error) { +func CollectionDayDetail(ctx context.Context, addr string) (*CollectionDayDetailResult, error) { if cachedRes, ok := rubbishCache.Lookup(addr); ok { - log.Printf("cached rubbish result for %q", addr) + slog.DebugContext(ctx, "found cached rubbish result", "addr", addr) return cachedRes, nil } - address, err := oneAddress(addr) + address, err := oneAddress(ctx, addr) if err != nil { return nil, err } start := time.Now() - result, err := fetchandparse(address.ID) + result, err := fetchandparse(ctx, address.ID) if err != nil { return nil, err } result.Address = address rubbishCache.Add(addr, result) - log.Printf("rubbish fetch and parse complete in %s", time.Since(start)) + slog.DebugContext(ctx, "rubbish fetch and parse complete", "duration", time.Since(start)) return result, nil } // fetchandparse retrieves the data from the webpage and attempts to parse it. -func fetchandparse(addressID string) (*CollectionDayDetailResult, error) { - resp, err := http.Get(fmt.Sprintf(collectionDayURI, addressID)) +func fetchandparse(ctx context.Context, addressID string) (*CollectionDayDetailResult, error) { + req, err := http.NewRequestWithContext(ctx, http.MethodGet, fmt.Sprintf(collectionDayURI, addressID), nil) + if err != nil { + return nil, err + } + resp, err := http.DefaultClient.Do(req) if err != nil { return nil, err } @@ -167,7 +172,7 @@ func (p *refuseParser) parse(r io.Reader) ([]RubbishCollection, error) { p.detail = p.detail[:i] break } - log.Println(err) + slog.Error("date parse error", "error", err, "day", p.detail[i].Day) continue } } diff --git a/rubbish_cache.go b/rubbish_cache.go deleted file mode 100644 index aa4d816..0000000 --- a/rubbish_cache.go +++ /dev/null @@ -1,29 +0,0 @@ -package aklapi - -type rubbishResultCache map[string]*CollectionDayDetailResult - -var rubbishCache rubbishResultCache = make(rubbishResultCache, 0) - -func (c rubbishResultCache) Lookup(searchText string) (result *CollectionDayDetailResult, ok bool) { - if NoCache { - return nil, false - } - result, ok = c[searchText] - if !ok { - return - } - - today := now() - for _, res := range result.Collections { - if today.After(res.Date) || res.Date.IsZero() { - // invalidate from cache. - delete(c, searchText) - return nil, false - } - } - return -} - -func (c rubbishResultCache) Add(searchText string, result *CollectionDayDetailResult) { - c[searchText] = result -} diff --git a/rubbish_cache_test.go b/rubbish_cache_test.go deleted file mode 100644 index c727811..0000000 --- a/rubbish_cache_test.go +++ /dev/null @@ -1,142 +0,0 @@ -package aklapi - -import ( - "reflect" - "testing" - "time" - - "github.com/stretchr/testify/assert" -) - -var ( - tomorrow = &CollectionDayDetailResult{ - Collections: []RubbishCollection{ - {Date: time.Now().Add(24 * time.Hour), Rubbish: true}, - {Date: time.Now().Add(7 * 24 * time.Hour), Rubbish: true}, - }, - Address: testAddr, - } - today = &CollectionDayDetailResult{ - Collections: []RubbishCollection{ - {Date: time.Now().Add(1 * time.Minute), Rubbish: true}, - {Date: time.Now().Add(7 * 24 * time.Hour), Rubbish: true}, - }, - Address: testAddr, - } - yesterday = &CollectionDayDetailResult{ - Collections: []RubbishCollection{ - {Date: time.Now().Add(-24 * time.Hour), Rubbish: true}, - {Date: time.Now().Add(5 * 24 * time.Hour), Rubbish: true}, - }, - Address: testAddr, - } - secondYesterday = &CollectionDayDetailResult{ - Collections: []RubbishCollection{ - {Date: time.Now().Add(24 * time.Hour), Rubbish: true}, - {Date: time.Now().Add(-24 * time.Hour), Rubbish: true}, - }, - Address: testAddr, - } -) - -func Test_rubbishResultCache_Lookup(t *testing.T) { - defer func() { - NoCache = false - }() - type args struct { - searchText string - } - tests := []struct { - name string - NoCache bool // if true, set NoCache to true before running test - c rubbishResultCache - args args - wantResult *CollectionDayDetailResult - wantOk bool - }{ - {"not in cache (empty)", false, rubbishResultCache{}, args{"blah"}, nil, false}, - {"not in cache", - false, - rubbishResultCache{ - testAddr.Address: tomorrow, - }, - args{"blah"}, - nil, false}, - {"in cache (future)", - false, - rubbishResultCache{ - testAddr.Address: tomorrow, - }, - args{testAddr.Address}, - tomorrow, true}, - {"in cache (today)", - false, - rubbishResultCache{ - testAddr.Address: today, - }, - args{testAddr.Address}, - today, true}, - {"in cache, expired", - false, - rubbishResultCache{ - testAddr.Address: yesterday, - }, - args{testAddr.Address}, - nil, false}, - {"in cache, second entry expired", - false, - rubbishResultCache{ - testAddr.Address: secondYesterday, - }, - args{testAddr.Address}, - nil, false}, - {"no cache", - true, - rubbishResultCache{ - testAddr.Address: today, - }, - args{testAddr.Address}, - nil, false}, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - NoCache = tt.NoCache - gotResult, gotOk := tt.c.Lookup(tt.args.searchText) - if !reflect.DeepEqual(gotResult, tt.wantResult) { - t.Errorf("rubbishResultCache.Lookup() gotResult = %v, want %v", gotResult, tt.wantResult) - } - if gotOk != tt.wantOk { - t.Errorf("rubbishResultCache.Lookup() gotOk = %v, want %v", gotOk, tt.wantOk) - } - }) - } -} - -func Test_rubbishResultCache_Add(t *testing.T) { - type args struct { - searchText string - ar *CollectionDayDetailResult - } - tests := []struct { - name string - c rubbishResultCache - args args - wantrubbishResultCache rubbishResultCache - }{ - {"add", - rubbishResultCache{ - testAddr.Address: tomorrow, - }, - args{testAddr.Address, tomorrow}, - rubbishResultCache{ - testAddr.Address: tomorrow, - }, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - tt.c.Add(tt.args.searchText, tt.args.ar) - assert.Equal(t, tt.wantrubbishResultCache, tt.c) - }) - } -} diff --git a/rubbish_test.go b/rubbish_test.go index fd913fd..0ee7865 100644 --- a/rubbish_test.go +++ b/rubbish_test.go @@ -153,7 +153,7 @@ func TestCollectionDayDetail(t *testing.T) { defer func() { addrURI = oldAddrURI; collectionDayURI = oldcollectionDayURI }() addrURI = tt.testSrv.URL + "/addr" collectionDayURI = tt.testSrv.URL + "/rubbish/?an=%s" - got, err := CollectionDayDetail(tt.args.addr) + got, err := CollectionDayDetail(t.Context(), tt.args.addr) if (err != nil) != tt.wantErr { t.Errorf("CollectionDayDetail() error = %v, wantErr %v", err, tt.wantErr) return