Files
job-search/internal/mock/findATender/catalog.go
T

190 lines
5.4 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(b []byte, name string) (*Catalog, error) {
var c Catalog
if err := json.Unmarshal(b, &c); err != nil {
return nil, fmt.Errorf("decoding %s: %w", name, err)
}
if err := c.validate(); err != nil {
return nil, fmt.Errorf("invalid %s: %w", name, err)
}
return &c, 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
}{
{"buyers", len(c.Buyers)},
{"suppliers", len(c.Suppliers)},
{"subjects", len(c.Subjects)},
{"methods", len(c.Methods)},
{"classifications", 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 _, k := range keys {
if _, ok := c.Classifications[k]; k != "" && !ok {
missing[k] = true
}
}
}
for _, s := range c.Subjects {
check(s.Classification)
check(s.AdditionalClassifications...)
for _, it := range s.Items {
check(it.AdditionalClassifications...)
}
}
if len(missing) > 0 {
keys := make([]string, 0, len(missing))
for k := range missing {
keys = append(keys, k)
}
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(r *rand.Rand) ocds.Organization {
return clone(c.Buyers[r.Intn(len(c.Buyers))])
}
func (c *Catalog) supplier(r *rand.Rand) ocds.Organization {
return clone(c.Suppliers[r.Intn(len(c.Suppliers))])
}
func (c *Catalog) method(r *rand.Rand) Method {
return c.Methods[r.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(r *rand.Rand) ocds.Tender {
s := clone(c.Subjects[r.Intn(len(c.Subjects))])
t := s.Tender
t.Classification = c.resolveOne(s.Classification)
t.AdditionalClassifications = c.resolve(s.AdditionalClassifications)
t.Items = nil
for _, it := range s.Items {
item := it.Item
item.AdditionalClassifications = c.resolve(it.AdditionalClassifications)
t.Items = append(t.Items, item)
}
return t
}
// resolveOne returns a copy of the classification with key, or nil if there
// is none.
func (c *Catalog) resolveOne(key string) *ocds.Classification {
if cl, ok := c.Classifications[key]; ok {
return &cl
}
return nil
}
func (c *Catalog) resolve(keys []string) []ocds.Classification {
var out []ocds.Classification
for _, k := range keys {
if cl, ok := c.Classifications[k]; ok {
out = append(out, cl)
}
}
return out
}
// clone deep-copies a value through JSON, which is all the generated types need.
func clone[T any](v T) T {
var out T
b, err := json.Marshal(v)
if err == nil {
_ = json.Unmarshal(b, &out)
}
return out
}