Help us shape the future of Go! Take the 2026 Go Developer Survey

rules

package
v0.3.4 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 26, 2026 License: AGPL-3.0 Imports: 25 Imported by: 0

Documentation

Overview

Package rules is the Rule module (ADR 0054): a stateless reaction to an Event. When any of a Rule's Trigger events is recorded for a Contact and its Condition holds, its Actions run at once, in order. The module writes and validates Rules, evaluates an Event against the active ones (Evaluate, the entry both the evaluation job and the tests call, no queue needed) and runs a Firing's Actions (RunActions).

It takes a *ent.Scoped on every call and holds no client. It imports none of the modules the Actions call: an Action is a Runner supplied by the caller (internal/jobs), so adding an Action kind is a new Runner and a new type name here, nothing in the evaluation.

Index

Constants

View Source
const (
	// MaxActions is how many Actions one Rule may run.
	MaxActions = 20
	// MaxConditionNodes is how many nodes (groups and rules, nested ones included) a Condition
	// may have.
	MaxConditionNodes = 50
)

The fixed caps on a Rule (ADR 0054). There is none on the number of Rules.

View Source
const ActionsKind = "rule_run_actions"

ActionsKind is the river kind of ActionsArgs.

View Source
const MaxHTTPActionsPerRule = 5

MaxHTTPActionsPerRule keeps one Rule from queueing an unbounded number of outbound calls.

Variables

View Source
var (
	// ErrInvalid: the Rule does not fit the rules of a Rule (no Trigger event, an unknown
	// Action, a tag Action without a tag, a Condition that does not parse or does not fit
	// the Contact schema). The wrapped message is safe to show to the author.
	ErrInvalid = validation.Refuse("rules: invalid rule", "errors.rule_invalid")
	// ErrNotFound: no such Rule in this Workspace.
	ErrNotFound = validation.Refuse("rules: rule not found", "errors.rule_not_found")
	// ErrEventNotFound: no such Event in this Workspace (a dry run reads a stored Event).
	ErrEventNotFound = validation.Refuse("rules: event not found", "errors.event_not_found")
)

Domain errors returned by Module. Callers match with errors.Is.

Functions

func CancelPending

func CancelPending(ctx context.Context, _ *ent.Scoped, contactID int64, purger events.QueuePurger) error

CancelPending cancels the queued evaluations that name a Contact (Erasure, ADR 0021): the job carries the triggering Event's properties. An Actions job carries only a Firing id and finds the Firing gone once EraseFirings has deleted it. It runs inside the caller's transaction.

func EnrollingInto

func EnrollingInto(ctx context.Context, s *ent.Scoped, automationID int64) ([]*ent.Rule, error)

EnrollingInto returns the active Rules that have an Action enrolling Contacts in the Automation. A removal of the Automation is refused while there are any (ADR 0049, ADR 0054).

func EraseFirings

func EraseFirings(ctx context.Context, s *ent.Scoped, contactID int64) error

EraseFirings deletes a Contact's Firings (Erasure, ADR 0021): they name the Contact and the Event that fired the Rule.

func Triggers

func Triggers(r *ent.Rule) []string

Triggers returns a Rule's Trigger event actions, in order.

func UsingTag

func UsingTag(ctx context.Context, s *ent.Scoped, name string) ([]*ent.Rule, error)

UsingTag returns the Workspace's active Rules, in id order, that name the Tag by its name: in a Condition leaf or in a tag Action. A draft or an archived Rule acts on nothing, so it does not hold the Tag.

Types

type Action

