rules

package
v0.1.0 Latest Latest
Warning

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

Go to latest
Published: Apr 13, 2026 License: MIT Imports: 9 Imported by: 0

README

anyexpr/rules

A typed rule evaluation engine built on anyexpr.

Define actions as a struct with typed fields, compile rule definitions with type-checked values, evaluate them against your environment, and read results through typed struct fields. Dispatch evaluation results to named handlers gated by expressions.

Install

go get github.com/rhyselsmore/anyexpr

Quick Start

type Email struct {
    From    string
    Subject string
    Amount  float64
}

// Declare actions as a generic struct. E is the environment type.
type Actions[E any] struct {
    Label    rules.Action[string, E]       `rule:"label,multi" description:"categorisation labels"`
    Move     rules.Action[string, E]       `rule:"move"`
    Read     rules.Action[bool, E]         `rule:"read"`
    Priority rules.Action[int, E]          `rule:"priority"`
    Delete   rules.Action[action.NoArgs, E] `rule:"delete,terminal"`
}

// Define actions — reflects over the struct once.
actions, err := rules.DefineActions[Email, Actions[Email]]()

// Build the expression compiler.
compiler, err := anyexpr.NewCompiler[Email]()

// Compile rules — values are type-checked against the action's type.
prog, err := rules.Compile(compiler, actions, []rules.Definition{
    {
        Name: "invoices",
        Tags: []string{"billing"},
        When: `has(Subject, "invoice")`,
        Then: []rules.ActionEntry{
            {Name: "label", Value: "billing"},
            {Name: "label", Value: "invoice"},
            {Name: "move", Value: "billing/invoices"},
            {Name: "read", Value: true},
            {Name: "priority", Value: 3},
        },
    },
    {
        Name: "spam",
        When: `has(From, "noreply@junk.com")`,
        Then: []rules.ActionEntry{
            {Name: "delete"},
        },
    },
})

// Evaluate.
evaluator, err := rules.NewEvaluator(prog)
eval, err := evaluator.Run(ctx, email)

// Read typed results.
eval.Result.Label.Values     // []string{"billing", "invoice"}
eval.Result.Move.Value       // "billing/invoices"
eval.Result.Read.Value       // true
eval.Result.Priority.Value   // 3
eval.Result.Delete.Triggered // false

Actions

Actions are declared as fields on a struct. Each field is an Action[V, E] where V is the value type and E is the environment type. The rule struct tag configures the action name and options. An optional description tag provides human-readable metadata.

Supported Value Types
Type Go type Example
String Action[string, E] "billing"
Boolean Action[bool, E] true
Integer Action[int, E] 42
Float Action[float64, E] 0.95
Presence Action[action.NoArgs, E] nil or action.NoArgs{}
Cardinality
  • Single (default) — at most once per rule. Across rules, last match wins.
  • Multi (rule:"name,multi") — accumulates across rules, duplicates stripped.
Terminal

An action marked rule:"name,terminal" halts evaluation when triggered. Multiple terminal actions are allowed in the struct (the user picks which to use). A single rule can reference at most one terminal action, enforced at compile time.

Action Results

After evaluation, each Action field on the result has:

eval.Result.Label.Triggered  // bool — was it triggered?
eval.Result.Label.Value      // string — last value (last wins for Single)
eval.Result.Label.Values     // []string — all values (deduped for Multi)
eval.Result.Label.Triggers   // []Trigger[string] — full provenance

Each Trigger records the rule name, tags, and value:

for _, t := range eval.Result.Label.Triggers {
    fmt.Println(t.Rule, t.Tags, t.Value)
}

Rule Definitions

Definitions are plain structs — build them from YAML, JSON, a database, or code:

rules.Definition{
    Name:    "my-rule",
    Tags:    []string{"billing"},
    Enabled: nil,          // nil = enabled (default)
    Stop:    false,         // halt evaluation after this rule
    When:    `has(Subject, "invoice")`,
    Skip:    `Amount < 10`, // optional skip expression
    Mode:    rules.WhenThenSkip, // or SkipThenWhen
    Then:    []rules.ActionEntry{
        {Name: "label", Value: "billing"},
    },
}

ActionEntry.Value is any — the type is checked against the action's constraint at compile time.

Skip Expressions

Rules can have an optional Skip expression evaluated against the environment. If it returns true, the rule is skipped.

Mode controls evaluation order:

  • WhenThenSkip (default) — evaluate When first. If it matches, check Skip. If Skip is true, suppress the match.
  • SkipThenWhen — check Skip first. If true, skip without evaluating When (avoids paying the expression cost).

