cel

package
v0.7.0 Latest Latest
Warning

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

Go to latest
Published: Feb 17, 2023 License: LGPL-3.0 Imports: 10 Imported by: 0

README

cel

Package cel provides an implementation of the Indigo evaluator and compiler interfaces backed by Google's cel-go rules engine.

See https://github.com/google/cel-go and https://opensource.google/projects/cel for more information about CEL.

The rule expressions you write must conform to the CEL spec: https://github.com/google/cel-spec.

Google has a nice tutorial here: https://codelabs.developers.google.com/codelabs/cel-go/index.html#0

Working with Protocol Buffers

While it is possible to use CEL with "native" simple types, it is built on protocol buffers. CEL does not support Go structs, so if you need to use native types to access fields in a struct, you must first "flatten" the fields into plain values to pass to CEL. See the makeStudentData() function in the tests in this package for an example of "flatting" a struct to individual data elements.

Organizing your input data using protocol buffers gives you the benefit of being able to move data between Go code and CEL expressions without needing to translate or reorganize the data. There a number of examples (they start with proto) that show how to use protocol buffers in Indigo. They use the protocol buffer definitions in indigo/testdata/proto, and there's a Makefile in that directory that shows how to generate the Go types.

Protocol Buffer Fields in Expressions

When refererring to fields of a protocol buffer in an expression, the field names are the proto names, NOT the generated Go names. For example, in the student.proto file, a field called "enrollment_date" is defined. When the Go struct is generated, this field name is now called "EnrollmentDate". In the CEL expression, you must use the "enrollment_date" name, as in the protocol buffer message definition.

In student.proto:

message Student {
  google.protobuf.Timestamp enrollment_date = 7;
}

In Go code:

s := school.Student{}
s.EnrollmentDate = &timestamp.Timestamp{
    Seconds: time.Date(2010, 5, 1, 12, 12, 59, 0, time.FixedZone("UTC-8", -8*60*60)).Unix()
}

In the CEL expression:

rule.Expr = ` now - student.enrollment_date > duration("4320h")`

Protocol Buffer Enums

The student protocol buffer definition includes an enum type for a student's status. When referring to enum values in the CEL expression, use the full protocol buffer name:

In student.proto:

message Student {
 status_type status = 4;
 ...
  enum status_type {
   ENROLLED=0;
   PROBATION=1;
 }
 ...
}

In Go Code:

s:= school.Student{}
s.Status = school.Student_PROBATION

In the CEL expression:

rule.Expr = `student.Status == testdata.school.Student.status_type.PROBATION`

Protocol Buffer Timestamps

The examples demonstrate how to convert to/from the Go time.Time type and the protocol buffer timestamp.

This package has generated types for google/protobuf/timestamp.proto:

import	"google.golang.org/protobuf/types/known/timestamppb"

To create a new timestamp:

now := timestamppb.Now()

To convert a Go time.Time to a proto timestamp:

prototime := timestamppb.New(time.Date(2009, time.November, 10, 23, 0, 0, 0, time.UTC))

To convert from a proto time to Go time:

goTime := pbtime.AsTime()

Protocol Buffer Durations

This package has generated types for google/protobuf/duration.proto:

import	"google.golang.org/protobuf/types/known/durationpb"

To convert a Go duration to a proto duration:

protodur := durationpb.New(godur)

To convert back from a protocol buffer duration to a Go duration:

goDur := protodur.AsDuration()

Examples

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,
		ResultType: indigo.Bool{},
		Expr:       `message == "hello world"`,
	}

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

	// 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)
	if err != nil {
		fmt.Println(err)
	} else {
		fmt.Println(results.ExpressionPass)
	}
}

Output:

true
NativeTimestampComparison
package main

import (
	"context"
	"fmt"
	"github.com/ezachrisen/indigo"
	"github.com/ezachrisen/indigo/cel"
	"google.golang.org/protobuf/types/known/timestamppb"
)

func main() {
	schema := indigo.Schema{
		Elements: []indigo.DataElement{
			{Name: "then", Type: indigo.String{}},
			{Name: "now", Type: indigo.Timestamp{}},
		},
	}

	data := map[string]interface{}{
		"then": "1972-01-01T10:00:20.021-05:00", //"2018-08-03T16:00:00-07:00",
		"now":  timestamppb.Now(),
	}

	rule := indigo.Rule{
		ID:     "time_check",
		Schema: schema,
		Expr:   `now > timestamp(then)`,
	}

	engine := indigo.NewEngine(cel.NewEvaluator())

	err := engine.Compile(&rule)
	if err != nil {
		fmt.Printf("Error adding rule %v", err)
		return
	}

	results, err := engine.Eval(context.Background(), &rule, data)
	if err != nil {
		fmt.Printf("Error evaluating: %v", err)
		return
	}
	fmt.Println(results.ExpressionPass)
}

Output:

true
ProtoConstruction

Demonstrate constructing a proto message in an expression

package main

import (
	"context"
	"fmt"
	"github.com/ezachrisen/indigo"
	"github.com/ezachrisen/indigo/cel"
	"github.com/ezachrisen/indigo/testdata/school"
)

func main() {

	education := indigo.Schema{
		Elements: []indigo.DataElement{
			{Name: "student", Type: indigo.Proto{Message: &school.Student{}}},
			{Name: "student_suspension", Type: indigo.Proto{Message: &school.Student_Suspension{}}},
			{Name: "studentSummary", Type: indigo.Proto{Message: &school.StudentSummary{}}},
		},
	}

	data := map[string]interface{}{
		"student": &school.Student{
			Grades: []float64{3.0, 2.9, 4.0, 2.1},
			Suspensions: []*school.Student_Suspension{
				&school.Student_Suspension{Cause: "Cheating"},
				&school.Student_Suspension{Cause: "Fighting"},
			},
		},
	}

	rule := indigo.Rule{
		ID:         "create_summary",
		Schema:     education,
		ResultType: indigo.Proto{Message: &school.StudentSummary{}},
		Expr: `
			testdata.school.StudentSummary {
				gpa: student.gpa,
				risk_factor: 2.0 + 3.0,
				tenure: duration("12h")
			}`,
	}

	engine := indigo.NewEngine(cel.NewEvaluator())

	err := engine.Compile(&rule)
	if err != nil {
		fmt.Printf("Error adding rule %v", err)
		return
	}

	result, err := engine.Eval(context.Background(), &rule, data)
	if err != nil {
		//		fmt.Printf("Error evaluating: %v", err)
		return
	}

	summary := result.Value.(*school.StudentSummary)

	fmt.Printf("%T\n", summary)
	fmt.Printf("%0.0f\n", summary.RiskFactor)
}

Output:

*school.StudentSummary
5
ProtoConstructionConditional

Demonstrate using the ? : operator to conditionally construct a proto message

package main

import (
	"context"
	"fmt"
	"github.com/ezachrisen/indigo"
	"github.com/ezachrisen/indigo/cel"
	"github.com/ezachrisen/indigo/testdata/school"
)

