File/package naming convention conforming better to go standards - as ugly as it is
This commit is contained in:
1 parent
be4770cbab
commit
d6688a6754
32 files changed
+36
-36
No files matched your search
@@ -0,0 +1,292 @@
|
||||
package mock
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"hash/fnv"
|
||||
"log"
|
||||
"math"
|
||||
"math/rand"
|
||||
"time"
|
||||
|
||||
"git.avsdev.uk/Go/job-search/internal/mock/findatender/ocds"
|
||||
)
|
||||
|
||||
const ocidPrefix = "ocds-mock-"
|
||||
|
||||
// Generator makes new releases from the catalog. About a third of the time it
|
||||
// moves an existing procedure on (planning -> tender -> award) so the same ocid
|
||||
// appears in several releases, as it does on the real service.
|
||||
type Generator struct {
|
||||
cat *Catalog
|
||||
store *Store
|
||||
rnd *rand.Rand
|
||||
}
|
||||
|
||||
// NewGenerator returns a generator drawing from cat and storing releases in
|
||||
// store. The same seed and store contents give the same releases.
|
||||
func NewGenerator(cat *Catalog, store *Store, seed int64) *Generator {
|
||||
return &Generator{
|
||||
cat: cat,
|
||||
store: store,
|
||||
rnd: rand.New(rand.NewSource(seed)), //nolint:gosec // seeded for repeatable mock data
|
||||
}
|
||||
}
|
||||
|
||||
// Make returns a new release dated at (to the second). About a third of the
|
||||
// time it moves on a procedure still open from the last 60 days; otherwise it
|
||||
// starts a new one.
|
||||
func (g *Generator) Make(at time.Time) ocds.Release {
|
||||
at = at.UTC().Truncate(time.Second)
|
||||
if g.rnd.Float64() < 0.35 {
|
||||
if prev, ok := g.store.RandomOpen(g.rnd, at.AddDate(0, 0, -60)); ok {
|
||||
return g.advance(prev, at)
|
||||
}
|
||||
}
|
||||
return g.newProcedure(at)
|
||||
}
|
||||
|
||||
// newProcedure returns the first release of a new procedure: a tender (or,
|
||||
// one time in five, a planning notice) from a random catalog buyer and
|
||||
// subject, using a random procurement method. Values are scaled by a random
|
||||
// factor, and a tender without lots gets a single lot covering it all.
|
||||
func (g *Generator) newProcedure(at time.Time) ocds.Release {
|
||||
buyer := g.cat.buyer(g.rnd)
|
||||
ensurePartyID(&buyer)
|
||||
buyer.Roles = []string{"buyer"}
|
||||
m := g.cat.method(g.rnd)
|
||||
|
||||
tag, status := "tender", "active"
|
||||
if g.rnd.Float64() < 0.2 {
|
||||
tag, status = "planning", "planned"
|
||||
}
|
||||
ocid := g.newOCID()
|
||||
|
||||
t := g.cat.subject(g.rnd)
|
||||
t.ID = ocds.ID(ocid)
|
||||
t.ProcurementMethod = m.Method
|
||||
t.ProcurementMethodDetails = m.Details
|
||||
|
||||
factor := 0.5 + g.rnd.Float64()*1.5
|
||||
if t.Value == nil {
|
||||
amount := float64((g.rnd.Intn(5000) + 10) * 1000)
|
||||
t.Value = &ocds.Value{
|
||||
Amount: &amount,
|
||||
Currency: "GBP",
|
||||
}
|
||||
} else {
|
||||
scaleValue(t.Value, factor)
|
||||
}
|
||||
|
||||
for i := range t.Lots {
|
||||
scaleValue(t.Lots[i].Value, factor)
|
||||
}
|
||||
|
||||
if len(t.Lots) == 0 {
|
||||
v := clone(*t.Value)
|
||||
t.Lots = []ocds.Lot{{
|
||||
ID: "1",
|
||||
Title: t.Title,
|
||||
Value: &v,
|
||||
}}
|
||||
}
|
||||
|
||||
setStatus(&t, status)
|
||||
if status == "active" {
|
||||
t.TenderPeriod = &ocds.Period{
|
||||
EndDate: ptr(at.AddDate(0, 0, 30+g.rnd.Intn(30))),
|
||||
}
|
||||
}
|
||||
|
||||
return ocds.Release{
|
||||
OCID: ocid,
|
||||
ID: g.store.NextNoticeID(at.Year()),
|
||||
Date: at,
|
||||
Tag: []string{tag},
|
||||
InitiationType: "tender",
|
||||
Parties: []ocds.Organization{buyer},
|
||||
Buyer: &ocds.OrganizationReference{
|
||||
ID: ocds.ID(buyer.ID),
|
||||
Name: buyer.Name,
|
||||
},
|
||||
Tender: &t,
|
||||
Language: "en",
|
||||
}
|
||||
}
|
||||
|
||||
// advance returns the next release in prev's procedure: planning moves to
|
||||
// tender, and a tender either has its deadline extended (tenderUpdate) or is
|
||||
// awarded to a supplier from the catalog.
|
||||
func (g *Generator) advance(prev ocds.Release, at time.Time) ocds.Release {
|
||||
r := clone(prev)
|
||||
r.ID = g.store.NextNoticeID(at.Year())
|
||||
r.Date = at
|
||||
r.Awards = nil
|
||||
if r.Tender == nil {
|
||||
r.Tender = &ocds.Tender{
|
||||
ID: ocds.ID(r.OCID),
|
||||
}
|
||||
}
|
||||
|
||||
next := "award"
|
||||
switch prev.Tag[0] {
|
||||
case "planning":
|
||||
next = "tender"
|
||||
case "tender":
|
||||
if g.rnd.Float64() < 0.25 {
|
||||
next = "tenderUpdate"
|
||||
}
|
||||
}
|
||||
r.Tag = []string{next}
|
||||
|
||||
switch next {
|
||||
case "tender":
|
||||
setStatus(r.Tender, "active")
|
||||
r.Tender.TenderPeriod = &ocds.Period{
|
||||
EndDate: ptr(at.AddDate(0, 0, 30+g.rnd.Intn(30))),
|
||||
}
|
||||
case "tenderUpdate":
|
||||
// A typical update: the deadline is extended by two weeks.
|
||||
end := at.AddDate(0, 0, 30)
|
||||
if tp := r.Tender.TenderPeriod; tp != nil && tp.EndDate != nil {
|
||||
end = *tp.EndDate
|
||||
}
|
||||
r.Tender.TenderPeriod = &ocds.Period{
|
||||
EndDate: ptr(end.AddDate(0, 0, 14)),
|
||||
}
|
||||
case "award":
|
||||
setStatus(r.Tender, "complete")
|
||||
sup := g.cat.supplier(g.rnd)
|
||||
ensurePartyID(&sup)
|
||||
sup.Roles = []string{"supplier"}
|
||||
r.Parties = append(r.Parties, sup)
|
||||
|
||||
var value *ocds.Value
|
||||
if r.Tender.Value != nil {
|
||||
v := clone(*r.Tender.Value)
|
||||
scaleValue(&v, 0.8+g.rnd.Float64()*0.2)
|
||||
value = &v
|
||||
}
|
||||
|
||||
var lotIDs []string
|
||||
for _, lot := range r.Tender.Lots {
|
||||
if lot.ID != "" {
|
||||
lotIDs = append(lotIDs, lot.ID)
|
||||
}
|
||||
}
|
||||
|
||||
r.Awards = []ocds.Award{{
|
||||
ID: "1",
|
||||
Status: "active",
|
||||
Date: ptr(at),
|
||||
Value: value,
|
||||
Suppliers: []ocds.OrganizationReference{
|
||||
{
|
||||
ID: ocds.ID(sup.ID),
|
||||
Name: sup.Name,
|
||||
},
|
||||
},
|
||||
RelatedLots: lotIDs,
|
||||
}}
|
||||
}
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
// newOCID returns a random ocid not already in the store.
|
||||
func (g *Generator) newOCID() string {
|
||||
for {
|
||||
id := fmt.Sprintf("%s%06x", ocidPrefix, g.rnd.Intn(1<<24))
|
||||
if !g.store.HasOCID(id) {
|
||||
return id
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// gap returns a random duration from min to max inclusive, or min if max is
|
||||
// not larger.
|
||||
func (g *Generator) gap(min, max time.Duration) time.Duration {
|
||||
if max <= min {
|
||||
return min
|
||||
}
|
||||
return min + time.Duration(g.rnd.Int63n(int64(max-min)+1))
|
||||
}
|
||||
|
||||
// CatchUp fills the period since the last stored release (or the last backfill
|
||||
// period, whichever is shorter) so restarts leave no gap in the timeline.
|
||||
func (g *Generator) CatchUp(
|
||||
now time.Time, backfill, min, max time.Duration,
|
||||
) (int, error) {
|
||||
start := now.Add(-backfill)
|
||||
if last, ok := g.store.Last(); ok && last.After(start) {
|
||||
start = last
|
||||
}
|
||||
|
||||
n := 0
|
||||
for t := start.Add(g.gap(min, max)); !t.After(now); t = t.Add(g.gap(min, max)) {
|
||||
if err := g.store.AppendNoSync(g.Make(t)); err != nil {
|
||||
return n, err
|
||||
}
|
||||
n++
|
||||
}
|
||||
|
||||
return n, g.store.Sync()
|
||||
}
|
||||
|
||||
// Run publishes a new release after a random gap between min and max until ctx ends.
|
||||
func (g *Generator) Run(ctx context.Context, min, max time.Duration) {
|
||||
for {
|
||||
d := g.gap(min, max)
|
||||
log.Printf("generator: next release in %s", d.Round(time.Second))
|
||||
|
||||
timer := time.NewTimer(d)
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
timer.Stop()
|
||||
return
|
||||
case <-timer.C:
|
||||
}
|
||||
|
||||
r := g.Make(time.Now())
|
||||
if err := g.store.Append(r); err != nil {
|
||||
log.Printf("generator: storing release: %v", err)
|
||||
continue
|
||||
}
|
||||
|
||||
log.Printf("generator: published %s (%s) %s", r.ID, r.Tag[0], r.OCID)
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- helpers ----------
|
||||
|
||||
func ptr[T any](v T) *T { return &v }
|
||||
|
||||
func setStatus(t *ocds.Tender, status string) {
|
||||
t.Status = status
|
||||
for i := range t.Lots {
|
||||
t.Lots[i].Status = status
|
||||
}
|
||||
}
|
||||
|
||||
// scaleValue multiplies v's amounts by f, rounded to the nearest 100.
|
||||
func scaleValue(v *ocds.Value, f float64) {
|
||||
if v == nil {
|
||||
return
|
||||
}
|
||||
for _, a := range []**float64{&v.Amount, &v.AmountGross} {
|
||||
if *a != nil {
|
||||
*a = ptr(math.Round(**a*f/100) * 100)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ensurePartyID gives p an id derived from its name if it has none, so the
|
||||
// same organisation always gets the same id.
|
||||
func ensurePartyID(p *ocds.Organization) {
|
||||
if p.ID != "" {
|
||||
return
|
||||
}
|
||||
h := fnv.New32a()
|
||||
h.Write([]byte(p.Name)) //nolint:gosec // never fails
|
||||
p.ID = fmt.Sprintf("GB-FTS-%d", h.Sum32()%1000000)
|
||||
}
|
||||
Reference in new issue
Block a user