Compile-Time Validation

Compile validates everything upfront:

  • Duplicate rule names
  • Expression parse/type errors (When and Skip clauses)
  • Unknown action names
  • Value type mismatches ("banana" for a bool action)
  • Single-cardinality action used multiple times in one rule
  • Multiple terminal actions in one rule

Evaluation

evaluator, err := rules.NewEvaluator(prog)
eval, err := evaluator.Run(ctx, email)

The Evaluation includes:

  • Env — the environment value that was evaluated
  • Result — the actions struct with triggered values
  • Matched — rule names that matched, in order
  • Stopped / StoppedBy — terminal/stop state
  • StartedAt / Duration — timing
  • Trace — per-rule steps (when tracing is enabled)
Tracing

Enable per-rule tracing to see what happened:

eval, err := evaluator.Run(ctx, email, rules.WithTrace(true))

for _, step := range eval.Trace {
    fmt.Println(step.Rule, step.Outcome, step.Duration)
}

Each step records the outcome (matched, skipped, disabled, excluded, skip-expr), expression duration, evaluation mode, and which actions were referenced.

Selectors

Filter which rules evaluate:

// Per-call selectors.
eval, _ := evaluator.Run(ctx, email,
    rules.WithTags("billing"),
    rules.ExcludeNames("spam"),
)

// Evaluator-level defaults.
evaluator, _ := rules.NewEvaluator(prog,
    rules.OnEvaluation(rules.WithTags("billing")),
)

// Expression-based selectors.
sel, _ := rules.WithSelector(`Name != "spam" && "billing" in Tags`)
eval, _ := evaluator.Run(ctx, email, sel)
Debug Output
fmt.Print(eval.Debug())

Prints a human-readable summary with timing, matched rules, results, and trace.

Introspection

Discover what actions are available:

for _, info := range actions.Describe() {
    fmt.Println(info.Name, info.ValueType, info.Description)
}

Returns []ActionInfo with name, description, cardinality, terminal flag, and value type for each action.

Registry

For dynamic rule management:

reg, err := rules.NewRegistry(compiler, actions)

reg.Add(def1, def2)       // errors on duplicate names
reg.Update(updatedDef1)   // errors on unknown names
reg.Upsert(def3)          // adds or replaces
reg.Remove("spam")         // silent on unknown

prog, err := reg.Compile()

Insertion order is preserved. Safe for concurrent use.

Expression Validation

Validate an expression without compiling a full program:

err := rules.Check(compiler, `has(Subject, "invoice")`)

Single Rule Testing

Test a rule in isolation:

eval, err := rules.TestRule(compiler, actions, def, email)

Returns a full evaluation with tracing enabled.

Assertions

Write assertions in the same expression language, evaluated against the actions struct:

result := rules.RunTestCase(compiler, actions, rules.TestCase[Email, Actions[Email]]{
    Name: "invoice labelling",
    Rule: def,
    Env:  Email{Subject: "Your Invoice"},
    Assertions: []string{
        `Label.Triggered`,
        `Label.Value == "billing"`,
        `Priority.Value >= 3`,
        `!Delete.Triggered`,
    },
})
fmt.Println(result.Passed, result.Failures)

Dispatch

The dispatch subpackage routes evaluation results to named handler functions, gated by expressions evaluated against the full evaluation.

Handlers

Register handlers immutably on a Dispatcher:

d, err := dispatch.New(
    dispatch.WithLogger[Email, Actions[Email]](logger),
    dispatch.Handle("move-email", moveHandler,
        dispatch.WithDescription("moves email to the target folder"),
    ),
    dispatch.Handle("alert", alertHandler,
        dispatch.WithDescription("sends a priority alert"),
    ),
    dispatch.Handle("audit", auditHandler),
)

Discover registered handlers:

for _, h := range d.Describe() {
    fmt.Println(h.Name, h.Description)
}
Plans

Build named, immutable plans that specify which handlers run and under what conditions:

plan, err := d.Plan(
    dispatch.WithName[Email, Actions[Email]]("billing"),
    dispatch.WithStrategy[Email, Actions[Email]](dispatch.AllContinue),
    dispatch.Gate[Email, Actions[Email]](`len(Matched) > 0`),
    dispatch.Run("move-email",
        dispatch.When[Email, Actions[Email]](`Result.Move.Triggered`),
    ),
    dispatch.Run("alert",
        dispatch.When[Email, Actions[Email]](`Result.Priority.Value >= 3`),
    ),
    dispatch.Run[Email, Actions[Email]]("audit"), // no When — always runs
)