func main() {

	education := indigo.Schema{
		Elements: []indigo.DataElement{
			{Name: "student", Type: indigo.Proto{Message: &school.Student{}}},
			{Name: "student_suspension", Type: indigo.Proto{Message: &school.Student_Suspension{}}},
			{Name: "studentSummary", Type: indigo.Proto{Message: &school.StudentSummary{}}},
		},
	}

	data := map[string]interface{}{
		"student": &school.Student{
			Gpa:    4.0,
			Grades: []float64{3.0, 2.9, 4.0, 2.1},
			Suspensions: []*school.Student_Suspension{
				&school.Student_Suspension{Cause: "Cheating"},
				&school.Student_Suspension{Cause: "Fighting"},
			},
		},
	}

	rule := indigo.Rule{
		ID:         "create_summary",
		Schema:     education,
		ResultType: indigo.Proto{Message: &school.StudentSummary{}},
		Expr: `
			student.gpa > 3.0 ?
				testdata.school.StudentSummary {
					gpa: student.gpa,
					risk_factor: 0.0
				}
			:
				testdata.school.StudentSummary {
					gpa: student.gpa,
					risk_factor: 2.0 + 3.0,
					tenure: duration("12h")
				}
			`,
	}

	engine := indigo.NewEngine(cel.NewEvaluator())

	err := engine.Compile(&rule)
	if err != nil {
		fmt.Printf("Error adding rule %v", err)
		return
	}

	results, err := engine.Eval(context.Background(), &rule, data)
	if err != nil {
		fmt.Printf("Error evaluating: %v", err)
		return
	}

	// The result is a fully-formed school.StudentSummary message.
	// There is no need to convert it.
	fmt.Printf("%T\n", results.Value)
	summ := results.Value.(*school.StudentSummary)
	fmt.Println(summ.RiskFactor)
}

Output:

*school.StudentSummary
0
ProtoDurationComparison

Demonstrates conversion between protobuf durations (google.protobuf.Duration) and time.Duration

package main

import (
	"context"
	"fmt"
	"github.com/ezachrisen/indigo"
	"github.com/ezachrisen/indigo/cel"
	"github.com/ezachrisen/indigo/testdata/school"
	"google.golang.org/protobuf/types/known/durationpb"
	"time"
)

func main() {

	education := indigo.Schema{
		Elements: []indigo.DataElement{
			{Name: "smry", Type: indigo.Proto{Message: &school.StudentSummary{}}},
		},
	}

	godur, _ := time.ParseDuration("10h")

	data := map[string]interface{}{
		"smry": &school.StudentSummary{
			Tenure: durationpb.New(godur),
		},
	}

	rule := indigo.Rule{
		ID:     "tenure_check",
		Schema: education,
		Expr:   `smry.tenure > duration("1h")`,
	}

	engine := indigo.NewEngine(cel.NewEvaluator())

	err := engine.Compile(&rule)
	if err != nil {
		fmt.Printf("Error adding rule %v", err)
		return
	}

	results, err := engine.Eval(context.Background(), &rule, data)
	if err != nil {
		fmt.Printf("Error evaluating: %v", err)
		return
	}
	fmt.Println(results.ExpressionPass)
}

Output:

true
ProtoExistsOperator

Demonstrates using the CEL exists function to check for a value in a slice

package main

import (
	"context"
	"fmt"
	"github.com/ezachrisen/indigo"
	"github.com/ezachrisen/indigo/cel"
	"github.com/ezachrisen/indigo/testdata/school"
)

func main() {

	education := indigo.Schema{
		Elements: []indigo.DataElement{
			{Name: "student", Type: indigo.Proto{Message: &school.Student{}}},
		},
	}
	data := map[string]interface{}{
		"student": &school.Student{
			Grades: []float64{3.0, 2.9, 4.0, 2.1},
		},
	}

	rule := indigo.Rule{
		ID:     "grade_check",
		Schema: education,
		Expr:   `student.grades.exists(g, g < 2.0)`,
	}

	engine := indigo.NewEngine(cel.NewEvaluator())

	err := engine.Compile(&rule)
	if err != nil {
		fmt.Printf("Error adding rule %v", err)
		return
	}

	results, err := engine.Eval(context.Background(), &rule, data)
	if err != nil {
		fmt.Printf("Error evaluating: %v", err)
		return
	}
	fmt.Println(results.ExpressionPass)
}

Output:

false
ProtoNestedMessages

Demonstrates using the exists macro to inspect the value of nested messages in the list

package main

import (
	"context"
	"fmt"
	"github.com/ezachrisen/indigo"
	"github.com/ezachrisen/indigo/cel"
	"github.com/ezachrisen/indigo/testdata/school"
)

func main() {

	education := indigo.Schema{
		Elements: []indigo.DataElement{
			{Name: "student", Type: indigo.Proto{Message: &school.Student{}}},
			{Name: "student_suspension", Type: indigo.Proto{Message: &school.Student_Suspension{}}},
		},
	}

	data := map[string]interface{}{
		"student": &school.Student{
			Grades: []float64{3.0, 2.9, 4.0, 2.1},
			Suspensions: []*school.Student_Suspension{
				&school.Student_Suspension{Cause: "Cheating"},
				&school.Student_Suspension{Cause: "Fighting"},
			},
		},
	}

	// Check if the student was ever suspended for fighting
	rule := indigo.Rule{
		ID:     "fighting_check",
		Schema: education,
		Expr:   `student.suspensions.exists(s, s.cause == "Fighting")`,
	}

	engine := indigo.NewEngine(cel.NewEvaluator())

	err := engine.Compile(&rule)
	if err != nil {
		fmt.Printf("Error adding rule %v", err)
		return
	}

	results, err := engine.Eval(context.Background(), &rule, data)
	if err != nil {
		fmt.Printf("Error evaluating: %v", err)
		return
	}
	fmt.Println(results.Value)
}

Output:

true
ProtoTimestampComparison

Demonstrates conversion between protobuf timestamps (google.protobuf.Timestamp) and time.Time

package main

import (
	"context"
	"fmt"
	"github.com/ezachrisen/indigo"
	"github.com/ezachrisen/indigo/cel"
	"github.com/ezachrisen/indigo/testdata/school"
	"google.golang.org/protobuf/types/known/timestamppb"
	"time"
)

