1. Introdução

Expr é uma solução de configuração dinâmica projetada para a linguagem Go, conhecida por sua sintaxe simples e poderosas características de desempenho. O núcleo do motor de expressão Expr é focado em segurança, velocidade e intuitividade, tornando-o adequado para cenários como controle de acesso, filtragem de dados e gerenciamento de recursos. Ao ser aplicado ao Go, o Expr melhora consideravelmente a capacidade de aplicativos lidarem com regras dinâmicas. Ao contrário de interpretadores ou motores de script em outras linguagens, o Expr adota verificação de tipo estático e gera bytecode para execução, garantindo desempenho e segurança.

2. Instalando o Expr

Você pode instalar o motor de expressão Expr usando a ferramenta de gerenciamento de pacotes da linguagem Go go get:

go get github.com/expr-lang/expr

Este comando irá baixar os arquivos da biblioteca Expr e instalá-los em seu projeto Go, permitindo que você importe e use o Expr em seu código Go.

3. Início Rápido

3.1 Compilando e Executando Expressões Básicas

Vamos começar com um exemplo básico: escrever uma expressão simples, compilá-la e depois executá-la para obter o resultado.

package main

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

func main() {
	// Compilando uma expressão básica de adição
	program, err := expr.Compile(`2 + 2`)
	if err != nil {
		panic(err)
	}

	// Executando a expressão compilada sem passar um ambiente, pois aqui não são necessárias variáveis
	output, err := expr.Run(program, nil)
	if err != nil {
		panic(err)
	}

	// Imprimindo o resultado
	fmt.Println(output)  // Saída 4
}

Neste exemplo, a expressão 2 + 2 é compilada em bytecode executável, que é então executado para produzir a saída.

3.2 Usando Expressões com Variáveis

A seguir, vamos criar um ambiente contendo variáveis, escrever uma expressão que usa essas variáveis, compilar e executar essa expressão.

package main

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

func main() {
	// Criando um ambiente com variáveis
	env := map[string]interface{}{
		"foo": 100,
		"bar": 200,
	}

	// Compilando uma expressão que usa variáveis do ambiente
	program, err := expr.Compile(`foo + bar`, expr.Env(env))
	if err != nil {
		panic(err)
	}

	// Executando a expressão
	output, err := expr.Run(program, env)
	if err != nil {
		panic(err)
	}

	// Imprimindo o resultado
	fmt.Println(output)  // Saída 300
}

Neste exemplo, o ambiente env contém as variáveis foo e bar. A expressão foo + bar infere os tipos de foo e bar do ambiente durante a compilação e usa os valores dessas variáveis em tempo de execução para avaliar o resultado da expressão.

4. Sintaxe do Expr em Detalhes

4.1 Variáveis e Literais

O motor de expressão Expr pode lidar com literais de tipos de dados comuns, incluindo números, strings e valores booleanos. Literais são valores de dados escritos diretamente no código, como 42, "hello" e true.

Números

Em Expr, você pode escrever diretamente números inteiros e de ponto flutuante:

42      // Representa o número inteiro 42
3.14    // Representa o número de ponto flutuante 3.14

Strings

Literais de string são de estilo de citação dupla " ou crase ``. Por exemplo:

"olá, mundo" // String entre aspas duplas, suporta caracteres de escape
`olá, mundo` // String entre crases, mantém o formato da string sem suporte a caracteres de escape

Booleanos

Existem apenas dois valores booleanos, true e false, representando verdadeiro e falso logicamente:

true   // Valor booleano verdadeiro
false  // Valor booleano falso

Variáveis

O Expr também permite a definição de variáveis no ambiente e depois referenciar essas variáveis na expressão. Por exemplo:

env := map[string]interface{}{
    "idade": 25,
    "nome": "Alice",
}

Então na expressão, você pode se referir a idade e nome:

idade > 18  // Verifica se a idade é maior que 18
nome == "Alice"  // Determina se o nome é igual a "Alice"

4.2 Operadores

O motor de expressão Expr suporta vários operadores, incluindo operadores aritméticos, operadores lógicos, operadores de comparação e operadores de conjunto, etc.

Operadores Aritméticos e Lógicos

Os operadores aritméticos incluem adição (+), subtração (-), multiplicação (*), divisão (/) e módulo (%). Os operadores lógicos incluem E lógico (&&), OU lógico (||) e NÃO lógico (!), por exemplo:

2 + 2 // Resultado é 4
7 % 3 // Resultado é 1
!true // Resultado é falso
idade >= 18 && nome == "Alice" // Verifica se a idade não é menor que 18 e se o nome é igual a "Alice"

Operadores de Comparação

Os operadores de comparação incluem igual a (==), diferente de (!=), menor que (<), menor ou igual a (<=), maior que (>) e maior ou igual a (>=), usados para comparar dois valores:

idade == 25 // Verifica se a idade é igual a 25
idade != 18 // Verifica se a idade não é igual a 18
idade > 20  // Verifica se a idade é maior que 20

Operadores de Conjunto

Expr também fornece alguns operadores para trabalhar com conjuntos, como in para verificar se um elemento está no conjunto. Conjuntos podem ser arrays, slices ou maps:

"user" in ["user", "admin"]  // true, porque "user" está no array
3 in {1: true, 2: false}     // false, porque 3 não é uma chave no mapa

Também existem algumas funções avançadas de operações de conjunto, como all, any, one e none, que exigem o uso de funções anônimas (lambda):

all(tweets, {.Len <= 240})  // Verifica se o campo Len de todos os tweets não excede 240
any(tweets, {.Len > 200})   // Verifica se existe um campo Len em tweets que excede 200

Operador de Membro

Na linguagem de expressão Expr, o operador de membro nos permite acessar as propriedades da struct na linguagem Go. Essa funcionalidade permite ao Expr manipular diretamente estruturas de dados complexas, tornando-o muito flexível e prático.

Usar o operador de membro é muito simples, basta usar o operador . seguido pelo nome da propriedade. Por exemplo, se tivermos a seguinte struct:

type Usuario struct {
    Nome string
    Idade int
}

Você pode escrever uma expressão para acessar a propriedade Nome da estrutura Usuario assim:

env := map[string]interface{}{
    "usuario": Usuario{Nome: "Alice", Idade: 25},
}

codigo := `usuario.Nome`

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

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

fmt.Println(saida) // Saída: Alice

Manipulação de Valores Nulos

Ao acessar propriedades, você pode encontrar situações em que o objeto é nil. Expr fornece acesso seguro a propriedades, portanto, mesmo que a estrutura ou propriedade aninhada seja nil, não lançará um erro de pânico em tempo de execução.

Use o operador ?. para fazer referência a propriedades. Se o objeto for nulo, ele retornará nulo em vez de lançar um erro.

autor.Usuario?.Nome

Expressão equivalente

autor.Usuario != nil ? autor.Usuario.Nome : nil

O uso do operador ?? é principalmente para retornar valores padrão:

autor.Usuario?.Nome ?? "Anônimo"

Expressão equivalente

autor.Usuario != nil ? autor.Usuario.Nome : "Anônimo"

Operador de tubulação

O operador de tubulação (|) em Expr é usado para passar o resultado de uma expressão como parâmetro para outra expressão. Isso é semelhante à operação de tubulação no shell Unix, permitindo que vários módulos funcionais sejam encadeados para formar um pipeline de processamento. Em Expr, seu uso pode criar expressões mais claras e concisas.

Por exemplo, se tivermos uma função para obter o nome de um usuário e um modelo para uma mensagem de boas-vindas:

env := map[string]interface{}{
    "user":      User{Name: "Bob", Age: 30},
    "get_name":  func(u User) string { return u.Name },
    "greet_msg": "Olá, %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) // Saída: Olá, Bob!

Neste exemplo, primeiro obtemos o nome do usuário através de get_name(user), e então passamos o nome para a função sprintf usando o operador de tubulação | para gerar a mensagem de boas-vindas final.

O uso do operador de tubulação pode modularizar nosso código, melhorar a reutilização de código e tornar as expressões mais legíveis.

4.3 Funções

Expr suporta funções incorporadas e funções personalizadas, tornando as expressões mais poderosas e flexíveis.

Uso de Funções Incorporadas

Funções incorporadas como len, all, none, any, etc. podem ser usadas diretamente na expressão.

// Exemplo de uso de uma função incorporada
program, err := expr.Compile(`all(users, {.Age >= 18})`, expr.Env(env))
if err != nil {
    panic(err)
}

// Observação: aqui o ambiente precisa conter a variável users, e cada usuário deve ter a propriedade de idade
output, err := expr.Run(program, env)
fmt.Print(output) // Se todos os usuários no ambiente têm 18 anos ou mais, retornará true

Como definir e usar funções personalizadas

Em Expr, você pode criar funções personalizadas passando definições de função no mapeamento de ambiente.

// Exemplo de função personalizada
env := map[string]interface{}{
    "greet": func(name string) string {
        return fmt.Sprintf("Olá, %s!", name)
    },
}

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

output, err := expr.Run(program, env)
fmt.Print(output) // Retorna Olá, Mundo!

Ao usar funções em Expr, você pode modularizar seu código e incorporar lógica complexa nas expressões. Ao combinar variáveis, operadores e funções, Expr se torna uma ferramenta poderosa e fácil de usar. Lembre-se sempre de garantir a segurança de tipo ao construir o ambiente Expr e executar expressões.

5. Documentação de Funções Incorporadas

A engine de expressão Expr fornece aos desenvolvedores um conjunto rico de funções incorporadas para lidar com vários cenários complexos. Abaixo, detalharemos essas funções incorporadas e seu uso.

all

A função all pode ser usada para verificar se todos os elementos em uma coleção satisfazem uma condição específica. Ela aceita dois parâmetros: a coleção e a expressão de condição.

// Verificando se todos os tweets têm um comprimento de conteúdo inferior a 240
código := `all(tweets, len(.Content) < 240)`

any

Similar ao all, a função any é usada para verificar se algum elemento em uma coleção satisfaz uma condição.

// Verificando se algum tweet tem um comprimento de conteúdo maior que 240
código := `any(tweets, len(.Content) > 240)`

none

A função none é usada para verificar se nenhum elemento em uma coleção satisfaz uma condição.

// Garantindo que nenhum tweet se repita
código := `none(tweets, .IsRepeated)`

one

A função one é usada para confirmar que apenas um elemento em uma coleção satisfaz uma condição.

// Verificando se apenas um tweet contém uma palavra-chave específica
código := `one(tweets, contains(.Content, "palavra-chave"))`

filter

A função filter pode filtrar os elementos da coleção que satisfazem uma condição específica.

// Filtrando todos os tweets marcados como prioritários
código := `filter(tweets, .IsPriority)`

map

A função map é usada para transformar os elementos de uma coleção de acordo com um método específico.

// Formatando o horário de publicação de todos os tweets
código := `map(tweets, {.PublishTime: Format(.Date)})`

len

A função len é usada para retornar o comprimento de uma coleção ou string.

// Obtendo o comprimento do nome de usuário
código := `len(username)`

contains

A função contains é usada para verificar se uma string contém uma subcadeia ou se uma coleção contém um elemento específico.

// Verificando se o nome de usuário contém caracteres ilegais
código := `contains(username, "caracteres ilegais")`

Os mencionados acima são apenas uma parte das funções integradas fornecidas pelo motor de expressão Expr. Com essas funções poderosas, você pode lidar com dados e lógica de forma mais flexível e eficiente. Para obter uma lista mais detalhada de funções e instruções de uso, consulte a documentação oficial do Expr.