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]any{
"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]any) string
- func SortRulesAlpha(rules []*Rule, i, j int) bool
- func SortRulesAlphaDesc(rules []*Rule, i, j int) bool
- func UniqueID() string
- 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 Parallel(batchSize, maxParallel int) 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 Mutation
- type MutationStats
- type ParallelConfig
- type Proto
- type Result
- type Rule
- func (r *Rule) Add(rr *Rule) error
- func (r *Rule) BuildShards() error
- func (r *Rule) Count() (rules, shards int)
- func (r *Rule) FindParent(id string) *Rule
- func (r *Rule) FindRule(id string) (rule *Rule, ancestors []*Rule)
- func (r *Rule) Path(id string) []*Rule
- func (r *Rule) String() string
- func (r *Rule) Tree() string
- type Schema
- type String
- type Timestamp
- type Type
- type ValueSource
- type Vault
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
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
func UniqueID ¶ added in v0.8.0
func UniqueID() string
From the Firestore Go Client: https://github.com/googleapis/google-cloud-go/blob/d14ee26877efc7c87f94a1acddff415628781b8d/firestore/collref.go
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]any, 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 any
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 Parallel ¶ added in v0.8.0
func Parallel(batchSize, maxParallel int) EvalOption
Parallel enables parallel evaluation of child rules with the specified batch size and maximum parallel goroutines.
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:"-"`
// Parallel controls parallel evaluation of child rules with batching.
Parallel ParallelConfig `json:"parallel"`
// 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]any, 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) (any, 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]any, expr string, s Schema,
self any, evalData any, resultType Type, returnDiagnostics bool) (any, *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 Mutation ¶ added in v0.8.0
type Mutation struct {
// contains filtered or unexported fields
}
Mutation defines a single change to the the vault Use the Add, Update and Delete functions to create a Mutation.
func Add ¶ added in v0.8.0
Add returns a mutation that adds the rule to the parent. The parent must exist. If the Vault rule is sharded the rule will be placed in the parent rule determined by the sharding rules instead of the parent.
If the rule already exists, it will be replaced.
func Delete ¶ added in v0.8.0
Delete deletes the rule with the id. If the rule does not exist, the operation is skipped with no error.
func LastUpdate ¶ added in v0.8.0
LastUpdate updates the LastUpdate timestamp in the Vault
func Move ¶ added in v0.8.0
Move moves the rule with the id to the newParent. The newParent must exist
func Update ¶ added in v0.8.0
Update returns a mutation that replaces the rule with the id r.ID with the new rule. Keep in mind that this not only updates the rule's fields, such as expression or meta, but also all of its children.
func Upsert ¶ added in v0.8.1
Upsert returns a mutation that either replaces the rule with the id r.ID with the new rule, or adds it if it doesn't already exists. If the vault is sharded, the updated rule may be moved to a different shard, and an added rule will be placed in the appropriate shard.
If you are not using sharding, use Add and Update instead for the added control over where the rule is placed.
type MutationStats ¶ added in v0.8.4
MutationStats contains counts of changes made to a Vault via [Mutate]. The counts reflect the operations performed, not the number of rules affected. For example, a Delete operation that deletes a rule that has 1,000 children will be counted as a single delete.
Upserts will be reported as either Added or Updated.
Updates that result in moving a rule to a different shard will be counted in the Updated count and the ChangedShards count.
Updates to the Vault lastUpdate timestamp are not counted.
type ParallelConfig ¶ added in v0.8.0
type ParallelConfig struct {
BatchSize int `json:"batch_size"`
MaxParallel int `json:"max_parallel"`
}
ParallelConfig enables parallel evaluation of child rules with batching. BatchSize controls how many rules are evaluated concurrently in each batch. MaxParallel limits the maximum number of goroutines used for evaluation. If BatchSize is 0, all rules are processed in a single batch. If MaxParallel is 0, no limit is imposed on parallel goroutines.
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 any
// 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
// Count of the number of rules whose expression was evaluated
EvaluationCount int
}
Result of evaluating a rule.
func (*Result) Flat ¶ added in v0.8.0
Flat returns all results from r as a single iterable list, without rule hierarchy. Skips all shard rules, but results from the shards is included. This is useful when you only care about which rules passed, and you don't care about the hierarchy of parent/child rules.
func (*Result) String ¶ added in v0.6.0
String produces a list of rules (including child rules) executed and the result of the evaluation.
func (*Result) Summary ¶ added in v0.6.7
String produces a list of rules (including child rules) executed and the result of the evaluation.
func (*Result) Unshard ¶ added in v0.8.0
Unshard reorganizes the results into the "original" structure of the rule without shards applied. If you defined shards and applied them with BuildShards, results will come organized by shard. This is normally not desired, since the client will need to know about the shard structure, which typically is used for performance reasons. This function removes the sharding from the results, returning a structure that the client is familiar with.
type Rule ¶
type Rule struct {
// A rule identifier. (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 set of child rules.
Rules map[string]*Rule `json:"rules,omitempty"`
// Reference to intermediate compilation / evaluation data.
Program any `json:"-"`
// A reference to any object.
// Not used by the rules engine.
Meta any `json:"-"`
// 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 any `json:"-"`
// Options determining how the child rules should be handled.
EvalOptions EvalOptions `json:"eval_options"`
// Shards is a list of rules that define a sharding strategy for the rule's children.
// If a rule has many rules, and some of them share a common characteristic, such as applying to
// cars registered in specific states, or to students in a particular status, the child rules can be
// grouped into a set of "shards", where all the children share the grouping criteria. For example,
// if you have 1000 rules that apply only to students in the "Admitted" status and 600 rules that apply only to
// students in the "Enrolled" status, you can create two shards, one for each of the statuses. By calling
// BuildShards on the rule, the child rules will be re-arranged into shards using the rules you provided.
// See the shard tests and the [BuildShards] function for more information.
Shards []*Rule
// 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
func NewRule ¶ added in v0.6.0
NewRule initializes a rule with the ID and rule expression. The ID and expression can be empty.
func (*Rule) BuildShards ¶ added in v0.8.0
BuildShards uses r's shard rules (if any) to sort child rules into their respective shards.
Shard definitions can be recursive, i.e., rule A can have shards X and Y, and X can be further subdivided into X1 and X2.
Recursively applies shard rules on r's children.
Since rules stored in a Vault should not be modified outside the vault, DO NOT call BuildShards on a rule inside the vault. You may of course call BuildShards before adding a rule to the vault, but the Vault will automatically call BuildShards on the rule you pass to NewVault.
When you apply mutations to the rule in the vault, the vault will automatically apply sharding specifications and place rules in the right shard.
If you call BuildShards multiple times on a rule, no changes will be made to the shards. To reshard the rule, build it from scratch.
func (*Rule) Count ¶ added in v0.8.3
Count returns the number of rules in r, including r and its children recursively, and the number of shards, if any. rules + shards = the total number of rules in r.
func (*Rule) FindParent ¶ added in v0.8.0
FindParent returns the parent of the rule with the id
func (*Rule) FindRule ¶ added in v0.8.0
FindRule returns the rule with the id in the rule or any of its children recursively, and a list of the parent rules in order, starting with the root of the rule tree and ending with the immediate parent of the rule with the id.
func (*Rule) Path ¶ added in v0.8.0
Path returns all of the ancestors of the rule with the ID, starting with the root of the rule tree.
func (*Rule) String ¶ added in v0.6.0
String returns a list of all the rules in hierarchy, with child rules sorted in evaluation order.
func (*Rule) Tree ¶ added in v0.8.0
Tree returns a tree representation of the rule hierarchy showing only rule IDs. The tree uses box-drawing characters to visualize parent-child relationships. Recursion is limited to a maximum depth of 20 levels.
Example output:
root
├── child_1
│ ├── grandchild_1
│ └── grandchild_2
└── child_2
└── grandchild_3
Example ¶
ExampleRule_Tree demonstrates the Tree method which generates a visual tree representation of a rule hierarchy using box-drawing characters.
package main
import (
"fmt"
"github.com/ezachrisen/indigo"
"github.com/ezachrisen/indigo/cel"
)
func main() {
// Create a root rule
root := indigo.NewRule("product_validation", "true")
root.EvalOptions.SortFunc = indigo.SortRulesAlpha
// Level 1: Create main category rules
pricing := indigo.NewRule("pricing_rules", "")
pricing.EvalOptions.SortFunc = indigo.SortRulesAlpha
inventory := indigo.NewRule("inventory_rules", "")
inventory.EvalOptions.SortFunc = indigo.SortRulesAlpha
quality := indigo.NewRule("quality_rules", "")
quality.EvalOptions.SortFunc = indigo.SortRulesAlpha
root.Rules["pricing_rules"] = pricing
root.Rules["inventory_rules"] = inventory
root.Rules["quality_rules"] = quality
// Level 2: Add sub-rules under pricing
discount := indigo.NewRule("discount_validation", "")
discount.EvalOptions.SortFunc = indigo.SortRulesAlpha
priceRange := indigo.NewRule("price_range_check", "")
pricing.Rules["discount_validation"] = discount
pricing.Rules["price_range_check"] = priceRange
// Level 2: Add sub-rules under inventory
stockAlert := indigo.NewRule("stock_alert", "")
inventory.Rules["stock_alert"] = stockAlert
inventory.EvalOptions.SortFunc = indigo.SortRulesAlpha
// Level 2: Add sub-rules under quality
reviewCount := indigo.NewRule("review_count", "")
verifiedReviews := indigo.NewRule("verified_reviews", "")
quality.Rules["review_count"] = reviewCount
quality.Rules["verified_reviews"] = verifiedReviews
// Level 3: Add great-grandchildren under discount_validation
minimumDiscount := indigo.NewRule("minimum_discount", "")
maximumDiscount := indigo.NewRule("maximum_discount", "")
discount.Rules["minimum_discount"] = minimumDiscount
discount.Rules["maximum_discount"] = maximumDiscount
e := indigo.NewEngine(cel.NewEvaluator())
err := e.Compile(root)
if err != nil {
fmt.Println("Error: ", err)
return
}
// Generate and print the tree
tree := root.Tree()
fmt.Println(tree)
}
Output: product_validation ├── inventory_rules │ └── stock_alert ├── pricing_rules │ ├── discount_validation │ │ ├── maximum_discount │ │ └── minimum_discount │ └── price_range_check └── quality_rules ├── review_count └── verified_reviews
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 any `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
type Vault ¶ added in v0.8.0
type Vault struct {
// contains filtered or unexported fields
}
Vault provides lock-free, hot-reloadable, hierarchical rule management with full support for add, update, delete, and move operations.
It provides safe access to an immutable rule which can be retrieved with the Rule function for evaluation or inspection.
The client can submit mutations to the Vault via the [Mutate] method.
Clients do not see the updated rules until all mutations submitted to [Mutate] succeed; if one fails no updates are applied.
To use the Vault, your rules must have globally unique IDs.
Clients should NOT modify rules outside the vault. Any rule added to the vault or retrieved from it should be considered read-only.
func NewVault ¶ added in v0.8.0
func NewVault(root *Rule, opts ...CompilationOption) (*Vault, error)
NewVault creates a new Vault with an nitial rule tree.
The Vault will call BuildShards on initialRoot if you provide it. When you apply mutations to the rule in the vault, the vault will automatically apply sharding.
func (*Vault) ImmutableRule ¶ added in v0.8.0
Rule returns the current immutable root rule for evaluation or inspection.
func (*Vault) LastUpdate ¶ added in v0.8.0
LastUpdate returns the last time the Vault was updated via a LastUpdate mutation
func (*Vault) Mutate ¶ added in v0.8.0
func (v *Vault) Mutate(mutations ...Mutation) (stats MutationStats, err error)
Mutate makes the changes to the rule stored in the Vault, applying the mutations in sequence. At the end of all mutations, the resulting root rule becomes the new active rule in the vault, and can be retrieved with the [ImmutableRule] function.
Clients do not see the updated rules until all mutations submitted to [Mutate] succeed; if any mutation fails no updates are applied. This ensures that clients see a consistent rule set without partial updates.
Mutations incur memory cost equal to the total size of the ancestor rules of the rule being mutated; only children who are affected are copied.
In this examople, if grandhchild_2 is modified, the root and the child_1 rules will be copied, and their child rule maps cloned (though the rules in the maps, such as child_2 and grandchild_2, grandchild_3 will not be cloned.)
root <-- Cloned
├── child_1 <-- Cloned
│ ├── grandchild_1
│ └── grandchild_2 <-- Inserted into the cloned child_1 Rules map
└── child_2
└── grandchild_3
Moves incur the cost in both the origin and destination ancestors.
To clear a vault, replace the root rule with a new, empty rule as the first mutation in the mutations variadic, followed by the rules you want in the new root.
Vaults support sharding, and will automatically place mutated rules in the correct shard.
Returns counts of operations performed on the Vault in the MutationStats.