Codelab de CEL-Go: Expresiones incorporadas rápidas y seguras

1. Introducción

¿Qué es CEL?

CEL es un lenguaje de expresión completo sin Turing diseñado para ser rápido, portátil y seguro de ejecutarse. CEL se puede usar por sí solo o incorporado en un producto más grande.

CEL se diseñó como un lenguaje en el que es seguro ejecutar el código de usuario. Si bien es peligroso llamar a ciegas a eval() en el código de Python de un usuario, puedes ejecutar de forma segura el código CEL de un usuario. Y debido a que CEL evita comportamientos que lo harían menos rendimiento, evalúa de forma segura en el orden de nanosegundos a microsegundos. es ideal para aplicaciones de rendimiento esencial.

CEL evalúa expresiones, que son similares a funciones de una sola línea o expresiones lambda. Aunque CEL se usa comúnmente para decisiones booleanas, también se puede usar para construir objetos más complejos, como mensajes JSON o protobuf.

¿CEL es adecuado para tu proyecto?

Debido a que CEL evalúa una expresión desde el AST en nanosegundos a microsegundos, el caso de uso ideal para CEL son aplicaciones con rutas de acceso críticas para el rendimiento. La compilación de código CEL en AST no se debe realizar en rutas críticas. aplicaciones ideales son aquellas en las que la configuración se ejecuta a menudo y se modifica con relativamente poca frecuencia.

Por ejemplo, ejecutar una política de seguridad con cada solicitud HTTP a un servicio es un caso de uso ideal para CEL porque la política de seguridad cambia rara vez, y CEL tendrá un impacto insignificante en el tiempo de respuesta. En este caso, CEL muestra un valor booleano, si la solicitud se debe permitir o no, pero podría mostrar un mensaje más complejo.

¿Qué se aborda en este codelab?

En el primer paso de este codelab, se explican la motivación para usar CEL y sus Core Concepts. El resto se dedica a codificar ejercicios que abarcan casos de uso comunes. Para obtener información más detallada sobre el lenguaje, la semántica y las funciones, consulta la definición del lenguaje CEL en GitHub y los documentos de CEL Go.

Este codelab está dirigido a desarrolladores que quieran aprender sobre CEL para usar servicios que ya admiten CEL. En este codelab, no se aborda cómo integrar CEL en tu propio proyecto.

Qué aprenderás

  • Conceptos básicos de CEL
  • Hello, World: Usa CEL para evaluar una string
  • Crea variables
  • Explicación del cortocircuito de CEL en operaciones AND/OR lógicas
  • Cómo usar CEL para compilar JSON
  • Cómo usar CEL para compilar búferes de Proto
  • Crea macros
  • Formas de ajustar tus expresiones en CEL

Requisitos

Requisitos previos

Este codelab se basa en una comprensión básica de búferes de protocolo y Go Lang.

Si no estás familiarizado con los búferes de protocolo, el primer ejercicio te dará una idea de cómo funciona CEL, pero como los ejemplos más avanzados usan búferes de protocolo como entrada a CEL, pueden ser más difíciles de entender. Considera la posibilidad de realizar primero uno de estos instructivos. Ten en cuenta que los búferes de protocolo no son necesarios para usar CEL, pero se usan ampliamente en este codelab.

Para probar que Go esté instalado, ejecuta el siguiente comando:

go --help

2. Conceptos clave

Aplicaciones

CEL es de uso general y se ha usado para diversas aplicaciones, desde RPC de enrutamiento hasta definir políticas de seguridad. CEL es extensible, independiente de la aplicación y optimizado para flujos de trabajo de compilación única y evaluación.

Muchos servicios y aplicaciones evalúan parámetros de configuración declarativos. Por ejemplo, el control de acceso basado en roles (RBAC) es una configuración declarativa que produce una decisión de acceso a partir de un rol y un conjunto de usuarios. Si el caso de uso del 80% es las configuraciones declarativas, entonces CEL es una herramienta útil para completar el 20% restante cuando los usuarios necesitan una potencia más expresiva.

Compilación

Se compila una expresión en un entorno. El paso de compilación produce un árbol de sintaxis abstracta (AST) en formato protobuf. Por lo general, las expresiones compiladas se almacenan para usarlas en el futuro a fin de mantener la evaluación lo más rápida posible. Una sola expresión compilada se puede evaluar con muchas entradas diferentes.

Expresiones

Los usuarios definen expresiones. servicios y aplicaciones definen el entorno en el que se ejecutan. Una firma de función declara las entradas y se escribe fuera de la expresión CEL. La biblioteca de funciones disponibles para CEL se importa automáticamente.

En el siguiente ejemplo, la expresión toma un objeto de solicitud y la solicitud incluye un token de reclamaciones. La expresión devuelve un valor booleano que indica si el token de reclamaciones sigue siendo válido.

// Check whether a JSON Web Token has expired by inspecting the 'exp' claim.
//
// Args:
//   claims - authentication claims.
//   now    - timestamp indicating the current system time.
// Returns: true if the token has expired.
//
timestamp(claims["exp"]) < now

Entorno

Los servicios definen los entornos. Los servicios y aplicaciones que incorporan CEL declaran el entorno de expresión. El entorno es la colección de variables y funciones que se pueden usar en expresiones.

El verificador de tipos CEL usa las declaraciones basadas en proto para garantizar que todos los identificadores y las referencias de funciones dentro de una expresión se declaren y usen de forma correcta.

Las tres fases para analizar una expresión

Hay tres fases en el procesamiento de una expresión: analizar, verificar y evaluar. El patrón más común para CEL es que un plano de control analice y verifique expresiones en el momento de la configuración y almacene el AST.

c71fc08068759f81.png

En el tiempo de ejecución, el plano de datos recupera y evalúa la AST repetidamente. CEL está optimizado para la eficiencia del entorno de ejecución, pero el análisis y la verificación no se deben realizar en las rutas de código críticas de latencia.

49ab7d8517143b66.png

CEL se analiza desde una expresión legible por humanos hasta un árbol de sintaxis abstracto mediante una gramática léxer / analizador ANTLR. La fase de análisis emite un árbol de sintaxis abstracta basado en proto en el que cada nodo Expr en AST contiene un ID de número entero que se usa para indexar los metadatos generados durante el análisis y la verificación. El syntax.proto producido durante el análisis representa fielmente la representación abstracta de lo que se escribió en la forma de cadena de la expresión.

Una vez que se analiza una expresión, se puede verificar con el entorno para garantizar que todos los identificadores de variables y funciones de la expresión se hayan declarado y se usen de forma correcta. El verificador de tipos produce un checked.proto que incluye metadatos de tipo, variable y resolución de función que pueden mejorar drásticamente la eficiencia de la evaluación.

El evaluador de CEL necesita 3 cosas:

  • Vinculaciones de funciones para cualquier extensión personalizada
  • Vinculaciones de variables
  • Un AST para evaluar

Las vinculaciones de funciones y variables deben coincidir con lo que se usó para compilar el AST. Cualquiera de estas entradas se puede volver a usar en varias evaluaciones, como un AST que se evalúa en muchos conjuntos de vinculaciones de variables, las mismas variables usadas en muchos AST o las vinculaciones de funciones usadas durante la vida útil de un proceso (un caso común).

3. Configurar

El código de este codelab se encuentra en la carpeta codelab del repositorio cel-go. La solución está disponible en la carpeta codelab/solution del mismo repositorio.

Clona y usa el comando cd en el repositorio:

git clone https://github.com/google/cel-go.git 
cd cel-go/codelab

Ejecuta el código con go run:

go run .

Deberías ver el siguiente resultado:

=== Exercise 1: Hello World ===

=== Exercise 2: Variables ===

=== Exercise 3: Logical AND/OR ===

=== Exercise 4: Customization ===

=== Exercise 5: Building JSON ===

=== Exercise 6: Building Protos ===

=== Exercise 7: Macros ===

=== Exercise 8: Tuning ===

¿Dónde están los paquetes de CEL?

En tu editor, abre codelab/codelab.go. Deberías ver la función principal que impulsa la ejecución de los ejercicios de este codelab, seguida de tres bloques de funciones auxiliares. El primer conjunto de asistentes ayuda con las fases de la evaluación de CEL:

  • Función Compile: Analiza y comprueba la expresión de entrada en un entorno.
  • Función Eval: Evalúa un programa compilado en función de una entrada
  • Función Report: Imprime con formato el resultado de la evaluación

Además, se proporcionaron los asistentes de request y auth para asistir en la creación de entradas para los distintos ejercicios.

Los ejercicios se referirán a los paquetes por su nombre corto de paquete. A continuación, se muestra la asignación del paquete a la ubicación de origen en el repositorio google/cel-go, por si quieres profundizar en los detalles:

Paquete

Ubicación de origen

Descripción

cel

cel-go/cel

Interfaces de nivel superior

ref

cel-go/common/types/ref

Interfaces de referencia

Tipos

cel-go/common/types

Valores de tipo de entorno de ejecución

4. ¡Hola, mundo!

Según la tradición de todos los lenguajes de programación, comenzaremos por crear y evaluar "Hello World!".

Configura el entorno

En tu editor, busca la declaración de exercise1 y completa lo siguiente para configurar el entorno:

// exercise1 evaluates a simple literal expression: "Hello, World!"
//
// Compile, eval, profit!
func exercise1() {
    fmt.Println("=== Exercise 1: Hello World ===\n")
    // Create the standard environment.
    env, err := cel.NewEnv()
    if err != nil {
        glog.Exitf("env error: %v", err)
    }
    // Will add the parse and check steps here
}

Las aplicaciones de CEL evalúan una expresión en relación con un entorno. env, err := cel.NewEnv() configura el entorno estándar.

El entorno se puede personalizar proporcionando las opciones cel.EnvOption a la llamada. Esas opciones pueden inhabilitar macros, declarar variables y funciones personalizadas, etcétera.

El entorno CEL estándar admite todos los tipos, operadores, funciones y macros definidos en la especificación de lenguaje.

Analiza y verifica la expresión

Una vez que se haya configurado el entorno, las expresiones se pueden analizar y verificar. Agrega lo siguiente a la función:

// exercise1 evaluates a simple literal expression: "Hello, World!"
//
// Compile, eval, profit!
func exercise1() {
    fmt.Println("=== Exercise 1: Hello World ===\n")
    // Create the standard environment.
    env, err := cel.NewEnv()
    if err != nil {
        glog.Exitf("env error: %v", err)
    }
    // Check that the expression compiles and returns a String.
    ast, iss := env.Parse(`"Hello, World!"`)
    // Report syntactic errors, if present.
    if iss.Err() != nil {
        glog.Exit(iss.Err())
    }
    // Type-check the expression for correctness.
    checked, iss := env.Check(ast)
    // Report semantic errors, if present.
    if iss.Err() != nil {
        glog.Exit(iss.Err())
    }
    // Check the output type is a string.
    if !reflect.DeepEqual(checked.OutputType(), cel.StringType) {
        glog.Exitf(
            "Got %v, wanted %v output type",
            checked.OutputType(), cel.StringType,
        )
    }
    // Will add the planning step here
}

El valor iss que muestran las llamadas a Parse y Check es una lista de problemas que podrían ser errores. Si iss.Err() no es nulo, hay un error en la sintaxis o en la semántica, y el programa no puede continuar. Cuando la expresión tiene el formato correcto, el resultado de estas llamadas es un cel.Ast ejecutable.

Evalúa la expresión

Una vez que la expresión se haya analizado y registrado en un cel.Ast, se podrá convertir en un programa evaluable cuyas vinculaciones de funciones y modos de evaluación se pueden personalizar con opciones funcionales. Ten en cuenta que también es posible leer un cel.Ast desde un proto mediante las funciones cel.CheckedExprToAst o cel.ParsedExprToAst.

Una vez que se planifica un cel.Program, se puede evaluar en función de la entrada llamando a Eval. El resultado de Eval contendrá el resultado, los detalles de la evaluación y el estado del error.

Agrega planificación y llama a eval:

// exercise1 evaluates a simple literal expression: "Hello, World!"
//
// Compile, eval, profit!
func exercise1() {
    fmt.Println("=== Exercise 1: Hello World ===\n")
    // Create the standard environment.
    env, err := cel.NewEnv()
    if err != nil {
        glog.Exitf("env error: %v", err)
    }
    // Check that the expression compiles and returns a String.
    ast, iss := env.Parse(`"Hello, World!"`)
    // Report syntactic errors, if present.
    if iss.Err() != nil {
        glog.Exit(iss.Err())
    }
    // Type-check the expression for correctness.
    checked, iss := env.Check(ast)
    // Report semantic errors, if present.
    if iss.Err() != nil {
        glog.Exit(iss.Err())
    }
    // Check the output type is a string.
    if !reflect.DeepEqual(checked.OutputType(), cel.StringType) {
        glog.Exitf(
            "Got %v, wanted %v output type",
            checked.OutputType(), cel.StringType)
    }
    // Plan the program.
    program, err := env.Program(checked)
    if err != nil {
        glog.Exitf("program error: %v", err)
    }
    // Evaluate the program without any additional arguments.
    eval(program, cel.NoVars())
    fmt.Println()
}

Por cuestiones de brevedad, omitiremos los casos de error incluidos antes de los ejercicios futuros.

Ejecuta el código

En la línea de comandos, vuelve a ejecutar el código:

go run .

Deberías ver el siguiente resultado, junto con los marcadores de posición para los ejercicios futuros.

=== Exercise 1: Hello World ===

------ input ------
(interpreter.emptyActivation)

------ result ------
value: Hello, World! (types.String)

5. Cómo usar variables en una función

La mayoría de las aplicaciones CEL declararán variables a las que se puede hacer referencia en las expresiones. Las declaraciones de variables especifican un nombre y un tipo. El tipo de una variable puede ser un tipo integrado de CEL, un tipo conocido de búfer de protocolo o cualquier tipo de mensaje protobuf, siempre que su descriptor también se proporcione a CEL.

Agrega la función

En el editor, busca la declaración de exercise2 y agrega lo siguiente:

// exercise2 shows how to declare and use variables in expressions.
//
// Given a request of type google.rpc.context.AttributeContext.Request
// determine whether a specific auth claim is set.
func exercise2() {
  fmt.Println("=== Exercise 2: Variables ===\n")
   env, err := cel.NewEnv(
    // Add cel.EnvOptions values here.
  )
  if err != nil {
    glog.Exit(err)
  }
  ast := compile(env, `request.auth.claims.group == 'admin'`, cel.BoolType)
  program, _ := env.Program(ast)

  // Evaluate a request object that sets the proper group claim.
  claims := map[string]string{"group": "admin"}
  eval(program, request(auth("user:me@acme.co", claims), time.Now()))
  fmt.Println()
}

Vuelve a ejecutar y comprende el error

Vuelve a ejecutar el programa:

go run .

Deberías ver el siguiente resultado:

ERROR: <input>:1:1: undeclared reference to 'request' (in container '')
 | request.auth.claims.group == 'admin'
 | ^

El verificador de tipos produce un error para el objeto de solicitud que incluye convenientemente el fragmento de origen en el que se produce el error.

6. Declara las variables

Agregar EnvOptions

En el editor, corrijamos el error resultante proporcionando una declaración para el objeto de la solicitud como un mensaje de tipo google.rpc.context.AttributeContext.Request, de la siguiente manera:

// exercise2 shows how to declare and use variables in expressions.
//
// Given a `request` of type `google.rpc.context.AttributeContext.Request`
// determine whether a specific auth claim is set.
func exercise2() {
  fmt.Println("=== Exercise 2: Variables ===\n")
  env, err := cel.NewEnv(
    cel.Variable("request",
      cel.ObjectType("google.rpc.context.AttributeContext.Request"),
    ),
  )
  if err != nil {
    glog.Exit(err)
  }
  ast := compile(env, `request.auth.claims.group == 'admin'`, cel.BoolType)
  program, _ := env.Program(ast)

  // Evaluate a request object that sets the proper group claim.
  claims := map[string]string{"group": "admin"}
  eval(program, request(auth("user:me@acme.co", claims), time.Now()))
  fmt.Println()
}

Vuelve a ejecutar y comprende el error

Vuelve a ejecutar el programa:

go run .

Debería ver el siguiente error:

ERROR: <input>:1:8: [internal] unexpected failed resolution of 'google.rpc.context.AttributeContext.Request'
 | request.auth.claims.group == 'admin'
 | .......^

Para usar variables que hacen referencia a los mensajes protobuf, el verificador de tipos debe conocer también el descriptor de tipo.

Usa cel.Types para determinar el descriptor para la solicitud en tu función:

// exercise2 shows how to declare and use variables in expressions.
//
// Given a `request` of type `google.rpc.context.AttributeContext.Request`
// determine whether a specific auth claim is set.
func exercise2() {
  fmt.Println("=== Exercise 2: Variables ===\n")
   env, err := cel.NewEnv(
    cel.Types(&rpcpb.AttributeContext_Request{}), 
    cel.Variable("request",
      cel.ObjectType("google.rpc.context.AttributeContext.Request"),
    ),
  )

  if err != nil {
    glog.Exit(err)
  }
  ast := compile(env, `request.auth.claims.group == 'admin'`, cel.BoolType)
  program, _ := env.Program(ast)

  // Evaluate a request object that sets the proper group claim.
  claims := map[string]string{"group": "admin"}
  eval(program, request(auth("user:me@acme.co", claims), time.Now()))
  fmt.Println()
}

Se volvió a ejecutar correctamente.

Vuelve a ejecutar el programa:

go run .

Deberías ver lo siguiente:

=== Exercise 2: Variables ===

request.auth.claims.group == 'admin'

------ input ------
request = time: <
  seconds: 1569255569
>
auth: <
  principal: "user:me@acme.co"
  claims: <
    fields: <
      key: "group"
      value: <
        string_value: "admin"
      >
    >
  >
>

------ result ------
value: true (types.Bool)

A modo de repaso, acabamos de declarar una variable para un error, le asignamos un descriptor de tipo y, luego, hacíamos referencia a la variable en la evaluación de la expresión.

7. Lógico AND/OR

Una de las funciones más únicas de CEL es su uso de operadores lógicos conmutativos. Cualquiera de los lados de una rama condicional puede hacer un cortocircuito en la evaluación, incluso si hay errores o entradas parciales.

En otras palabras, CEL encuentra un orden de evaluación que da un resultado siempre que sea posible, ignorando errores o incluso datos faltantes que puedan ocurrir en otros pedidos de evaluación. Las aplicaciones pueden basarse en esta propiedad para minimizar el costo de la evaluación aplazando la recopilación de entradas costosas cuando se puede obtener un resultado sin ellas.

Agregaremos un ejemplo de tipo Y/O y, luego, lo probaremos con diferentes entradas para comprender cómo CEL produce cortocircuitos en la evaluación.

Crea la función

En el editor, agrega el siguiente contenido al ejercicio 3:

// exercise3 demonstrates how CEL's commutative logical operators work.
//
// Construct an expression which checks if the `request.auth.claims.group`
// value is equal to admin or the `request.auth.principal` is
// `user:me@acme.co`. Issue two requests, one that specifies the proper 
// user, and one that specifies an unexpected user.
func exercise3() {
  fmt.Println("=== Exercise 3: Logical AND/OR ===\n")
  env, _ := cel.NewEnv(
    cel.Types(&rpcpb.AttributeContext_Request{}),
    cel.Variable("request",
      cel.ObjectType("google.rpc.context.AttributeContext.Request"),
    ),
  )
}

Luego, incluye esta sentencia OR que mostrará el valor true si el usuario es miembro del grupo admin o tiene un identificador de correo electrónico particular:

// exercise3 demonstrates how CEL's commutative logical operators work.
//
// Construct an expression which checks if the `request.auth.claims.group`
// value is equal to admin or the `request.auth.principal` is
// `user:me@acme.co`. Issue two requests, one that specifies the proper 
// user, and one that specifies an unexpected user.
func exercise3() {
  fmt.Println("=== Exercise 3: Logical AND/OR ===\n")
  env, _ := cel.NewEnv(
    cel.Types(&rpcpb.AttributeContext_Request{}),
    cel.Variable("request",
      cel.ObjectType("google.rpc.context.AttributeContext.Request"),
    ),
  )

  ast := compile(env,
    `request.auth.claims.group == 'admin'
        || request.auth.principal == 'user:me@acme.co'`,
    cel.BoolType)
  program, _ := env.Program(ast)
}

Por último, agrega el caso eval, que evalúa al usuario con un conjunto de reclamaciones vacío:

// exercise3 demonstrates how CEL's commutative logical operators work.
//
// Construct an expression which checks whether the request.auth.claims.group
// value is equal to admin or the request.auth.principal is
// user:me@acme.co. Issue two requests, one that specifies the proper user,
// and one that specifies an unexpected user.
func exercise3() {
  fmt.Println("=== Exercise 3: Logical AND/OR ===\n")
  env, _ := cel.NewEnv(
    cel.Types(&rpcpb.AttributeContext_Request{}),
    cel.Variable("request",
      cel.ObjectType("google.rpc.context.AttributeContext.Request"),
    ),
  )

  ast := compile(env,
    `request.auth.claims.group == 'admin'
        || request.auth.principal == 'user:me@acme.co'`,
    cel.BoolType)
  program, _ := env.Program(ast)

  emptyClaims := map[string]string{}
  eval(program, request(auth("user:me@acme.co", emptyClaims), time.Now()))
}

Ejecuta el código con el conjunto de reclamaciones vacías

Si vuelves a ejecutar el programa, deberías ver el siguiente resultado nuevo:

=== Exercise 3: Logical AND/OR ===

request.auth.claims.group == 'admin'
    || request.auth.principal == 'user:me@acme.co'

------ input ------
request = time: <
  seconds: 1569302377
>
auth: <
  principal: "user:me@acme.co"
  claims: <
  >
>

------ result ------
value: true (types.Bool)

Actualizar el caso de evaluación

A continuación, actualiza el caso de evaluación para pasar una principal diferente con el conjunto de reclamaciones vacías:

// exercise3 demonstrates how CEL's commutative logical operators work.
//
// Construct an expression which checks whether the request.auth.claims.group
// value is equal to admin or the request.auth.principal is
// user:me@acme.co. Issue two requests, one that specifies the proper user,
// and one that specifies an unexpected user.
func exercise3() {
  fmt.Println("=== Exercise 3: Logical AND/OR ===\n")
  env, _ := cel.NewEnv(
    cel.Types(&rpcpb.AttributeContext_Request{}),
    cel.Variable("request",
      cel.ObjectType("google.rpc.context.AttributeContext.Request"),
    ),
  )

  ast := compile(env,
    `request.auth.claims.group == 'admin'
        || request.auth.principal == 'user:me@acme.co'`,
    cel.BoolType)
  program, _ := env.Program(ast)

  emptyClaims := map[string]string{}
  eval(program, request(auth("other:me@acme.co", emptyClaims), time.Now()))
}

Ejecuta el código con un tiempo

Volver a ejecutar el programa

go run .

Deberías ver el siguiente error:

=== Exercise 3: Logical AND/OR ===

request.auth.claims.group == 'admin'
    || request.auth.principal == 'user:me@acme.co'

------ input ------
request = time: <
  seconds: 1588989833
>
auth: <
  principal: "user:me@acme.co"
  claims: <
  >
>

------ result ------
value: true (types.Bool)

------ input ------
request = time: <
  seconds: 1588989833
>
auth: <
  principal: "other:me@acme.co"
  claims: <
  >
>

------ result ------
error: no such key: group

En protobuf, sabemos qué campos y tipos esperar. En los valores map y json, no sabemos si estará presente una clave. Como no hay un valor predeterminado seguro para una clave faltante, CEL tiene un error predeterminado.

8. Funciones personalizadas

Si bien CEL incluye muchas funciones integradas, hay ocasiones en las que una función personalizada es útil. Por ejemplo, las funciones personalizadas se pueden usar para mejorar la experiencia del usuario en condiciones comunes o para exponer estados sensibles al contexto.

En este ejercicio, exploraremos cómo exponer una función para empaquetar las verificaciones de uso general.

Cómo llamar a una función personalizada

Primero, crea el código para configurar una anulación llamada contains que determine si una clave existe en un mapa y tiene un valor particular. Deja marcadores de posición para la definición y vinculación de la función:

// exercise4 demonstrates how to extend CEL with custom functions.
// Declare a `contains` member function on map types that returns a boolean
// indicating whether the map contains the key-value pair.
func exercise4() {
  fmt.Println("=== Exercise 4: Customization ===\n")
  // Determine whether an optional claim is set to the proper value. The
  // custom map.contains(key, value) function is used as an alternative to:
  //   key in map && map[key] == value
  env, _ := cel.NewEnv(
    cel.Types(&rpcpb.AttributeContext_Request{}),
    // Declare the request.
    cel.Variable("request",
      cel.ObjectType("google.rpc.context.AttributeContext.Request"),
    ),
    // Add the custom function declaration and binding here with cel.Function()
  )
  ast := compile(env,
    `request.auth.claims.contains('group', 'admin')`,
    cel.BoolType)

  // Construct the program plan and provide the 'contains' function impl.
  // Output: false
  program, _ := env.Program(ast)
  emptyClaims := map[string]string{}
  eval(
    program,
    request(auth("user:me@acme.co", emptyClaims), time.Now()),
  )
  fmt.Println()
}  

Ejecuta el código y comprende el error

Si vuelves a ejecutar el código, deberías ver el siguiente error:

ERROR: <input>:1:29: found no matching overload for 'contains' applied to 'map(string, dyn).(string, string)'
 | request.auth.claims.contains('group', 'admin')
 | ............................^

Para corregir el error, debemos agregar la función contains a la lista de declaraciones que actualmente declaran la variable de solicitud.

Para declarar un tipo con parámetros, agrega las siguientes 3 líneas. (Esto es tan complicado como lo será cualquier sobrecarga de funciones para CEL).

// exercise4 demonstrates how to extend CEL with custom functions.
// Declare a `contains` member function on map types that returns a boolean
// indicating whether the map contains the key-value pair.
func exercise4() {
  fmt.Println("=== Exercise 4: Customization ===\n")
  // Determine whether an optional claim is set to the proper value. The custom
  // map.contains(key, value) function is used as an alternative to:
  //   key in map && map[key] == value

  // Useful components of the type-signature for 'contains'.
  typeParamA := cel.TypeParamType("A")
  typeParamB := cel.TypeParamType("B")
  mapAB := cel.MapType(typeParamA, typeParamB)

  env, _ := cel.NewEnv(
    cel.Types(&rpcpb.AttributeContext_Request{}),
    // Declare the request.
    cel.Variable("request",
      cel.ObjectType("google.rpc.context.AttributeContext.Request"),
    ),
    // Add the custom function declaration and binding here with cel.Function()
  )
  ast := compile(env,
    `request.auth.claims.contains('group', 'admin')`,
    cel.BoolType)

  // Construct the program plan.
  // Output: false
  program, _ := env.Program(ast)
  emptyClaims := map[string]string{}
  eval(program, request(auth("user:me@acme.co", emptyClaims), time.Now()))
  fmt.Println()
}

Agrega la función personalizada

A continuación, agregaremos una nueva función contiene que usará los tipos parametrizados:

// exercise4 demonstrates how to extend CEL with custom functions.
// Declare a `contains` member function on map types that returns a boolean
// indicating whether the map contains the key-value pair.
func exercise4() {
  fmt.Println("=== Exercise 4: Customization ===\n")
  // Determine whether an optional claim is set to the proper value. The custom
  // map.contains(key, value) function is used as an alternative to:
  //   key in map && map[key] == value

  // Useful components of the type-signature for 'contains'.
  typeParamA := cel.TypeParamType("A")
  typeParamB := cel.TypeParamType("B")
  mapAB := cel.MapType(typeParamA, typeParamB)
 
  // Env declaration.
  env, _ := cel.NewEnv(
    cel.Types(&rpcpb.AttributeContext_Request{}),
    // Declare the request.
    cel.Variable("request",
      cel.ObjectType("google.rpc.context.AttributeContext.Request"),
    ),   
    // Declare the custom contains function and its implementation.
    cel.Function("contains",
      cel.MemberOverload(
        "map_contains_key_value",
        []*cel.Type{mapAB, typeParamA, typeParamB},
        cel.BoolType,
        // Provide the implementation using cel.FunctionBinding()
      ),
    ),
  )
  ast := compile(env,
    `request.auth.claims.contains('group', 'admin')`,
    cel.BoolType)

  // Construct the program plan and provide the 'contains' function impl.
  // Output: false
  program, _ := env.Program(ast)
  emptyClaims := map[string]string{}
  eval(program, request(auth("user:me@acme.co", emptyClaims), time.Now()))
  fmt.Println()
}

Ejecuta el programa para comprender el error

Ejecuta el ejercicio. Deberías ver el siguiente error sobre la función del entorno de ejecución faltante:

------ result ------
error: no such overload: contains

Proporciona la implementación de la función a la declaración NewEnv con la función cel.FunctionBinding():

// exercise4 demonstrates how to extend CEL with custom functions.
//
// Declare a contains member function on map types that returns a boolean
// indicating whether the map contains the key-value pair.
func exercise4() {
  fmt.Println("=== Exercise 4: Customization ===\n")
  // Determine whether an optional claim is set to the proper value. The custom
  // map.contains(key, value) function is used as an alternative to:
  //   key in map && map[key] == value

  // Useful components of the type-signature for 'contains'.
  typeParamA := cel.TypeParamType("A")
  typeParamB := cel.TypeParamType("B")
  mapAB := cel.MapType(typeParamA, typeParamB)

  // Env declaration.
  env, _ := cel.NewEnv(
    cel.Types(&rpcpb.AttributeContext_Request{}),
    // Declare the request.
    cel.Variable("request",
      cel.ObjectType("google.rpc.context.AttributeContext.Request"),
    ),   
    // Declare the custom contains function and its implementation.
    cel.Function("contains",
      cel.MemberOverload(
        "map_contains_key_value",
        []*cel.Type{mapAB, typeParamA, typeParamB},
        cel.BoolType,
        cel.FunctionBinding(mapContainsKeyValue)),
    ),
  )
  ast := compile(env, 
    `request.auth.claims.contains('group', 'admin')`, 
    cel.BoolType)

  // Construct the program plan.
  // Output: false
  program, err := env.Program(ast)
  if err != nil {
    glog.Exit(err)
  }

  eval(program, request(auth("user:me@acme.co", emptyClaims), time.Now()))
  claims := map[string]string{"group": "admin"}
  eval(program, request(auth("user:me@acme.co", claims), time.Now()))
  fmt.Println()
}

Ahora, el programa debería ejecutarse correctamente:

=== Exercise 4: Custom Functions ===

request.auth.claims.contains('group', 'admin')

------ input ------
request = time: <
  seconds: 1569302377
>
auth: <
  principal: "user:me@acme.co"
  claims: <
  >
>

------ result ------
value: false (types.Bool)

¿Qué sucede cuando existe el reclamo?

Para obtener crédito adicional, intenta configurar la reclamación del administrador en la entrada para verificar que la sobrecarga del video también muestre el valor "true" cuando la reclamación exista. Deberías ver el siguiente resultado:

=== Exercise 4: Customization ===

request.auth.claims.contains('group','admin')

------ input ------
request = time: <
  seconds: 1588991010
>
auth: <
  principal: "user:me@acme.co"
  claims: <
  >
>

------ result ------
value: false (types.Bool)

------ input ------
request = time: <
  seconds: 1588991010
>
auth: <
  principal: "user:me@acme.co"
  claims: <
    fields: <
      key: "group"
      value: <
        string_value: "admin"
      >
    >
  >
>

------ result ------
value: true (types.Bool)

Antes de continuar, vale la pena inspeccionar la función mapContainsKeyValue en sí:

// mapContainsKeyValue implements the custom function:
//   map.contains(key, value) -> bool.
func mapContainsKeyValue(args ...ref.Val) ref.Val {
  // The declaration of the function ensures that only arguments which match
  // the mapContainsKey signature will be provided to the function.
  m := args[0].(traits.Mapper)

  // CEL has many interfaces for dealing with different type abstractions.
  // The traits.Mapper interface unifies field presence testing on proto
  // messages and maps.
  key := args[1]
  v, found := m.Find(key)

  // If not found and the value was non-nil, the value is an error per the
  // `Find` contract. Propagate it accordingly. Such an error might occur with
  // a map whose key-type is listed as 'dyn'.
  if !found {
    if v != nil {
      return types.ValOrErr(v, "unsupported key type")
    }
    // Return CEL False if the key was not found.
    return types.False
  }
  // Otherwise whether the value at the key equals the value provided.
  return v.Equal(args[2])
}

Para ofrecer la mayor facilidad de extensión, la firma para las funciones personalizadas espera los argumentos de tipo ref.Val. La desventaja aquí es que la facilidad de extensión agrega una carga al implementador para garantizar que todos los tipos de valores se manejen correctamente. Cuando los tipos de argumento de entrada o el recuento no coinciden con la declaración de la función, se debe mostrar un error no such overload.

cel.FunctionBinding() agrega una protección de tipo de entorno de ejecución para garantizar que el contrato de entorno de ejecución coincida con la declaración de tipo verificado en el entorno.

9. Compila JSON

CEL también puede producir salidas no booleanas, como JSON. Agrega lo siguiente a la función:

// exercise5 covers how to build complex objects as CEL literals.
//
// Given the input now, construct a JWT with an expiry of 5 minutes.
func exercise5() {
    fmt.Println("=== Exercise 5: Building JSON ===\n")
    env, _ := cel.NewEnv(
      // Declare the 'now' variable as a Timestamp.
      // cel.Variable("now", cel.TimestampType),
    )
    // Note the quoted keys in the CEL map literal. For proto messages the
    // field names are unquoted as they represent well-defined identifiers.
    ast := compile(env, `
        {'sub': 'serviceAccount:delegate@acme.co',
         'aud': 'my-project',
         'iss': 'auth.acme.com:12350',
         'iat': now,
         'nbf': now,
         'exp': now + duration('300s'),
         'extra_claims': {
             'group': 'admin'
         }}`,
        cel.MapType(cel.StringType, cel.DynType))

    program, _ := env.Program(ast)
    out, _, _ := eval(
        program,
        map[string]interface{}{
            "now": &tpb.Timestamp{Seconds: time.Now().Unix()},
        },
    )
    fmt.Printf("------ type conversion ------\n%v\n", out)
    fmt.Println()
}

Ejecuta el código

Si vuelves a ejecutar el código, deberías ver el siguiente error:

ERROR: <input>:5:11: undeclared reference to 'now' (in container '')
 |    'iat': now,
 | ..........^
... and more ...

Agrega una declaración para la variable now de tipo cel.TimestampType a cel.NewEnv() y vuelve a ejecutarla:

// exercise5 covers how to build complex objects as CEL literals.
//
// Given the input now, construct a JWT with an expiry of 5 minutes.
func exercise5() {
    fmt.Println("=== Exercise 5: Building JSON ===\n")
    env, _ := cel.NewEnv(
      cel.Variable("now", cel.TimestampType),
    )
    // Note the quoted keys in the CEL map literal. For proto messages the
    // field names are unquoted as they represent well-defined identifiers.
    ast := compile(env, `
        {'sub': 'serviceAccount:delegate@acme.co',
         'aud': 'my-project',
         'iss': 'auth.acme.com:12350',
         'iat': now,
         'nbf': now,
         'exp': now + duration('300s'),
         'extra_claims': {
             'group': 'admin'
         }}`,
        cel.MapType(cel.StringType, cel.DynType))

     // Hint:
     // Convert `out` to JSON using the valueToJSON() helper method.
     // The valueToJSON() function calls ConvertToNative(&structpb.Value{})
     // to adapt the CEL value to a protobuf JSON representation and then
     // uses the jsonpb.Marshaler utilities on the output to render the JSON
     // string.
    program, _ := env.Program(ast)
    out, _, _ := eval(
        program,
        map[string]interface{}{
            "now": time.Now(),
        },
    )
    fmt.Printf("------ type conversion ------\n%v\n", out)
    fmt.Println()
}

Vuelve a ejecutar el código. Debería tener éxito:

=== Exercise 5: Building JSON ===

  {'aud': 'my-project',
   'exp': now + duration('300s'),
   'extra_claims': {
    'group': 'admin'
   },
   'iat': now,
   'iss': 'auth.acme.com:12350',
   'nbf': now,
   'sub': 'serviceAccount:delegate@acme.co'
   }

------ input ------
now = seconds: 1569302377

------ result ------
value: &{0xc0002eaf00 map[aud:my-project exp:{0xc0000973c0} extra_claims:0xc000097400 iat:{0xc000097300} iss:auth.acme.com:12350 nbf:{0xc000097300} sub:serviceAccount:delegate@acme.co] {0x8c8f60 0xc00040a6c0 21}} (*types.baseMap)

------ type conversion ------
&{0xc000313510 map[aud:my-project exp:{0xc000442510} extra_claims:0xc0004acdc0 iat:{0xc000442450} iss:auth.acme.com:12350 nbf:{0xc000442450} sub:serviceAccount:delegate@acme.co] {0x9d0ce0 0xc0004424b0 21}}

El programa se ejecuta, pero el valor de salida out debe convertirse explícitamente en JSON. En este caso, la representación en CEL interna es convertible en JSON, ya que solo se refiere a los tipos que admite JSON o para los que existe una asignación de Proto a JSON conocida.

// exercise5 covers how to build complex objects as CEL literals.
//
// Given the input now, construct a JWT with an expiry of 5 minutes.
func exercise5() {
    fmt.Println("=== Exercise 5: Building JSON ===\n")
...
    fmt.Printf("------ type conversion ------\n%v\n", valueToJSON(out))
    fmt.Println()
}

Una vez que se haya convertido el tipo mediante la función auxiliar valueToJSON dentro del archivo codelab.go, deberías ver el siguiente resultado adicional:

------ type conversion ------
  {
   "aud": "my-project",
   "exp": "2019-10-13T05:54:29Z",
   "extra_claims": {
      "group": "admin"
     },
   "iat": "2019-10-13T05:49:29Z",
   "iss": "auth.acme.com:12350",
   "nbf": "2019-10-13T05:49:29Z",
   "sub": "serviceAccount:delegate@acme.co"
  }

10. Cómo compilar prototipos

CEL puede crear mensajes protobuf para cualquier tipo de mensaje compilado en la aplicación. Agrega la función para compilar un google.rpc.context.AttributeContext.Request a partir de un jwt de entrada.

// exercise6 describes how to build proto message types within CEL.
//
// Given an input jwt and time now construct a
// google.rpc.context.AttributeContext.Request with the time and auth
// fields populated according to the go/api-attributes specification.
func exercise6() {
  fmt.Println("=== Exercise 6: Building Protos ===\n")

  // Construct an environment and indicate that the container for all references
  // within the expression is `google.rpc.context.AttributeContext`.
  requestType := &rpcpb.AttributeContext_Request{}
  env, _ := cel.NewEnv(
      // Add cel.Container() option for 'google.rpc.context.AttributeContext'
      cel.Types(requestType),
      // Add cel.Variable() option for 'jwt' as a map(string, Dyn) type
      // and for 'now' as a timestamp.
  )

  // Compile the Request message construction expression and validate that
  // the resulting expression type matches the fully qualified message name.
  //
  // Note: the field names within the proto message types are not quoted as they
  // are well-defined names composed of valid identifier characters. Also, note
  // that when building nested proto objects, the message name needs to prefix 
  // the object construction.
  ast := compile(env, `
    Request{
        auth: Auth{
            principal: jwt.iss + '/' + jwt.sub,
            audiences: [jwt.aud],
            presenter: 'azp' in jwt ? jwt.azp : "",
            claims: jwt
        },
        time: now
    }`,
    cel.ObjectType("google.rpc.context.AttributeContext.Request"),
  )
  program, _ := env.Program(ast)

  // Construct the message. The result is a ref.Val that returns a dynamic
  // proto message.
  out, _, _ := eval(
      program,
      map[string]interface{}{
          "jwt": map[string]interface{}{
              "sub": "serviceAccount:delegate@acme.co",
              "aud": "my-project",
              "iss": "auth.acme.com:12350",
              "extra_claims": map[string]string{
                  "group": "admin",
              },
          },
          "now": time.Now(),
      },
  )

  // Hint: Unwrap the CEL value to a proto. Make sure to use the
  // `ConvertToNative(reflect.TypeOf(requestType))` to convert the dynamic proto
  // message to the concrete proto message type expected.
  fmt.Printf("------ type unwrap ------\n%v\n", out)
  fmt.Println()
}

Ejecuta el código

Si vuelves a ejecutar el código, deberías ver el siguiente error:

ERROR: <input>:2:10: undeclared reference to 'Request' (in container '')
 |   Request{
 | .........^

El contenedor es básicamente el equivalente de un espacio de nombres o un paquete, pero incluso puede ser tan detallado como el nombre de un mensaje protobuf. Los contenedores de CEL usan las mismas reglas de resolución de espacio de nombres que Protobuf y C++ para determinar dónde se declara una variable, una función o un nombre de tipo determinados.

Dado el contenedor google.rpc.context.AttributeContext, el verificador de tipos y el evaluador probarán los siguientes nombres de identificadores para todas las variables, tipos y funciones:

  • google.rpc.context.AttributeContext.<id>
  • google.rpc.context.<id>
  • google.rpc.<id>
  • google.<id>
  • <id>

Para los nombres absolutos, se debe anteponer un punto inicial a la variable, el tipo o la referencia de la función. En el ejemplo, la expresión .<id> solo buscará el identificador <id> de nivel superior sin verificar primero dentro del contenedor.

Intenta especificar la opción cel.Container("google.rpc.context.AttributeContext") en el entorno CEL y vuelve a ejecutarla:

// exercise6 describes how to build proto message types within CEL.
//
// Given an input jwt and time now construct a
// google.rpc.context.AttributeContext.Request with the time and auth
// fields populated according to the go/api-attributes specification.
func exercise6() {
  fmt.Println("=== Exercise 6: Building Protos ===\n")
  // Construct an environment and indicate that the container for all references
  // within the expression is `google.rpc.context.AttributeContext`.
  requestType := &rpcpb.AttributeContext_Request{}
  env, _ := cel.NewEnv(
    // Add cel.Container() option for 'google.rpc.context.AttributeContext'
    cel.Container("google.rpc.context.AttributeContext.Request"),
    cel.Types(requestType),
    // Later, add cel.Variable() options for 'jwt' as a map(string, Dyn) type
    // and for 'now' as a timestamp.
    // cel.Variable("now", cel.TimestampType),
    // cel.Variable("jwt", cel.MapType(cel.StringType, cel.DynType)),
  )

 ...
}

Deberías obtener el siguiente resultado:

ERROR: <input>:4:16: undeclared reference to 'jwt' (in container 'google.rpc.context.AttributeContext')
 |     principal: jwt.iss + '/' + jwt.sub,
 | ...............^

... y muchos más errores...

A continuación, declara las variables jwt y now. El programa debería ejecutarse como se espera:

=== Exercise 6: Building Protos ===

  Request{
   auth: Auth{
    principal: jwt.iss + '/' + jwt.sub,
    audiences: [jwt.aud],
    presenter: 'azp' in jwt ? jwt.azp : "",
    claims: jwt
   },
   time: now
  }

------ input ------
jwt = {
  "aud": "my-project",
  "extra_claims": {
    "group": "admin"
  },
  "iss": "auth.acme.com:12350",
  "sub": "serviceAccount:delegate@acme.co"
}
now = seconds: 1588993027

------ result ------
value: &{0xc000485270 0xc000274180 {0xa6ee80 0xc000274180 22} 0xc0004be140 0xc0002bf700 false} (*types.protoObj)

----- type unwrap ----
&{0xc000485270 0xc000274180 {0xa6ee80 0xc000274180 22} 0xc0004be140 0xc0002bf700 false}

Si quieres obtener crédito adicional, también debes desenvolver el tipo llamando a out.Value() en el mensaje para ver cómo cambia el resultado.

11. Macros

Las macros se pueden usar para manipular el programa CEL en el momento del análisis. Las macros coinciden con una firma de llamada y manipulan la llamada de entrada y sus argumentos para producir una nueva subexpresión AST.

Las macros se pueden usar para implementar una lógica compleja en AST que no se puede escribir directamente en CEL. Por ejemplo, la macro has habilita la prueba de presencia de campo. Las macros de comprensión como existen y todas reemplazan una llamada a función con una iteración delimitada en una lista o un mapa de entrada. Ninguno de los conceptos es posible a nivel sintáctico, pero es posible a través de macroexpansión.

Agrega y ejecuta el siguiente ejercicio:

// exercise7 introduces macros for dealing with repeated fields and maps.
//
// Determine whether the jwt.extra_claims has at least one key that starts
// with the group prefix, and ensure that all group-like keys have list
// values containing only strings that end with '@acme.co`.
func exercise7() {
    fmt.Println("=== Exercise 7: Macros ===\n")
    env, _ := cel.NewEnv(
      cel.ClearMacros(),
      cel.Variable("jwt", cel.MapType(cel.StringType, cel.DynType)),
    )
    ast := compile(env,
        `jwt.extra_claims.exists(c, c.startsWith('group'))
                && jwt.extra_claims
                      .filter(c, c.startsWith('group'))
                      .all(c, jwt.extra_claims[c]
                                 .all(g, g.endsWith('@acme.co')))`,
        cel.BoolType)
    program, _ := env.Program(ast)

    // Evaluate a complex-ish JWT with two groups that satisfy the criteria.
    // Output: true.
    eval(program,
        map[string]interface{}{
            "jwt": map[string]interface{}{
                "sub": "serviceAccount:delegate@acme.co",
                "aud": "my-project",
                "iss": "auth.acme.com:12350",
                "extra_claims": map[string][]string{
                    "group1": {"admin@acme.co", "analyst@acme.co"},
                    "labels": {"metadata", "prod", "pii"},
                    "groupN": {"forever@acme.co"},
                },
            },
        })

    fmt.Println()
}

Deberías ver los siguientes errores:

ERROR: <input>:1:25: undeclared reference to 'c' (in container '')
 | jwt.extra_claims.exists(c, c.startsWith('group'))
 | ........................^

... y muchos más ...

Estos errores se producen porque las macros aún no están habilitadas. Para habilitar las macros, quita cel.ClearMacros() y vuelve a ejecutar:

=== Exercise 7: Macros ===

jwt.extra_claims.exists(c, c.startsWith('group'))
    && jwt.extra_claims
       .filter(c, c.startsWith('group'))
       .all(c, jwt.extra_claims[c]
              .all(g, g.endsWith('@acme.co')))

------ input ------
jwt = {
  "aud": "my-project",
  "extra_claims": {
    "group1": [
      "admin@acme.co",
      "analyst@acme.co"
    ],
    "groupN": [
      "forever@acme.co"
    ],
    "labels": [
      "metadata",
      "prod",
      "pii"
    ]
  },
  "iss": "auth.acme.com:12350",
  "sub": "serviceAccount:delegate@acme.co"
}

------ result ------
value: true (types.Bool)

Estas son las macros que se admiten actualmente:

Macro

Firma

Descripción

todos

r.all(var, cond)

Prueba si cond se evalúa como verdadero para todas las variables en el rango r.

existe

r.exists(var, cond)

Prueba si cond se evalúa como verdadero para cualquier variable en el rango r.

exists_one

r.exists_one(var, cond)

Prueba si cond se evalúa como verdadero para solo una var en el rango r.

filter

r.filter(var, cond)

Para las listas, crea una lista nueva en la que cada variable de elemento del rango r cumpla con la condición cond. Para los mapas, crea una lista nueva en la que cada variable clave del rango r cumpla con la condición cond.

mapa

r.map(var, expr)

Crea una lista nueva en la que cada variable del rango r sea transformada por expr.

r.map(var, cond, expr)

Igual que el mapa de dos argumentos, pero con un filtro de condición condicional antes de que se transforme el valor.

tiene

has(a.b)

Prueba de presencia para b en el valor a : En el caso de los mapas, la definición de prueba JSON. Para los protocolos, prueba el valor primitivo no predeterminado o un campo de mensaje o un conjunto.

Cuando el argumento de rango r es de tipo map, var será la clave del mapa, y para los valores de tipo list, var será el valor del elemento de la lista. Las macros all, exists, exists_one, filter y map realizan una reescritura de AST que realiza una iteración para cada iteración delimitada por el tamaño de la entrada.

Las comprensións limitadas garantizan que los programas CEL no estén completos, pero evalúan en tiempo superlineal con respecto a la entrada. Usa estas macros con moderación o no las uses. El uso intensivo de la comprensión suele ser un buen indicador de que una función personalizada proporcionaría una mejor experiencia del usuario y un mejor rendimiento.

12. Ajuste

Por el momento, hay varias funciones que son exclusivas de CEL-Go, pero que indican planes futuros para otras implementaciones de CEL. En el siguiente ejercicio, se muestran diferentes planes del programa para la misma AST:

// exercise8 covers features of CEL-Go which can be used to improve
// performance and debug evaluation behavior.
//
// Turn on the optimization, exhaustive eval, and state tracking
// ProgramOption flags to see the impact on evaluation behavior.
func exercise8() {
    fmt.Println("=== Exercise 8: Tuning ===\n")
    // Declare the x and 'y' variables as input into the expression.
    env, _ := cel.NewEnv(
        cel.Variable("x", cel.IntType),
        cel.Variable("y", cel.UintType),
    )
    ast := compile(env,
        `x in [1, 2, 3, 4, 5] && type(y) == uint`,
        cel.BoolType)

    // Try the different cel.EvalOptions flags when evaluating this AST for
    // the following use cases:
    // - cel.OptOptimize: optimize the expression performance.
    // - cel.OptExhaustiveEval: turn off short-circuiting.
    // - cel.OptTrackState: track state and compute a residual using the
    //   interpreter.PruneAst function.
    program, _ := env.Program(ast)
    eval(program, cel.NoVars())

    fmt.Println()
}

Optimize

Cuando la marca de optimización esté activada, CEL dedicará más tiempo a crear literales de listas y mapas de antemano, y optimizará ciertas llamadas, como el operador in, para que sean una prueba de membresía verdadera:

    // Turn on optimization.
    trueVars := map[string]interface{}{"x": int64(4), "y": uint64(2)}
    program, _ := env.Program(ast, cel.EvalOptions(cel.OptOptimize))
    // Try benchmarking with the optimization flag on and off.
    eval(program, trueVars)

Cuando se evalúa el mismo programa muchas veces con diferentes entradas, la optimización es una buena opción. Sin embargo, cuando el programa solo se evaluará una vez, la optimización agregará sobrecarga.

Evaluación exhaustiva

La evaluación exhaustiva puede ser útil para depurar el comportamiento de evaluación de expresiones, ya que proporciona estadísticas sobre el valor observado en cada paso de esa evaluación.

    // Turn on exhaustive eval to see what the evaluation state looks like.
    // The input is structure to show a false on the first branch, and true
    // on the second.
    falseVars := map[string]interface{}{"x": int64(6), "y": uint64(2)}
    program, _ = env.Program(ast, cel.EvalOptions(cel.OptExhaustiveEval))
    eval(program, falseVars)

Deberías ver una lista del estado de evaluación de la expresión para cada ID de expresión:

------ eval states ------
1: 6 (types.Int)
2: false (types.Bool)
3: &{0xc0000336b0 [1 2 3 4 5] {0x89f020 0xc000434760 151}} (*types.baseList)
4: 1 (types.Int)
5: 2 (types.Int)
6: 3 (types.Int)
7: 4 (types.Int)
8: 5 (types.Int)
9: uint (*types.Type)
10: 2 (types.Uint)
11: true (types.Bool)
12: uint (*types.Type)
13: false (types.Bool)

El ID de expresión 2 corresponde al resultado del operador in en el primero. y la expresión id 11 corresponde al operador == en la segunda. En una evaluación normal, la expresión habría tenido un cortocircuito después de que se calcularan 2. Si no hubiera sido uint, el estado habría mostrado dos razones por las que la expresión habría fallado y no solo una.

13. ¿Qué temas se abordaron?

Si necesitas un motor de expresiones, considera usar CEL. CEL es ideal para proyectos que necesitan ejecutar una configuración de usuario en la que el rendimiento es fundamental.

En los ejercicios anteriores, esperamos que te hayas familiarizado al pasar tus datos a CEL y obtener el resultado o la decisión.

Esperamos que tengas una idea del tipo de operaciones que puedes realizar, desde una decisión booleana hasta la generación de mensajes JSON y Protobúfer.

Esperamos que tengas una idea de cómo trabajar con las expresiones y de lo que hacen. Y entendemos formas comunes de ampliarlo.