1. Введение

Expr - это динамическое решение для конфигурации, разработанное для языка Go и известное своим простым синтаксисом и мощными функциями производительности. Основой движка выражений Expr является безопасность, скорость и интуитивность, что делает его подходящим для сценариев, таких как контроль доступа, фильтрация данных и управление ресурсами. Применение Expr в Go значительно повышает способность приложений обрабатывать динамические правила. В отличие от интерпретаторов или скриптовых движков в других языках, Expr использует статическую проверку типов и генерацию байткода для выполнения, обеспечивая как производительность, так и безопасность.

2. Установка Expr

Движок выражений Expr можно установить с помощью инструмента управления пакетами языка Go go get:

go get github.com/expr-lang/expr

Эта команда загрузит файлы библиотеки Expr и установит их в ваш проект Go, позволяя импортировать и использовать Expr в вашем коде Go.

3. Быстрый старт

3.1 Компиляция и запуск базовых выражений

Давайте начнем с базового примера: написание простого выражения, его компиляция и выполнение для получения результата.

package main

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

func main() {
	// Компиляция основного выражения сложения
	program, err := expr.Compile(`2 + 2`)
	if err != nil {
		panic(err)
	}

	// Запуск скомпилированного выражения без передачи окружения, так как здесь не требуются переменные
	output, err := expr.Run(program, nil)
	if err != nil {
		panic(err)
	}

	// Вывод результата
	fmt.Println(output)  // Выводит 4
}

В этом примере выражение 2 + 2 компилируется в исполняемый байткод, который затем выполняется для получения результата.

3.2 Использование переменных в выражениях

Затем создадим окружение, содержащее переменные, напишем выражение, использующее эти переменные, скомпилируем и выполним это выражение.

package main

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

func main() {
	// Создание окружения с переменными
	env := map[string]interface{}{
		"foo": 100,
		"bar": 200,
	}

	// Компиляция выражения, использующего переменные из окружения
	program, err := expr.Compile(`foo + bar`, expr.Env(env))
	if err != nil {
		panic(err)
	}

	// Выполнение выражения
	output, err := expr.Run(program, env)
	if err != nil {
		panic(err)
	}

	// Вывод результата
	fmt.Println(output)  // Выводит 300
}

В этом примере окружение env содержит переменные foo и bar. Выражение foo + bar выводит типы foo и bar из окружения во время компиляции и использует их значения во время выполнения для вычисления результата выражения.

4. Синтаксис Expr в деталях

4.1 Переменные и литералы

Движок выражений Expr может обрабатывать литералы общих типов данных, включая числа, строки и логические значения. Литералы - это значения данных, напрямую записанные в коде, такие как 42, "привет" и true.

Числа

В Expr можно напрямую записывать целые и числа с плавающей точкой:

42      // Представляет целое число 42
3.14    // Представляет число с плавающей точкой 3.14

Строки

Строковые литералы заключаются в двойные кавычки " или обратные кавычки ``. Например:

"привет, мир" // Строка, заключенная в двойные кавычки, поддерживает символы экранирования
`привет, мир` // Строка, заключенная в обратные кавычки, сохраняет формат строки без поддержки символов экранирования

Булевы значения

Существуют только два логических значения, true и false, представляющие логическое и ложное значения:

true   // Логическое true
false  // Логическое false

Переменные

Expr также позволяет определять переменные в окружении, а затем ссылаться на эти переменные в выражении. Например:

env := map[string]interface{}{
    "возраст": 25,
    "имя": "Алиса",
}

Затем в выражении можно ссылаться на возраст и имя:

возраст > 18  // Проверить, больше ли возраст 18 лет
имя == "Алиса"  // Определить, равно ли имя "Алисе"

4.2 Операторы

Движок выражений Expr поддерживает различные операторы, включая арифметические операторы, логические операторы, операторы сравнения и операторы множеств и т. д.

Арифметические и логические операторы

Арифметические операторы включают в себя сложение (+), вычитание (-), умножение (*), деление (/) и взятие остатка (%). Логические операторы включают в себя логическое И (&&), логическое ИЛИ (||) и логическое НЕ (!), например:

2 + 2 // Результат 4
7 % 3 // Результат 1
!true // Результат false
возраст >= 18 && имя == "Alice" // Проверить, что возраст не меньше 18 и что имя равно "Alice"

Операторы сравнения

Операторы сравнения включают равно (==), не равно (!=), меньше (<), меньше или равно (<=), больше (>), и больше или равно (>=), используемые для сравнения двух значений:

возраст == 25 // Проверить, что возраст равен 25
возраст != 18 // Проверить, что возраст не равен 18
возраст > 20  // Проверить, что возраст больше 20

Операторы для работы с множествами

Expr также предоставляет некоторые операторы для работы с множествами, такие как in для проверки, принадлежит ли элемент множеству. Множествами могут быть массивы, срезы или карты:

"user" in ["user", "admin"]  // true, потому что "user" есть в массиве
3 in {1: true, 2: false}     // false, потому что 3 не является ключом в карте

Также имеются некоторые продвинутые функции операций с множествами, такие как all, any, one и none, требующие использования анонимных функций (лямбда):

all(tweets, {.Len <= 240})  // Проверить, что поле Len во всех твитах не превышает 240
any(tweets, {.Len > 200})   // Проверить, есть ли поле Len в твитах, превышающее 200

Оператор члена