func main() {

	education := indigo.Schema{
		Elements: []indigo.DataElement{
			{Name: "student", Type: indigo.Proto{Message: &school.Student{}}},
			{Name: "now", Type: indigo.Timestamp{}},
		},
	}

	data := map[string]interface{}{
		"student": &school.Student{
			// Make a protobuf timestamp from a time.Time
			EnrollmentDate: timestamppb.New(time.Date(2009, time.November, 10, 23, 0, 0, 0, time.UTC)),
			Grades:         []float64{3.0, 2.9, 4.0, 2.1},
		},
		"now": timestamppb.Now(),
	}

	// The rule will return the earlier of the two dates (enrollment date or now)
	rule := indigo.Rule{
		ID:         "grade_check",
		Schema:     education,
		ResultType: indigo.Timestamp{},
		Expr: `student.enrollment_date < now
			  ?
			  student.enrollment_date
			  :
			  now
			  `,
	}

	engine := indigo.NewEngine(cel.NewEvaluator())

	err := engine.Compile(&rule)
	if err != nil {
		fmt.Printf("Error adding rule %v", err)
		return
	}

	results, err := engine.Eval(context.Background(), &rule, data)
	if err != nil {
		fmt.Printf("Error evaluating: %v", err)
		return
	}

	if ts, ok := results.Value.(time.Time); ok {
		fmt.Printf("Gotime is %v\n", ts)
	}
}

Output:

Gotime is 2009-11-10 23:00:00 +0000 UTC

Readme created from Go doc with goreadme

Documentation

Overview

Package cel provides an implementation of the Indigo evaluator and compiler interfaces backed by Google's cel-go rules engine.

See https://github.com/google/cel-go and https://opensource.google/projects/cel for more information about CEL.

The rule expressions you write must conform to the CEL spec: https://github.com/google/cel-spec.

Google has a nice tutorial here: https://codelabs.developers.google.com/codelabs/cel-go/index.html#0

Working with Protocol Buffers

While it is possible to use CEL with "native" simple types, it is built on protocol buffers. CEL does not support Go structs, so if you need to use native types to access fields in a struct, you must first "flatten" the fields into plain values to pass to CEL. See the makeStudentData() function in the tests in this package for an example of "flatting" a struct to individual data elements.

Organizing your input data using protocol buffers gives you the benefit of being able to move data between Go code and CEL expressions without needing to translate or reorganize the data. There a number of examples (they start with proto) that show how to use protocol buffers in Indigo. They use the protocol buffer definitions in indigo/testdata/proto, and there's a Makefile in that directory that shows how to generate the Go types.

Protocol Buffer Fields in Expressions

When refererring to fields of a protocol buffer in an expression, the field names are the proto names, NOT the generated Go names. For example, in the student.proto file, a field called "enrollment_date" is defined. When the Go struct is generated, this field name is now called "EnrollmentDate". In the CEL expression, you must use the "enrollment_date" name, as in the protocol buffer message definition.

In student.proto:

message Student {
  google.protobuf.Timestamp enrollment_date = 7;
}

In Go code:

s := school.Student{}
s.EnrollmentDate = &timestamp.Timestamp{
    Seconds: time.Date(2010, 5, 1, 12, 12, 59, 0, time.FixedZone("UTC-8", -8*60*60)).Unix()
}

In the CEL expression:

rule.Expr = ` now - student.enrollment_date > duration("4320h")`

Protocol Buffer Enums

The student protocol buffer definition includes an enum type for a student's status. When referring to enum values in the CEL expression, use the full protocol buffer name:

In student.proto:

message Student {
 status_type status = 4;
 ...
  enum status_type {
   ENROLLED=0;
   PROBATION=1;
 }
 ...
}

In Go Code:

s:= school.Student{}
s.Status = school.Student_PROBATION

In the CEL expression:

rule.Expr = `student.Status == testdata.school.Student.status_type.PROBATION`

Protocol Buffer Timestamps

The examples demonstrate how to convert to/from the Go time.Time type and the protocol buffer timestamp.

This package has generated types for google/protobuf/timestamp.proto:

import	"google.golang.org/protobuf/types/known/timestamppb"

To create a new timestamp:

now := timestamppb.Now()

To convert a Go time.Time to a proto timestamp:

prototime := timestamppb.New(time.Date(2009, time.November, 10, 23, 0, 0, 0, time.UTC))

To convert from a proto time to Go time:

goTime := pbtime.AsTime()

Protocol Buffer Durations

This package has generated types for google/protobuf/duration.proto:

import	"google.golang.org/protobuf/types/known/durationpb"

To convert a Go duration to a proto duration:

protodur := durationpb.New(godur)

To convert back from a protocol buffer duration to a Go duration:

goDur := protodur.AsDuration()
Example
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,
		ResultType: indigo.Bool{},
		Expr:       `message == "hello world"`,
	}

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

	// 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)
	if err != nil {
		fmt.Println(err)
	} else {
		fmt.Println(results.ExpressionPass)
	}
}
Output:

true
Example (Alarms)

Example_alarms illustrates using the FailAction option to only return true rules from evaluation

package main

import (
	"context"
	"fmt"

	"github.com/ezachrisen/indigo"
	"github.com/ezachrisen/indigo/cel"
)

func main() {

	sysmetrics := indigo.Schema{
		Elements: []indigo.DataElement{
			{Name: "cpu_utilization", Type: indigo.Int{}},
			{Name: "disk_free_space", Type: indigo.Int{}},
			{Name: "memory_utilization", Type: indigo.Int{}},
		},
	}

	rule := indigo.Rule{
		ID:    "alarm_check",
		Rules: map[string]*indigo.Rule{},
	}

	// Setting this option so we only get back
	// rules that evaluate to 'true'
	rule.EvalOptions.DiscardFail = indigo.Discard

	rule.Rules["cpu_alarm"] = &indigo.Rule{
		ID:     "cpu_alarm",
		Schema: sysmetrics,
		Expr:   "cpu_utilization > 90",
	}

	rule.Rules["disk_alarm"] = &indigo.Rule{
		ID:     "disk_alarm",
		Schema: sysmetrics,
		Expr:   "disk_free_space < 70",
	}

	rule.Rules["memory_alarm"] = &indigo.Rule{
		ID:     "memory_alarm",
		Schema: sysmetrics,
		Expr:   "memory_utilization > 90",
	}

	engine := indigo.NewEngine(cel.NewEvaluator())

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

	data := map[string]interface{}{
		"cpu_utilization":    99,
		"disk_free_space":    85,
		"memory_utilization": 89,
	}

	results, err := engine.Eval(context.Background(), &rule, data)
	if err != nil {
		fmt.Println(err)
	}

	for k := range results.Results {
		fmt.Println(k)
	}

}
Output:

cpu_alarm
Example (AlarmsTwoLevel)

Example_alarms illustrates using the FailAction option to only return true rules from evaluation with a multi-level hierarchy

package main

import (
	"context"
	"fmt"

	"github.com/ezachrisen/indigo"
	"github.com/ezachrisen/indigo/cel"
)

func main() {

	sysmetrics := indigo.Schema{
		Elements: []indigo.DataElement{
			{Name: "cpu_utilization", Type: indigo.Int{}},
			{Name: "disk_free_space", Type: indigo.Int{}},
			{Name: "memory_utilization", Type: indigo.Int{}},
			{Name: "memory_mb_remaining", Type: indigo.Int{}},
		},
	}

	rule := indigo.Rule{
		ID:    "alarm_check",
		Rules: map[string]*indigo.Rule{},
	}

	// Setting this option so we only get back
	// rules that evaluate to 'true'
	rule.EvalOptions.DiscardFail = indigo.Discard

	rule.Rules["cpu_alarm"] = &indigo.Rule{
		ID:     "cpu_alarm",
		Schema: sysmetrics,
		Expr:   "cpu_utilization > 90",
	}

	rule.Rules["disk_alarm"] = &indigo.Rule{
		ID:     "disk_alarm",
		Schema: sysmetrics,
		Expr:   "disk_free_space < 70",
	}

	memory_alarm := &indigo.Rule{
		ID:    "memory_alarm",
		Rules: map[string]*indigo.Rule{},
		EvalOptions: indigo.EvalOptions{
			DiscardFail: indigo.KeepAll,
			TrueIfAny:   true,
		},
	}

	memory_alarm.Rules["memory_utilization_alarm"] = &indigo.Rule{
		ID:     "memory_utilization_alarm",
		Schema: sysmetrics,
		Expr:   "memory_utilization > 90",
	}

	memory_alarm.Rules["memory_remaining_alarm"] = &indigo.Rule{
		ID:     "memory_remaining_alarm",
		Schema: sysmetrics,
		Expr:   "memory_mb_remaining < 16",
	}

	rule.Rules["memory_alarm"] = memory_alarm

	engine := indigo.NewEngine(cel.NewEvaluator())

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

	data := map[string]interface{}{
		"cpu_utilization":     99,
		"disk_free_space":     85,
		"memory_utilization":  89,
		"memory_mb_remaining": 7,
	}

	results, err := engine.Eval(context.Background(), &rule, data)
	if err != nil {
		fmt.Println(err)
	}

	for k := range results.Results {
		fmt.Println(k)
	}

}
Output:

cpu_alarm
memory_alarm
Example (Basic)
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: "x", Type: indigo.Int{}},
			{Name: "y", Type: indigo.String{}},
		},
	}

	// Step 2: Create rules
	rule := indigo.Rule{
		Schema:     schema,
		ResultType: indigo.Bool{},
		Expr:       `x > 10 && y != "blue"`,
	}

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

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

	data := map[string]interface{}{
		"x": 11,
		"y": "red",
	}

	// Step 5: Evaluate and check the results
	results, err := engine.Eval(context.Background(), &rule, data)
	if err != nil {
		fmt.Println(err)
		return
	}
	fmt.Println(results.ExpressionPass)

}
Output:

true
Example (Indigo)
package main

import (
	"context"
	"fmt"

	"github.com/ezachrisen/indigo"
	"github.com/ezachrisen/indigo/cel"

	"github.com/ezachrisen/indigo/testdata/school"
)

func main() {

	// In this example we're going to determine which, if any,
	// communications the administration should send to the student.
	education := indigo.Schema{
		ID: "education",
		Elements: []indigo.DataElement{
			{Name: "s", Type: indigo.Proto{Message: &school.Student{}}},
		},
	}

	data := map[string]interface{}{
		"s": &school.Student{
			Id:      927312,
			Age:     21,
			Credits: 16,
			Gpa:     3.1,
			Attrs:   map[string]string{"major": "Accounting", "home_town": "Chicago"},
			Status:  school.Student_ENROLLED,
			Grades:  []float64{3, 3, 4, 2, 3, 3.5, 4},
		},
	}

	root := indigo.NewRule("root", "")
	root.Schema = education
	root.Rules["accounting_honors"] = &indigo.Rule{
		ID:     "accounting_honors",
		Schema: education,
		Expr:   `s.attrs.exists(k, k == "major" && s.attrs[k] == "Accounting") && s.gpa > 3`,
	}

	root.Rules["arts_honors"] = &indigo.Rule{
		ID:     "arts_honors",
		Schema: education,
		Expr:   `s.attrs.exists(k, k == "major" && s.attrs[k] == "Arts") && s.gpa > 3`,
	}

	root.Rules["last_3_grades_above_3"] = &indigo.Rule{
		ID:     "last_3_grades_above_3",
		Schema: education,
		Expr: `size(s.grades) >=3 
         && s.grades[size(s.grades)-1] >= 3.0 
         && s.grades[size(s.grades)-2] >= 3.0 
         && s.grades[size(s.grades)-3] >= 3.0 `,
	}

	engine := indigo.NewEngine(cel.NewEvaluator())

	err := engine.Compile(root)
	if err != nil {
		fmt.Printf("Error adding rule %v", err)
		return
	}

	results, err := engine.Eval(context.Background(), root, data)
	if err != nil {
		fmt.Printf("Error evaluating: %v", err)
		return
	}

	for k, v := range results.Results {
		fmt.Printf("%s? %t\n", k, v.ExpressionPass)
	}

}
Output:

accounting_honors? true
arts_honors? false
last_3_grades_above_3? true
Example (List)
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: "grades", Type: indigo.List{ValueType: indigo.Float{}}},
		},
	}

	// Step 2: Create rules
	rule := indigo.Rule{
		Schema:     schema,
		ResultType: indigo.Bool{},
		Expr:       `size(grades) > 3`,
	}

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

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

	data := map[string]interface{}{
		"grades": []float64{3.4, 3.6, 3.8, 2.9},
	}

	// Step 5: Evaluate and check the results
	results, err := engine.Eval(context.Background(), &rule, data)
	if err != nil {
		fmt.Println(err)
	} else {
		fmt.Println("Is size(grades) > 3? ", results.ExpressionPass)
	}

	// Check the value of a specific element
	rule.Expr = `grades[1] == 3.6`
	engine.Compile(&rule)
	results, _ = engine.Eval(context.Background(), &rule, data)
	fmt.Println("Is element 1 == 3.6? ", results.ExpressionPass)

	// Check if the list contains a value
	rule.Expr = `grades.exists(g, g < 3.0)`
	engine.Compile(&rule)
	results, _ = engine.Eval(context.Background(), &rule, data)
	fmt.Println("Any grades below 3.0? ", results.ExpressionPass)

}
Output:

Is size(grades) > 3?  true
Is element 1 == 3.6?  true
Any grades below 3.0?  true
Example (Manual)

Example_manual demonstrates evaluating multiple rules and processing the results manually

package main

import (
	"context"
	"fmt"

	"github.com/ezachrisen/indigo"
	"github.com/ezachrisen/indigo/cel"

	"github.com/ezachrisen/indigo/testdata/school"
)

func main() {

	// In this example we're going to determine which, if any,
	// communications the administration should send to the student.
	education := indigo.Schema{
		Elements: []indigo.DataElement{
			{Name: "s", Type: indigo.Proto{Message: &school.Student{}}},
		},
	}

	data := map[string]interface{}{
		"s": &school.Student{
			Id:      927312,
			Age:     21,
			Credits: 16,
			Gpa:     3.1,
			Attrs:   map[string]string{"major": "Accounting", "home_town": "Chicago"},
			Status:  school.Student_ENROLLED,
			Grades:  []float64{3, 3, 4, 2, 3, 3.5, 4},
		},
	}

	accounting_honors := indigo.Rule{
		Schema: education,
		Expr:   `s.attrs.exists(k, k == "major" && s.attrs[k] == "Accounting") && s.gpa > 3`,
	}

	arts_honors := indigo.Rule{
		Schema: education,
		Expr:   `s.attrs.exists(k, k == "major" && s.attrs[k] == "Arts") && s.gpa > 3`,
	}

	last_3_grades_3_or_above := indigo.Rule{
		Schema: education,
		Expr: `size(s.grades) >=3 
                 && s.grades[size(s.grades)-1] >= 3.0 
                 && s.grades[size(s.grades)-2] >= 3.0 
                 && s.grades[size(s.grades)-3] >= 3.0 `,
	}

	engine := indigo.NewEngine(cel.NewEvaluator())

	err := engine.Compile(&accounting_honors)
	if err != nil {
		fmt.Printf("Error adding rule %v", err)
		return
	}

	results, err := engine.Eval(context.Background(), &accounting_honors, data)
	if err != nil {
		fmt.Printf("Error evaluating: %v", err)
		return
	}
	fmt.Println("accounting_honors?", results.ExpressionPass)

	err = engine.Compile(&arts_honors)
	if err != nil {
		fmt.Printf("Error adding rule %v", err)
		return
	}

	results, err = engine.Eval(context.Background(), &arts_honors, data)
	if err != nil {
		fmt.Printf("Error evaluating: %v", err)
		return
	}
	fmt.Println("arts_honors?", results.ExpressionPass)

	err = engine.Compile(&last_3_grades_3_or_above)
	if err != nil {
		fmt.Printf("Error adding rule %v", err)
		return
	}

	results, err = engine.Eval(context.Background(), &last_3_grades_3_or_above, data)
	if err != nil {
		fmt.Printf("Error evaluating: %v", err)
		return
	}
	fmt.Println("last_3_grades_above_3?", results.ExpressionPass)

}
Output:

accounting_honors? true
arts_honors? false
last_3_grades_above_3? true
Example (Map)
package main

import (
	"context"
	"fmt"

	"github.com/ezachrisen/indigo"
	"github.com/ezachrisen/indigo/cel"
)

func main() {

	schema := indigo.Schema{
		Elements: []indigo.DataElement{
			{Name: "flights", Type: indigo.Map{KeyType: indigo.String{}, ValueType: indigo.String{}}},
		},
	}

	rule := indigo.Rule{
		Schema:     schema,
		ResultType: indigo.Bool{},
		Expr:       `flights.exists(k, flights[k] == "Delayed")`,
	}

	engine := indigo.NewEngine(cel.NewEvaluator())

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

	data := map[string]interface{}{
		"flights": map[string]string{"UA1500": "On Time", "DL232": "Delayed", "AA1622": "Delayed"},
	}

	// Step 5: Evaluate and check the results
	results, err := engine.Eval(context.Background(), &rule, data)
	if err != nil {
		fmt.Println(err)
	} else {
		fmt.Println("Are any flights delayed?", results.ExpressionPass)
	}

}
Output:

Are any flights delayed? true
Example (NativeTimestampComparison)
package main

import (
	"context"
	"fmt"

	"github.com/ezachrisen/indigo"
	"github.com/ezachrisen/indigo/cel"
	"google.golang.org/protobuf/types/known/timestamppb"
)

func main() {
	schema := indigo.Schema{
		Elements: []indigo.DataElement{
			{Name: "then", Type: indigo.String{}},
			{Name: "now", Type: indigo.Timestamp{}},
		},
	}

	data := map[string]interface{}{
		"then": "1972-01-01T10:00:20.021-05:00", //"2018-08-03T16:00:00-07:00",
		"now":  timestamppb.Now(),
	}

	rule := indigo.Rule{
		ID:     "time_check",
		Schema: schema,
		Expr:   `now > timestamp(then)`,
	}

	engine := indigo.NewEngine(cel.NewEvaluator())

	err := engine.Compile(&rule)
	if err != nil {
		fmt.Printf("Error adding rule %v", err)
		return
	}

	results, err := engine.Eval(context.Background(), &rule, data)
	if err != nil {
		fmt.Printf("Error evaluating: %v", err)
		return
	}
	fmt.Println(results.ExpressionPass)
}
Output:

true
Example (ProtoBasic)

Demonstrates basic protocol buffer usage in a rule

package main

import (
	"context"
	"fmt"

	"github.com/ezachrisen/indigo"
	"github.com/ezachrisen/indigo/cel"

	"github.com/ezachrisen/indigo/testdata/school"
)

func main() {

	education := indigo.Schema{
		Elements: []indigo.DataElement{
			{Name: "student", Type: indigo.Proto{Message: &school.Student{}}},
		},
	}
	data := map[string]interface{}{
		"student": &school.Student{
			Age: 21,
		},
	}

	rule := indigo.Rule{
		Schema: education,
		Expr:   `student.age > 21`,
	}

	engine := indigo.NewEngine(cel.NewEvaluator())

	err := engine.Compile(&rule)
	if err != nil {
		fmt.Printf("Error adding rule %v", err)
		return
	}

	results, err := engine.Eval(context.Background(), &rule, data)
	if err != nil {
		fmt.Printf("Error evaluating: %v", err)
		return
	}
	fmt.Println(results.ExpressionPass)
}
Output:

false
Example (ProtoConstruction)

Demonstrate constructing a proto message in an expression

package main

import (
	"context"
	"fmt"

	"github.com/ezachrisen/indigo"
	"github.com/ezachrisen/indigo/cel"

	"github.com/ezachrisen/indigo/testdata/school"
)

func main() {

	education := indigo.Schema{
		Elements: []indigo.DataElement{
			{Name: "s", Type: indigo.Proto{Message: &school.Student{}}},
			{Name: "student_suspension", Type: indigo.Proto{Message: &school.Student_Suspension{}}},
			{Name: "studentSummary", Type: indigo.Proto{Message: &school.StudentSummary{}}},
		},
	}

	data := map[string]interface{}{
		"s": &school.Student{
			Grades: []float64{3.0, 2.9, 4.0, 2.1},
			Suspensions: []*school.Student_Suspension{
				&school.Student_Suspension{Cause: "Cheating"},
				&school.Student_Suspension{Cause: "Fighting"},
			},
		},
	}

	rule := indigo.Rule{
		ID:         "create_summary",
		Schema:     education,
		ResultType: indigo.Proto{Message: &school.StudentSummary{}},
		Expr: `
			testdata.school.StudentSummary {
				gpa: s.gpa,
				risk_factor: 2.0 + 3.0,
				tenure: duration("12h")
			}`,
	}

	engine := indigo.NewEngine(cel.NewEvaluator())

	err := engine.Compile(&rule)
	if err != nil {
		fmt.Printf("Error adding rule %v", err)
		return
	}

	result, err := engine.Eval(context.Background(), &rule, data)
	if err != nil {
		//		fmt.Printf("Error evaluating: %v", err)
		return
	}

	summary := result.Value.(*school.StudentSummary)

	fmt.Printf("%T\n", summary)
	fmt.Printf("%0.0f\n", summary.RiskFactor)

}
Output:

