indigo

package module
v0.8.4 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Feb 25, 2026 License: LGPL-3.0 Imports: 19 Imported by: 0

README

Go Reference

Indigo

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 application logic. Indigo does not itself provide a language for expressions, relying instead on a backend compiler (interface ExpressionCompiler) and evaluator (interface ExpressionEvaluator) to provide that. You can create your own backend evaluator, or use the default one, Google's Common Expression Language, CEL.

Development Status

Indigo is pre-release and there may be breaking changes between releases. When 1.0.0 is released, the API will be frozen.

Google's Common Expression Language

Indigo supports the CEL language as an evaluation back-end. See https://github.com/google/cel-go and https://opensource.google/projects/cel for more information about CEL and the Go implementation of it. CEL is a rich expression language, providing not only boolean expression output, but also calculations and object construction. Particularly powerful is the "native" use of protocol buffers in CEL's schema, rule language and return types.

Usage

The example ExampleHelloWorld shows basic usage of the engine.


func ExampleHelloWorld() {

	// 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"`,
	}

	// Step 3: Create Indigo and give it an evaluator
	// In this case, CEL
	evaluator := cel.NewEvaluator()
	engine := indigo.NewEngine(evaluator)

	// Step 4: Compile the rule
	err := engine.Compile(&rule)
	if err != nil {
		fmt.Println(err)
		return
	}

	data := map[string]interface{}{
		"message": "hello world",
	}

	// Step 5: Evaluate and check the results
	results, err := engine.Eval(context.Background(), &rule, data)
	fmt.Println(results.ExpressionPass)
	// Output: true
}

For More Information

The Indigo Guide gives a deeper guide to using Indigo.

The use cases page gives examples of ways you can structure rules in Indigo.

The documentation for the CEL package gives examples of using the CEL expression language.

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:

  1. Allow atomic rule updates (see separate section)
  2. Use options on the parent rule to control if child rules are evaluated (in effect, child rules "inherit" the parent rule's condition)
  3. Use options on the parent rule to control which child rules are returned as results (such as returning true or false results, or both)
  4. 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:

  1. You must not allow changes to a rule during compilation.
  2. You may not modify the rule after compilation and before evaluation.
  3. You must not allow changes to a rule during evaluation.
  4. You should not modify a rule after it's been evaluated and before the results have been consumed.
  5. 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

Examples

Constants

This section is empty.

Variables

This section is empty.

Functions

func ApplyToRule added in v0.6.0

func ApplyToRule(r *Rule, f func(r *Rule) error) error

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

func DiagnosticsReport(u *Result, data map[string]any) string

DiagnosticsReport produces an ASCII report of the input rules, input data, the evaluation diagnostics and the results.

func SortRulesAlpha added in v0.6.9

func SortRulesAlpha(rules []*Rule, i, j int) bool

SortRulesAlpha will sort rules alphabetically by their rule ID

func SortRulesAlphaDesc added in v0.6.9

func SortRulesAlphaDesc(rules []*Rule, i, j int) bool

SortRulesAlphaDesc will sort rules alphabetically (descending) by their rule ID

Types

type Any

type Any struct{}

Any defines an Indigo type for an "undefined" or unspecified type.

func (Any) String

func (Any) String() string

type Bool

type Bool struct{}

Bool defines an Indigo type for true/false.

func (Bool) String

func (Bool) String() string

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

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 Duration

type Duration struct{}

Duration defines an Indigo type for the time.Duration type.

func (Duration) String

func (Duration) String() string

type Engine

type Engine interface {
	Compiler
	Evaluator
}

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.

func (Float) String

func (Float) String() string

type Int

type Int struct{}

Int defines an Indigo int type. The exact "Int" implementation and size depends on the evaluator used.

func (Int) String

func (Int) String() string

String Methods

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

func (List) String

func (t List) String() string

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.

func (Map) String

func (t Map) String() string

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

func Add(r *Rule, parent string) Mutation

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

func Delete(id string) Mutation

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

func LastUpdate(t time.Time) Mutation

LastUpdate updates the LastUpdate timestamp in the Vault

func Move added in v0.8.0

func Move(id string, newParent string) Mutation

Move moves the rule with the id to the newParent. The newParent must exist

func Update added in v0.8.0

func Update(r *Rule) Mutation

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

func Upsert(r *Rule) Mutation

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

type MutationStats struct {
	Added         int
	Moved         int
	Updated       int
	Deleted       int
	ChangedShards int
}

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

type Proto struct {
	Message proto.Message // an instance of the proto message
}

Proto defines an Indigo type for a protobuf type.

func (*Proto) ProtoFullName added in v0.6.5

func (p *Proto) ProtoFullName() (string, error)

ProtoFullName uses protocol buffer reflection to obtain the full name of the proto type.

func (Proto) String

func (p Proto) String() string

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

func (r *Result) Flat() iter.Seq[*Result]

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

func (u *Result) String() string

String produces a list of rules (including child rules) executed and the result of the evaluation.

func (*Result) Summary added in v0.6.7

func (u *Result) Summary() string

String produces a list of rules (including child rules) executed and the result of the evaluation.

func (*Result) Unshard added in v0.8.0

func (u *Result) Unshard() error

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

func NewRule(id string, expr string) *Rule

NewRule initializes a rule with the ID and rule expression. The ID and expression can be empty.

func (*Rule) Add added in v0.8.0

func (r *Rule) Add(rr *Rule) error

Add is a convenience function to add rr to r's Rules list

func (*Rule) BuildShards added in v0.8.0

func (r *Rule) BuildShards() error

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

func (r *Rule) Count() (rules, shards int)

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

func (r *Rule) FindParent(id string) *Rule

FindParent returns the parent of the rule with the id

func (*Rule) FindRule added in v0.8.0

func (r *Rule) FindRule(id string) (rule *Rule, ancestors []*Rule)

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

func (r *Rule) Path(id string) []*Rule

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

func (r *Rule) String() string

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

func (r *Rule) Tree() string

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.

func (*Schema) String added in v0.6.0

func (s *Schema) String() string

String returns a human-readable representation of the schema

type String

type String struct{}

String defines an Indigo string type.

func (String) String

func (String) String() string

type Timestamp

type Timestamp struct{}

Timestamp defines an Indigo type for the time.Time type.

func (Timestamp) String

func (Timestamp) String() string

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

func ParseType(t string) (Type, error)

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

func (v *Vault) ImmutableRule() *Rule

Rule returns the current immutable root rule for evaluation or inspection.

func (*Vault) LastUpdate added in v0.8.0

func (v *Vault) LastUpdate() time.Time

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.

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.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL