Files
job-search/internal/mock/findatender/catalog.go
T
bwilliams 465358609a Naming, layout and comments: descriptive full-word names, one item per line in wrapped calls and literals, and doc comments on exported names
Short and single-letter names in application code become full words (tests
keep theirs). Wrapped calls, signatures and composite literals put one item
per line, and one-line function bodies are expanded. Exported names and
groups of constants get doc comments, and comments refer to the renamed
identifiers.
2026-10-15 16:12:56 +01:00

207 lines
5.9 KiB
Go

package mock
import (
_ "embed"
"encoding/json"
"errors"
"fmt"
"math/rand"
"sort"
"strings"
"git.avsdev.uk/Go/job-search/internal/mock/findatender/ocds"
)
// Method is a procurement method and its details, as published on a tender.
type Method struct {
Method string `json:"procurementMethod"`
Details string `json:"procurementMethodDetails,omitempty"`
}
// embeddedCatalog is data/catalog.json, compiled into the binary. Rebuild after
// changing the file.
//
//go:embed data/catalog.json
var embeddedCatalog []byte
// Catalog is the pool the generator draws from. It is read from
// ./data/catalog.json. Classifications holds every classification the subjects
// use, keyed "{scheme}-{id}" (e.g. "CPV-90513000"), and subjects and their
// items refer to entries by that key.
type Catalog struct {
Buyers []ocds.Organization `json:"buyers"`
Suppliers []ocds.Organization `json:"suppliers"`
Subjects []Subject `json:"subjects"`
Methods []Method `json:"methods"`
Classifications map[string]ocds.Classification `json:"classifications"`
}
// Subject is a tender as stored in the catalog: an ocds.Tender whose main
// classification and additionalClassifications, on the tender and on each
// item, are keys into Catalog.Classifications. The outer fields shadow the
// embedded ones of the same JSON name, so everything else is read as
// ocds.Tender.
type Subject struct {
ocds.Tender
Classification string `json:"classification,omitempty"`
AdditionalClassifications []string `json:"additionalClassifications,omitempty"`
Items []SubjectItem `json:"items,omitempty"`
}
// SubjectItem is an ocds.Item whose additionalClassifications are keys into
// Catalog.Classifications.
type SubjectItem struct {
ocds.Item
AdditionalClassifications []string `json:"additionalClassifications,omitempty"`
}
// LoadCatalog parses the embedded catalog and checks it is usable: every
// pool has at least one entry and every classification key resolves.
func LoadCatalog() (*Catalog, error) {
return parseCatalog(embeddedCatalog, "embedded catalog")
}
// parseCatalog decodes and validates catalog JSON; name labels any error.
func parseCatalog(data []byte, name string) (*Catalog, error) {
var catalog Catalog
if err := json.Unmarshal(data, &catalog); err != nil {
return nil, fmt.Errorf("decoding %s: %w", name, err)
}
if err := catalog.validate(); err != nil {
return nil, fmt.Errorf("invalid %s: %w", name, err)
}
return &catalog, nil
}
// validate checks that every pool has at least one entry and that every
// classification key used by a subject or its items is in Classifications.
// It reports all the problems found, listing at most five missing keys.
func (c *Catalog) validate() error {
var errs []error
for _, pool := range []struct {
name string
n int
}{
{
name: "buyers",
n: len(c.Buyers),
},
{
name: "suppliers",
n: len(c.Suppliers),
},
{
name: "subjects",
n: len(c.Subjects),
},
{
name: "methods",
n: len(c.Methods),
},
{
name: "classifications",
n: len(c.Classifications),
},
} {
if pool.n == 0 {
errs = append(errs, fmt.Errorf("no %s", pool.name))
}
}
missing := map[string]bool{}
check := func(keys ...string) {
for _, key := range keys {
if _, ok := c.Classifications[key]; key != "" && !ok {
missing[key] = true
}
}
}
for _, subject := range c.Subjects {
check(subject.Classification)
check(subject.AdditionalClassifications...)
for _, item := range subject.Items {
check(item.AdditionalClassifications...)
}
}
if len(missing) > 0 {
keys := make([]string, 0, len(missing))
for key := range missing {
keys = append(keys, key)
}
sort.Strings(keys)
if len(keys) > 5 {
keys = append(keys[:5], "...")
}
errs = append(errs, fmt.Errorf(
"%d classification key(s) not found in classifications: %s",
len(missing), strings.Join(keys, ", "),
))
}
return errors.Join(errs...)
}
// ---------- picking (always returns a copy the caller may modify) ----------
func (c *Catalog) buyer(rng *rand.Rand) ocds.Organization {
return clone(c.Buyers[rng.Intn(len(c.Buyers))])
}
func (c *Catalog) supplier(rng *rand.Rand) ocds.Organization {
return clone(c.Suppliers[rng.Intn(len(c.Suppliers))])
}
func (c *Catalog) method(rng *rand.Rand) Method {
return c.Methods[rng.Intn(len(c.Methods))]
}
// subject picks a subject and returns it as an ocds.Tender, with its
// classification keys resolved to full classifications.
func (c *Catalog) subject(rng *rand.Rand) ocds.Tender {
subject := clone(c.Subjects[rng.Intn(len(c.Subjects))])
tender := subject.Tender
tender.Classification = c.resolveOne(subject.Classification)
tender.AdditionalClassifications = c.resolve(subject.AdditionalClassifications)
tender.Items = nil
for _, subjectItem := range subject.Items {
item := subjectItem.Item
item.AdditionalClassifications = c.resolve(subjectItem.AdditionalClassifications)
tender.Items = append(tender.Items, item)
}
return tender
}
// resolveOne returns a copy of the classification with key, or nil if there
// is none.
func (c *Catalog) resolveOne(key string) *ocds.Classification {
if classification, ok := c.Classifications[key]; ok {
return &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 _, key := range keys {
if classification, ok := c.Classifications[key]; ok {
out = append(out, classification)
}
}
return out
}
// clone deep-copies a value through JSON, which is all the generated types need.
func clone[T any](value T) T {
var out T
data, err := json.Marshal(value)
if err == nil {
json.Unmarshal(data, &out) //nolint:errcheck,gosec // b was just marshalled from a T
}
return out
}