Documentation
¶
Overview ¶
Package expression is what a condition is, and nothing about how one is evaluated.
A rule's condition and a retention rule's condition are the same language (ADR-0009, automation.md §1.2, data-retention.md §2), and this port is what makes that one sentence rather than two implementations that drift. The engine is CEL and lives in an adapter: the core states what it may ask and what it may be told, and never learns that a third-party evaluator exists (rule 1).
The three limits automation.md §1.2 names - a maximum expression length, the evaluator's cost limit, and a timeout per evaluation - are the adapter's to enforce, because they are properties of the engine rather than of the language. What is here is the vocabulary for their refusals, so that a caller can tell "this expression is too expensive" from "this expression is wrong" without reading a sentence somebody wrote.
Index ¶
Constants ¶
const ( // MaxLength is the longest expression the compiler accepts, in bytes. Generous for a condition // somebody writes and far short of a payload somebody pastes. MaxLength = 4096 // Timeout bounds one evaluation. Fifty milliseconds is automation.md §1.2's number, and it is // per expression rather than per rule: a rule with three conditions has three of them. Timeout = 50 * time.Millisecond // CostLimit is the evaluator's own budget, in CEL's cost units. It is the limit that catches // what a timeout catches too late - a deeply nested expression or a long concatenation is // refused before it has been allowed to spend the fifty milliseconds. CostLimit = 1_000_000 // MaxDepth is how deeply an expression may nest, as the parser counts it: a sum of n terms is n // deep, n parentheses n+1, a chain of n calls n+1. The length limit does not bound the work of // type-checking - each `.map` in a chain wraps the type once more, and the checker's cost grows // about with the cube of that depth - so the depth is what keeps a compile in the tens of // milliseconds. The deepest expression this repository ships is 6. MaxDepth = 64 // CompileTimeout bounds one compilation (rule 7). A backstop, not the bound: MaxDepth is what // keeps a compile short, and this catches only a shape nobody has found yet. Generous, because // a run compiles its conditions every time, and a deadline near the real cost would refuse a // legitimate rule on a loaded machine. CompileTimeout = time.Second )
Limits are what the adapter enforces, and they are here so that the numbers automation.md §1.2 names have one home rather than being repeated in a configuration file and a comment.
The values are the document's. An installation does not tune them: they are what makes "no I/O, no loops, terminating" true in practice rather than in principle, and an operator who raised them would be lifting a safety property rather than a performance one.
const ( // CodeSyntax is an expression that is not one. CodeSyntax = "expression.syntax" // CodeUnknownName is an expression naming something the environment does not declare - which is // the check that makes the documented variable list a contract rather than a suggestion. CodeUnknownName = "expression.unknown_name" // CodeTooLong is an expression past MaxLength. CodeTooLong = "expression.too_long" // CodeTooComplex is an expression nested past MaxDepth, or one whose compilation outlasted // CompileTimeout. Not a syntax error: the expression may be well formed. CodeTooComplex = "expression.too_complex" // CodeNotBoolean is a condition that compiles and does not answer true or false. A condition // that returned a number would be a condition whose author believes it filters. CodeNotBoolean = "expression.not_boolean" // CodeNotString is a template that does not produce text. CodeNotString = "expression.not_string" // CodeTooExpensive is an evaluation that exceeded the cost limit. CodeTooExpensive = "expression.too_expensive" // CodeTimedOut is an evaluation that exceeded the timeout. CodeTimedOut = "expression.timed_out" // CodeEvaluationFailed is an evaluation the expression itself stopped - a division by zero, a // key the value does not have, a conversion that cannot be made. It carries `kind`, one of the // Failure values, and never the engine's words: those can quote the data (rule 10). CodeEvaluationFailed = "expression.evaluation_failed" CodeUnavailable = "expression.value_unavailable" )
The message codes this port's refusals carry. Here rather than in each adapter, because a caller switching on them is switching on the port's contract.
Variables ¶
This section is empty.
Functions ¶
This section is empty.
Types ¶
type Activation ¶
type Activation interface {
// Resolve answers one name, or reports that it has no value here. A name the environment
// declared and the activation cannot produce is an evaluation error rather than a silent null:
// a condition that quietly read absent as false would match the opposite of what it says.
Resolve(ctx context.Context, name string) (any, bool, error)
}
Activation is the values an evaluation reads, resolved when the expression asks for them.
A function per name rather than a map of values, because an expression that touches only `event` must cost no reads: a rule engine evaluates every enabled rule against every event, and building `collection`, `hub` and `parent` for each of them would turn one event into four queries per rule. The adapter calls the function at most once per evaluation and remembers the answer, so an expression naming `item` three times reads it once.
type Compiler ¶
type Compiler interface {
// Compile parses and type-checks one expression against the environment it will be evaluated
// in, and reports an error wrapping shared.ErrValidation when the text is not a valid
// expression, names a variable the environment does not declare, is longer than the engine
// accepts, or does not produce what the caller asked for.
//
// The last of those is why `want` is a parameter rather than a property of the result. A
// condition that evaluates to a string and a template that evaluates to a boolean are both
// programs somebody wrote by mistake, and both are silently wrong at run time - the first
// filters nothing and the second renders "true". CEL knows the output type after the check, so
// the mistake is answerable when it is written rather than discoverable when it runs.
//
// The refusal carries the position, because an editor points at a column and a person reads
// one - see Position.
Compile(text string, env Environment, want Result) (Program, error)
}
Compiler turns the text of a condition into something that can be evaluated, or refuses it.
Compiling is separate from evaluating because the two happen at different moments and for different people. A condition is compiled when somebody writes a rule - so that a mistake is answered to its author, with a position, while they are still looking at it - and evaluated later, thousands of times, by nobody. A design that only checked at evaluation would report a typo to a log at three in the morning.
type Environment ¶
type Environment struct {
// Variables are the names and their kinds. A name absent from here is a compile error.
Variables []Variable
}
Environment is the set of names an expression may use, and what each one is.
Declared rather than inferred, so that an expression naming something the engine will not provide fails when it is written rather than when it runs. automation.md §1.2 lists the names; a retention rule uses a smaller set of the same ones, which is what makes this a parameter rather than a constant.
type Failure ¶
type Failure string
Failure is the `kind` parameter of CodeEvaluationFailed: a closed list, so that what reaches a log or a client is a word this system chose and not text the evaluator built from the data.
const ( FailureDivisionByZero Failure = "division_by_zero" FailureModulusByZero Failure = "modulus_by_zero" FailureOverflow Failure = "overflow" FailureNoSuchKey Failure = "no_such_key" FailureNoSuchOverload Failure = "no_such_overload" FailureConversion Failure = "conversion" FailureIndexOutOfRange Failure = "index_out_of_range" FailureOther Failure = "other" )
type Kind ¶
type Kind string
Kind is what an expression may assume about a variable.
Coarse on purpose. CEL can carry a full type graph, and modelling this system's aggregates in it would make every field of every entry part of the expression language's contract - so a rule written today would break when a field is renamed. A map of dynamic values keeps the contract at the names in automation.md §1.2, and a condition reaching into one gets a runtime answer rather than a compile-time promise.
const ( // KindMap is a document an expression reads fields out of: `item`, `event`, `payload`. KindMap Kind = "map" // KindTimestamp is an instant: `now`. KindTimestamp Kind = "timestamp" // KindString, KindInt and KindBool are the scalars. KindString Kind = "string" KindInt Kind = "int" KindBool Kind = "bool" )
type Position ¶
Position is where in the text a refusal is about, as a compiler reports it.
One-based, because that is how an editor counts and how every compiler anybody has used reports it. Zero means the refusal is about the expression as a whole - too long, or empty - rather than about a place in it.
type Program ¶
type Program interface {
// Evaluate answers the expression against one set of values.
//
// The context bounds it. The adapter additionally applies its own timeout and cost limit, and
// exceeding either is an error wrapping shared.ErrUnavailable with a `expression.` detail -
// not a validation error, because the expression was valid when it was written and what failed
// is this evaluation of it.
Evaluate(ctx context.Context, in Activation) (Value, error)
}
Program is a compiled expression, ready to be evaluated as often as necessary.
Reusable and safe for concurrent use: compiling is the expensive half, and an engine that recompiled per event would spend a rule's whole budget on parsing.
type Refusal ¶
type Refusal struct {
// Code is the message code, `expression.` something.
Code string
// Message is the engine's own words, for the log. Never answered to a caller: it is a
// third-party library's English, and no display text comes out of the backend (ADR-0011).
Message string
Position
}
Refusal is a compile error with its position, so that a caller can turn it into a field error pointing at the right part of the right condition.
A type rather than a coded error alone, because the position is structured and a caller has to put it somewhere: an automation rule's conditions render it into `/conditions/0/expr` with the line and column as parameters, and a retention rule renders it into `/condition`.
type Value ¶
type Value struct {
// Bool is the answer a condition gives. False when the expression produced something that is
// not a boolean, which Program.Evaluate reports as an error rather than as false.
Bool bool
// Text is the answer a template gives.
Text string
}
Value is what an evaluation produced.
type Variable ¶
type Variable struct {
Name string
Kind Kind
// Description is what a rule editor shows beside the name. Protocol documentation rather than
// display text, like a use case's summary - it never reaches an end user's screen as prose the
// backend wrote (ADR-0011).
Description string
}
Variable is one name an expression may use.