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.
207 lines
5.9 KiB
Go
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
|
|
}
|