type Action struct {
	Type ActionType `json:"type"`
	// Tag is the Tag name of apply_tag and remove_tag.
	Tag string `json:"tag,omitempty"`
	// AutomationID is the Automation enroll_automation enrolls the Contact in.
	AutomationID int64 `json:"automation_id,omitempty"`

	// An http Action (ADR 0039 shape, shared through internal/httpcall) has ID, URL, Headers, Body
	// (a Liquid JSON template that sees the Contact and the `event`) and Secret. Stored, URL,
	// header values and Secret are ciphertext, sealed per field; in an Action being saved, an
	// empty one keeps the stored value of the Action of the same ID, and ClearSecret drops the
	// signing secret.
	ID          string            `json:"id,omitempty"`
	URL         string            `json:"url,omitempty"`
	Headers     []httpcall.Header `json:"headers,omitempty"`
	Body        string            `json:"body,omitempty"`
	Secret      string            `json:"secret,omitempty"`
	ClearSecret bool              `json:"-"`
}

Action is one Action of a Rule as stored: its type and the parameters of that type.

func DecodeActions

func DecodeActions(stored string) ([]Action, error)

DecodeActions parses a stored Actions array.

func (Action) EnrollsInto

func (a Action) EnrollsInto(automationID int64) bool

EnrollsInto reports whether the Action enrolls Contacts in the Automation.

func (Action) NamesTag

func (a Action) NamesTag(name string) bool

NamesTag reports whether the Action applies or removes the Tag.

func (Action) Summary

func (a Action) Summary() string

Summary describes an Action in one line, without running it.

type ActionInput

type ActionInput struct {
	Action    Action
	Index     int
	ContactID int64
	Firing    *ent.RuleFiring
}

ActionInput is what a Runner is handed: the Action and the Firing it runs for, and the Action's position in the Firing (where its Result is recorded).

type ActionRunners

type ActionRunners map[ActionType]Runner

ActionRunners maps an Action type to the Runner that does it.

type ActionType

type ActionType string

ActionType is the kind of an Action; its values are the contract's SiteRuleActionType.

const (
	ActionApplyTag  ActionType = "apply_tag"
	ActionRemoveTag ActionType = "remove_tag"
	// ActionEnroll enrolls the Contact in an Automation (once ever, ADR 0046).
	ActionEnroll ActionType = "enroll_automation"
	// ActionHTTP calls a URL with a body rendered for the Contact and the Event.
	ActionHTTP ActionType = "http"
)

The Action types. Each has a Runner (ActionRunners) that does it.

type ActionsArgs

type ActionsArgs struct {
	FiringID int64 `json:"firing_id"`
}

ActionsArgs is the job that runs a Firing's Actions. It carries no personal data: the Firing is read from the database, and goes with its Contact on Erasure.

func (ActionsArgs) Kind

func (ActionsArgs) Kind() string

type CreateInput

type CreateInput struct {
	Name          string
	TriggerEvents []string
	// Condition is the encoded tree; nil or empty means every Contact.
	Condition *string
	Actions   []Action
}

CreateInput is a new Rule, always a draft.

type DryRun

type DryRun struct {
	Outcome DryRunOutcome
	Actions []PlannedAction
}

DryRun is what a stored Event would do to a Rule. Actions are the ones that would run, in order, and empty unless the Outcome is WouldFire.

type DryRunOutcome

type DryRunOutcome string

DryRunOutcome is whether a stored Event would fire a Rule, and if not, why.

const (
	// WouldFire: the Event's action is a Trigger event and the Condition holds.
	WouldFire DryRunOutcome = "would_fire"
	// NotTriggered: the Event's action is none of the Rule's Trigger events.
	NotTriggered DryRunOutcome = "not_triggered"
	// Machine: software made the Event (a mail scanner's open or click, ADR 0053), and an Event
	// like that never triggers a Rule.
	Machine DryRunOutcome = "machine"
	// Backfill: the Event was loaded as history, and history never triggers a Rule.
	Backfill DryRunOutcome = "backfill"
	// NoContact: the Event is about no Contact, so a Rule never reacts to it.
	NoContact DryRunOutcome = "no_contact"
	// ConditionNotMet: the Condition does not hold for the Contact right now.
	ConditionNotMet DryRunOutcome = "condition_not_met"
)

