Fixed lint issues and missing documentation

This commit is contained in:
bwilliams committed 2026-10-13 12:21:32 +01:00
1 parent 7fd321df97
commit 6e6dec68c0
34 files changed
+346 -156

No files matched your search

+48 -25
View File
@@ -18,19 +18,37 @@ import (
resty "resty.dev/v3"
)
var ErrNoNext = errors.New("no next link")
var ErrNoReleases = errors.New("no releases")
var ErrRateLimitExceeded = errors.New("rate limit has been exceeded")
var ErrFetchCancelled = errors.New("fetch cancelled")
var ErrUnexpectedStatus = errors.New("unexpected response status")
// Errors returned when fetching from the API.
var (
// ErrNoNext means a release package has no next page link.
ErrNoNext = errors.New("no next link")
// ErrNoReleases means the API returned no releases.
ErrNoReleases = errors.New("no releases")
// ErrRateLimitExceeded means the API refused a request with 429 Too Many
// Requests, and waiting for it was not allowed or did not help.
ErrRateLimitExceeded = errors.New("rate limit has been exceeded")
// ErrFetchCancelled means the fetch's context was cancelled while waiting
// out a rate limit.
ErrFetchCancelled = errors.New("fetch cancelled")
// ErrUnexpectedStatus wraps an API response status other than 200 OK.
ErrUnexpectedStatus = errors.New("unexpected response status")
)
// RequestOptions selects the releases a fetch asks for.
type RequestOptions struct {
Since time.Time
Limit int
// Since asks for releases updated from this time. Zero means 24 hours ago.
Since time.Time
// Limit is the number of releases per page. Zero means 100.
Limit int
// AllowRateLimit waits out a 429 response and retries once, instead of
// failing with ErrRateLimitExceeded.
AllowRateLimit bool
}
// ---------- Utility methods for handling API irregularities -----------
// safeReleasePackage is ocds.ReleasePackage with the releases left undecoded,
// so safeUnpackReleases can repair or skip each one.
type safeReleasePackage struct {
URI string `json:"uri,omitempty"`
Version string `json:"version,omitempty"`
@@ -127,7 +145,7 @@ func isDateField(name string) bool {
strings.HasSuffix(name, "Deadline")
}
// ReleaseIDs reads just the ocid and the release id from a release, without
// releaseIDs reads just the ocid and the release id from a release, without
// decoding the rest of it. It reads the top-level fields in order and stops
// as soon as it has both, so an error later in the release (invalid JSON or a
// value that wouldn't fit the release struct) doesn't matter. Fields before
@@ -202,15 +220,16 @@ func missingIDsError(haveOCID, haveID bool) error {
}
}
// Unpacks an API response into an OCDS ReleasePackage. Handles API
// irregularities (malformed dates, missing fields, invalid releases, etc).
// safeUnpackReleases unpacks an API response into an OCDS ReleasePackage,
// handling API irregularities (malformed dates, missing fields, invalid
// releases, etc).
//
// This method takes ownership of the response body and closes it when done.
func safeUnpackReleases(response *resty.Response) (*ocds.ReleasePackage, error) {
var pkg safeReleasePackage
// Take ownership of the response body.
defer response.Body.Close()
defer response.Body.Close() //nolint:errcheck // read only
decoder := json.NewDecoder(response.Body)
if err := decoder.Decode(&pkg); err != nil {
@@ -255,9 +274,10 @@ func safeUnpackReleases(response *resty.Response) (*ocds.ReleasePackage, error)
}
// ---------- API interaction methods -----------
// Sleep the fetch worker until either the context is cancelled (shutdown) or
// the duration has expired. The duration is usually provided by the API - a
// nil duration will return immediately.
// rateSleep sleeps the fetch worker until either the context is cancelled
// (shutdown), returning ctx.Err(), or the duration has expired. The duration
// is usually provided by the API; a zero duration returns immediately.
func rateSleep(ctx context.Context, duration time.Duration) error {
timer := time.NewTimer(duration)
defer timer.Stop()
@@ -270,14 +290,16 @@ func rateSleep(ctx context.Context, duration time.Duration) error {
}
}
// Check and close the response body - avoids double close issues
// closeBody closes res's body if it has one, avoiding a double close.
func closeBody(res *resty.Response) {
if res.Body != nil {
res.Body.Close()
res.Body.Close() //nolint:errcheck,gosec // read only
}
}
// Actually perform a single fetch against the API
// fetchURI performs a single GET of uri, using ft's context, and traces it at
// debug level. Unlike the method of the same name it does not check the
// response status.
func fetchURI(ft *FindATender, uri string) (*resty.Response, error) {
withTrace := ft.config.Logging.LogLevel() <= slog.LevelDebug
@@ -310,7 +332,7 @@ func fetchURI(ft *FindATender, uri string) (*resty.Response, error) {
return res, err
}
// Perform a fetch against the API. If the API returns with a rate limit
// fetchURI performs a fetch against the API. If the API returns with a rate limit
// reached response and allowRateLimit is true, then wait and retry like a good
// client. If allowRateLimit is false, return to the caller informing them of
// the rate limit exceeded error. If, after waiting, the API still thinks we
@@ -367,7 +389,8 @@ func (ft *FindATender) fetchURI(
return res, nil
}
// Perform a fetch against the API with some sanitised default options.
// fetchWithOpts performs a fetch against the API with some sanitised default
// options (see RequestOptions).
func (ft *FindATender) fetchWithOpts(
opts RequestOptions,
) (*resty.Response, error) {
@@ -388,9 +411,9 @@ func (ft *FindATender) fetchWithOpts(
return ft.fetchURI(uri, opts.AllowRateLimit)
}
// If the response provided a "Next" link, perform another API call to get the
// next page. Error if there is no "Next" link - the caller can decide if they
// have attempted to fetch next in error or not.
// fetchNext fetches the page pkg's "Next" link points to. It returns
// ErrNoNext if there is no "Next" link; the caller can decide whether asking
// for it was an error.
func (ft *FindATender) fetchNext(
pkg *ocds.ReleasePackage,
allowRateLimit bool,
@@ -401,7 +424,7 @@ func (ft *FindATender) fetchNext(
return ft.fetchURI(pkg.Links.Next, allowRateLimit)
}
// Make one call on the API. Do not follow any "Next" links.
// fetchOnce makes one call on the API. It does not follow any "Next" links.
func (ft *FindATender) fetchOnce(opts RequestOptions) ([]ocds.Release, error) {
res, err := ft.fetchWithOpts(opts)
if err != nil {
@@ -420,8 +443,8 @@ func (ft *FindATender) fetchOnce(opts RequestOptions) ([]ocds.Release, error) {
return pkg.Releases, nil
}
// Make a call on the API. If there are any "Next" links, call them in
// succession until the end has been reached.
// fetchAll makes a call on the API. If there are any "Next" links, it calls
// them in succession until the end has been reached.
func (ft *FindATender) fetchAll(opts RequestOptions) ([]ocds.Release, error) {
var releases []ocds.Release
+8 -2
View File
@@ -2,6 +2,7 @@ package findATender
import (
"context"
"log/slog"
"time"
"git.avsdev.uk/Go/job-search/internal/config"
@@ -9,6 +10,8 @@ import (
resty "resty.dev/v3"
)
// FindATender is a client for the Find a Tender OCDS API (or the mock
// server). Create it with New and call Shutdown when finished with it.
type FindATender struct {
httpClient *resty.Client
config *config.JobSearchConfig
@@ -47,9 +50,12 @@ func (ft *FindATender) With(ctx context.Context) *FindATender {
return &copied
}
// Shutdown closes the client's HTTP connections.
// Shutdown closes the client's HTTP connections. A failure to close is
// logged, as there is nothing a caller could do about it.
func (ft *FindATender) Shutdown() {
ft.httpClient.Close()
if err := ft.httpClient.Close(); err != nil {
slog.Warn("findATender: closing HTTP client", "error", err)
}
}
// FetchOnce fetches the first page of releases updated since since, and