Inspect a plan:

fmt.Println(plan.Name())
for _, entry := range plan.Describe() {
    fmt.Println(entry.Handler, entry.Whens)
}
Execution
result := plan.Execute(ctx, eval)

fmt.Println(result.OK())        // true if no errors
fmt.Println(result.Dispatched)  // what ran, timing, errors
fmt.Print(result.Debug())       // human-readable summary
Strategies
  • AllContinue (default) — run all matching handlers, collect errors.
  • AllHaltOnError — stop on first error.
  • FirstMatch — run only the first matching handler.
Features
  • Panic recovery — handler panics are caught and reported as errors.
  • Context cancellation — respects ctx.Done() between handlers.
  • Gate expressions — top-level expression that must pass before any handler runs.
  • When expressions — per-handler gating. Multiple When expressions per handler; any match triggers the handler.
  • Structured logging — optional slog.Logger for dispatch events.
  • Timing — per-handler and total duration on the result.

License

MIT

Documentation

Overview

Package rules is a typed rule evaluation engine built on anyexpr.

It provides a when/then model: define actions as typed struct fields, compile rule definitions with type-checked values, evaluate them against a typed environment, and read results through typed fields — no string keys, no type assertions.

Type Parameters

Two type parameters flow through the package:

  • E is the environment type — the struct that expressions evaluate against (e.g. Email, Transaction).
  • A is the actions struct — a user-defined struct containing Action fields with `rule` struct tags.

Workflow

  1. Define an actions struct with Action fields and `rule` tags.
  2. Call DefineActions to reflect over the struct and build the schema.
  3. Call Compile with rule definitions to produce a Program.
  4. Call NewEvaluator with the program.
  5. Call Evaluator.Run to evaluate rules against an environment value.
  6. Read typed results from the returned Evaluation.

Registry

For dynamic rule management, use Registry to add, update, upsert, and remove rule definitions, then compile on demand.

Testing

Use Check to validate expressions, TestRule to evaluate a single rule in isolation, and RunTestCase to run assertions against evaluation results using the same expression language.

Example
package main

import (
	"context"
	"fmt"
	"log"

	"github.com/rhyselsmore/anyexpr"
	rules "github.com/rhyselsmore/anyexpr/rules"
	"github.com/rhyselsmore/anyexpr/rules/action"
)

type Email struct {
	From    string
	Subject string
	Amount  float64
}

type EmailActions[E any] struct {
	Label    rules.Action[string, E]        `rule:"label,multi" description:"categorisation labels"`
	Move     rules.Action[string, E]        `rule:"move" description:"destination folder"`
	Read     rules.Action[bool, E]          `rule:"read"`
	Priority rules.Action[int, E]           `rule:"priority"`
	Delete   rules.Action[action.NoArgs, E] `rule:"delete,terminal"`
}

func main() {
	// Define actions from struct tags.
	actions, err := rules.DefineActions[Email, EmailActions[Email]]()
	if err != nil {
		log.Fatal(err)
	}

	// Build the expression compiler.
	compiler, err := anyexpr.NewCompiler[Email]()
	if err != nil {
		log.Fatal(err)
	}

	// Compile rules — values are type-checked at compile time.
	prog, err := rules.Compile(compiler, actions, []rules.Definition{
		{
			Name: "invoices",
			Tags: []string{"billing"},
			When: `has(Subject, "invoice")`,
			Then: []rules.ActionEntry{
				{Name: "label", Value: "billing"},
				{Name: "label", Value: "invoice"},
				{Name: "move", Value: "billing/invoices"},
				{Name: "read", Value: true},
				{Name: "priority", Value: 3},
			},
		},
		{
			Name: "large",
			Tags: []string{"alerts"},
			When: `Amount > 1000`,
			Then: []rules.ActionEntry{
				{Name: "label", Value: "high-value"},
				{Name: "priority", Value: 5},
			},
		},
	})
	if err != nil {
		log.Fatal(err)
	}

	// Create evaluator and run.
	evaluator, err := rules.NewEvaluator(prog)
	if err != nil {
		log.Fatal(err)
	}

	eval, err := evaluator.Run(context.Background(), Email{
		From:    "billing@stripe.com",
		Subject: "Your January Invoice",
		Amount:  1500,
	})
	if err != nil {
		log.Fatal(err)
	}

	// Read typed results directly from struct fields.
	fmt.Println("Labels:", eval.Result.Label.Values)
	fmt.Println("Move:", eval.Result.Move.Value)
	fmt.Println("Read:", eval.Result.Read.Value)
	fmt.Println("Priority:", eval.Result.Priority.Value)
	fmt.Println("Matched:", eval.Matched)

}
Output:
Labels: [billing invoice high-value]
Move: billing/invoices
Read: true
Priority: 5
Matched: [invoices large]
Example (Introspection)
package main

