1. Introduction

Expr is a dynamic configuration solution designed for the Go language, known for its simple syntax and powerful performance features. The core of the Expr expression engine is focused on safety, speed, and intuitiveness, making it suitable for scenarios such as access control, data filtering, and resource management. When applied to Go, Expr greatly enhances the ability of applications to handle dynamic rules. Unlike interpreters or script engines in other languages, Expr adopts static type checking and generates bytecode for execution, ensuring both performance and security.

2. Installing Expr

You can install the Expr expression engine using the Go language's package management tool go get:

go get github.com/expr-lang/expr

This command will download the Expr library files and install them into your Go project, allowing you to import and use Expr in your Go code.

3. Quick Start

3.1 Compiling and Running Basic Expressions

Let's start with a basic example: writing a simple expression, compiling it, and then running it to obtain the result.

package main

import (
	"fmt"
	"github.com/expr-lang/expr"
)

func main() {
	// Compiling a basic addition expression
	program, err := expr.Compile(`2 + 2`)
	if err != nil {
		panic(err)
	}

	// Running the compiled expression without passing an environment, as no variables are needed here
	output, err := expr.Run(program, nil)
	if err != nil {
		panic(err)
	}

	// Printing the result
	fmt.Println(output)  // Outputs 4
}

In this example, the expression 2 + 2 is compiled into executable bytecode, which is then executed to produce the output.

3.2 Using Variable Expressions

Next, we'll create an environment containing variables, write an expression that uses these variables, compile and run this expression.

package main

import (
	"fmt"
	"github.com/expr-lang/expr"
)

func main() {
	// Creating an environment with variables
	env := map[string]interface{}{
		"foo": 100,
		"bar": 200,
	}

	// Compiling an expression that uses variables from the environment
	program, err := expr.Compile(`foo + bar`, expr.Env(env))
	if err != nil {
		panic(err)
	}

	// Running the expression
	output, err := expr.Run(program, env)
	if err != nil {
		panic(err)
	}

	// Printing the result
	fmt.Println(output)  // Outputs 300
}

In this example, the environment env contains variables foo and bar. The expression foo + bar infers the types of foo and bar from the environment during compilation, and uses the values of these variables at runtime to evaluate the expression result.

4. Expr Syntax in Detail

4.1 Variables and Literals

The Expr expression engine can handle common data type literals, including numbers, strings, and boolean values. Literals are data values directly written in the code, such as 42, "hello", and true.

Numbers

In Expr, you can directly write integers and floating point numbers:

42      // Represents the integer 42
3.14    // Represents the floating point number 3.14

Strings

String literals are enclosed in double quotes " or backticks ``. For example:

"hello, world" // String enclosed in double quotes, supports escape characters
`hello, world` // String enclosed in backticks, maintains the string format without support for escape characters

Booleans

There are only two boolean values, true and false, representing logical true and false:

true   // Boolean true value
false  // Boolean false value

Variables

Expr also allows the definition of variables in the environment, and then references these variables in the expression. For example:

env := map[string]interface{}{
    "age": 25,
    "name": "Alice",
}

Then in the expression, you can refer to age and name:

age > 18  // Check if age is greater than 18
name == "Alice"  // Determine if name is equal to "Alice"

4.2 Operators

The Expr expression engine supports various operators, including arithmetic operators, logical operators, comparison operators, and set operators, etc.

Arithmetic and Logical Operators

Arithmetic operators include addition (+), subtraction (-), multiplication (*), division (/), and modulo (%). Logical operators include logical AND (&&), logical OR (||), and logical NOT (!), for example:

2 + 2 // Result is 4
7 % 3 // Result is 1
!true // Result is false
age >= 18 && name == "Alice" // Check if age is not less than 18 and if name is equal to "Alice"

Comparison Operators

Comparison operators include equal to (==), not equal to (!=), less than (<), less than or equal to (<=), greater than (>), and greater than or equal to (>=), used to compare two values:

age == 25 // Check if age is equal to 25
age != 18 // Check if age is not equal to 18
age > 20  // Check if age is greater than 20

Set Operators

Expr also provides some operators for working with sets, such as in to check if an element is in the set. Sets can be arrays, slices, or maps:

"user" in ["user", "admin"]  // true, because "user" is in the array
3 in {1: true, 2: false}     // false, because 3 is not a key in the map

There are also some advanced set operation functions, such as all, any, one, and none, which require the use of anonymous functions (lambda):

all(tweets, {.Len <= 240})  // Check if the Len field of all tweets does not exceed 240
any(tweets, {.Len > 200})   // Check if there exists a Len field in tweets that exceeds 200

Member Operator

In the Expr expression language, the member operator allows us to access the properties of the struct in Go language. This feature enables Expr to directly manipulate complex data structures, making it very flexible and practical.

Using the member operator is very simple, just use the . operator followed by the property name. For example, if we have the following struct:

type User struct {
    Name string
    Age  int
}

You can write an expression to access the Name property of the User structure like this:

env := map[string]interface{}{
    "user": User{Name: "Alice", Age: 25},
}

code := `user.Name`

program, err := expr.Compile(code, expr.Env(env))
if err != nil {
    panic(err)
}

output, err := expr.Run(program, env)
if err != nil {
    panic(err)
}

fmt.Println(output) // Output: Alice

Handling nil Values

When accessing properties, you may encounter situations where the object is nil. Expr provides safe property access, so even if the struct or nested property is nil, it will not throw a runtime panic error.

Use the ?. operator to reference properties. If the object is nil, it will return nil instead of throwing an error.

author.User?.Name

Equivalent expression

author.User != nil ? author.User.Name : nil

Using the ?? operator is primarily for returning default values:

author.User?.Name ?? "Anonymous"

Equivalent expression

author.User != nil ? author.User.Name : "Anonymous"

Pipe Operator

The pipe operator (|) in Expr is used to pass the result of one expression as a parameter to another expression. This is similar to the pipe operation in Unix shell, allowing multiple functional modules to be chained together to form a processing pipeline. In Expr, using it can create more clear and concise expressions.

For example, if we have a function to get a user's name and a template for a welcome message:

env := map[string]interface{}{
    "user":      User{Name: "Bob", Age: 30},
    "get_name":  func(u User) string { return u.Name },
    "greet_msg": "Hello, %s!",
}

code := `get_name(user) | sprintf(greet_msg)`

program, err := expr.Compile(code, expr.Env(env))
if err != nil {
    panic(err)
}

output, err := expr.Run(program, env)
if err != nil {
    panic(err)
}

fmt.Println(output) // Output: Hello, Bob!

In this example, we first get the user's name through get_name(user), then pass the name to the sprintf function using the pipe operator | to generate the final welcome message.

Using the pipe operator can modularize our code, improve code reusability, and make the expressions more readable.

4.3 Functions

Expr supports built-in functions and custom functions, making expressions more powerful and flexible.

Using Built-in Functions

Built-in functions like len, all, none, any, etc. can be used directly in the expression.

// Example of using a built-in function
program, err := expr.Compile(`all(users, {.Age >= 18})`, expr.Env(env))
if err != nil {
    panic(err)
}

// Note: here the env needs to contain the users variable, and each user must have the Age property
output, err := expr.Run(program, env)
fmt.Print(output) // If all users in env are aged 18 or above, it will return true

How to define and use custom functions

In Expr, you can create custom functions by passing function definitions in the environment mapping.

// Custom function example
env := map[string]interface{}{
    "greet": func(name string) string {
        return fmt.Sprintf("Hello, %s!", name)
    },
}

program, err := expr.Compile(`greet("World")`, expr.Env(env))
if err != nil {
    panic(err)
}

output, err := expr.Run(program, env)
fmt.Print(output) // Returns Hello, World!

When using functions in Expr, you can modularize your code and incorporate complex logic into the expressions. By combining variables, operators, and functions, Expr becomes a powerful and easy-to-use tool. Remember to always ensure type safety when building the Expr environment and running expressions.

5. Built-in Function Documentation

Expr expression engine provides developers with a rich set of built-in functions to handle various complex scenarios. Below, we will detail these built-in functions and their usage.

all

The all function can be used to check if all elements in a collection satisfy a given condition. It takes two parameters: the collection and the condition expression.

// Checking if all tweets have a content length less than 240
code := `all(tweets, len(.Content) < 240)`

any

Similar to all, the any function is used to check if any element in a collection satisfies a condition.

// Checking if any tweet has a content length greater than 240
code := `any(tweets, len(.Content) > 240)`

none

The none function is used to check if no element in a collection satisfies a condition.

// Ensuring no tweets are repeated
code := `none(tweets, .IsRepeated)`

one

The one function is used to confirm that only one element in a collection satisfies a condition.

// Checking if only one tweet contains a specific keyword
code := `one(tweets, contains(.Content, "keyword"))`

filter

The filter function can filter out collection elements that satisfy a given condition.

// Filtering out all tweets marked as priority
code := `filter(tweets, .IsPriority)`

map

The map function is used to transform elements in a collection according to a specified method.

// Formatting the publish time of all tweets
code := `map(tweets, {.PublishTime: Format(.Date)})`

len

The len function is used to return the length of a collection or string.

// Getting the length of the username
code := `len(username)`

contains

The contains function is used to check whether a string contains a substring or if a collection contains a specific element.

// Checking if the username contains illegal characters
code := `contains(username, "illegal characters")`

The ones mentioned above are just a part of the built-in functions provided by the Expr expression engine. With these powerful functions, you can handle data and logic more flexibly and efficiently. For a more detailed list of functions and usage instructions, please refer to the official Expr documentation.