type Event

type Event struct {
	ContactID int64
	// Action is the Event's action, matched against the Rules' Trigger events.
	Action string
	// SourceID identifies the Event: the DedupKey, else the envelope's id (what persist stores
	// as the Event's source_id). A Rule fires once per SourceID.
	SourceID string
	// RecordedAt is when the Event was recorded (published to the bus, not when the client says
	// it happened); a Rule acts only on Events recorded at or after its activation.
	RecordedAt time.Time
	// Properties are the properties the Event carries.
	Properties map[string]any
}

Event is what Evaluate needs of a recorded Event.

type Module

type Module struct {
	// contains filtered or unexported fields
}

Module is the one place a Rule is written, validated, evaluated and run.

func New

func New(cipher *secrets.Cipher, bus *events.Bus) *Module

New builds the Rules module. The cipher seals an http Action's secrets; the bus runs a Firing and the job that executes it in one transaction.

func (*Module) Activate

func (m *Module) Activate(ctx context.Context, s *ent.Scoped, id int64) (*ent.Rule, error)

Activate makes a Rule react to the Events recorded from now on (it never replays history). Activating an active Rule leaves it as it is. A Rule with no Actions does nothing, so it is refused (ErrInvalid).

func (*Module) Archive

func (m *Module) Archive(ctx context.Context, s *ent.Scoped, id int64) error

Archive discards a Rule (ADR 0049): it leaves the lists and acts on nothing, and its Firings stay. Archiving also deactivates it, so restoring it brings back a draft that cannot start firing before its author activates it again. Archiving an archived Rule changes nothing.

func (*Module) Create

func (m *Module) Create(ctx context.Context, s *ent.Scoped, in CreateInput) (*ent.Rule, error)

Create validates and stores a draft Rule with its Trigger events.

func (*Module) Deactivate

func (m *Module) Deactivate(ctx context.Context, s *ent.Scoped, id int64) (*ent.Rule, error)

Deactivate stops a Rule from firing. Deactivating a draft leaves it as it is.

func (*Module) Delete

func (m *Module) Delete(ctx context.Context, s *ent.Scoped, id int64) error

Delete purges a Rule that has no Firings (ADR 0049). One with any Firing is history and is archived instead: an *inuse.Error carrying their count.

func (*Module) DryRun

func (m *Module) DryRun(ctx context.Context, s *ent.Scoped, ruleID, eventID int64) (DryRun, error)