import (
	"fmt"
	"log"

	rules "github.com/rhyselsmore/anyexpr/rules"
	"github.com/rhyselsmore/anyexpr/rules/action"
)

type Email struct {
	From    string
	Subject string
	Amount  float64
}

type EmailActions[E any] struct {
	Label    rules.Action[string, E]        `rule:"label,multi" description:"categorisation labels"`
	Move     rules.Action[string, E]        `rule:"move" description:"destination folder"`
	Read     rules.Action[bool, E]          `rule:"read"`
	Priority rules.Action[int, E]           `rule:"priority"`
	Delete   rules.Action[action.NoArgs, E] `rule:"delete,terminal"`
}

func main() {
	actions, err := rules.DefineActions[Email, EmailActions[Email]]()
	if err != nil {
		log.Fatal(err)
	}

	for _, info := range actions.Describe() {
		line := fmt.Sprintf("%-10s %s", info.Name, info.ValueType)
		if info.Description != "" {
			line += " — " + info.Description
		}
		fmt.Println(line)
	}

}
Output:
label      string — categorisation labels
move       string — destination folder
read       bool
priority   int
delete     action.NoArgs
Example (Registry)
package main

import (
	"fmt"
	"log"

	"github.com/rhyselsmore/anyexpr"
	rules "github.com/rhyselsmore/anyexpr/rules"
	"github.com/rhyselsmore/anyexpr/rules/action"
)

type Email struct {
	From    string
	Subject string
	Amount  float64
}

type EmailActions[E any] struct {
	Label    rules.Action[string, E]        `rule:"label,multi" description:"categorisation labels"`
	Move     rules.Action[string, E]        `rule:"move" description:"destination folder"`
	Read     rules.Action[bool, E]          `rule:"read"`
	Priority rules.Action[int, E]           `rule:"priority"`
	Delete   rules.Action[action.NoArgs, E] `rule:"delete,terminal"`
}

func main() {
	actions, err := rules.DefineActions[Email, EmailActions[Email]]()
	if err != nil {
		log.Fatal(err)
	}
	compiler, err := anyexpr.NewCompiler[Email]()
	if err != nil {
		log.Fatal(err)
	}

	reg, err := rules.NewRegistry(compiler, actions)
	if err != nil {
		log.Fatal(err)
	}

	reg.Add(rules.Definition{
		Name: "invoices",
		When: `has(Subject, "invoice")`,
		Then: []rules.ActionEntry{{Name: "label", Value: "billing"}},
	})

	reg.Add(rules.Definition{
		Name: "spam",
		When: `has(From, "junk")`,
		Then: []rules.ActionEntry{{Name: "delete"}},
	})

	fmt.Println("Rules:", reg.Len())

	reg.Remove("spam")
	fmt.Println("After remove:", reg.Len())

	prog, err := reg.Compile()
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println("Compiled:", !prog.IsZero())

}
Output:
Rules: 2
After remove: 1
Compiled: true
Example (TestCase)
package main

import (
	"fmt"
	"log"

	"github.com/rhyselsmore/anyexpr"
	rules "github.com/rhyselsmore/anyexpr/rules"
	"github.com/rhyselsmore/anyexpr/rules/action"
)

type Email struct {
	From    string
	Subject string
	Amount  float64
}

type EmailActions[E any] struct {
	Label    rules.Action[string, E]        `rule:"label,multi" description:"categorisation labels"`
	Move     rules.Action[string, E]        `rule:"move" description:"destination folder"`
	Read     rules.Action[bool, E]          `rule:"read"`
	Priority rules.Action[int, E]           `rule:"priority"`
	Delete   rules.Action[action.NoArgs, E] `rule:"delete,terminal"`
}

