Skip to content
quarry Artifact [e02092e64b]
Login

Artifact [e02092e64b]

Artifact e02092e64bdb2e4318ca201a78fed9341fa8e4245279a9435b79baf78a6c5589:


// Package evt is quarry's event vocabulary (RFC 0005): the names a
// pipeline, a webhook subscription or a notification rule may ask for.
//
// It is a leaf on purpose. The vocabulary is a PUBLISHED INTERFACE — a
// pipeline definition names an event in its `on:` clause, and that definition
// is a versioned artifact in someone else's store — so the names must be
// nameable from anywhere without dragging a transport in. This package
// imports nothing but the standard library, and nothing here knows that
// events travel over NATS.
package evt

import "strings"

// Kind is one event: a noun and an action, `ticket.closed`.
//
// Noun-and-action rather than one name per pair (RFC 0005 §Motivation).
// Gitea mints a permanent name for every verb — issue_assign, issue_label,
// issue_milestone — so a subscriber wanting "any issue activity" must list
// them all and remember the next one. Here a subscription can name the noun
// and get every action on it, and adding an action later costs no name.
type Kind string

// The vocabulary. An action marked "reserved" below is not raised by any code
// path yet and exists so that raising it later is not a vocabulary change —
// which for a published interface is the expensive kind.
const (
	CheckinCreated Kind = "checkin.created"

	BranchCreated Kind = "branch.created"
	BranchMoved   Kind = "branch.moved"
	BranchClosed  Kind = "branch.closed" // reserved

	TagCreated Kind = "tag.created"
	TagRemoved Kind = "tag.removed" // reserved

	TicketOpened   Kind = "ticket.opened"
	TicketChanged  Kind = "ticket.changed"
	TicketClosed   Kind = "ticket.closed"
	TicketReopened Kind = "ticket.reopened"

	WikiCreated Kind = "wiki.created"
	WikiEdited  Kind = "wiki.edited"
	WikiDeleted Kind = "wiki.deleted"

	TechnoteCreated Kind = "technote.created"
	TechnoteEdited  Kind = "technote.edited"

	ForumPosted    Kind = "forum.posted"
	ForumReplied   Kind = "forum.replied"
	ForumEdited    Kind = "forum.edited" // reserved
	ForumModerated Kind = "forum.moderated"

	LandingOpened    Kind = "landing.opened"
	LandingCommented Kind = "landing.commented"
	LandingAccepted  Kind = "landing.accepted"
	LandingRejected  Kind = "landing.rejected"

	CIStarted   Kind = "ci.started"
	CICompleted Kind = "ci.completed"

	MirrorPulled Kind = "mirror.pulled"
	MirrorFailed Kind = "mirror.failed"

	RepoCreated  Kind = "repo.created"
	RepoRemoved  Kind = "repo.removed"
	RepoSettings Kind = "repo.settings"

	AccountAdded   Kind = "account.added"
	AccountCaps    Kind = "account.caps"
	AccountRemoved Kind = "account.removed"

	CloneCloned  Kind = "clone.cloned"
	CloneFetched Kind = "clone.fetched"
)

// Derived reports whether this event is a reading of artifacts the store
// holds, and so can be re-derived by walking the index after a missed
// delivery (RFC 0005 §2.1).
//
// The distinction is load-bearing rather than tidy. The bus is a ONE-HOUR
// window: a consumer outside it does not replay, it rebuilds. A derived event
// missed costs latency; an operational one missed is a fact that is simply
// gone — which is the difference between a mirror.failed an operator can page
// on and one that silently did not happen. Operational events are therefore
// recorded in OLTP as they are raised, and the bus carries notice of a row
// that already exists.
func (k Kind) Derived() bool {
	switch k.Noun() {
	case "checkin", "branch", "tag", "ticket", "wiki", "technote", "forum", "landing":
		return true
	}
	return false
}

// Noun is the thing the event is about.
func (k Kind) Noun() string {
	noun, _, _ := strings.Cut(string(k), ".")
	return noun
}

// Action is what happened to it, empty when k names only a noun.
func (k Kind) Action() string {
	_, action, _ := strings.Cut(string(k), ".")
	return action
}

// Valid reports whether k is in the vocabulary. An unknown name is refused at
// the moment a subscription is written rather than silently never firing,
// because a pipeline that never runs and a pipeline that runs and fails look
// nothing alike to the person waiting for it.
func (k Kind) Valid() bool {
	for _, known := range All {
		if k == known {
			return true
		}
	}
	return false
}

// Matches reports whether a subscription pattern selects this event.
//
// A pattern is either a full kind (`ticket.closed`) or a bare noun
// (`ticket`), which selects every action on it — including actions added
// after the subscription was written. That is the whole reason for the
// noun-and-action shape.
func (k Kind) Matches(pattern string) bool {
	if pattern == "" {
		return false
	}
	if pattern == string(k) {
		return true
	}
	return !strings.Contains(pattern, ".") && pattern == k.Noun()
}

// All is the vocabulary, in the order RFC 0005 §1 lists it. Iterated by
// Valid, and by the API doc so the set is discoverable rather than folklore.
var All = []Kind{
	CheckinCreated,
	BranchCreated, BranchMoved, BranchClosed,
	TagCreated, TagRemoved,
	TicketOpened, TicketChanged, TicketClosed, TicketReopened,
	WikiCreated, WikiEdited, WikiDeleted,
	TechnoteCreated, TechnoteEdited,
	ForumPosted, ForumReplied, ForumEdited, ForumModerated,
	LandingOpened, LandingCommented, LandingAccepted, LandingRejected,
	CIStarted, CICompleted,
	MirrorPulled, MirrorFailed,
	RepoCreated, RepoRemoved, RepoSettings,
	AccountAdded, AccountCaps, AccountRemoved,
	CloneCloned, CloneFetched,
}

// Nouns lists each noun once, in All's order.
func Nouns() []string {
	var out []string
	seen := map[string]bool{}
	for _, k := range All {
		if n := k.Noun(); !seen[n] {
			seen[n] = true
			out = append(out, n)
		}
	}
	return out
}