Documentation ¶
Overview ¶
Package indigo provides a rules engine.
Indigo is a rules engine created to enable application developers to build systems whose logic can be controlled by end-users via rules. Rules are expressions (such as "a > b") that are evaluated, and the outcomes used to direct appliation logic.
Indigo does not specify a language for rules, relying instead on a rule evaluator to perform the work. The default rule evaluator (in the cel package) is the Common Expression Language from Google (https://github.com/google/cel-go).
Compilation and Evaluation ¶
Indigo provides methods to compile and evaluate rules. The compilation step gives the evaluator a chance to pre-process the rule, provide feedback on rule correctness, and store an intermediate form of the rule for evaluation efficiency. The evaluation evaluates the rule against input data and provides the output.
Basic Structure ¶
Indigo organizes rules in hierarchies. A parent rule can have 0 or many child rules. You do not have to organize rules in a complex tree; a single parent with 1,000s of child rules is OK. There are 3 main reasons for using a tree to organize rules:
- Allow atomic rule updates (see separate section)
- Use options on the parent rule to control if child rules are evaluated (in effect, child rules "inherit" the parent rule's condition)
- Use options on the parent rule to control which child rules are returned as results (such as returning true or false results, or both)
- Logically separate disparate groups of rules
Rule Ownership ¶
The calling application is responsible for managing the lifecycle of rules, including ensuring concurrency safety. Some things to keep in mind:
- You must not allow changes to a rule during compilation.
- You may not modify the rule after compilation and before evaluation.
- You must not allow changes to a rule during evaluation.
- You should not modify a rule after it's been evaluated and before the results have been consumed.
- A rule must not be a child rule of more than one parent.
Updating Rules ¶
To add or remove rules, you do so by modifying the parent rule's map of Rules
delete(parent.Rules, "child-id-to-delete")
and
myNewRule.Compile(myCompiler) parent.Rules["my-new-rule"] = myNewRule
It is not recommended to update a rule IN PLACE, unless you manage the rule lifecycle beyond evaluation and use of the rule in interpreting the results. Users of your result should expect that the definition of the rule stays constant. Instead, we recommend creating a new rule with a new version number in the ID to separate updates.
WARNING! YOU **MUST** COMPILE THE RULE AFTER MAKING MODIFICATIONS TO THE RULE, INCLUDING THE LIST OF CHILD RULES.
Structuring Rule Hierarchies for Updates ¶
The ability to organize rules in a hierarchy is useful to ensure that rule updates are atomic and consistent.
You should structure the hierarchy so that a rule and its children can be seen as a "transaction" as far as updates are concerned.
In this example, where Indigo is being used to enforce firewall rules, being able to update ALL firewall rules as a group, rather than one by one (where one update may fail) is important.
Firewall Rules (parent) "Deny all traffic" (child 1) "Allow traffic from known_IPs" (child 2)
If the user changes child 1 to be "Allow all traffic" and changes child 2 to "Deny all traffic, except for known_IPs", there's a risk that child 1 is changed first, without the child 2 change happening. This would leave us with this:
Firewall Rules (parent) "Allow all traffic" (child 1) "Allow traffic from known_IPs" (child 2)
This is clearly bad!
Instead of accepting a change to child 1 and child 2 separately, ONLY accept a change to your rule hierarchy for the Firewall Rules parent. That way the update succeeds or fails as a "transaction".
If Firewall Rules is itself a child of a larger set of parent rules, it's recommended to compile the Firewall Rules parent and children BEFORE adding it to its eventual parent. That way you ensure that if compilation of Firewall Rules fails, the "production" firewall rules are still intact.
Example ¶
Example showing basic use of the Indigo rules engine with the CEL evaluator
package main import ( "context" "fmt" "github.com/ezachrisen/indigo" "github.com/ezachrisen/indigo/cel" ) func main() { // Step 1: Create a schema schema := indigo.Schema{ Elements: []indigo.DataElement{ {Name: "message", Type: indigo.String{}}, }, } // Step 2: Create rules rule := indigo.Rule{ ID: "hello_check", Schema: schema, Expr: `message == "hello world"`, ResultType: indigo.Bool{}, } // Step 3: Create an Engine with a CEL evaluator engine := indigo.NewEngine(cel.NewEvaluator()) // Step 4: Compile the rule err := engine.Compile(&rule) if err != nil { fmt.Println(err) return } // The data we wish to evaluate the rule on data := map[string]interface{}{ "message": "hello world", } // Step 5: Evaluate and check the results results, err := engine.Eval(context.Background(), &rule, data) if err != nil { fmt.Println(err) } else { fmt.Println(results.ExpressionPass) } }
Output: true
Index ¶
- func ApplyToRule(r *Rule, f func(r *Rule) error) error
- func DiagnosticsReport(u *Result, data map[string]interface{}) string
- func SortRulesAlpha(rules []*Rule, i, j int) bool
- func SortRulesAlphaDesc(rules []*Rule, i, j int) bool
- type Any
- type Bool
- type CompilationOption
- type Compiler
- type DataElement
- type DefaultEngine
- type Diagnostics
- type Duration
- type Engine
- type EvalOption
- func DiscardFail(k FailAction) EvalOption
- func DiscardPass(b bool) EvalOption
- func ReturnDiagnostics(b bool) EvalOption
- func SortFunc(x func(rules []*Rule, i, j int) bool) EvalOption
- func StopFirstNegativeChild(b bool) EvalOption
- func StopFirstPositiveChild(b bool) EvalOption
- func StopIfParentNegative(b bool) EvalOption
- type EvalOptions
- type Evaluator
- type ExpressionCompiler
- type ExpressionCompilerEvaluator
- type ExpressionEvaluator
- type FailAction
- type Float
- type Int
- type List
- type Map
- type Proto
- type Result
- type Rule
- type Schema
- type String
- type Timestamp
- type Type
- type ValueSource
Examples ¶
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func ApplyToRule ¶ added in v0.6.0
ApplyToRule applies the function f to the rule r and its children recursively.
Example ¶
Demonstrate applying a function to a rule
r := makeRule() err := indigo.ApplyToRule(r, func(r *indigo.Rule) error { fmt.Printf("%s ", r.ID) return nil }) if err != nil { fmt.Println("Failure!") } fmt.Printf("\n") // Output unordered: rule1 B b1 b2 b3 b4 b4-1 b4-2 E e1 e2 e3 D d1 d2 d3
Output:
func DiagnosticsReport ¶ added in v0.6.2
DiagnosticsReport produces an ASCII report of the input rules, input data, the evaluation diagnostics and the results.
func SortRulesAlpha ¶ added in v0.6.9
SortRulesAlpha will sort rules alphabetically by their rule ID
func SortRulesAlphaDesc ¶ added in v0.6.9
SortRulesAlphaDesc will sort rules alphabetically (descending) by their rule ID
Types ¶
type CompilationOption ¶ added in v0.6.0
type CompilationOption func(f *compileOptions)
CompilationOption is a functional option to specify compilation behavior.
func CollectDiagnostics ¶
func CollectDiagnostics(b bool) CompilationOption
CollectDiagnostics instructs the engine and its evaluator to save any intermediate results of compilation in order to provide good diagnostic information after evaluation. Not all evaluators need to have this option set.
func DryRun ¶ added in v0.6.0
func DryRun(b bool) CompilationOption
DryRun specifies to perform all compilation steps, but do not save the results. This is to allow a client to check all rules in a rule tree before committing the actual compilation results to the rule.
type Compiler ¶ added in v0.6.5
type Compiler interface {
Compile(r *Rule, opts ...CompilationOption) error
}
Compiler is the interface that wraps the Compile method. Compile pre-processes the rule recursively using an ExpressionCompiler, which is applied to each rule.
type DataElement ¶
type DataElement struct { // Short, user-friendly name of the variable. This is the name // that will be used in rules to refer to data passed in. // // RESERVED NAMES: // selfKey (see const) Name string `json:"name"` // One of the Type interface defined. Type Type `json:"type"` // Optional description of the type. Description string `json:"description"` }
DataElement defines a named variable in a schema
func (*DataElement) String ¶ added in v0.6.0
func (e *DataElement) String() string
String returns a human-readable representation of the element
type DefaultEngine ¶ added in v0.6.0
type DefaultEngine struct {
// contains filtered or unexported fields
}
DefaultEngine provides an implementation of the Indigo Engine interface to evaluate rules locally.
func NewEngine ¶
func NewEngine(e ExpressionCompilerEvaluator) *DefaultEngine
NewEngine initializes and returns a DefaultEngine.
func (*DefaultEngine) Compile ¶ added in v0.6.0
func (e *DefaultEngine) Compile(r *Rule, opts ...CompilationOption) error
Compile uses the Evaluator's compile method to check the rule and its children, returning any validation errors. Stores a compiled version of the rule in the rule.Program field (if the compiler returns a program).
func (*DefaultEngine) Eval ¶ added in v0.6.0
func (e *DefaultEngine) Eval(ctx context.Context, r *Rule, d map[string]interface{}, opts ...EvalOption) (*Result, error)
Eval evaluates the expression of the rule and its children. It uses the evaluation options of each rule to determine what to do with the results, and whether to proceed evaluating. Options passed to this function will override the options set on the rules. Eval uses the Evaluator provided to the engine to perform the expression evaluation.
type Diagnostics ¶ added in v0.6.2
type Diagnostics struct { Expr string // the part of the rule expression evaluated Interface interface{} Source ValueSource // where the value came from: input data, or evaluted by the engine Line int // the 1-based line number in the original source expression Column int // the 0-based column number in the original source expression Offset int // the 0-based character offset from the start of the original source expression Children []Diagnostics // one child per sub-expression. Each Evaluator may produce different results. }
Diagnostics holds the internal rule-engine intermediate results. Request diagnostics for an evaluation to help understand how the engine reached the final output value. Diagnostics is a nested set of nodes, with 1 root node per rule evaluated. The children represent elements of the expression evaluated.
func (*Diagnostics) String ¶ added in v0.6.2
func (d *Diagnostics) String() string
String produces an ASCII table with human-readable diagnostics.
type Engine ¶
Engine is the interface that groups the Compiler and Evaluator interfaces. An Engine is used to compile and evaluate rules.
type EvalOption ¶
type EvalOption func(f *EvalOptions)
EvalOption is a functional option for specifying how evaluations behave.
func DiscardFail ¶ added in v0.6.0
func DiscardFail(k FailAction) EvalOption
DiscardFail specifies whether to omit failed rules from the results.
func DiscardPass ¶ added in v0.6.0
func DiscardPass(b bool) EvalOption
DiscardPass specifies whether to omit passed rules from the results.
func ReturnDiagnostics ¶
func ReturnDiagnostics(b bool) EvalOption
ReturnDiagnostics specifies that diagnostics should be returned from this evaluation. You must first turn on diagnostic collectionat the engine level when compiling the rule.
func SortFunc ¶
func SortFunc(x func(rules []*Rule, i, j int) bool) EvalOption
SortFunc specifies the function used to sort child rules before evaluation. Sorting is only performed if the evaluation order of the child rules is important (i.e., if an option such as StopFirstNegativeChild is set).
Example ¶
Demonstrate setting the sorting function for all rules to be alphabetical, based on the rule ID
r := makeRule() err := indigo.ApplyToRule(r, func(r *indigo.Rule) error { r.EvalOptions.SortFunc = func(rules []*indigo.Rule, i, j int) bool { return rules[i].ID < rules[j].ID } return nil }) if err != nil { fmt.Println("Failure!") } fmt.Println("Ok")
Output: Ok
func StopFirstNegativeChild ¶
func StopFirstNegativeChild(b bool) EvalOption
StopFirstNegativeChild stops the evaluation of child rules once the first negative child has been found.
func StopFirstPositiveChild ¶
func StopFirstPositiveChild(b bool) EvalOption
StopFirstPositiveChild stops the evaluation of child rules once the first positive child has been found.
func StopIfParentNegative ¶
func StopIfParentNegative(b bool) EvalOption
StopIfParentNegative prevents the evaluation of child rules if the parent rule itself is negative.
type EvalOptions ¶
type EvalOptions struct { // TrueIfAny makes a parent rule Pass = true if any of its child rules are true. // The default behavior is that a rule is only true if all of its child rules are true, and // the parent rule itself is true. // Setting TrueIfAny changes this behvior so that the parent rule is true if at least one of its child rules // are true, and the parent rule itself is true. TrueIfAny bool `json:"true_if_any"` // StopIfParentNegative prevents the evaluation of child rules if the parent's expression is false. // Use case: apply a "global" rule to all the child rules. StopIfParentNegative bool `json:"stop_if_parent_negative"` // Stops the evaluation of child rules when the first positive child is encountered. // Results will be partial. Only the child rules that were evaluated will be in the results. // Use case: role-based access; allow action if any child rule (permission rule) allows it. StopFirstPositiveChild bool `json:"stop_first_positive_child"` // Stops the evaluation of child rules when the first negative child is encountered. // Results will be partial. Only the child rules that were evaluated will be in the results. // Use case: you require ALL child rules to be satisfied. StopFirstNegativeChild bool `json:"stop_first_negative_child"` // Do not return rules that passed // Default: all rules are returned DiscardPass bool `json:"discard_pass"` // Decide what to do to rules that failed // Default: all rules are returned DiscardFail FailAction // Include diagnostic information with the results. // To enable this option, you must first turn on diagnostic // collection at the engine level with the CollectDiagnostics EngineOption. ReturnDiagnostics bool `json:"return_diagnostics"` // Specify the function used to sort the child rules before evaluation. // Useful in scenarios where you are asking the engine to stop evaluating // after either the first negative or first positive child in order to // select a rule with some relative characteristic, such as the "highest // priority rule". // // See the ExampleSortFunc() for an example. // The function returns whether rules[i] < rules[j] for some attribute. // Default: No sort SortFunc func(rules []*Rule, i, j int) bool `json:"-"` // contains filtered or unexported fields }
EvalOptions determines how the engine should treat the results of evaluating a rule.
type Evaluator ¶
type Evaluator interface {
Eval(ctx context.Context, r *Rule, d map[string]interface{}, opts ...EvalOption) (*Result, error)
}
Evaluator is the interface that wraps the Evaluate method. Evaluate tests the rule recursively against the input data using an ExpressionEvaluator, which is applied to each rule.
type ExpressionCompiler ¶ added in v0.6.5
type ExpressionCompiler interface {
Compile(expr string, s Schema, resultType Type, collectDiagnostics, dryRun bool) (interface{}, error)
}
ExpressionCompiler is the interface that wraps the Compile method. Compile pre-processes the expression, returning a compiled version. The Indigo Compiler will store the compiled version, later providing it back to the evaluator.
collectDiagnostics instructs the compiler to generate additional information to help provide diagnostic information on the evaluation later. dryRun performs the compilation, but doesn't store the results, mainly for the purpose of checking rule correctness.
type ExpressionCompilerEvaluator ¶ added in v0.6.5
type ExpressionCompilerEvaluator interface { ExpressionCompiler ExpressionEvaluator }
ExpressionCompilerEvaluator is the interface that groups the ExpressionCompiler and ExpressionEvaluator interfaces for back-end evaluators that require a compile and an evaluate step.
type ExpressionEvaluator ¶ added in v0.6.5
type ExpressionEvaluator interface { Evaluate(data map[string]interface{}, expr string, s Schema, self interface{}, evalData interface{}, resultType Type, returnDiagnostics bool) (interface{}, *Diagnostics, error) }
ExpressionEvaluator is the interface that wraps the Evaluate method. Evaluate tests the rule expression against the data. Returns the result of the evaluation and a string containing diagnostic information. Diagnostic information is only returned if explicitly requested. Evaluate should check the result against the expected resultType and return an error if the result does not match.
type FailAction ¶ added in v0.7.0
type FailAction int
FailAction is used to tell Indigo what to do with the results of rules that did not pass.
const ( // KeepAll means that all results, whether the rule passed or not, // are returned by Indigo after evaluation. KeepAll FailAction = iota // Discard means that the results of rules that failed are not // returned by Indigo after evaluation, though their effect on a parent // rule's pass/fail state is retained. Discard // DiscardOnlyIfExpressionFailed means that the result of a rule will // not be discarded unless it's ExpressionPass result is false. // Even if the rule itself has result of Pass = false, the rule will // be returned in the result. DiscardOnlyIfExpressionFailed )
type Float ¶
type Float struct{}
Float defines an Indigo float type. The implementation of the float (size, precision) depends on the evaluator used.
type Int ¶
type Int struct{}
Int defines an Indigo int type. The exact "Int" implementation and size depends on the evaluator used.
type List ¶
type List struct {
ValueType Type // the type of element stored in the list
}
List defines an Indigo type representing a slice of values
type Map ¶
type Map struct { KeyType Type // the type of the map key ValueType Type // the type of the value stored in the map }
Map defines an Indigo type representing a map of keys and values.
type Proto ¶
Proto defines an Indigo type for a protobuf type.
func (*Proto) ProtoFullName ¶ added in v0.6.5
ProtoFullName uses protocol buffer reflection to obtain the full name of the proto type.
type Result ¶
type Result struct { // The Rule that was evaluated Rule *Rule // Whether the rule is true. // The default is TRUE. // Pass is the result of rolling up all child rules and evaluating the // rule's own expression. All child rules and the rule's expression must be // true for Pass to be true. Pass bool // Whether evaluating the rule expression yielded a TRUE logical value. // The default is TRUE. // The result will not be affected by the results of the child rules. // If no rule expression is supplied for a rule, the result will be TRUE. ExpressionPass bool // The raw result of evaluating the expression. Boolean for logical expressions. // Calculations, object constructions or string manipulations will return the appropriate Go type. // This value is never affected by child rules. Value interface{} // Results of evaluating the child rules. Results map[string]*Result // Diagnostic data; only available if you turn on diagnostics for the evaluation Diagnostics *Diagnostics // The evaluation options used EvalOptions EvalOptions // A list of the rules evaluated, in the order they were evaluated // Only available if you turn on diagnostics for the evaluation // This may be different from the rules represented in Results, if // If we're discarding failed/passed rules, they will not be in the results, // and will not show up in diagnostics, but they will be in this list. RulesEvaluated []*Rule }
Result of evaluating a rule.
type Rule ¶
type Rule struct { // A rule identifer. (required) ID string `json:"id"` // The expression to evaluate (optional) // The expression can return a boolean (true or false), or any // other value the underlying expression engine can produce. // All values are returned in the Results.Value field. // Boolean values are also returned in the results as Pass = true / false // If the expression is blank, the result will be true. Expr string `json:"expr"` // The output type of the expression. Evaluators with the ability to check // whether an expression produces the desired output should return an error // if the expression does not. // If no type is provided, evaluation and compilation will default to Bool ResultType Type `json:"result_type,omitempty"` // The schema describing the data provided in the Evaluate input. (optional) // Some implementations of Evaluator require a schema. Schema Schema `json:"schema,omitempty"` // A reference to an object whose values can be used in the rule expression. // Add the corresponding object in the data with the reserved key name selfKey // (see constants). // Child rules do not inherit the self value. Self interface{} `json:"-"` // A set of child rules. Rules map[string]*Rule `json:"rules,omitempty"` // Reference to intermediate compilation / evaluation data. Program interface{} `json:"-"` // A reference to any object. // Not used by the rules engine. Meta interface{} `json:"-"` // Options determining how the child rules should be handled. EvalOptions EvalOptions `json:"eval_options"` // contains filtered or unexported fields }
A Rule defines logic that can be evaluated by an Evaluator. The logic for evaluation is specified by an expression. A rule can have child rules. Rule options specify to the Evaluator how child rules should be handled. Child rules can in turn have children, enabling you to create a hierarchy of rules.
Example Rule Structures ¶
A hierchy of parent/child rules, combined with evaluation options give many different ways of using the rules engine.
Rule with expression, no child rules: Parent rule expression is evaluated and result returned. Rule with expression and child rules: No options specified - Parent rule xpression is evaluated, and so are all the child rules. - All children and their evaluation results are returned Rule with expression and child rules Option set: StopIfParentNegative - Parent rule expression is evaluated - If the parent rule is a boolean, and it returns FALSE, the children are NOT evaluated - If the parent rule returns TRUE, or if it's not a boolean, all the children and their resulsts are returned
type Schema ¶
type Schema struct { // Identifier for the schema. Useful for the hosting application; not used by Indigo internally. ID string `json:"id,omitempty"` // User-friendly name for the schema Name string `json:"name,omitempty"` // A user-friendly description of the schema Description string `json:"description,omitempty"` // User-defined value Meta interface{} `json:"-"` // List of data elements supported by this schema Elements []DataElement `json:"elements,omitempty"` }
Schema defines the variable names and their data types used in a rule expression. The same keys and types must be supplied in the data map when rules are evaluated.
type Type ¶
type Type interface { // Implements the stringer interface String() string }
Type defines a type in the Indigo type system. These types are used to define schemas and define required evaluation results. Not all implementations of Evaluator support all types.
func ParseType ¶ added in v0.6.0
ParseType parses a string that represents an Indigo type and returns the type. The primitive types are their lower-case names (string, int, duration, etc.) Maps and lists look like Go maps and slices: map[string]float and []string. Proto types look like this: proto(protoname) Before parsing types, protocol buffer types must be available in the global protocol buffer registry, either by importing at compile time or registering them separately from a descriptor file at run time. ParseType returns an error if a protocol buffer type is missing.
Example ¶
Demonstrate parsing indigo types represented as strings
package main import ( "fmt" "github.com/ezachrisen/indigo" ) func main() { // Parse a string to obtain the Indigo type. raw, err := indigo.ParseType("map[int]float") if err != nil { fmt.Println(err) } // Check that we actually got a Map type t, ok := raw.(indigo.Map) if !ok { fmt.Println("Incorrect type!") } fmt.Println(t.KeyType, t.ValueType) }
Output: int float
type ValueSource ¶ added in v0.6.2
type ValueSource int
ValueSource indicates the source of a value within a diagnostic report
const ( // Input means that the value in the diagnostic output came from the // input provided to rule evaluation from the user. // Some rule evaluators may not accurately distinguish between evaluated and input. Input ValueSource = iota // Evaluated means that the value in the diagnostic output was // calculated by the rule evaluator. // Some rule evaluators may not accurately distinguish between evaluated and input. Evaluated )
func (ValueSource) String ¶ added in v0.6.2
func (i ValueSource) String() string
Source Files ¶
Directories ¶
Path | Synopsis |
---|---|
Package cel provides an implementation of the Indigo evaluator and compiler interfaces backed by Google's cel-go rules engine.
|
Package cel provides an implementation of the Indigo evaluator and compiler interfaces backed by Google's cel-go rules engine. |
examples
|
|