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 ¶
- Define an actions struct with Action fields and `rule` tags.
- Call DefineActions to reflect over the struct and build the schema.
- Call Compile with rule definitions to produce a Program.
- Call NewEvaluator with the program.
- Call Evaluator.Run to evaluate rules against an environment value.
- 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 ¶
- Variables
- func Check[E any](compiler *anyexpr.Compiler[E], when string) error
- type Action
- type ActionEntry
- type ActionInfo
- type Actions
- type Assertion
- type CompileOpt
- type CompileOpts
- type Definition
- type EvalMode
- type Evaluation
- type EvaluationOpt
- func ExcludeNames(names ...string) EvaluationOpt
- func ExcludeTags(tags ...string) EvaluationOpt
- func MustWithSelector(expr string) EvaluationOpt
- func WithNames(names ...string) EvaluationOpt
- func WithSelector(expr string) (EvaluationOpt, error)
- func WithTags(tags ...string) EvaluationOpt
- func WithTrace(enabled bool) EvaluationOpt
- type Evaluator
- type EvaluatorOpt
- type Outcome
- type Program
- type Registry
- func (r *Registry[E, A]) Add(defs ...Definition) error
- func (r *Registry[E, A]) Compile(opts ...CompileOpt[E, A]) (*Program[E, A], error)
- func (r *Registry[E, A]) Definitions() []Definition
- func (r *Registry[E, A]) Len() int
- func (r *Registry[E, A]) Remove(names ...string)
- func (r *Registry[E, A]) Update(defs ...Definition) error
- func (r *Registry[E, A]) Upsert(defs ...Definition)
- type RuleMeta
- type Step
- type TestCase
- type TestResult
- type Trigger
Examples ¶
Constants ¶
This section is empty.
Variables ¶
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 ¶
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 ¶
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 ¶
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 ¶
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.
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 ¶
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 ¶
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 ¶
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 )
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 ¶
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.
- Matches rules against the environment, collecting matched rule names and which actions they reference.
- If any rules matched, copies the action schema and triggers only the actions that were referenced by matched rules.
- 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 )
type Program ¶
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.
type Registry ¶
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]) Remove ¶
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 ¶
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.