package delivery import ( "crypto/sha256" "encoding/hex" "encoding/json" "fmt" "reflect" "regexp" "strconv" "strings" "git.novox.be/novox/mesh-sdk/go/asks" ) // The gate's buckets and the teller's (ADR 0437 §6, §8). The gate writes merges (each pull request's expected // head, its state and each merge commit), its asks (`merge..`, the router's ask whole) and owed // (one key per act it owes the forge, which the forge bridge performs once per revision). The teller writes its // answer under the ask's own key in its bucket answers. const ( MergesBucket = GateModule + "_merges" AsksBucket = GateModule + "_asks" OwedBucket = GateModule + "_owed" AnswersBucket = TellerModule + "_answers" MergesHistory = 10 AsksHistory = 5 OwedHistory = 5 AnswersHistory = 5 ) // AskKindMerge is the kind of the gate's ask for a group's merge, the first part of its key. const AskKindMerge = "merge" // The acts the gate owes the forge, the last part of an owed key. const ( OwedStatus = "status" OwedView = "view" OwedNote = "note" OwedComment = "comment" ) // The states 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" ) // The states a status the gate owes the forge may carry. var statusStates = []string{"pending", "success", "failure", "error", "warning"} var hashOK = regexp.MustCompile(`^[0-9a-f]{12}$`) // MergesKey is the key, in MergesBucket, of a group: ``. func MergesKey(group string) (string, error) { if err := checkName("group", group); err != nil { return "", err } return group, nil } // MergeAskKey is the key, in AsksBucket, of the ask for merging a group at these heads: `merge..`. The teller's answer has the same key in AnswersBucket (AnswerKey). func MergeAskKey(group string, heads []Head) (string, error) { if err := checkName("group", group); err != nil { return "", err } hash, err := HeadsHash(heads) if err != nil { return "", err } return AskKindMerge + "." + group + "." + hash, nil } // ParseAskKey is the inverse of MergeAskKey: the kind, the group and the heads hash. func ParseAskKey(key string) (kind, group, hash string, err error) { parts := strings.Split(key, ".") if len(parts) != 3 || parts[0] != AskKindMerge { return "", "", "", fmt.Errorf("delivery: %q is not an ask key, merge..", key) } if err := checkName("group", parts[1]); err != nil { return "", "", "", fmt.Errorf("delivery: %q is not an ask key: %w", key, err) } if !hashOK.MatchString(parts[2]) { return "", "", "", fmt.Errorf("delivery: %q is not an ask key: %q is not 12 hex digits", key, parts[2]) } return parts[0], parts[1], parts[2], nil } // AnswerKey is the key, in AnswersBucket, of the teller's answer to the ask at askKey: the same string. func AnswerKey(askKey string) string { return askKey } // AskID is the id the teller gives the router for the ask at askKey. The router's ids refuse dots (asks.UsableID), // so a dot becomes `-` and a slash `_`; a key too long for an id is cut and ends in a digest of the whole key. // It cannot be read back: the teller keeps the key beside it. func AskID(askKey string) string { id := strings.NewReplacer(".", "-", "/", "_").Replace(askKey) const most = 64 if len(id) > most { sum := sha256.Sum256([]byte(askKey)) id = id[:most-13] + "-" + hex.EncodeToString(sum[:])[:12] } return id } func checkOwedAct(act string) (kind, context string, err error) { kind, context, hasContext := strings.Cut(act, "=") switch kind { case OwedStatus: if !hasContext { return "", "", fmt.Errorf("delivery: the owed act %q is a status without its context, status=", act) } if err := checkName("status context", context); err != nil { return "", "", err } return kind, context, nil case OwedView, OwedNote, OwedComment: if hasContext { return "", "", fmt.Errorf("delivery: the owed act %q takes no context", act) } return kind, "", nil } return "", "", fmt.Errorf("delivery: the owed act %q is not status=, view, note or comment", act) } // ParseOwedAct splits an owed act into its kind and, for a status, its context. func ParseOwedAct(act string) (kind, context string, err error) { return checkOwedAct(act) } // OwedKey is the key, in OwedBucket, of an act the gate owes the forge on a pull request at a head: // `...`, the act being `status=` (mesh/merge-gate), `view`, `note` or // `comment`. func OwedKey(repository string, number int, head, act string) (string, error) { if err := checkRepository(repository); err != nil { return "", err } if number <= 0 { return "", fmt.Errorf("delivery: an owed key needs the pull request number, not %d", number) } if err := checkName("head", head); err != nil { return "", err } if _, _, err := checkOwedAct(act); err != nil { return "", err } return repository + "." + strconv.Itoa(number) + "." + head + "." + act, nil } // ParseOwedKey is the inverse of OwedKey; act is whole, as `status=` for a status. func ParseOwedKey(key string) (repository string, number int, head, act string, err error) { parts := strings.Split(key, ".") if len(parts) != 4 { return "", 0, "", "", fmt.Errorf("delivery: %q is not an owed key, ...", key) } number, convErr := strconv.Atoi(parts[1]) if convErr != nil { return "", 0, "", "", fmt.Errorf("delivery: %q is not an owed key: %q is not a pull request number", key, parts[1]) } if _, err := OwedKey(parts[0], number, parts[2], parts[3]); err != nil { return "", 0, "", "", fmt.Errorf("delivery: %q is not an owed key: %w", key, err) } return parts[0], number, parts[2], parts[3], nil } // Merge is the value of a merges key: the ask approved and, for each pull request, the head the approval expects // and how far its merge has come. type Merge struct { V int `json:"v"` Group string `json:"group"` Ask string `json:"ask"` Pulls []MergePull `json:"pulls"` } // MergePull is one pull request of a merge: its expected head, its state, the merge commit once merged, the // forge's words when refused. type MergePull struct { Head State MergeState `json:"state"` Commit string `json:"commit,omitempty"` Words string `json:"words,omitempty"` } func (m Merge) check() error { if err := checkName("group", m.Group); err != nil { return err } if m.Ask == "" { return fmt.Errorf("delivery: a merge names no ask") } if len(m.Pulls) == 0 { return fmt.Errorf("delivery: a merge names no pull request") } for _, p := range m.Pulls { who := fmt.Sprintf("%s#%d", p.Repository, p.Number) switch p.State { case MergeMerging: case MergeMerged: if p.Commit == "" { return fmt.Errorf("delivery: the merged pull request %s has no merge commit", who) } case MergeRefused: if p.Words == "" { return fmt.Errorf("delivery: the refused pull request %s has no words of the forge", who) } default: return fmt.Errorf("delivery: the state %q of %s is not merging, merged or refused", p.State, who) } } return nil } // Ask is the value of an asks key: the router's ask whole as the gate composes it, so the teller publishes it // unchanged, and the group and heads it was composed for. type Ask struct { V int `json:"v"` Group string `json:"group"` Heads []Head `json:"heads"` Ask asks.Ask `json:"ask"` } func (a Ask) check() error { key, err := MergeAskKey(a.Group, a.Heads) if err != nil { return fmt.Errorf("delivery: an ask needs its group and at least one pull request's head: %w", err) } if want := AskID(key); a.Ask.ID != want { return fmt.Errorf("delivery: the ask's id %q is not %q, the id its group and heads give", a.Ask.ID, want) } if len(a.Ask.Options) == 0 { return fmt.Errorf("delivery: the ask has no option") } return nil } // Answer is the teller's answer to an ask: the option chosen, the warrant's id and its words. type Answer struct { V int `json:"v"` Option string `json:"option"` Warrant string `json:"warrant"` Words string `json:"words"` } func (a Answer) check() error { for _, f := range []struct{ name, value string }{{"option", a.Option}, {"warrant", a.Warrant}, {"words", a.Words}} { if f.value == "" { return fmt.Errorf("delivery: an answer has no %s", f.name) } } return nil } // Owed is the value of an owed key: what the forge bridge performs. A status needs Context and State, a view and a // comment need Text, a note needs Text and Ref (the notes ref, mesh-plan). type Owed struct { V int `json:"v"` Act string `json:"act"` Context string `json:"context,omitempty"` State string `json:"state,omitempty"` Text string `json:"text,omitempty"` Ref string `json:"ref,omitempty"` } func (o Owed) check() error { switch o.Act { case OwedStatus: if o.Context == "" { return fmt.Errorf("delivery: an owed status has no context") } for _, s := range statusStates { if o.State == s { return nil } } return fmt.Errorf("delivery: the owed status state %q is not %s", o.State, strings.Join(statusStates, ", ")) case OwedView, OwedComment: if o.Text == "" { return fmt.Errorf("delivery: an owed %s has no text", o.Act) } case OwedNote: if o.Text == "" { return fmt.Errorf("delivery: an owed note has no text") } if o.Ref == "" { return fmt.Errorf("delivery: an owed note has no ref") } default: return fmt.Errorf("delivery: the owed act %q is not status, view, note or comment", o.Act) } return nil } // EncodeMerge is m as JSON at Version; it refuses a merged pull request without its merge commit, a refused one // without the forge's words and a state outside the list. func EncodeMerge(m Merge) ([]byte, error) { if err := m.check(); err != nil { return nil, err } m.V = Version return json.Marshal(m) } // DecodeMerge reads a merge; it refuses an unknown version (ErrUnknownVersion) and what EncodeMerge refuses. func DecodeMerge(data []byte) (Merge, error) { var m Merge if err := decode(data, &m); err != nil { return Merge{}, err } if err := m.check(); err != nil { return Merge{}, err } return m, nil } // EncodeAsk is a as JSON at Version; it refuses an ask whose id is not the one its group and heads give, or with // no option. func EncodeAsk(a Ask) ([]byte, error) { if err := a.check(); err != nil { return nil, err } a.V = Version return json.Marshal(a) } // DecodeAsk reads an ask; it refuses an unknown version (ErrUnknownVersion) and what EncodeAsk refuses. func DecodeAsk(data []byte) (Ask, error) { var a Ask if err := decode(data, &a); err != nil { return Ask{}, err } if err := a.check(); err != nil { return Ask{}, err } return a, nil } // EncodeAnswer is a as JSON at Version; it refuses an answer missing its option, warrant or words. func EncodeAnswer(a Answer) ([]byte, error) { if err := a.check(); err != nil { return nil, err } a.V = Version return json.Marshal(a) } // DecodeAnswer reads an answer; it refuses an unknown version (ErrUnknownVersion) and what EncodeAnswer refuses. func DecodeAnswer(data []byte) (Answer, error) { var a Answer if err := decode(data, &a); err != nil { return Answer{}, err } if err := a.check(); err != nil { return Answer{}, err } return a, nil } // EncodeOwed is o as JSON at Version; it refuses an act missing what it needs. func EncodeOwed(o Owed) ([]byte, error) { if err := o.check(); err != nil { return nil, err } o.V = Version return json.Marshal(o) } // DecodeOwed reads an owed act; it refuses an unknown version (ErrUnknownVersion) and what EncodeOwed refuses. func DecodeOwed(data []byte) (Owed, error) { var o Owed if err := decode(data, &o); err != nil { return Owed{}, err } if err := o.check(); err != nil { return Owed{}, err } return o, nil } // SameMerge says whether two merges are the same value, so a writer writes only when they differ. func SameMerge(a, b Merge) bool { return reflect.DeepEqual(a, b) } // SameAsk says whether two asks are the same value. func SameAsk(a, b Ask) bool { return reflect.DeepEqual(a, b) } // SameAnswer says whether two answers are the same value. func SameAnswer(a, b Answer) bool { return a == b } // SameOwed says whether two owed acts are the same value. func SameOwed(a, b Owed) bool { return a == b }