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 }