408 lines
14 KiB
Go
408 lines
14 KiB
Go
package meshdeliveryclient
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"fmt"
|
|
"reflect"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
seat "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshdelivery"
|
|
"git.novox.be/novox/mesh-sdk/go/keys"
|
|
)
|
|
|
|
// The values of the seat's states `source`, `merges`, `asks`, `owed` and `turns`, in the seat's types: each is stamped
|
|
// with Version when encoded, read strictly (an unknown field refused) and its version checked when decoded, and its rules
|
|
// held on both sides, so the gate that writes them and the readers (the forge bridge, the teller, the assigner, the view)
|
|
// agree. What the schema cannot say is written here once (ADR 0625 §4).
|
|
|
|
// Version is the layout version of the seat's values.
|
|
const Version = 1
|
|
|
|
// MergeState is the state of a pull request in a merge.
|
|
type MergeState string
|
|
|
|
// The states a merge's pull request is in.
|
|
const (
|
|
MergeMerging MergeState = "merging"
|
|
MergeMerged MergeState = "merged"
|
|
MergeRefused MergeState = "refused"
|
|
)
|
|
|
|
// SourceMainKey is the one key of the state source.
|
|
const SourceMainKey = "main"
|
|
|
|
func encodeValue(v any, check error, stamp func()) ([]byte, error) {
|
|
if check != nil {
|
|
return nil, check
|
|
}
|
|
stamp()
|
|
return json.Marshal(v)
|
|
}
|
|
|
|
// ApprovedKey is the key, in a merge's approved heads, of a pull request: `<repository>@<number>`.
|
|
func ApprovedKey(repository string, number int) string {
|
|
return repository + "@" + strconv.Itoa(number)
|
|
}
|
|
|
|
// TurnsKey is the key, in the state turns, of a repository: `<owner>.<name>`, lower-case and escaped.
|
|
func TurnsKey(repository string) (string, error) {
|
|
repository = strings.ToLower(repository)
|
|
part, err := escapeRepository(repository)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return strings.Replace(part, "/", ".", 1), nil
|
|
}
|
|
|
|
// ParseTurnsKey is the inverse of TurnsKey; a key that does not name a lower-case <owner>/<name> is refused.
|
|
func ParseTurnsKey(key string) (string, error) {
|
|
owner, name, ok := strings.Cut(key, ".")
|
|
if !ok || owner == "" || name == "" || strings.Contains(name, ".") {
|
|
return "", fmt.Errorf("meshdeliveryclient: %q is not a turns key, <owner>.<name>", key)
|
|
}
|
|
repository, err := unescapeRepository(owner + "/" + name)
|
|
if err != nil {
|
|
return "", fmt.Errorf("meshdeliveryclient: %q is not a turns key: %w", key, err)
|
|
}
|
|
if again, err := TurnsKey(repository); err != nil || again != key {
|
|
return "", fmt.Errorf("meshdeliveryclient: %q is not a turns key: it does not name a lowercase <owner>/<name>", key)
|
|
}
|
|
return repository, nil
|
|
}
|
|
|
|
// ----- source
|
|
|
|
// CheckSource refuses a source naming no repository, a repository with no name or an empty head.
|
|
func CheckSource(s seat.SourceByMainValue) error {
|
|
if len(s.Heads) == 0 {
|
|
return fmt.Errorf("meshdeliveryclient: a source names no repository")
|
|
}
|
|
for repository, head := range s.Heads {
|
|
if repository == "" {
|
|
return fmt.Errorf("meshdeliveryclient: a source names a repository with no name")
|
|
}
|
|
if head == "" {
|
|
return fmt.Errorf("meshdeliveryclient: the source's head of %s is empty", repository)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// EncodeSource is s as the value of the key main.
|
|
func EncodeSource(s seat.SourceByMainValue) ([]byte, error) {
|
|
return encodeValue(&s, CheckSource(s), func() { s.V = Version })
|
|
}
|
|
|
|
// DecodeSource reads the value of the key main.
|
|
func DecodeSource(data []byte) (seat.SourceByMainValue, error) {
|
|
if err := keys.CheckVersion(data); err != nil {
|
|
return seat.SourceByMainValue{}, err
|
|
}
|
|
s, err := seat.DecodeSourceByMainValue(data)
|
|
if err != nil {
|
|
return s, fmt.Errorf("meshdeliveryclient: a source does not fit its shape: %w", err)
|
|
}
|
|
return s, CheckSource(s)
|
|
}
|
|
|
|
// SameSource says whether two sources are the same.
|
|
func SameSource(a, b seat.SourceByMainValue) bool { return reflect.DeepEqual(a, b) }
|
|
|
|
// ----- merge
|
|
|
|
// CheckMerge refuses a merge naming no group, ask or pull request, a pull request outside the approved heads or at
|
|
// another head than the Approve was for, and a state that lacks what it needs: merged its commit, refused the forge's words.
|
|
func CheckMerge(m seat.MergesByGroupValue) error {
|
|
if m.Group == "" {
|
|
return fmt.Errorf("meshdeliveryclient: a merge names no group")
|
|
}
|
|
if m.Ask == "" {
|
|
return fmt.Errorf("meshdeliveryclient: a merge names no ask")
|
|
}
|
|
if len(m.Pulls) == 0 {
|
|
return fmt.Errorf("meshdeliveryclient: a merge names no pull request")
|
|
}
|
|
for key, sha := range m.Approved {
|
|
if sha == "" {
|
|
return fmt.Errorf("meshdeliveryclient: the approved head of %s is empty", key)
|
|
}
|
|
}
|
|
for _, p := range m.Pulls {
|
|
who := fmt.Sprintf("%s#%d", p.Repository, p.Number)
|
|
if len(m.Approved) > 0 {
|
|
sha, ok := m.Approved[ApprovedKey(p.Repository, int(p.Number))]
|
|
if !ok {
|
|
return fmt.Errorf("meshdeliveryclient: the pull request %s is handed to the merge but is not among the approved heads", who)
|
|
}
|
|
if sha != p.Head {
|
|
return fmt.Errorf("meshdeliveryclient: the pull request %s is handed at head %q but the Approve was for %q", who, p.Head, sha)
|
|
}
|
|
}
|
|
switch MergeState(p.State) {
|
|
case MergeMerging:
|
|
case MergeMerged:
|
|
if p.Commit == "" {
|
|
return fmt.Errorf("meshdeliveryclient: the merged pull request %s has no merge commit", who)
|
|
}
|
|
case MergeRefused:
|
|
if p.Words == "" {
|
|
return fmt.Errorf("meshdeliveryclient: the refused pull request %s has no words of the forge", who)
|
|
}
|
|
default:
|
|
return fmt.Errorf("meshdeliveryclient: the state %q of %s is not merging, merged or refused", p.State, who)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// EncodeMerge is m as the value of a group's key.
|
|
func EncodeMerge(m seat.MergesByGroupValue) ([]byte, error) {
|
|
return encodeValue(&m, CheckMerge(m), func() { m.V = Version })
|
|
}
|
|
|
|
// DecodeMerge reads the value of a group's key.
|
|
func DecodeMerge(data []byte) (seat.MergesByGroupValue, error) {
|
|
if err := keys.CheckVersion(data); err != nil {
|
|
return seat.MergesByGroupValue{}, err
|
|
}
|
|
m, err := seat.DecodeMergesByGroupValue(data)
|
|
if err != nil {
|
|
return m, fmt.Errorf("meshdeliveryclient: a merge does not fit its shape: %w", err)
|
|
}
|
|
return m, CheckMerge(m)
|
|
}
|
|
|
|
// SameMerge says whether two merges are the same.
|
|
func SameMerge(a, b seat.MergesByGroupValue) bool { return reflect.DeepEqual(a, b) }
|
|
|
|
// ----- ask
|
|
|
|
// HeadsOf are an ask's heads as Heads (for HeadsHash and MergeAskKey).
|
|
func HeadsOf(items []seat.AsksByMergeGroupHeadsValueHeadsItem) []Head {
|
|
out := make([]Head, len(items))
|
|
for i, h := range items {
|
|
out[i] = Head{Repository: h.Repository, Number: int(h.Number), Head: h.Head}
|
|
}
|
|
return out
|
|
}
|
|
|
|
// CheckAsk refuses an ask with no group or heads, a negative count, or an ask that is no object.
|
|
func CheckAsk(a seat.AsksByMergeGroupHeadsValue) error {
|
|
if a.Asking < 0 {
|
|
return fmt.Errorf("meshdeliveryclient: the ask's asking %d is negative", a.Asking)
|
|
}
|
|
if _, err := MergeAskKey(a.Group, HeadsOf(a.Heads)); err != nil {
|
|
return fmt.Errorf("meshdeliveryclient: an ask needs its group and at least one pull request's head: %w", err)
|
|
}
|
|
raw, err := json.Marshal(a.Ask)
|
|
var fields map[string]json.RawMessage
|
|
if err != nil || len(bytes.TrimSpace(raw)) == 0 || json.Unmarshal(raw, &fields) != nil || fields == nil {
|
|
return fmt.Errorf("meshdeliveryclient: the value carries no router's ask")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// EncodeAsk is a as the value of a merge ask's key; a.Ask may be a json.RawMessage.
|
|
func EncodeAsk(a seat.AsksByMergeGroupHeadsValue) ([]byte, error) {
|
|
return encodeValue(&a, CheckAsk(a), func() { a.V = Version })
|
|
}
|
|
|
|
// DecodeAsk reads the value of a merge ask's key; its Ask is the router's ask as a decoded object.
|
|
func DecodeAsk(data []byte) (seat.AsksByMergeGroupHeadsValue, error) {
|
|
if err := keys.CheckVersion(data); err != nil {
|
|
return seat.AsksByMergeGroupHeadsValue{}, err
|
|
}
|
|
a, err := seat.DecodeAsksByMergeGroupHeadsValue(data)
|
|
if err != nil {
|
|
return a, fmt.Errorf("meshdeliveryclient: an ask does not fit its shape: %w", err)
|
|
}
|
|
return a, CheckAsk(a)
|
|
}
|
|
|
|
// SameAsk says whether two asks are the same (the router's ask by its content).
|
|
func SameAsk(a, b seat.AsksByMergeGroupHeadsValue) bool { return reflect.DeepEqual(a, b) }
|
|
|
|
// ----- owed
|
|
|
|
// EncodeOwed is o as the value of an owed key, after CheckOwed.
|
|
func EncodeOwed(o seat.OwedByRepositoryNumberHeadActValue) ([]byte, error) {
|
|
return encodeValue(&o, CheckOwed(o), func() { o.V = Version })
|
|
}
|
|
|
|
// DecodeOwed reads the value of an owed key.
|
|
func DecodeOwed(data []byte) (seat.OwedByRepositoryNumberHeadActValue, error) {
|
|
if err := keys.CheckVersion(data); err != nil {
|
|
return seat.OwedByRepositoryNumberHeadActValue{}, err
|
|
}
|
|
o, err := seat.DecodeOwedByRepositoryNumberHeadActValue(data)
|
|
if err != nil {
|
|
return o, fmt.Errorf("meshdeliveryclient: an owed value does not fit its shape: %w", err)
|
|
}
|
|
return o, CheckOwed(o)
|
|
}
|
|
|
|
// SameOwed says whether two owed values are the same.
|
|
func SameOwed(a, b seat.OwedByRepositoryNumberHeadActValue) bool { return a == b }
|
|
|
|
// ----- turns
|
|
|
|
// CheckTurn refuses a turn whose repository is not lower-case, that names no delivery, or whose deliveries repeat a heads
|
|
// hash, lack their times, are out of the order of the Approve, or name a wait without a verdict key.
|
|
func CheckTurn(t seat.TurnsByRepositoryValue) error {
|
|
repository := strings.ToLower(t.Repository)
|
|
if repository != t.Repository {
|
|
return fmt.Errorf("meshdeliveryclient: the turn's repository %q is not lowercase", t.Repository)
|
|
}
|
|
if _, err := escapeRepository(repository); err != nil {
|
|
return err
|
|
}
|
|
if len(t.Waiting) == 0 {
|
|
return fmt.Errorf("meshdeliveryclient: the turn of %s names no delivery: its key is deleted instead", t.Repository)
|
|
}
|
|
seen := map[string]bool{}
|
|
for i, w := range t.Waiting {
|
|
if w.Group == "" {
|
|
return fmt.Errorf("meshdeliveryclient: a delivery in the turn of %s names no group", t.Repository)
|
|
}
|
|
if !hashOK.MatchString(w.Heads) {
|
|
return fmt.Errorf("meshdeliveryclient: the heads hash %q in the turn of %s is not 12 lowercase hex digits", w.Heads, t.Repository)
|
|
}
|
|
if seen[w.Heads] {
|
|
return fmt.Errorf("meshdeliveryclient: the turn of %s names the heads %s twice", t.Repository, w.Heads)
|
|
}
|
|
seen[w.Heads] = true
|
|
if w.Approved.IsZero() || w.Since.IsZero() {
|
|
return fmt.Errorf("meshdeliveryclient: the delivery %s in the turn of %s says no time of its Approve or of its wait", w.Group, t.Repository)
|
|
}
|
|
if i > 0 && w.Approved.Before(t.Waiting[i-1].Approved) {
|
|
return fmt.Errorf("meshdeliveryclient: the turn of %s is not in the order of the Approve: %s was approved before %s", t.Repository, w.Group, t.Waiting[i-1].Group)
|
|
}
|
|
if w.WaitsFor != "" {
|
|
if _, _, err := parseVerdictKey(w.WaitsFor); err != nil {
|
|
return fmt.Errorf("meshdeliveryclient: the delivery %s in the turn of %s waits for what is not a verdict key: %w", w.Group, t.Repository, err)
|
|
}
|
|
} else if (w.WaitsSince != nil && !w.WaitsSince.IsZero()) || w.Step != "" {
|
|
return fmt.Errorf("meshdeliveryclient: the delivery %s in the turn of %s says when and where a check runs, and names no check", w.Group, t.Repository)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// parseVerdictKey is the checker's verdict key `<group>.<heads hash>` (the checker's client has the same rule over its
|
|
// own seat; a client uses its own seat's contract alone).
|
|
func parseVerdictKey(key string) (group, heads string, err error) {
|
|
i := strings.LastIndex(key, ".")
|
|
if i < 0 || !hashOK.MatchString(key[i+1:]) || i == 0 || strings.Contains(key[:i], ".") {
|
|
return "", "", fmt.Errorf("%q is not a verdict key, <group>.<heads hash>", key)
|
|
}
|
|
if group, err = keys.UnescapeName(key[:i]); err != nil {
|
|
return "", "", err
|
|
}
|
|
return group, key[i+1:], nil
|
|
}
|
|
|
|
// TurnHolds says whether a delivery at these heads is not in the turn while the turn names others: the delivery's
|
|
// check or merge holds back for the one before it.
|
|
func TurnHolds(t seat.TurnsByRepositoryValue, heads string) bool {
|
|
if len(t.Waiting) == 0 {
|
|
return false
|
|
}
|
|
for _, w := range t.Waiting {
|
|
if w.Heads == heads {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
// EncodeTurn is t as the value of a repository's key.
|
|
func EncodeTurn(t seat.TurnsByRepositoryValue) ([]byte, error) {
|
|
if err := CheckTurn(t); err != nil {
|
|
return nil, err
|
|
}
|
|
t.V = Version
|
|
return json.Marshal(t)
|
|
}
|
|
|
|
// DecodeTurn reads the value of a repository's key.
|
|
func DecodeTurn(data []byte) (seat.TurnsByRepositoryValue, error) {
|
|
if err := keys.CheckVersion(data); err != nil {
|
|
return seat.TurnsByRepositoryValue{}, err
|
|
}
|
|
t, err := seat.DecodeTurnsByRepositoryValue(data)
|
|
if err != nil {
|
|
return t, fmt.Errorf("meshdeliveryclient: a turn does not fit its shape: %w", err)
|
|
}
|
|
return t, CheckTurn(t)
|
|
}
|
|
|
|
// SameTurn says whether two turns are the same, a time by the instant it names.
|
|
func SameTurn(a, b seat.TurnsByRepositoryValue) bool {
|
|
if len(a.Waiting) != len(b.Waiting) || a.Repository != b.Repository || a.V != b.V {
|
|
return false
|
|
}
|
|
for i := range a.Waiting {
|
|
x, y := a.Waiting[i], b.Waiting[i]
|
|
if !x.Approved.Equal(y.Approved) || !x.Since.Equal(y.Since) || !sameTime(x.WaitsSince, y.WaitsSince) {
|
|
return false
|
|
}
|
|
x.Approved, y.Approved, x.Since, y.Since, x.WaitsSince, y.WaitsSince = time.Time{}, time.Time{}, time.Time{}, time.Time{}, nil, nil
|
|
if !reflect.DeepEqual(x, y) {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
func sameTime(a, b *time.Time) bool {
|
|
if a == nil || a.IsZero() {
|
|
return b == nil || b.IsZero()
|
|
}
|
|
return b != nil && a.Equal(*b)
|
|
}
|
|
|
|
// Answer is the operator's answer to a merge ask as the gate acts on it: the option chosen, the warrant that proves who
|
|
// answered, and the words said. It is the teller's state `answers` under the ask's key (the teller's own record; this is
|
|
// its layout for the gate that reads it).
|
|
type Answer struct {
|
|
V int `json:"v"`
|
|
Option string `json:"option"`
|
|
Warrant string `json:"warrant"`
|
|
Words string `json:"words"`
|
|
}
|
|
|
|
// CheckAnswer refuses an answer lacking its option, warrant or words.
|
|
func CheckAnswer(a Answer) error {
|
|
for _, f := range []struct{ name, value string }{{"option", a.Option}, {"warrant", a.Warrant}, {"words", a.Words}} {
|
|
if f.value == "" {
|
|
return fmt.Errorf("meshdeliveryclient: an answer has no %s", f.name)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// EncodeAnswer is a as the value of an answer's key.
|
|
func EncodeAnswer(a Answer) ([]byte, error) {
|
|
return encodeValue(&a, CheckAnswer(a), func() { a.V = Version })
|
|
}
|
|
|
|
// DecodeAnswer reads the value of an answer's key.
|
|
func DecodeAnswer(data []byte) (Answer, error) {
|
|
if err := keys.CheckVersion(data); err != nil {
|
|
return Answer{}, err
|
|
}
|
|
var a Answer
|
|
dec := json.NewDecoder(bytes.NewReader(data))
|
|
dec.DisallowUnknownFields()
|
|
if err := dec.Decode(&a); err != nil {
|
|
return Answer{}, fmt.Errorf("meshdeliveryclient: an answer does not fit its shape: %w", err)
|
|
}
|
|
return a, CheckAnswer(a)
|
|
}
|
|
|
|
// SameAnswer says whether two answers are the same.
|
|
func SameAnswer(a, b Answer) bool { return a == b }
|