Documentation
¶
Overview ¶
Package expression - Expression evaluator implementation
Package expression — Dotted-path access into MessageFields payloads.
Both `$message.*` substitution (in processor/rule/message_substitution.go) and `$message.*` condition evaluation (Evaluator.EvaluateWithStateAndMessage) walk the same map shape via the same dotted-path rules. This helper is the single source of truth — shared via the expression package so the rule package can import it without introducing a cycle.
Package expression - Regex pattern caching using framework's cache package ¶
Package expression - Simple DSL for rule condition evaluation
Index ¶
- Constants
- func ExtractMessageValue(data MessageFields, path string) (any, bool)
- func RegisteredOperators() map[string]struct{}
- type ConditionExpression
- type EvaluationError
- type Evaluator
- func (e *Evaluator) Distance(entity1, entity2 *gtypes.EntityState) (float64, error)
- func (e *Evaluator) Evaluate(entityState *gtypes.EntityState, expr LogicalExpression) (bool, error)
- func (e *Evaluator) EvaluateWithStateAndMessage(entityState *gtypes.EntityState, stateFields StateFields, ...) (bool, error)
- func (e *Evaluator) GetOutgoing(entity *gtypes.EntityState, predicate string) []string
- func (e *Evaluator) HasTriple(entity *gtypes.EntityState, predicate string) bool
- type FieldType
- type LogicalExpression
- type MessageFields
- type OperatorFunc
- type StateFields
- type TypeDetector
Constants ¶
const ( // Numeric operators OpEqual = "eq" OpNotEqual = "ne" OpLessThan = "lt" OpLessThanEqual = "lte" OpGreaterThan = "gt" OpGreaterThanEqual = "gte" OpBetween = "between" // String operators OpContains = "contains" OpStartsWith = "starts_with" OpEndsWith = "ends_with" OpRegexMatch = "regex" // Array operators OpIn = "in" OpNotIn = "not_in" OpLengthEq = "length_eq" OpLengthGt = "length_gt" OpLengthLt = "length_lt" OpArrayContains = "array_contains" // State transition operator OpTransition = "transition" )
Supported operators by field type
const ( LogicAnd = "and" LogicOr = "or" )
Logic operators
Variables ¶
This section is empty.
Functions ¶
func ExtractMessageValue ¶
func ExtractMessageValue(data MessageFields, path string) (any, bool)
ExtractMessageValue walks data along a dotted path. Returns the terminal value and true on success; the second return is false (and the caller must treat the result as "field not present") when:
- any non-terminal segment is missing from the current map, OR
- any non-terminal segment resolves to a non-map value (can't descend further), OR
- the terminal segment is missing.
A nil terminal value is considered a successful resolution (the JSON `null` distinct from "missing"). Callers can render `<nil>` via fmt; the surfacing safety net is the regex-leftover warning at the substitution layer, or the `Required` flag at the condition layer.
The path is the bare dotted form (no `$message.` prefix). Callers trim the prefix before calling.
func RegisteredOperators ¶
func RegisteredOperators() map[string]struct{}
RegisteredOperators returns the set of operator names a freshly- constructed Evaluator knows how to evaluate, INCLUDING the special-cased OpTransition (which evaluateConditionWithStateAndMessage dispatches outside the regular operator map). Intended for the rule-config validator at load time so the validator's "is this operator name supported" check derives from the evaluator's actual wiring rather than a hardcoded list — #147 surfaced the split-brain class where length_eq / length_gt / length_lt / array_contains were validator-listed but evaluator-unregistered, causing rules to pass config validation and fail at fire time with "unsupported operator". Deriving the validator's set from the same construction path the evaluator uses prevents the class structurally.
The set is computed by spinning up a throwaway evaluator and reading its operator map. Cheap (no I/O); typical validator paths call this at boot, not per-rule.
Types ¶
type ConditionExpression ¶
type ConditionExpression struct {
Field string `json:"field"` // Predicate field (e.g., "robotics.battery.level")
Operator string `json:"operator"` // Comparison operator (e.g., "lte", "eq", "contains")
Value interface{} `json:"value"` // Comparison value (20.0, "active", true)
// Required: when true, a missing field causes evaluation to error
// rather than silently match false. Scalar operators (eq, lt,
// contains, ...) honour this strictly: missing predicate +
// Required=true → EvaluationError.
//
// Array operators (length_eq, length_gt, length_lt, array_contains
// — see isArrayOperator) DELIBERATELY DIVERGE: a missing predicate
// resolves to an empty array (`[]interface{}{}`) and the operator
// runs against it. length_eq with value=0 then matches; length_gt
// with value=N>0 doesn't; array_contains returns false. This is
// the intended semantic for the #147 / ADR-046 Phase 1 counter
// pattern, where a join rule wanting "fire before any children
// complete" is a legitimate authoring intent. Authors who want
// "this predicate MUST exist on the entity" should pair the array
// operator with a separate scalar eq/exists condition over the
// same field.
Required bool `json:"required"`
From interface{} `json:"from,omitempty"` // For transition operator: allowed previous value(s)
}
ConditionExpression represents a single field/operator/value condition
type EvaluationError ¶
EvaluationError represents an error during expression evaluation
func (*EvaluationError) Error ¶
func (e *EvaluationError) Error() string
func (*EvaluationError) Unwrap ¶
func (e *EvaluationError) Unwrap() error
type Evaluator ¶
type Evaluator struct {
// contains filtered or unexported fields
}
Evaluator processes expressions against entity state
func NewExpressionEvaluator ¶
func NewExpressionEvaluator() *Evaluator
NewExpressionEvaluator creates a new expression evaluator with all supported operators
func (*Evaluator) Distance ¶
func (e *Evaluator) Distance(entity1, entity2 *gtypes.EntityState) (float64, error)
Distance calculates the great-circle distance between two entities in meters using the Haversine formula. Returns error if either entity lacks position data. Position is now extracted from geo.location.* triples (single source of truth).
func (*Evaluator) Evaluate ¶
func (e *Evaluator) Evaluate(entityState *gtypes.EntityState, expr LogicalExpression) (bool, error)
Evaluate evaluates a logical expression against an entity state. Equivalent to EvaluateWithStateAndMessage with nil stateFields and nil messageFields — entity triples are the only resolution source.
func (*Evaluator) EvaluateWithStateAndMessage ¶
func (e *Evaluator) EvaluateWithStateAndMessage(entityState *gtypes.EntityState, stateFields StateFields, messageFields MessageFields, expr LogicalExpression) (bool, error)
EvaluateWithStateAndMessage evaluates a logical expression against the full field-resolution surface: entity triples, rule match-state (`$state.*`/`$prev.*`), and the inbound message payload (`$message.*`).
Field resolution precedence per condition:
- `$state.*` / `$prev.*` → stateFields map
- `$message.<dotted.path>` → messageFields via deep-walk
- Bare field name → entity triples first (when entity is non-nil); falls through to messageFields if not present on the entity. When entity is nil (message-path rules), bare names resolve from messageFields directly.
This is the single condition evaluator for both rule-level conditions (where the rule fires on message-path or entity-state events) and action-level `when` guards. See ADR-041 for the unification rationale.
`$message.command` is the recommended form when both rule-level and when-clause use the same field, because it makes the resolution source explicit. Bare `command` works equivalently on message-path rules but implicitly prefers entity triples on entity-path rules.
func (*Evaluator) GetOutgoing ¶
func (e *Evaluator) GetOutgoing(entity *gtypes.EntityState, predicate string) []string
GetOutgoing returns a list of entity IDs for outgoing relationships with the given predicate. Only returns valid entity IDs (4-part dotted notation), not literal values. Returns empty slice if entity is nil or no relationships found.
type FieldType ¶
type FieldType int
FieldType represents the detected type of a field
const ( // FieldTypeUnknown represents an unknown or unsupported field type FieldTypeUnknown FieldType = iota // FieldTypeFloat64 represents a floating point number field FieldTypeFloat64 // FieldTypeString represents a string field FieldTypeString // FieldTypeBool represents a boolean field FieldTypeBool // FieldTypeArray represents an array field FieldTypeArray )
type LogicalExpression ¶
type LogicalExpression struct {
Conditions []ConditionExpression `json:"conditions"`
Logic string `json:"logic"` // "and", "or"
}
LogicalExpression combines multiple conditions with logic operators
type MessageFields ¶
MessageFields carries the inbound NATS message payload for `$message.*` pseudo-field resolution in condition expressions. The map shape is `map[string]any` (matching `GenericJSONPayload.Data` and what `ExecutionContext.MessageData` carries through the rule pipeline).
Deep-path access is supported: `$message.tool_args.command` walks the map along the dotted path. Bare field names (e.g. `command`) also resolve here when entity state is nil or when the field isn't present as an entity triple — see Evaluator.EvaluateWithStateAndMessage for the full precedence rule.
Empty or nil maps cause `$message.*` and bare-name message lookups to return "not found" — same surfacing behaviour as missing entity triples (condition fails when Required=false, errors when Required=true).
type OperatorFunc ¶
OperatorFunc defines the signature for operator implementations
type StateFields ¶
type StateFields map[string]interface{}
StateFields provides rule match state values for $state.* pseudo-field resolution in condition expressions. Keys are the full field names (e.g., "$state.iteration"). This avoids circular dependencies between the expression and rule packages.
type TypeDetector ¶
type TypeDetector interface {
GetFieldValue(entityState *gtypes.EntityState, field string) (value interface{}, exists bool, err error)
// GetFieldValuesAll returns every triple Object matching the given
// predicate as a []interface{}. Used by array operators (length_eq,
// length_gt, length_lt, array_contains — see isArrayOperator) where
// the natural semantics are "how many triples carry this predicate"
// rather than "what is the first triple's Object". Returns
// (nil, false) when no triple matches. #147 / ADR-046 Phase 1 join
// gap: the counter pattern stamps N triples with the same
// predicate keyed by child ID; the join rule counts them via
// length_eq on this multi-valued path.
GetFieldValuesAll(entityState *gtypes.EntityState, field string) (values []interface{}, exists bool, err error)
DetectFieldType(value interface{}) FieldType
}
TypeDetector determines field type and extracts values from entity state