163 lines
4.5 KiB
Go
163 lines
4.5 KiB
Go
|
// bexpr is an implementation of a generic boolean expression evaluator.
|
||
|
// The general goal is to be able to evaluate some expression against some
|
||
|
// arbitrary data and get back a boolean of whether or not the data
|
||
|
// was matched by the expression
|
||
|
package bexpr
|
||
|
|
||
|
import (
|
||
|
"fmt"
|
||
|
"reflect"
|
||
|
)
|
||
|
|
||
|
const (
|
||
|
defaultMaxMatches = 32
|
||
|
defaultMaxRawValueLength = 512
|
||
|
)
|
||
|
|
||
|
// MatchExpressionEvaluator is the interface to implement to provide custom evaluation
|
||
|
// logic for a selector. This could be used to enable synthetic fields or other
|
||
|
// more complex logic that the default behavior does not support
|
||
|
type MatchExpressionEvaluator interface {
|
||
|
// FieldConfigurations returns the configuration for this field and any subfields
|
||
|
// it may have. It must be valid to call this method on nil.
|
||
|
FieldConfigurations() FieldConfigurations
|
||
|
|
||
|
// EvaluateMatch returns whether there was a match or not. We are not also
|
||
|
// expecting any errors because all the validation bits are handled
|
||
|
// during parsing and cross checking against the output of FieldConfigurations.
|
||
|
EvaluateMatch(sel Selector, op MatchOperator, value interface{}) (bool, error)
|
||
|
}
|
||
|
|
||
|
type Evaluator struct {
|
||
|
// The syntax tree
|
||
|
ast Expression
|
||
|
|
||
|
// A few configurations for extra validation of the AST
|
||
|
config EvaluatorConfig
|
||
|
|
||
|
// Once an expression has been run against a particular data type it cannot be executed
|
||
|
// against a different data type. Some coerced value memoization occurs which would
|
||
|
// be invalid against other data types.
|
||
|
boundType reflect.Type
|
||
|
|
||
|
// The field configuration of the boundType
|
||
|
fields FieldConfigurations
|
||
|
}
|
||
|
|
||
|
// Extra configuration used to perform further validation on a parsed
|
||
|
// expression and to aid in the evaluation process
|
||
|
type EvaluatorConfig struct {
|
||
|
// Maximum number of matching expressions allowed. 0 means unlimited
|
||
|
// This does not include and, or and not expressions within the AST
|
||
|
MaxMatches int
|
||
|
// Maximum length of raw values. 0 means unlimited
|
||
|
MaxRawValueLength int
|
||
|
// The Registry to use for validating expressions for a data type
|
||
|
// If nil the `DefaultRegistry` will be used. To disable using a
|
||
|
// registry all together you can set this to `NilRegistry`
|
||
|
Registry Registry
|
||
|
}
|
||
|
|
||
|
func CreateEvaluator(expression string, config *EvaluatorConfig) (*Evaluator, error) {
|
||
|
return CreateEvaluatorForType(expression, config, nil)
|
||
|
}
|
||
|
|
||
|
func CreateEvaluatorForType(expression string, config *EvaluatorConfig, dataType interface{}) (*Evaluator, error) {
|
||
|
ast, err := Parse("", []byte(expression))
|
||
|
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
|
||
|
eval := &Evaluator{ast: ast.(Expression)}
|
||
|
|
||
|
if config == nil {
|
||
|
config = &eval.config
|
||
|
}
|
||
|
err = eval.validate(config, dataType, true)
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
|
||
|
return eval, nil
|
||
|
}
|
||
|
|
||
|
func (eval *Evaluator) Evaluate(datum interface{}) (bool, error) {
|
||
|
if eval.fields == nil {
|
||
|
err := eval.validate(&eval.config, datum, true)
|
||
|
if err != nil {
|
||
|
return false, err
|
||
|
}
|
||
|
} else if reflect.TypeOf(datum) != eval.boundType {
|
||
|
return false, fmt.Errorf("This evaluator can only be used to evaluate matches against %s", eval.boundType)
|
||
|
}
|
||
|
|
||
|
return evaluate(eval.ast, datum, eval.fields)
|
||
|
}
|
||
|
|
||
|
func (eval *Evaluator) validate(config *EvaluatorConfig, dataType interface{}, updateEvaluator bool) error {
|
||
|
if config == nil {
|
||
|
return fmt.Errorf("Invalid config")
|
||
|
}
|
||
|
|
||
|
var fields FieldConfigurations
|
||
|
var err error
|
||
|
var rtype reflect.Type
|
||
|
if dataType != nil {
|
||
|
registry := DefaultRegistry
|
||
|
if config.Registry != nil {
|
||
|
registry = config.Registry
|
||
|
}
|
||
|
|
||
|
switch t := dataType.(type) {
|
||
|
case reflect.Type:
|
||
|
rtype = t
|
||
|
case *reflect.Type:
|
||
|
rtype = *t
|
||
|
case reflect.Value:
|
||
|
rtype = t.Type()
|
||
|
case *reflect.Value:
|
||
|
rtype = t.Type()
|
||
|
default:
|
||
|
rtype = reflect.TypeOf(dataType)
|
||
|
}
|
||
|
|
||
|
fields, err = registry.GetFieldConfigurations(rtype)
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
|
||
|
if len(fields) < 1 {
|
||
|
return fmt.Errorf("Data type %s has no evaluatable fields", rtype.String())
|
||
|
}
|
||
|
}
|
||
|
|
||
|
maxMatches := config.MaxMatches
|
||
|
if maxMatches == 0 {
|
||
|
maxMatches = defaultMaxMatches
|
||
|
}
|
||
|
|
||
|
maxRawValueLength := config.MaxRawValueLength
|
||
|
if maxRawValueLength == 0 {
|
||
|
maxRawValueLength = defaultMaxRawValueLength
|
||
|
}
|
||
|
|
||
|
err = validate(eval.ast, fields, config.MaxMatches, config.MaxRawValueLength)
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
|
||
|
if updateEvaluator {
|
||
|
eval.config = *config
|
||
|
eval.fields = fields
|
||
|
eval.boundType = rtype
|
||
|
}
|
||
|
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
// Validates an existing expression against a possibly different configuration
|
||
|
func (eval *Evaluator) Validate(config *EvaluatorConfig, dataType interface{}) error {
|
||
|
return eval.validate(config, dataType, false)
|
||
|
}
|