В языке выражений Expr оператор члена позволяет нам получить доступ к свойствам struct в языке Go. Эта функция позволяет Expr напрямую манипулировать сложными структурами данных, что делает его очень гибким и практичным.

Использование оператора члена очень просто, просто используйте оператор . перед именем свойства. Например, если у нас есть следующая struct:

type User struct {
    Name string
    Age  int
}

Вы можете написать выражение для доступа к свойству Name структуры User следующим образом:

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) // Вывод: Alice

Обработка значений null

При доступе к свойствам вы можете столкнуться с ситуациями, когда объект является nil. Expr предоставляет безопасный доступ к свойствам, поэтому даже если структура или вложенное свойство является nil, это не вызовет ошибку времени выполнения.

Используйте оператор ?. для ссылки на свойства. Если объект является nil, он вернет nil вместо вызова ошибки.

author.User?.Name

Эквивалентное выражение

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

Использование оператора ?? прежде всего для возврата значений по умолчанию:

author.User?.Name ?? "Anonymous"

Эквивалентное выражение

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

Оператор "Pipe"

Оператор "Pipe" (|) в Expr используется для передачи результата одного выражения в качестве параметра другому выражению. Это аналогично операции "pipe" в оболочке Unix, позволяя объединять несколько функциональных модулей для создания конвейера обработки. В Expr его использование позволяет создавать более понятные и краткие выражения.

Например, если у нас есть функция получения имени пользователя и шаблон приветственного сообщения:

env := map[string]interface{}{
    "user":      User{Name: "Bob", Age: 30},
    "get_name":  func(u User) string { return u.Name },
    "greet_msg": "Привет, %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) // Вывод: Привет, Bob!

В этом примере мы сначала получаем имя пользователя с помощью get_name(user), затем передаем имя функции sprintf, используя оператор "Pipe" |, для создания окончательного приветственного сообщения.

Использование оператора "Pipe" позволяет модуляризировать наш код, улучшить его повторное использование и сделать выражения более читаемыми.

4.3 Функции

Expr поддерживает встроенные функции и пользовательские функции, что делает выражения более мощными и гибкими.

Использование встроенных функций

Встроенные функции, такие как len, all, none, any и т. д., могут быть использованы напрямую в выражении.

// Пример использования встроенной функции
program, err := expr.Compile(`all(users, {.Age >= 18})`, expr.Env(env))
if err != nil {
    panic(err)
}

// Примечание: здесь env должен содержать переменную users, и каждый пользователь должен иметь свойство Age
output, err := expr.Run(program, env)
fmt.Print(output) // Если все пользователи в env достигли 18 лет или старше, вернется true

Как определить и использовать пользовательские функции

В Expr вы можете создавать пользовательские функции, передавая определения функций в карту окружения.

// Пример пользовательской функции
env := map[string]interface{}{
    "greet": func(name string) string {
        return fmt.Sprintf("Привет, %s!", name)
    },
}

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

output, err := expr.Run(program, env)
fmt.Print(output) // Возвращает Привет, Мир!

При использовании функций в Expr вы можете модуляризировать свой код и включать сложную логику в выражения. Путем объединения переменных, операторов и функций Expr становится мощным и простым в использовании инструментом. Помните всегда обеспечивать безопасность типов при построении окружения Expr и выполнении выражений.

5. Документация по встроенным функциям

Движок выражений Expr предоставляет разработчикам широкий набор встроенных функций для обработки различных сложных сценариев. Ниже мы подробно рассмотрим эти встроенные функции и их использование.

all

Функция all может использоваться для проверки, удовлетворяют ли все элементы коллекции заданному условию. Она принимает два параметра: коллекцию и выражение условия.

// Проверяем, удовлетворяют ли все твиты условию длины содержимого менее 240 символов
code := `all(tweets, len(.Content) < 240)`

any

Аналогично all, функция any используется для проверки, удовлетворяет ли какой-либо элемент коллекции условию.

// Проверяем, удовлетворяет ли какой-либо твит условию длины содержимого более 240 символов
code := `any(tweets, len(.Content) > 240)`

none

Функция none используется для проверки, удовлетворяет ли какой-либо элемент коллекции заданному условию.

// Убеждаемся, что ни один твит не повторяется
code := `none(tweets, .IsRepeated)`

one

Функция one используется для подтверждения того, что только один элемент коллекции удовлетворяет условию.

// Проверяем, содержит ли только один твит определенное ключевое слово
code := `one(tweets, contains(.Content, "ключевое слово"))`

filter

Функция filter используется для фильтрации элементов коллекции, удовлетворяющих заданному условию.

// Фильтрация всех твитов, отмеченных как приоритетные
code := `filter(tweets, .IsPriority)`

map

Функция map используется для преобразования элементов коллекции в соответствии с указанным методом.

// Форматирование времени публикации всех твитов
code := `map(tweets, {.PublishTime: Format(.Date)})`

len

Функция len используется для возврата длины коллекции или строки.

// Получение длины имени пользователя
code := `len(username)`

contains

Функция contains используется для проверки, содержит ли строка подстроку или содержит ли коллекция определенный элемент.

// Проверка, содержит ли имя пользователя недопустимые символы
code := `contains(username, "недопустимые символы")`

Упомянутые выше функции представляют лишь часть встроенных функций, предоставляемых движком выражений Expr. С их помощью вы можете более гибко и эффективно обрабатывать данные и логику. Для более подробного списка функций и инструкций по использованию обратитесь к официальной документации по Expr.