func main() {
	actions, err := rules.DefineActions[Email, EmailActions[Email]]()
	if err != nil {
		log.Fatal(err)
	}
	compiler, err := anyexpr.NewCompiler[Email]()
	if err != nil {
		log.Fatal(err)
	}

	result := rules.RunTestCase(compiler, actions, rules.TestCase[Email, EmailActions[Email]]{
		Name: "invoice labelling",
		Rule: rules.Definition{
			Name: "invoices",
			When: `has(Subject, "invoice")`,
			Then: []rules.ActionEntry{
				{Name: "label", Value: "billing"},
				{Name: "read", Value: true},
			},
		},
		Env: Email{Subject: "Your Invoice"},
		Assertions: []string{
			`Label.Triggered`,
			`Label.Value == "billing"`,
			`Read.Value == true`,
			`!Delete.Triggered`,
		},
	})

	fmt.Println("Passed:", result.Passed)

}
Output:
Passed: true

Index

Examples

Constants

This section is empty.

Variables

View Source
var (
	// ErrDefine is returned when DefineActions fails validation.
	ErrDefine = errors.New("rules: action definition failed")

	// ErrDuplicateRegistration is returned when two action fields share
	// the same tag name.
	ErrDuplicateRegistration = errors.New("rules: duplicate registration")

	// ErrDefinitionDuplicate is returned when two rule definitions carry
	// the same name during compilation.
	ErrDefinitionDuplicate = errors.New("rules: duplicate definition")

	// ErrCompile is returned when a when-expression fails to compile.
	ErrCompile = errors.New("rules: compilation failed")

	// ErrNoDefinitions is returned when Compile is called with an
	// empty definitions slice.
	ErrNoDefinitions = errors.New("rules: no rule definitions provided")

	// ErrUnknownAction is returned when a rule references an action
	// name that was not registered.
	ErrUnknownAction = errors.New("rules: unknown action")

	// ErrCardinalityViolation is returned when a single-cardinality
	// action appears more than once in the same rule.
	ErrCardinalityViolation = errors.New("rules: single-use action used multiple times")

	// ErrMultipleTerminals is returned when a single rule contains
	// more than one terminal action.
	ErrMultipleTerminals = errors.New("rules: multiple terminal actions in rule")

	// ErrActionValueType is returned when an action's value does not
	// match the expected type from the definition.
	ErrActionValueType = errors.New("rules: action value type mismatch")

	// ErrProgramZero is returned when a nil or uncompiled Program is
	// passed to NewEvaluator.
	ErrProgramZero = errors.New("rules: program is nil or not compiled")

	// ErrActionsZero is returned when a nil or uninitialised Actions
	// registry is passed to Compile.
	ErrActionsZero = errors.New("rules: actions registry is nil or not initialized")

	// ErrUnknownDefinition is returned when Update is called with a
	// definition name that is not registered.
	ErrUnknownDefinition = errors.New("rules: unknown definition")

	// ErrAssert is returned when an assertion expression fails to
	// compile or evaluate.
	ErrAssert = errors.New("rules: assertion error")

	// ErrAssertFailed is returned when an assertion expression
	// evaluates to false.
	ErrAssertFailed = errors.New("rules: assertion failed")
)

Functions

func Check

func Check[E any](compiler *anyexpr.Compiler[E], when string) error

Check validates a when-expression against the environment type without compiling a full program. Useful for iterating on expressions before committing them to rule definitions.

Types

type Action

type Action[V action.Valuable, E any] struct {

	// Triggered is true if any rule set this action.
	Triggered bool

	// Value is the resolved value. For Single cardinality, last wins.
	// For Multi, the last trigger's value.
	Value V

	// Values holds all resolved values. Populated for Multi cardinality
	// (deduped). For Single, contains zero or one element.
	Values []V

	// Triggers holds the full provenance — every rule that set this
	// action, with its tags and value.
	Triggers []Trigger[V]
	// contains filtered or unexported fields
}

Action is a typed action field within an actions struct.

  • V is the value type (string, bool, int, float64, or NoArgs), constrained by action.Valuable.
  • E is the environment type (e.g. Email).

type ActionEntry

type ActionEntry struct {
	Name  string
	Value any
}

ActionEntry is a single action within a rule's Then list.

type ActionInfo

type ActionInfo struct {
	// Name is the action's registered name from the struct tag.
	Name string

	// Description is the human-readable description from the
	// `description` struct tag, if present.
	Description string

	// Cardinality is Single or Multi.
	Cardinality action.Cardinality

	// Terminal is true if triggering this action halts evaluation.
	Terminal bool

	// ValueType is the Go type name of the value (e.g. "string", "bool").
	ValueType string
}

ActionInfo is the type-erased metadata for a defined action, returned by Actions.Describe.

type Actions

type Actions[E any, A any] struct {
	// contains filtered or unexported fields
}

Actions holds the action schema for type A, bound to environment type E. Created via DefineActions.

  • E is the environment type — the struct that expressions evaluate against (e.g. Email, Transaction).
  • A is the actions struct containing Action[V, E] fields with `rule` tags.

