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 pools := []struct { name string count int }{ {name: "buyers", count: len(c.Buyers)}, {name: "suppliers", count: len(c.Suppliers)}, {name: "subjects", count: len(c.Subjects)}, {name: "methods", count: len(c.Methods)}, {name: "classifications", count: len(c.Classifications)}, } for _, pool := range pools { if pool.count == 0 { errs = append(errs, fmt.Errorf("mock: 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( "mock: %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 // data was just marshalled from a T } return out }