Files
mesh-catalog/modules/mesh-knowledge/cmd/mesh-knowledge/list.go
T

217 lines
6.2 KiB
Go

package main
// list (novox/hq issue 1064): one short row per record, filtered by kind, status, topic or a flag, and paged
// newest first, number by number, like issue-tracker.list.
import (
"context"
"fmt"
"sort"
"strings"
"time"
)
// The flags list may be asked for.
const (
FlagResearchDone = "research-done"
FlagProposedOld = "proposed-old"
)
// ListFlags are the flags list takes.
var ListFlags = []string{FlagResearchDone, FlagProposedOld}
// ProposedOldAfter is how long a proposal may sit at proposed before flag proposed-old names it.
const ProposedOldAfter = 24 * time.Hour
// MostListResult is the most rows list may answer in one call.
const MostListResult = 500
// AllStatuses are every status a record of either kind may hold.
var AllStatuses = []string{StatusReserved, StatusProposed, StatusAccepted, StatusSuperseded, StatusWithdrawn,
StatusActive, StatusGraduated, StatusAbandoned}
// ListAsk narrows a list.
type ListAsk struct {
Kind string
Status string
Topic string
// Flag keeps only the research efforts done (research-done) or the proposals grown old (proposed-old).
Flag string
// Before keeps only the records numbered below it: the cursor that pages the newest-first list.
Before int
Limit int
}
// ListRow is one record in a list: short enough to read in a chat.
type ListRow struct {
// Number is the record's number as the records print it: "0276" for a decision, "research 045" for
// research.
Number string `json:"number"`
Kind string `json:"kind"`
Title string `json:"title"`
Status string `json:"status"`
Topic string `json:"topic,omitempty"`
Date string `json:"date,omitempty"`
Versions int `json:"versions"`
// Pending says a proposal waits for the knowledge base's own review or the operator's Approve.
Pending bool `json:"pending,omitempty"`
}
// Listed is the answer to a list.
type Listed struct {
Rows []ListRow `json:"rows"`
Matched int `json:"matched"`
// Next is the before that continues the list where this page ends; 0 when nothing more matches.
Next int `json:"next,omitempty"`
Said string `json:"said"`
}
// rowNumber is a record's number as list's row prints it.
func rowNumber(kind string, number int) string {
if kind == KindResearch {
return fmt.Sprintf("research %03d", number)
}
return fmt.Sprintf("%04d", number)
}
// List is the records that match, newest first, number by number (novox/hq issue 1064).
func (k *KB) List(a ListAsk) (*Listed, error) {
if a.Kind != "" && !contains(Kinds, a.Kind) {
return nil, fmt.Errorf("kind is decision or research, not %q", a.Kind)
}
if a.Status != "" && !contains(AllStatuses, a.Status) {
return nil, fmt.Errorf("status %q is not one of %s", a.Status, strings.Join(AllStatuses, ", "))
}
if a.Flag != "" && !contains(ListFlags, a.Flag) {
return nil, fmt.Errorf("flag is %s, not %q", strings.Join(ListFlags, " or "), a.Flag)
}
limit := a.Limit
if limit <= 0 {
limit = 50
}
limit = min(limit, MostListResult)
ctx := context.Background()
all, err := k.Store.All(ctx)
if err != nil {
return nil, err
}
byKey := map[Key]*Entry{}
for _, e := range all {
byKey[Key{e.Current.Kind, e.Current.Number}] = e
}
keys := make([]Key, 0, len(byKey))
for key := range byKey {
if a.Before == 0 || key.Number < a.Before {
keys = append(keys, key)
}
}
sort.Slice(keys, func(i, j int) bool {
if keys[i].Number != keys[j].Number {
return keys[i].Number > keys[j].Number
}
return keys[i].Kind < keys[j].Kind
})
out := &Listed{Rows: []ListRow{}}
var lastNumber int
for _, key := range keys {
e := byKey[key]
r := e.Current
if a.Kind != "" && r.Kind != a.Kind {
continue
}
if a.Status != "" && r.Status != a.Status {
continue
}
if a.Topic != "" && r.Topic != a.Topic {
continue
}
if a.Flag != "" {
ok, err := k.matchesListFlag(ctx, a.Flag, key, e, byKey)
if err != nil {
return nil, err
}
if !ok {
continue
}
}
out.Matched++
if len(out.Rows) < limit {
out.Rows = append(out.Rows, ListRow{
Number: rowNumber(r.Kind, r.Number), Kind: r.Kind, Title: r.Title, Status: r.Status,
Topic: r.Topic, Date: r.Date, Versions: e.Versions, Pending: e.Pending != nil || r.Ask != nil,
})
lastNumber = key.Number
}
}
below := ""
if a.Before != 0 {
below = fmt.Sprintf(" below %d", a.Before)
}
switch {
case out.Matched == 0:
out.Said = "No record matches" + below + "."
case out.Matched > len(out.Rows):
out.Next = lastNumber
out.Said = fmt.Sprintf("%d records match%s; the newest %d are shown. The next page is before %d.", out.Matched,
below, len(out.Rows), out.Next)
default:
out.Said = fmt.Sprintf("%d records match%s, newest first.", out.Matched, below)
}
return out, nil
}
// matchesListFlag says whether an entry matches a list flag.
func (k *KB) matchesListFlag(ctx context.Context, flag string, key Key, e *Entry, byKey map[Key]*Entry) (bool, error) {
switch flag {
case FlagResearchDone:
return researchDone(e, byKey), nil
case FlagProposedOld:
return k.proposedOld(ctx, key, e)
}
return false, nil
}
// researchDone says whether a research effort is active, names what it became in at least one became link to a
// decision (a became to a design's path is ignored), and every decision it names that way is accepted: a
// candidate for graduated (novox/hq issue 1064).
func researchDone(e *Entry, byKey map[Key]*Entry) bool {
r := e.Current
if r.Kind != KindResearch || r.Status != StatusActive {
return false
}
any := false
for _, l := range r.Links {
if l.Kind != LinkBecame {
continue
}
target, isRecord := l.To.Record()
if !isRecord || target.Kind != KindDecision {
continue
}
any = true
d := byKey[target]
if d == nil || d.Current.Status != StatusAccepted {
return false
}
}
return any
}
// proposedOld says whether a record is proposed and its current version is older than ProposedOldAfter, by the
// KB's clock (novox/hq issue 1064).
func (k *KB) proposedOld(ctx context.Context, key Key, e *Entry) (bool, error) {
if e.Current.Status != StatusProposed {
return false, nil
}
vs, err := k.Store.Versions(ctx, key)
if err != nil {
return false, err
}
for _, v := range vs {
if v.N == e.CurrentVersion {
return k.now().Sub(v.At) > ProposedOldAfter, nil
}
}
return false, nil
}