expr-lang/expr

repository·master·Indexed 27 days ago

https://github.com/expr-lang/expr

A 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.

Tokens
9.4K
Snippets
37
Records
57
Agent score
93%

What's inside expr

  1. Pass variables using a map environment

    master

    You 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) // 300
  2. Generate documentation in Markdown format with DocGen

    master

    To 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())
    }
  3. Access All Variables via `$env`

    master

    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 $env
  4. Use Predicates in Collection Functions

    master

    Predicates are expressions used in functions like filter, all, any, one, and none.

    • Use {} braces to define a predicate block.
    • Use # to refer to the current item in a predicate (e.g., {# % 2 == 0}).
    • If items are structs/maps, you can omit # and use . (e.g., .Value).
    • In nested predicates, use 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)
    })
  5. Generate documentation in JSON format with DocGen

    master

    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))
    }
  6. Transform expressions into function calls with custom Patchers

    master

    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{}))
  7. Implement a Visitor to traverse the AST

    master

    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]
  8. Define custom functions via the environment map

    master

    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
        },
    }
  9. Use Optional Chaining and Nil Coalescing

    master

    Optional Chaining (?.)

    Access a field or map item without checking for nil. If the parent is nil, the expression returns nil instead of an error.

    Nil Coalescing (??)

    Returns the left-hand side if it is not nil; otherwise, returns the right-hand side.

  10. Use a struct as an environment

    master

    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.

    • Fields: Accessible as variables. Use the expr struct tag to rename a field for use in the expression.
    • Methods: Accessible as functions.
    • Embedded Structs: Methods defined on embedded structs are also accessible.

    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"},
    })
  11. Access Fields and Map Items

    master

    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