func DefineActions

func DefineActions[E any, A any]() (*Actions[E, A], error)

DefineActions reflects over A to build the action schema.

  • E is the environment type (e.g. Email).
  • A is the actions struct (e.g. EmailActions).

It walks exported fields of A, looking for Action[V, E] types with a `rule` struct tag. Each field is configured with its name, cardinality, and terminal flag parsed from the tag. Values are bound at compile time via Compile.

func (*Actions[E, A]) Describe

func (ac *Actions[E, A]) Describe() []ActionInfo

Describe returns metadata for all defined actions, in struct field order. Useful for introspection — an agent or UI can discover what actions are available, their types, and descriptions.

func (*Actions[E, A]) IsZero

func (ac *Actions[E, A]) IsZero() bool

IsZero returns true if the registry was not created via DefineActions.

type Assertion

type Assertion[A any] struct {
	// contains filtered or unexported fields
}

Assertion is an expression evaluated against the actions struct A after rule evaluation. Written in the same expression language as rules, but targeting the result instead of the environment.

Example assertions (given actions struct with Label, Priority):

"Label.Triggered"
"Label.Value == \"billing\""
"len(Label.Values) == 3"
"Priority.Value > 2"
"!Delete.Triggered"

func NewAssertion

func NewAssertion[A any](expr string) (*Assertion[A], error)

NewAssertion compiles an assertion expression against the actions struct type A. The expression has access to all exported fields on A and on each Action field (Triggered, Value, Values, Triggers).

func (*Assertion[A]) Assert

func (a *Assertion[A]) Assert(eval *Evaluation[any, A]) error

Assert evaluates the assertion against an evaluation result. Returns nil if the assertion passes (expression returns true), or ErrAssertFailed with context if it returns false.

func (*Assertion[A]) AssertResult

func (a *Assertion[A]) AssertResult(result A) error

AssertResult evaluates the assertion against an actions struct directly.

type CompileOpt

type CompileOpt[E any, A any] func(*CompileOpts[E, A]) error

CompileOpt configures a Compile call.

type CompileOpts

type CompileOpts[E any, A any] struct {
}

CompileOpts holds the accumulated configuration for Compile.

type Definition

type Definition struct {
	Name    string
	Tags    []string
	Enabled *bool
	Stop    bool
	When    string
	Skip    string // optional expression — if true, rule is skipped
	Mode    EvalMode
	Then    []ActionEntry
}

Definition is the input to rule compilation. Consumers construct definitions however they like — from YAML, JSON, a database, or directly in code.

func (Definition) IsEnabled

func (d Definition) IsEnabled() bool

IsEnabled returns whether the rule is enabled. Rules are enabled by default (nil Enabled field is treated as true).

type EvalMode

type EvalMode int

EvalMode controls the order of When and Skip expression evaluation.

const (
	// WhenThenSkip evaluates When first. If it matches, Skip is
	// checked. If Skip returns true, the rule is skipped despite
	// matching. This is the default.
	WhenThenSkip EvalMode = iota

	// SkipThenWhen evaluates Skip first. If Skip returns true, the
	// When expression is never evaluated — the rule is skipped
	// without paying the cost of the match expression.
	SkipThenWhen
)

func (EvalMode) String

func (m EvalMode) String() string

type Evaluation

type Evaluation[E any, A any] struct {
	// Env is the environment value that was evaluated.
	Env E

	// Result holds the actions struct with triggered values populated.
	Result A

	// Matched lists the names of rules that matched, in evaluation order.
	Matched []string

	// Stopped is true if evaluation was halted by a stop or terminal.
	Stopped bool

	// StoppedBy is the name of the rule that halted evaluation.
	StoppedBy string

	// StartedAt is when the evaluation began.
	StartedAt time.Time

	// Duration is the total evaluation time.
	Duration time.Duration

	// Traced is true if tracing was enabled for this evaluation.
	Traced bool

	// Trace holds per-rule evaluation steps. Only populated when
	// tracing is enabled via WithTrace.
	Trace []Step
}

Evaluation is the result of evaluating rules against an environment.

  • E is the environment type (e.g. Email).
  • A is the actions struct (e.g. EmailActions).

func TestRule

func TestRule[E any, A any](
	compiler *anyexpr.Compiler[E],
	actions *Actions[E, A],
	def Definition,
	env E,
) (*Evaluation[E, A], error)

TestRule compiles and evaluates a single rule definition against a single environment value. Returns the evaluation result for that rule only, with tracing enabled. Useful for testing rules in isolation.

func (*Evaluation[E, A]) Debug

func (e *Evaluation[E, A]) Debug() string

Debug returns a human-readable summary of the evaluation, suitable for logging or printing. Includes matched rules, timing, action results, and trace (if enabled).

type EvaluationOpt

type EvaluationOpt func(*evaluationConfig)

EvaluationOpt configures a single evaluation (Run call).

func ExcludeNames

func ExcludeNames(names ...string) EvaluationOpt

ExcludeNames excludes rules with any of the given names.

func ExcludeTags

func ExcludeTags(tags ...string) EvaluationOpt

ExcludeTags excludes rules with any of the given tags.

func MustWithSelector

func MustWithSelector(expr string) EvaluationOpt

MustWithSelector is like WithSelector but panics on error.

func WithNames

func WithNames(names ...string) EvaluationOpt

WithNames limits evaluation to rules with matching names.

func WithSelector

func WithSelector(expr string) (EvaluationOpt, error)

WithSelector filters rules using an expression evaluated against RuleMeta (Name string, Tags []string). The expression is compiled once when the option is created. Rules that don't pass the expression are excluded.

Example: WithSelector(`Name != "spam" && "billing" in Tags`)

func WithTags

func WithTags(tags ...string) EvaluationOpt

WithTags limits evaluation to rules with at least one matching tag.

func WithTrace

func WithTrace(enabled bool) EvaluationOpt

WithTrace enables per-rule tracing on the evaluation. Off by default. When enabled, the Evaluation.Trace slice is populated with a Step per rule showing outcome and expression duration.

type Evaluator

type Evaluator[E any, A any] struct {
	// contains filtered or unexported fields
}

Evaluator evaluates rules against an environment and produces typed action results. Safe for concurrent use.

  • E is the environment type (e.g. Email).
  • A is the actions struct (e.g. EmailActions).

func NewEvaluator

func NewEvaluator[E any, A any](
	program *Program[E, A],
	opts ...EvaluatorOpt,
) (*Evaluator[E, A], error)

NewEvaluator creates an evaluator from a compiled Program.

  • E is the environment type (e.g. Email).
  • A is the actions struct (e.g. EmailActions).

func (*Evaluator[E, A]) Run

func (ev *Evaluator[E, A]) Run(ctx context.Context, env E, opts ...EvaluationOpt) (*Evaluation[E, A], error)

Run evaluates rules top-to-bottom against env.

  1. Matches rules against the environment, collecting matched rule names and which actions they reference.
  2. If any rules matched, copies the action schema and triggers only the actions that were referenced by matched rules.
  3. Returns an Evaluation with the populated actions, matched rules, timing, and optional trace.

Per-call EvaluationOpts are additive with the evaluator's defaults set via OnEvaluation.

type EvaluatorOpt

type EvaluatorOpt func(*evaluatorConfig)

EvaluatorOpt configures an Evaluator.

func OnEvaluation

func OnEvaluation(opts ...EvaluationOpt) EvaluatorOpt

OnEvaluation sets default evaluation options applied to every Run call. Per-call options passed to Run clobber these defaults.

type Outcome

type Outcome int

Outcome describes what happened when a rule was evaluated.

const (
	// OutcomeMatched means the rule's expression evaluated to true.
	OutcomeMatched Outcome = iota
	// OutcomeSkipped means the When expression evaluated to false.
	OutcomeSkipped
	// OutcomeDisabled means the rule's Enabled field was false.
	OutcomeDisabled
	// OutcomeExcluded means the rule was filtered by a selector.
	OutcomeExcluded
	// OutcomeSkipExpr means the rule's Skip expression evaluated to
	// true, causing the rule to be skipped.
	OutcomeSkipExpr
)

func (Outcome) String

func (o Outcome) String() string

type Program

type Program[E any, A any] struct {
	// contains filtered or unexported fields
}

Program holds compiled rules ready for evaluation.

  • E is the environment type (e.g. Email).
  • A is the actions struct (e.g. EmailActions).

func Compile

func Compile[E any, A any](
	compiler *anyexpr.Compiler[E],
	actions *Actions[E, A],
	defs []Definition,
	opts ...CompileOpt[E, A],
) (*Program[E, A], error)

Compile validates and compiles rule definitions against the registered actions and expression compiler.

  • E is the environment type (e.g. Email).
  • A is the actions struct (e.g. EmailActions).

