Install Expr in Go
masterTo use Expr in your Go project, install it using go get:
go get github.com/expr-lang/exprrepository·master·Indexed 27 days ago
https://github.com/expr-lang/exprA Go-centric expression language for dynamic configurations providing a safe, fast, and type-checked way to evaluate expressions. It features a compiler (expr.Compile) and runtime (expr.Run), support for struct and map environments, built-in collection functions (all, none, any, one, filter, map), and a terminal-based interactive debugger. It includes advanced configuration options for return type verification, constant folding via expr.ConstExpr, and documentation generation through the docgen package.
To use Expr in your Go project, install it using go get:
go get github.com/expr-lang/exprYou can provide variables to an expression by passing a map[string]any as the environment to both expr.Compile and expr.Run. To enable type safety and allow Expr to infer types, you must pass the environment to expr.Compile using the expr.Env(env) option.
env := map[string]any{
"foo": 100,
"bar": 200,
}
// Pass env to Compile via expr.Env for type checking
program, err := expr.Compile(`foo + bar`, expr.Env(env))
if err != nil {
panic(err)
}
// Pass the actual data to Run
output, err := expr.Run(program, env)
if err != nil {
panic(err)
}
fmt.Print(output) // 300To generate documentation as a Markdown string, call the .Markdown() method on the documentation object returned by docgen.CreateDoc(env).
package main
import "github.com/expr-lang/expr/docgen"
func main() {
// TODO: Replace env with your own types.
doc := docgen.CreateDoc(env)
print(doc.Markdown())
}The $env variable is a map containing all variables passed to the expression. It can be used to check if a variable is defined or to access variables with special names (like those containing spaces).
foo.Name == $env["foo"].Name
$env["var with spaces"]
// Check if a variable is defined
'foo' in $envPredicates are expressions used in functions like filter, all, any, one, and none.
{} braces to define a predicate block.# to refer to the current item in a predicate (e.g., {# % 2 == 0}).# and use . (e.g., .Value).let to capture the outer scope variable.// Filter even numbers from a range
filter(0..9, {# % 2 == 0})
// Accessing fields in a collection of structs
filter(tweets, len(.Content) > 240)
// Nested predicate using let to capture outer scope
filter(posts, {
let post = #;
any(.Comments, .Author == post.Author)
})Use the docgen.CreateDoc(env) function to generate documentation for your Expr environment. Pass your environment type (the object used in your Expr expressions) to CreateDoc. The resulting documentation object can then be marshaled into JSON using standard Go encoding packages.
package main
import (
"encoding/json"
"fmt"
"github.com/expr-lang/expr/docgen"
)
func main() {
// TODO: Replace env with your own types.
doc := docgen.CreateDoc(env)
buf, err := json.MarshalIndent(doc, "", " ")
if err != nil {
panic(err)
}
fmt.Println(string(buf))
}For complex transformations, such as converting arithmetic operators into function calls for specific types (e.g., Decimal), you can implement a visitor that inspects node types and operators.
When constructing new nodes (like ast.CallNode) within a patcher, you must manually call (*node).SetType(type) on the node. This ensures the patcher can be applied recursively by providing the necessary type information for subsequent visits.
type DecimalPatcher struct{}
var decimalType = reflect.TypeOf(Decimal{})
func (DecimalPatcher) Visit(node *ast.Node) {
if n, ok := (*node).(*ast.BinaryNode); ok && n.Operator == "+" {
if !n.Left.Type().AssignableTo(decimalType) {
return
}
if !n.Right.Type().AssignableTo(decimalType) {
return
}
callNode := &ast.CallNode{
Callee: &ast.IdentifierNode{Value: "add"},
Arguments: []ast.Node{n.Left, n.Right},
}
ast.Patch(node, callNode)
(*node).SetType(decimalType)
}
}
// Usage
env := map[string]interface{}{
"a": Decimal{1},
"b": Decimal{2},
"c": Decimal{3},
"add": func(x, y Decimal) Decimal { return Decimal{x.Value + y.Value} },
}
program, err := expr.Compile(`a + b + c`, expr.Env(env), expr.Patch(DecimalPatcher{}))You can traverse the Abstract Syntax Tree (AST) of an expression before compilation using the Visitor interface. To use it, implement the Visit(*ast.Node) method. This method is called for every node in the AST, allowing you to collect information, modify the expression, or generate new ones.
To traverse the tree, use the ast.Walk function, passing a pointer to the root node and your Visitor implementation.
type Visitor struct {
Identifiers []string
}
func (v *Visitor) Visit(node *ast.Node) {
if n, ok := (*node).(*ast.IdentifierNode); ok {
v.Identifiers = append(v.Identifiers, n.Value)
}
}
tree, err := parser.Parse(`foo + bar`)
if err != nil {
panic(err)
}
v := &Visitor{}
ast.Walk(&tree.Node, v)
fmt.Println(v.Identifiers) // [foo, bar]The simplest way to add custom functions to Expr is by including them in the environment map passed to the evaluator. You can map a string name to a Go function.
env := map[string]any{
"add": func(a, b int) int {
return a + b
},
}?.)Access a field or map item without checking for nil. If the parent is nil, the expression returns nil instead of an error.
??)Returns the left-hand side if it is not nil; otherwise, returns the right-hand side.
You can provide a Go struct as the environment for an expression. Expr uses reflection to expose the struct's fields and methods as variables and functions within the expression.
expr struct tag to rename a field for use in the expression.To use a struct, pass an instance of it to expr.Env() during compilation, and ensure you pass the same struct type during execution.
type Env struct {
UpdatedAt time.Time
Posts []Post
Map map[string]string `expr:"tags"` // Renamed to 'tags' in expression
}
func (Env) Format(t time.Time) string {
return t.Format(time.RFC822)
}
// Compilation
program, err := expr.Compile(code, expr.Env(Env{}))
// Execution
output, err := expr.Run(program, Env{
UpdatedAt: time.Now(),
Posts: []Post{{Title: "Hello, World!"}},
Map: map[string]string{"tag1": "value1"},
})Use the . operator for struct fields or the [] operator for map items. They are equivalent for field access.
For arrays and slices, use []. Negative indices are supported (e.g., -1 for the last element).
user.Name
user["Name"]
array[0] // first element
array[-1] // last element