DryRun evaluates a stored Event against one Rule exactly as Evaluate does (the same Trigger event match and the same Condition evaluation, against the Contact's data now) and reports what would happen. It writes nothing and runs nothing: no Firing, no job, no Action. Unlike a real firing it ignores the Rule's status and activation time, so a draft can be tried.

func (*Module) Evaluate

func (m *Module) Evaluate(ctx context.Context, s *ent.Scoped, ev Event) ([]*ent.RuleFiring, error)

Evaluate matches an Event against the Workspace's active Rules (ADR 0054). For each Rule that has the Event's action as a Trigger event, was activated before the Event and whose Condition holds for the Contact right now, it writes a Firing and queues its Actions job in one transaction. The Firing is unique per Rule and SourceID, so an Event delivered twice fires a Rule once. A Rule whose Condition is false leaves nothing. Rules for one Event are independent: a failure on one is returned after the others have been tried.

It returns the Firings it wrote. It is queue-free apart from the enqueue inside the transaction, so a test drives it directly.

func (*Module) Firings

func (m *Module) Firings(ctx context.Context, s *ent.Scoped, ruleID int64, p pagination.Params) (pagination.Page[*ent.RuleFiring], error)

Firings returns one page of a Rule's Firings, newest first.

func (*Module) Get

func (m *Module) Get(ctx context.Context, s *ent.Scoped, id int64) (*ent.Rule, error)

Get returns one Rule of the Workspace with its Trigger events, archived or not.

func (*Module) List

func (m *Module) List(ctx context.Context, s *ent.Scoped, p pagination.Params, archived bool) (pagination.Page[*ent.Rule], error)

List returns one page of the Workspace's Rules, newest first: the kept ones, or with archived the archived ones.

func (*Module) OpenHTTP

func (m *Module) OpenHTTP(a Action) (httpcall.Opened, error)

OpenHTTP decrypts a stored http Action's URL, header values and secret, for its Runner.

func (*Module) Record

func (m *Module) Record(ctx context.Context, s *ent.Scoped, firingID int64, index int, res Result) ([]Result, error)

Record stores the Result of the index-th Action of a Firing and finishes the Firing when none is left to run or Pending. It locks the Firing row, because the Actions job and the job of a Pending Action write the same results. A new Result also queues the job it carries, in the same transaction. A Result already recorded (a retry of the writer) changes nothing. It returns the Firing's results as stored.

func (*Module) RunActions

func (m *Module) RunActions(ctx context.Context, s *ent.Scoped, firingID int64, runners ActionRunners) error

RunActions runs a Firing's Actions in order, each independent of the others: one that fails is recorded as failed and the next still runs. Each result is stored as soon as it is known, so a retry of the job (after a database error) resumes after the last recorded Action instead of repeating it. An Action that outlives this job (an http call, retried for days) is recorded Pending and its own job queued in the same transaction; the Actions after it do not wait for it, and the Firing finishes when the last Pending one is recorded. A Firing that is gone (erased with its Contact) or finished does nothing.

func (*Module) Unarchive

func (m *Module) Unarchive(ctx context.Context, s *ent.Scoped, id int64) error

Unarchive brings an archived Rule back to the lists, as a draft. Restoring one that is not archived changes nothing.

func (*Module) Update

func (m *Module) Update(ctx context.Context, s *ent.Scoped, id int64, in UpdateInput) (*ent.Rule, error)

Update changes a Rule's name, Trigger events, Condition or Actions, validated as Create validates. The status is untouched.

type Outcome

type Outcome string

Outcome is how one Action ended.

const (
	Succeeded Outcome = "succeeded"
	Failed    Outcome = "failed"
	// Skipped is an expected outcome (an Automation that is not active, a Contact already
	// enrolled), not an error.
	Skipped Outcome = "skipped"
	// Pending is an Action handed to a job of its own (an http Action, which is retried for days):
	// it has no outcome yet, and that job records it with Record.
	Pending Outcome = "pending"
)

type PlannedAction

type PlannedAction struct {
	Type    ActionType
	Summary string
}

PlannedAction is one Action a Rule would run, described for a person: its type and a one-line summary. A new Action kind needs only a case in Summary; an unknown one shows its type.

type Result

type Result struct {
	Type   ActionType `json:"type"`
	Status Outcome    `json:"status"`
	Detail string     `json:"detail,omitempty"`

	// Job is set by a Runner that hands the Action to a job (Status Pending): Record queues it, with
	// the options its args declare, in the transaction that records the Pending result. It is
	// never stored.
	Job river.JobArgs `json:"-"`
}

Result is what one Action did, as stored on its Firing.

func DecodeResults

func DecodeResults(stored string) ([]Result, error)

DecodeResults parses a Firing's stored results.

type Runner

type Runner interface {
	Run(ctx context.Context, s *ent.Scoped, in ActionInput) (Result, error)
}

Runner performs one kind of Action. It reports how it ended in the Result and never fails the others: an Action that cannot be done returns a Failed Result, and only an infrastructure error (the database) is returned as an error, which retries the Actions job. A Runner for an Action that outlives the Actions job returns a Pending Result with the job that finishes it.

type UpdateInput

type UpdateInput struct {
	Name          *string
	TriggerEvents *[]string
	Condition     *string
	Actions       *[]Action
}

UpdateInput changes a Rule; nil fields are left as they are.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL