1. Introducción

Expr es una solución de configuración dinámica diseñada para el lenguaje Go, conocida por su sintaxis simple y potentes características de rendimiento. El núcleo del motor de expresiones Expr se centra en la seguridad, la velocidad y la intuición, lo que lo hace adecuado para escenarios como el control de acceso, la filtración de datos y la gestión de recursos. Cuando se aplica a Go, Expr mejora en gran medida la capacidad de las aplicaciones para manejar reglas dinámicas. A diferencia de intérpretes o motores de script en otros lenguajes, Expr adopta comprobación de tipos estáticos y genera código de bytes para la ejecución, garantizando tanto el rendimiento como la seguridad.

2. Instalación de Expr

Puede instalar el motor de expresiones Expr utilizando la herramienta de gestión de paquetes del lenguaje Go, go get:

go get github.com/expr-lang/expr

Este comando descargará los archivos de la biblioteca Expr e instalará en su proyecto Go, lo que le permitirá importar y usar Expr en su código Go.

3. Inicio rápido

3.1 Compilación y Ejecución de Expresiones Básicas

Comencemos con un ejemplo básico: escribir una expresión simple, compilarla y luego ejecutarla para obtener el resultado.

package main

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

func main() {
	// Compilando una expresión básica de adición
	program, err := expr.Compile(`2 + 2`)
	if err != nil {
		panic(err)
	}

	// Ejecutando la expresión compilada sin pasar un entorno, ya que aquí no se necesitan variables
	output, err := expr.Run(program, nil)
	if err != nil {
		panic(err)
	}

	// Imprimiendo el resultado
	fmt.Println(output)  // Resultado 4
}

En este ejemplo, la expresión 2 + 2 se compila en código de bytes ejecutable, que luego se ejecuta para producir la salida.

3.2 Uso de Expresiones con Variables

A continuación, crearemos un entorno que contenga variables, escribiremos una expresión que use estas variables, la compilaremos y ejecutaremos esta expresión.

package main

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

func main() {
	// Creando un entorno con variables
	env := map[string]interface{}{
		"foo": 100,
		"bar": 200,
	}

	// Compilando una expresión que usa variables del entorno
	program, err := expr.Compile(`foo + bar`, expr.Env(env))
	if err != nil {
		panic(err)
	}

	// Ejecutando la expresión
	output, err := expr.Run(program, env)
	if err != nil {
		panic(err)
	}

	// Imprimiendo el resultado
	fmt.Println(output)  // Resultado 300
}

En este ejemplo, el entorno env contiene las variables foo y bar. La expresión foo + bar infiere los tipos de foo y bar del entorno durante la compilación, y usa los valores de estas variables en tiempo de ejecución para evaluar el resultado de la expresión.

4. Sintaxis de Expr en Detalle

4.1 Variables y Literales

El motor de expresiones Expr puede manejar literales de tipos de datos comunes, incluyendo números, cadenas y valores booleanos. Los literales son valores de datos escritos directamente en el código, como 42, "hola" y true.

Números

En Expr, puedes escribir directamente números enteros y números de punto flotante:

42      // Representa el entero 42
3.14    // Representa el número de punto flotante 3.14

Cadenas

Los literales de cadena están encerrados entre comillas dobles " o acentos graves ``. Por ejemplo:

"hola, mundo" // Cadena encerrada entre comillas dobles, compatible con caracteres de escape
`hola, mundo` // Cadena encerrada entre acentos graves, mantiene el formato de cadena sin soporte para caracteres de escape

Booleanos

Solo hay dos valores booleanos, true y false, que representan verdadero y falso lógicos:

true   // Valor booleano verdadero
false  // Valor booleano falso

Variables

Expr también permite la definición de variables en el entorno, y luego hacer referencia a estas variables en la expresión. Por ejemplo:

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

Luego, en la expresión, puedes hacer referencia a age y name:

age > 18  // Comprobar si la edad es mayor de 18
name == "Alice"  // Determinar si el nombre es igual a "Alice"

4.2 Operadores

El motor de expresiones Expr admite varios operadores, incluidos operadores aritméticos, operadores lógicos, operadores de comparación y operadores de conjunto, etc.

Operadores aritméticos y lógicos

Los operadores aritméticos incluyen la suma (+), resta (-), multiplicación (*), división (/) y módulo (%). Los operadores lógicos incluyen el AND lógico (&&), OR lógico (||) y NOT lógico (!), por ejemplo:

2 + 2 // El resultado es 4
7 % 3 // El resultado es 1
!true // El resultado es falso
edad >= 18 && nombre == "Alice" // Comprueba si la edad no es menor de 18 y si el nombre es igual a "Alice"

Operadores de comparación

Los operadores de comparación incluyen igual a (==), no igual a (!=), menor que (<), menor o igual que (<=), mayor que (>) y mayor o igual que (>=), utilizados para comparar dos valores:

edad == 25 // Comprueba si la edad es igual a 25
edad != 18 // Comprueba si la edad no es igual a 18
edad > 20  // Comprueba si la edad es mayor que 20

Operadores de conjunto

Expr también proporciona algunos operadores para trabajar con conjuntos, como in para verificar si un elemento está en el conjunto. Los conjuntos pueden ser matrices, slices o maps:

"user" in ["user", "admin"]  // verdadero, porque "user" está en la matriz
3 in {1: true, 2: false}     // falso, porque 3 no es una clave en el mapa

También hay algunas funciones avanzadas de operación de conjunto, como all, any, one y none, que requieren el uso de funciones anónimas (lambda):

all(tweets, {.Len <= 240})  // Comprueba si el campo Len de todos los tweets no excede 240
any(tweets, {.Len > 200})   // Comprueba si existe un campo Len en los tweets que excede 200

Operador de miembro

En el lenguaje de expresión Expr, el operador de miembro nos permite acceder a las propiedades de la struct en el lenguaje Go. Esta característica permite a Expr manipular directamente estructuras de datos complejas, lo que lo hace muy flexible y práctico.

Usar el operador de miembro es muy sencillo, simplemente utiliza el operador . seguido del nombre de la propiedad. Por ejemplo, si tenemos la siguiente struct:

type User struct {
    Name string
    Age  int
}

Puedes escribir una expresión para acceder a la propiedad Name de la estructura User de la siguiente manera:

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

codigo := `user.Name`

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

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

fmt.Println(resultado) // Salida: Alice

Manejo de valores nulos

Al acceder a propiedades, es posible que te encuentres con situaciones en las que el objeto es nil. Expr proporciona acceso seguro a las propiedades, por lo que incluso si la estructura o la propiedad anidada es nil, no generará un error de pánico en tiempo de ejecución.

Utiliza el operador ?. para hacer referencia a propiedades. Si el objeto es nil, devolverá nil en lugar de generar un error.

autor.Usuario?.Nombre

Expresión equivalente

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

Usar el operador ?? es principalmente para devolver valores predeterminados:

autor.Usuario?.Nombre ?? "Anónimo"

Expresión equivalente

autor.Usuario != nil ? autor.Usuario.Nombre : "Anónimo"

Operador Pipe

El operador pipe (|) en Expr se utiliza para pasar el resultado de una expresión como parámetro a otra expresión. Esto es similar a la operación pipe en la terminal Unix, permitiendo encadenar varios módulos funcionales para formar un proceso en serie. En Expr, su uso puede crear expresiones más claras y concisas.

Por ejemplo, si tenemos una función para obtener el nombre de un usuario y una plantilla para un mensaje de bienvenida:

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

En este ejemplo, primero obtenemos el nombre del usuario a través de get_name(user), luego pasamos el nombre a la función sprintf usando el operador pipe | para generar el mensaje de bienvenida final.

El uso del operador pipe puede modularizar nuestro código, mejorar la reutilización del código y hacer que las expresiones sean más legibles.

4.3 Funciones

Expr admite funciones integradas y funciones personalizadas, lo que hace que las expresiones sean más potentes y flexibles.

Uso de Funciones Integradas

Funciones integradas como len, all, none, any, etc. se pueden utilizar directamente en la expresión.

// Ejemplo de uso de una función integrada
program, err := expr.Compile(`all(users, {.Age >= 18})`, expr.Env(env))
if err != nil {
    panic(err)
}

// Nota: aquí el entorno necesita contener la variable users, y cada usuario debe tener la propiedad Age
output, err := expr.Run(program, env)
fmt.Print(output) // Si todos los usuarios en el entorno tienen 18 años o más, devolverá true

Cómo definir y usar funciones personalizadas

En Expr, puedes crear funciones personalizadas pasando definiciones de funciones en el mapeo del entorno.

// Ejemplo de función personalizada
env := map[string]interface{}{
    "greet": func(name string) string {
        return fmt.Sprintf("¡Hola, %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) // Devuelve ¡Hola, Mundo!

Al utilizar funciones en Expr, puedes modularizar tu código e incorporar lógica compleja en las expresiones. Al combinar variables, operadores y funciones, Expr se convierte en una herramienta potente y fácil de usar. Recuerda siempre garantizar la seguridad del tipo al construir el entorno de Expr y ejecutar las expresiones.

5. Documentación de Funciones Integradas

El motor de expresiones Expr proporciona a los desarrolladores un conjunto amplio de funciones integradas para manejar diversos escenarios complejos. A continuación, detallaremos estas funciones integradas y su uso.

all

La función all se puede utilizar para comprobar si todos los elementos en una colección satisfacen una condición dada. Toma dos parámetros: la colección y la expresión de condición.

// Comprobar si todos los tweets tienen una longitud de contenido menor a 240
code := `all(tweets, len(.Content) < 240)`

any

Similar a all, la función any se utiliza para comprobar si algún elemento en una colección satisface una condición.

// Comprobar si algún tweet tiene una longitud de contenido mayor a 240
code := `any(tweets, len(.Content) > 240)`

none

La función none se utiliza para comprobar si ningún elemento en una colección satisface una condición.

// Asegurarse de que no haya tweets repetidos
code := `none(tweets, .IsRepeated)`

one

La función one se utiliza para confirmar que solo un elemento en una colección satisface una condición.

// Comprobar si solo un tweet contiene una palabra clave específica
code := `one(tweets, contains(.Content, "palabraclave"))`

filter

La función filter puede filtrar los elementos de una colección que satisfacen una condición dada.

// Filtrar todos los tweets marcados como prioritarios
code := `filter(tweets, .IsPriority)`

map

La función map se utiliza para transformar los elementos en una colección de acuerdo a un método especificado.

// Formatear la hora de publicación de todos los tweets
code := `map(tweets, {.PublishTime: Format(.Date)})`

len

La función len se utiliza para devolver la longitud de una colección o cadena.

// Obteniendo la longitud del nombre de usuario
código := `len(username)`

contains

La función contains se utiliza para verificar si una cadena contiene una subcadena o si una colección contiene un elemento específico.

// Comprobando si el nombre de usuario contiene caracteres ilegales
código := `contains(username, "caracteres ilegales")`

Los mencionados anteriormente son solo una parte de las funciones integradas proporcionadas por el motor de expresiones Expr. Con estas poderosas funciones, puedes manejar los datos y la lógica de manera más flexible y eficiente. Para obtener una lista más detallada de funciones e instrucciones de uso, consulta la documentación oficial de Expr.