Action names in definitions are checked against the bound actions. Values are type-checked against the action's value type. Expressions are compiled via the anyexpr compiler.

func (*Program[E, A]) IsZero

func (p *Program[E, A]) IsZero() bool

IsZero returns true if the program was not created via Compile.

type Registry

type Registry[E any, A any] struct {
	// contains filtered or unexported fields
}

Registry manages rule definitions and compiles them into Programs on demand. It holds the compiler and actions schema, letting callers add, update, upsert, and remove rules by name, then compile when ready.

  • E is the environment type (e.g. Email).
  • A is the actions struct (e.g. EmailActions).

Safe for concurrent use.

func NewRegistry

func NewRegistry[E any, A any](
	compiler *anyexpr.Compiler[E],
	actions *Actions[E, A],
) (*Registry[E, A], error)

NewRegistry creates a Registry with the given compiler and actions schema.

func (*Registry[E, A]) Add

func (r *Registry[E, A]) Add(defs ...Definition) error

Add registers one or more definitions. Returns an error if any definition name is already registered.

func (*Registry[E, A]) Compile

func (r *Registry[E, A]) Compile(opts ...CompileOpt[E, A]) (*Program[E, A], error)

Compile compiles all registered definitions into a Program. Returns an error if there are no definitions or if compilation fails.

func (*Registry[E, A]) Definitions

func (r *Registry[E, A]) Definitions() []Definition

Definitions returns a copy of all registered definitions in insertion order.

func (*Registry[E, A]) Len

func (r *Registry[E, A]) Len() int

Len returns the number of registered definitions.

func (*Registry[E, A]) Remove

func (r *Registry[E, A]) Remove(names ...string)

Remove deletes one or more definitions by name. Unknown names are silently ignored.

func (*Registry[E, A]) Update

func (r *Registry[E, A]) Update(defs ...Definition) error

Update replaces one or more existing definitions. Returns an error if any definition name is not registered.

func (*Registry[E, A]) Upsert

func (r *Registry[E, A]) Upsert(defs ...Definition)

Upsert adds or updates one or more definitions. If a definition name exists, it is replaced. If it does not exist, it is added.

type RuleMeta

type RuleMeta struct {
	Name string
	Tags []string
}

RuleMeta is the struct that expression-based selectors evaluate against. It exposes rule metadata as fields that can be referenced in selector expressions.

type Step

type Step struct {
	// Rule is the name of the rule.
	Rule string

	// Outcome is what happened — Matched, Skipped, Disabled, Excluded,
	// or SkipExpr.
	Outcome Outcome

	// Duration is the expression evaluation time for this rule.
	Duration time.Duration

	// Actions lists which action names this rule referenced.
	// Nil if the rule did not match.
	Actions []string

	// Mode is the evaluation mode used for this rule.
	Mode EvalMode

	// Selector is the expression that excluded the rule, if the
	// outcome was Excluded and an expression selector was active.
	Selector string

	// Skip is the skip expression that fired, if the outcome was
	// SkipExpr.
	Skip string
}

Step records the evaluation of a single rule.

type TestCase

type TestCase[E any, A any] struct {
	// Name identifies this test case.
	Name string

	// Rule is the definition to test.
	Rule Definition

	// Env is the environment to evaluate against.
	Env E

	// Assertions are expressions evaluated against the actions struct
	// after evaluation. All must pass.
	Assertions []string
}

TestCase bundles a rule definition, a test environment, and assertions to run against the result. Use with RunTestCase.

type TestResult

type TestResult[E any, A any] struct {
	// Name is the test case name.
	Name string

	// Evaluation is the full evaluation result.
	Evaluation *Evaluation[E, A]

	// Passed is true if all assertions passed.
	Passed bool

	// Failures lists assertion expressions that failed.
	Failures []string

	// Error is set if compilation or evaluation failed before
	// assertions could run.
	Error error
}

TestResult is the outcome of running a TestCase.

func RunTestCase

func RunTestCase[E any, A any](
	compiler *anyexpr.Compiler[E],
	actions *Actions[E, A],
	tc TestCase[E, A],
) TestResult[E, A]

RunTestCase compiles and evaluates a single rule, then runs all assertions against the result. Returns a TestResult with pass/fail status and any failures.

type Trigger

type Trigger[V action.Valuable] struct {
	Rule  string
	Tags  []string
	Value V
}

Trigger records a single action triggered by a matched rule.

Directories

Path Synopsis
Package dispatch routes evaluation results to named handler functions.
Package dispatch routes evaluation results to named handler functions.

Jump to

Keyboard shortcuts

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