*school.StudentSummary
5
Example (ProtoConstructionConditional)

Demonstrate using the ? : operator to conditionally construct a proto message

package main

import (
	"context"
	"fmt"

	"github.com/ezachrisen/indigo"
	"github.com/ezachrisen/indigo/cel"

	"github.com/ezachrisen/indigo/testdata/school"
)

func main() {

	education := indigo.Schema{
		Elements: []indigo.DataElement{
			{Name: "student", Type: indigo.Proto{Message: &school.Student{}}},
			{Name: "student_suspension", Type: indigo.Proto{Message: &school.Student_Suspension{}}},
			{Name: "studentSummary", Type: indigo.Proto{Message: &school.StudentSummary{}}},
		},
	}

	data := map[string]interface{}{
		"student": &school.Student{
			Gpa:    4.0,
			Grades: []float64{3.0, 2.9, 4.0, 2.1},
			Suspensions: []*school.Student_Suspension{
				&school.Student_Suspension{Cause: "Cheating"},
				&school.Student_Suspension{Cause: "Fighting"},
			},
		},
	}

	rule := indigo.Rule{
		ID:         "create_summary",
		Schema:     education,
		ResultType: indigo.Proto{Message: &school.StudentSummary{}},
		Expr: `
			student.gpa > 3.0 ?
				testdata.school.StudentSummary {
					gpa: student.gpa,
					risk_factor: 0.0
				}
			:
	           testdata.school.StudentSummary {
					gpa: student.gpa,
					risk_factor: 2.0 + 3.0,
					tenure: duration("12h")
				}
			`,
	}

	engine := indigo.NewEngine(cel.NewEvaluator())

	err := engine.Compile(&rule)
	if err != nil {
		fmt.Printf("Error adding rule %v", err)
		return
	}

	results, err := engine.Eval(context.Background(), &rule, data)
	if err != nil {
		fmt.Printf("Error evaluating: %v", err)
		return
	}

	// The result is a fully-formed school.StudentSummary message.
	// There is no need to convert it.
	fmt.Printf("%T\n", results.Value)
	summ := results.Value.(*school.StudentSummary)
	fmt.Println(summ.RiskFactor)
}
Output:

*school.StudentSummary
0
Example (ProtoDurationCalculation)

Demonstrates writing rules on timestamps and durations

package main

import (
	"context"
	"fmt"
	"time"

	"github.com/ezachrisen/indigo"
	"github.com/ezachrisen/indigo/cel"
	"google.golang.org/protobuf/types/known/timestamppb"

	"github.com/ezachrisen/indigo/testdata/school"
)

func main() {

	education := indigo.Schema{
		Elements: []indigo.DataElement{
			{Name: "s", Type: indigo.Proto{Message: &school.Student{}}},
			{Name: "now", Type: indigo.Timestamp{}},
			{Name: "ssum", Type: indigo.Proto{Message: &school.StudentSummary{}}},
		},
	}

	data := map[string]interface{}{
		"s": &school.Student{
			EnrollmentDate: timestamppb.New(time.Date(2009, time.November, 10, 23, 0, 0, 0, time.UTC)),
		},
		"now": timestamppb.Now(),
	}

	rule := indigo.Rule{
		ID:     "",
		Schema: education,
		Expr: `// 2,400h = 100 days * 24 hours
               now - s.enrollment_date  > duration("2400h")
			  `,
	}

	engine := indigo.NewEngine(cel.NewEvaluator())

	err := engine.Compile(&rule)
	if err != nil {
		fmt.Printf("Error adding rule %v", err)
		return
	}

	results, err := engine.Eval(context.Background(), &rule, data)
	if err != nil {
		fmt.Printf("Error evaluating: %v", err)
		return
	}

	fmt.Println(results.ExpressionPass)
}
Output:

true
Example (ProtoDurationComparison)

Demonstrates conversion between protobuf durations (google.protobuf.Duration) and time.Duration

package main

import (
	"context"
	"fmt"
	"time"

	"github.com/ezachrisen/indigo"
	"github.com/ezachrisen/indigo/cel"
	"google.golang.org/protobuf/types/known/durationpb"

	"github.com/ezachrisen/indigo/testdata/school"
)

func main() {

	education := indigo.Schema{
		Elements: []indigo.DataElement{
			{Name: "smry", Type: indigo.Proto{Message: &school.StudentSummary{}}},
		},
	}

	godur, _ := time.ParseDuration("10h")

	data := map[string]interface{}{
		"smry": &school.StudentSummary{
			Tenure: durationpb.New(godur),
		},
	}

	rule := indigo.Rule{
		ID:     "tenure_check",
		Schema: education,
		Expr:   `smry.tenure > duration("1h")`,
	}

	engine := indigo.NewEngine(cel.NewEvaluator())

	err := engine.Compile(&rule)
	if err != nil {
		fmt.Printf("Error adding rule %v", err)
		return
	}

	results, err := engine.Eval(context.Background(), &rule, data)
	if err != nil {
		fmt.Printf("Error evaluating: %v", err)
		return
	}
	fmt.Println(results.ExpressionPass)
}
Output:

true
Example (ProtoEnum)

Demonstrates using a protocol buffer enum value in a rule

package main

import (
	"context"
	"fmt"

	"github.com/ezachrisen/indigo"
	"github.com/ezachrisen/indigo/cel"

	"github.com/ezachrisen/indigo/testdata/school"
)

func main() {

	education := indigo.Schema{
		Elements: []indigo.DataElement{
			{Name: "student", Type: indigo.Proto{Message: &school.Student{}}},
		},
	}
	data := map[string]interface{}{
		"student": &school.Student{
			Status: school.Student_ENROLLED,
		},
	}

	rule := indigo.Rule{
		Schema: education,
		Expr:   `student.status == testdata.school.Student.status_type.PROBATION`,
	}

	engine := indigo.NewEngine(cel.NewEvaluator())

	err := engine.Compile(&rule)
	if err != nil {
		fmt.Printf("Error adding rule %v", err)
		return
	}

	results, err := engine.Eval(context.Background(), &rule, data)
	if err != nil {
		fmt.Printf("Error evaluating: %v", err)
		return
	}
	fmt.Println(results.ExpressionPass)
}
Output:

false
Example (ProtoExistsOperator)

Demonstrates using the CEL exists function to check for a value in a slice

package main

import (
	"context"
	"fmt"

	"github.com/ezachrisen/indigo"
	"github.com/ezachrisen/indigo/cel"

	"github.com/ezachrisen/indigo/testdata/school"
)

func main() {

	education := indigo.Schema{
		Elements: []indigo.DataElement{
			{Name: "student", Type: indigo.Proto{Message: &school.Student{}}},
		},
	}
	data := map[string]interface{}{
		"student": &school.Student{
			Grades: []float64{3.0, 2.9, 4.0, 2.1},
		},
	}

	rule := indigo.Rule{
		ID:     "grade_check",
		Schema: education,
		Expr:   `student.grades.exists(g, g < 2.0)`,
	}

	engine := indigo.NewEngine(cel.NewEvaluator())

	err := engine.Compile(&rule)
	if err != nil {
		fmt.Printf("Error adding rule %v", err)
		return
	}

	results, err := engine.Eval(context.Background(), &rule, data)
	if err != nil {
		fmt.Printf("Error evaluating: %v", err)
		return
	}
	fmt.Println(results.ExpressionPass)
}
Output:

false
Example (ProtoNestedMessages)

Demonstrates using the exists macro to inspect the value of nested messages in the list

package main

import (
	"context"
	"fmt"

	"github.com/ezachrisen/indigo"
	"github.com/ezachrisen/indigo/cel"

	"github.com/ezachrisen/indigo/testdata/school"
)

func main() {

	education := indigo.Schema{
		Elements: []indigo.DataElement{
			{Name: "x", Type: indigo.Proto{Message: &school.Student{}}}},
	}

	data := map[string]interface{}{
		"x": &school.Student{
			Grades: []float64{3.0, 2.9, 4.0, 2.1},
			Suspensions: []*school.Student_Suspension{
				&school.Student_Suspension{Cause: "Cheating"},
				&school.Student_Suspension{Cause: "Fighting"},
			},
		},
	}

	// Check if the student was ever suspended for fighting
	rule := indigo.Rule{
		ID:     "fighting_check",
		Schema: education,
		Expr:   `x.suspensions.exists(s, s.cause == "Fighting")`,
	}

	engine := indigo.NewEngine(cel.NewEvaluator())

	err := engine.Compile(&rule)
	if err != nil {
		fmt.Printf("Error adding rule %v", err)
		return
	}

	results, err := engine.Eval(context.Background(), &rule, data)
	if err != nil {
		fmt.Printf("Error evaluating: %v", err)
		return
	}
	fmt.Println(results.Value)
}
Output:

true
Example (ProtoOneof)

Demonstrates using a protocol buffer oneof value in a rule

package main

import (
	"context"
	"fmt"

	"github.com/ezachrisen/indigo"
	"github.com/ezachrisen/indigo/cel"

	"github.com/ezachrisen/indigo/testdata/school"
)

func main() {

	education := indigo.Schema{
		Elements: []indigo.DataElement{
			{Name: "student", Type: indigo.Proto{Message: &school.Student{}}},
		},
	}
	data := map[string]interface{}{
		"student": &school.Student{
			Status: school.Student_ENROLLED,
			HousingAddress: &school.Student_OnCampus{
				&school.Student_CampusAddress{
					Building: "Hershey",
					Room:     "308",
				},
			},
		},
	}

	rule := indigo.Rule{
		Schema: education,
		Expr:   `has(student.on_campus) && student.on_campus.building == "Hershey"`,
	}

	engine := indigo.NewEngine(cel.NewEvaluator())

	err := engine.Compile(&rule)
	if err != nil {
		fmt.Printf("Error adding rule %v", err)
		return
	}

	results, err := engine.Eval(context.Background(), &rule, data)
	if err != nil {
		fmt.Printf("Error evaluating: %v", err)
		return
	}
	fmt.Println(results.ExpressionPass)
}
Output:

true
Example (ProtoTimestampAndDurationComparison)

Demonstrates writing rules on timestamps and durations

package main

import (
	"context"
	"fmt"
	"time"

	"github.com/ezachrisen/indigo"
	"github.com/ezachrisen/indigo/cel"
	"google.golang.org/protobuf/types/known/durationpb"
	"google.golang.org/protobuf/types/known/timestamppb"

	"github.com/ezachrisen/indigo/testdata/school"
)

func main() {

	education := indigo.Schema{
		Elements: []indigo.DataElement{
			{Name: "s", Type: indigo.Proto{Message: &school.Student{}}},
			{Name: "now", Type: indigo.Timestamp{}},
			{Name: "ssum", Type: indigo.Proto{Message: &school.StudentSummary{}}},
		},
	}

	data := map[string]interface{}{
		"s": &school.Student{
			EnrollmentDate: timestamppb.New(time.Date(2009, time.November, 10, 23, 0, 0, 0, time.UTC)),
		},
		"ssum": &school.StudentSummary{
			Tenure: durationpb.New(time.Duration(time.Hour * 24 * 451)),
		},
		"now": timestamppb.Now(),
	}

	rule := indigo.Rule{
		ID:     "",
		Schema: education,
		Expr: `s.enrollment_date < now
               && 
               // 12,000h = 500 days * 24 hours
               ssum.tenure > duration("12000h")
			  `,
	}

	engine := indigo.NewEngine(cel.NewEvaluator())

	err := engine.Compile(&rule)
	if err != nil {
		fmt.Printf("Error adding rule %v", err)
		return
	}

	results, err := engine.Eval(context.Background(), &rule, data)
	if err != nil {
		fmt.Printf("Error evaluating: %v", err)
		return
	}

	fmt.Println(results.ExpressionPass)
}
Output:

false
Example (ProtoTimestampComparison)

Demonstrates conversion between protobuf timestamps (google.protobuf.Timestamp) and time.Time

package main

import (
	"context"
	"fmt"
	"time"

	"github.com/ezachrisen/indigo"
	"github.com/ezachrisen/indigo/cel"
	"google.golang.org/protobuf/types/known/timestamppb"

	"github.com/ezachrisen/indigo/testdata/school"
)

func main() {

	education := indigo.Schema{
		Elements: []indigo.DataElement{
			{Name: "student", Type: indigo.Proto{Message: &school.Student{}}},
			{Name: "now", Type: indigo.Timestamp{}},
		},
	}

	data := map[string]interface{}{
		"student": &school.Student{
			// Make a protobuf timestamp from a time.Time
			EnrollmentDate: timestamppb.New(time.Date(2009, time.November, 10, 23, 0, 0, 0, time.UTC)),
			Grades:         []float64{3.0, 2.9, 4.0, 2.1},
		},
		"now": timestamppb.Now(),
	}

	// The rule will return the earlier of the two dates (enrollment date or now)
	rule := indigo.Rule{
		ID:         "grade_check",
		Schema:     education,
		ResultType: indigo.Timestamp{},
		Expr: `student.enrollment_date < now
			  ?
			  student.enrollment_date
			  :
			  now
			  `,
	}

	engine := indigo.NewEngine(cel.NewEvaluator())

	err := engine.Compile(&rule)
	if err != nil {
		fmt.Printf("Error adding rule %v", err)
		return
	}

	results, err := engine.Eval(context.Background(), &rule, data)
	if err != nil {
		fmt.Printf("Error evaluating: %v", err)
		return
	}

	if ts, ok := results.Value.(time.Time); ok {
		fmt.Printf("Gotime is %v\n", ts)
	}
}
Output:

Gotime is 2009-11-10 23:00:00 +0000 UTC
Example (ProtoTimestampPart)

Demonstrates writing rules on timestamps and durations

package main

import (
	"context"
	"fmt"
	"time"

	"github.com/ezachrisen/indigo"
	"github.com/ezachrisen/indigo/cel"
	"google.golang.org/protobuf/types/known/timestamppb"

	"github.com/ezachrisen/indigo/testdata/school"
)

func main() {

	education := indigo.Schema{
		Elements: []indigo.DataElement{
			{Name: "s", Type: indigo.Proto{Message: &school.Student{}}},
		},
	}

	data := map[string]interface{}{
		"s": &school.Student{
			EnrollmentDate: timestamppb.New(time.Date(2022, time.April, 8, 23, 0, 0, 0, time.UTC)),
		},
	}

	rule := indigo.Rule{
		ID:         "",
		ResultType: indigo.Bool{},
		Schema:     education,
		Expr:       `s.enrollment_date.getDayOfWeek() == 5 // Friday`,
	}

	engine := indigo.NewEngine(cel.NewEvaluator())

	err := engine.Compile(&rule)
	if err != nil {
		fmt.Printf("Error adding rule %v", err)
		return
	}

	results, err := engine.Eval(context.Background(), &rule, data)
	if err != nil {
		fmt.Printf("Error evaluating: %v", err)
		return
	}

	fmt.Println(results.Value)
}
Output:

true
Example (ProtoTimestampPartTZ)

Demonstrates writing rules on timestamps and durations

package main

import (
	"context"
	"fmt"
	"time"

	"github.com/ezachrisen/indigo"
	"github.com/ezachrisen/indigo/cel"
	"google.golang.org/protobuf/types/known/timestamppb"

	"github.com/ezachrisen/indigo/testdata/school"
)

func main() {

	education := indigo.Schema{
		Elements: []indigo.DataElement{
			{Name: "s", Type: indigo.Proto{Message: &school.Student{}}},
		},
	}

	data := map[string]interface{}{
		"s": &school.Student{
			EnrollmentDate: timestamppb.New(time.Date(2022, time.April, 8, 23, 0, 0, 0, time.UTC)),
		},
	}

	rule := indigo.Rule{
		ID:         "",
		ResultType: indigo.Bool{},
		Schema:     education,
		Expr:       `s.enrollment_date.getDayOfWeek("Asia/Kolkata") == 6 // Saturday`,
	}

	engine := indigo.NewEngine(cel.NewEvaluator())

	err := engine.Compile(&rule)
	if err != nil {
		fmt.Printf("Error adding rule %v", err)
		return
	}

	results, err := engine.Eval(context.Background(), &rule, data)
	if err != nil {
		fmt.Printf("Error evaluating: %v", err)
		return
	}

	fmt.Println(results.Value)
}
Output:

true
Example (StopIfParentNegative)
package main

import (
	"context"
	"fmt"

	"github.com/ezachrisen/indigo"
	"github.com/ezachrisen/indigo/cel"

	"github.com/ezachrisen/indigo/testdata/school"
)

func main() {

	education := indigo.Schema{
		ID: "education",
		Elements: []indigo.DataElement{
			{Name: "s", Type: indigo.Proto{Message: &school.Student{}}},
		},
	}

	data := map[string]interface{}{
		"s": &school.Student{
			Id:      927312,
			Age:     21,
			Credits: 16,
			Gpa:     3.1,
			Attrs:   map[string]string{"major": "Accounting", "home_town": "Chicago"},
			Status:  school.Student_ENROLLED,
			Grades:  []float64{3, 3, 4, 2, 3, 3.5, 4},
		},
	}

	root := indigo.NewRule("root", "")
	root.Schema = education
	accounting := indigo.NewRule("accounting_majors_only", `s.attrs.exists(k, k == "major" && s.attrs[k] == "Accounting")`)
	accounting.Schema = education
	accounting.EvalOptions.StopIfParentNegative = true

	root.Rules[accounting.ID] = accounting

	accounting.Rules["honors"] = &indigo.Rule{
		ID:     "honors",
		Schema: education,
		Expr:   "s.gpa > 3.0",
	}

	accounting.Rules["at_risk"] = &indigo.Rule{
		ID:     "at_risk",
		Schema: education,
		Expr:   "s.gpa < 2.0",
	}

	accounting.Rules["rookie"] = &indigo.Rule{
		ID:     "rookie",
		Schema: education,
		Expr:   "s.credits < 5",
	}

	engine := indigo.NewEngine(cel.NewEvaluator())

	err := engine.Compile(root)
	if err != nil {
		fmt.Printf("Error adding rule %v", err)
		return
	}
	//	fmt.Println(root)
	results, err := engine.Eval(context.Background(), root, data)
	if err != nil {
		fmt.Printf("Error evaluating: %v", err)
		return
	}

	for k, v := range results.Results["accounting_majors_only"].Results {
		fmt.Printf("%s? %t\n", k, v.ExpressionPass)
	}
	//fmt.Println(results)

}
Output:

rookie? false
honors? true
at_risk? false

Index

Examples

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type CelOption added in v0.6.8

type CelOption func(e *Evaluator)

func FixedSchema added in v0.6.8

func FixedSchema(schema *indigo.Schema) CelOption

FixedSchema mandates that the evaluator will use this schema for all compilations and evaluations. Schemas set on rules will be ignored. CEL's process to create a celgo.Env from a schema is time consuming; setting the FixedSchema option reduces compilation time by 25% or more. The schema will be evaluated the first time compilation runs.

type Evaluator added in v0.6.2

type Evaluator struct {
	// contains filtered or unexported fields
}

Evaluator implements the indigo.ExpressionEvaluator and indigo.ExpressionCompiler interfaces. It uses the CEL-Go package to compile and evaluate rules.

func NewEvaluator

func NewEvaluator(opts ...CelOption) *Evaluator

NewEvaluator creates a new CEL Evaluator. The evaluator contains internal data used to facilitate CEL expression evaluation.

func (*Evaluator) Compile added in v0.6.2

func (e *Evaluator) Compile(expr string, s indigo.Schema, resultType indigo.Type, collectDiagnostics bool, _ bool) (interface{}, error)

Compile checks a rule, prepares a compiled CELProgram, and stores the program in rule.Program. CELProgram contains the compiled program used to evaluate the rules, and if we're collecting diagnostics, CELProgram also contains the CEL AST to provide type and symbol information in diagnostics.

Any errors in compilation are returned with a nil program

func (*Evaluator) Evaluate added in v0.6.2

func (*Evaluator) Evaluate(data map[string]interface{}, expr string, _ indigo.Schema, _ interface{},
	evalData interface{}, expectedResultType indigo.Type, returnDiagnostics bool) (interface{}, *indigo.Diagnostics, error)

Evaluate a rule against the input data. Called by indigo.Engine.Evaluate for the rule and its children.

Jump to

Keyboard shortcuts

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