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

+2
View File
@@ -168,6 +168,8 @@ func (c *Catalog) resolveOne(key string) *ocds.Classification {
return nil
}
// resolve returns copies of the classifications with keys, skipping keys
// that have none.
func (c *Catalog) resolve(keys []string) []ocds.Classification {
var out []ocds.Classification
for _, k := range keys {
+10 -2
View File
@@ -29,7 +29,7 @@ func NewGenerator(cat *Catalog, store *Store, seed int64) *Generator {
return &Generator{
cat: cat,
store: store,
rnd: rand.New(rand.NewSource(seed)),
rnd: rand.New(rand.NewSource(seed)), //nolint:gosec // seeded for repeatable mock data
}
}
@@ -46,6 +46,10 @@ func (g *Generator) Make(at time.Time) ocds.Release {
return g.newProcedure(at)
}
// newProcedure returns the first release of a new procedure: a tender (or,
// one time in five, a planning notice) from a random catalog buyer and
// subject, using a random procurement method. Values are scaled by a random
// factor, and a tender without lots gets a single lot covering it all.
func (g *Generator) newProcedure(at time.Time) ocds.Release {
buyer := g.cat.buyer(g.rnd)
ensurePartyID(&buyer)
@@ -189,6 +193,7 @@ func (g *Generator) advance(prev ocds.Release, at time.Time) ocds.Release {
return r
}
// newOCID returns a random ocid not already in the store.
func (g *Generator) newOCID() string {
for {
id := fmt.Sprintf("%s%06x", ocidPrefix, g.rnd.Intn(1<<24))
@@ -263,6 +268,7 @@ func setStatus(t *ocds.Tender, status string) {
}
}
// scaleValue multiplies v's amounts by f, rounded to the nearest 100.
func scaleValue(v *ocds.Value, f float64) {
if v == nil {
return
@@ -274,11 +280,13 @@ func scaleValue(v *ocds.Value, f float64) {
}
}
// ensurePartyID gives p an id derived from its name if it has none, so the
// same organisation always gets the same id.
func ensurePartyID(p *ocds.Organization) {
if p.ID != "" {
return
}
h := fnv.New32a()
h.Write([]byte(p.Name))
_, _ = h.Write([]byte(p.Name)) // never fails
p.ID = fmt.Sprintf("GB-FTS-%d", h.Sum32()%1000000)
}
+6 -1
View File
@@ -22,6 +22,7 @@ const (
BasePath = "/api/1.0/ocdsReleasePackages"
)
// server serves release packages from a Store; see NewHandler.
type server struct {
store *Store
baseURL string
@@ -39,6 +40,8 @@ func NewHandler(
return (&server{store: store, baseURL: baseURL, loc: loc}).handle
}
// encodeCursor makes the opaque cursor for a next link that resumes at store
// position i.
func encodeCursor(i int) string {
return base64.RawURLEncoding.EncodeToString([]byte("idx:" + strconv.Itoa(i)))
}
@@ -62,7 +65,7 @@ func decodeCursor(c string) (int, error) {
func badRequest(w http.ResponseWriter, msg string) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusBadRequest)
json.NewEncoder(w).Encode(map[string]string{"error": msg})
_ = json.NewEncoder(w).Encode(map[string]string{"error": msg}) // the client has gone
}
// parseTime reads a query timestamp. Values without an offset (the documented
@@ -93,6 +96,8 @@ type pageQuery struct {
limit int
}
// handle answers a GET for a page of releases as an OCDS release package,
// with a links.next when more releases match.
func (s *server) handle(w http.ResponseWriter, req *http.Request) {
if req.Method != http.MethodGet {
w.WriteHeader(http.StatusMethodNotAllowed)
+7 -1
View File
@@ -11,6 +11,7 @@ import (
// ---------- rate limiting ----------
// clientWindow is one client's rate limiting state.
type clientWindow struct {
hour time.Time // clock hour the anchor belongs to
anchor time.Time // first request of that hour
@@ -18,6 +19,10 @@ type clientWindow struct {
count int // accepted requests in the current window
}
// RateLimiter limits each client (by remote host) to a number of requests per
// fixed window, answering the rest with 429 Too Many Requests and a
// Retry-After header, as the Find a Tender API does. A client's windows are
// counted from its first request in each clock hour.
type RateLimiter struct {
mu sync.Mutex
limit int
@@ -26,6 +31,7 @@ type RateLimiter struct {
now func() time.Time
}
// NewRateLimiter returns a RateLimiter allowing limit requests per window.
func NewRateLimiter(limit int, window time.Duration) *RateLimiter {
return &RateLimiter{
limit: limit,
@@ -108,7 +114,7 @@ func (rl *RateLimiter) Middleware(next http.HandlerFunc) http.HandlerFunc {
w.Header().Set("Retry-After", strconv.Itoa(retry))
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
w.WriteHeader(http.StatusTooManyRequests)
fmt.Fprintf(
_, _ = fmt.Fprintf(
w, "Rate limit of %d exceeded. Please retry after %d seconds.",
rl.limit, retry,
)
+11 -7
View File
@@ -4,6 +4,7 @@ import (
"bufio"
"bytes"
"encoding/json"
"errors"
"fmt"
"log"
"math/rand"
@@ -69,14 +70,14 @@ func OpenStore(path string) (*Store, error) {
// readReleases reads the releases stored at path, skipping lines that are
// blank or can't be decoded. A missing file holds no releases.
func readReleases(path string) ([]ocds.Release, error) {
in, err := os.Open(path)
in, err := os.Open(filepath.Clean(path))
if os.IsNotExist(err) {
return nil, nil
}
if err != nil {
return nil, err
}
defer in.Close()
defer in.Close() //nolint:errcheck // read only
var releases []ocds.Release
sc := bufio.NewScanner(in)
@@ -103,7 +104,7 @@ func readReleases(path string) ([]ocds.Release, error) {
// mid-write, it ends the partial record so the next one starts on a fresh
// line.
func openForAppend(path string) (*os.File, error) {
f, err := os.OpenFile(path, os.O_CREATE|os.O_RDWR|os.O_APPEND, 0o600)
f, err := os.OpenFile(filepath.Clean(path), os.O_CREATE|os.O_RDWR|os.O_APPEND, 0o600)
if err != nil {
return nil, err
}
@@ -111,8 +112,7 @@ func openForAppend(path string) (*os.File, error) {
last := make([]byte, 1)
if _, err := f.ReadAt(last, st.Size()-1); err == nil && last[0] != '\n' {
if _, err := f.Write([]byte("\n")); err != nil {
f.Close()
return nil, fmt.Errorf("repairing %s: %w", path, err)
return nil, errors.Join(fmt.Errorf("repairing %s: %w", path, err), f.Close())
}
}
}
@@ -166,8 +166,9 @@ func (s *Store) Sync() error {
return s.f.Sync()
}
// A date earlier than the newest stored release (for example after a clock
// change) is moved up so the order stays valid.
// append stores r, syncing the file to disk if sync is true. A date earlier
// than the newest stored release (for example after a clock change) is moved
// up so the order stays valid.
func (s *Store) append(r ocds.Release, sync bool) error {
s.mu.Lock()
defer s.mu.Unlock()
@@ -224,12 +225,14 @@ func (s *Store) Last() (time.Time, bool) {
return s.times[len(s.times)-1], true
}
// Len returns the number of stored releases.
func (s *Store) Len() int {
s.mu.RLock()
defer s.mu.RUnlock()
return len(s.releases)
}
// HasOCID reports whether any release of the procedure ocid is stored.
func (s *Store) HasOCID(ocid string) bool {
s.mu.RLock()
defer s.mu.RUnlock()
@@ -261,6 +264,7 @@ func (s *Store) RandomOpen(r *rand.Rand, since time.Time) (ocds.Release, bool) {
return ocds.Release{}, false
}
// Close closes the store's file. The store must not be used afterwards.
func (s *Store) Close() error {
s.mu.Lock()
defer s.mu.Unlock()