Codelab CEL-Go: expressões incorporadas rápidas, seguras e seguras

1. Introdução

O que é CEL?

A CEL é uma linguagem de expressão sem completude de Turing projetada para ser rápida, portátil e segura de executar. A CEL pode ser usada sozinha ou incorporada a um produto maior.

A CEL foi projetada como uma linguagem em que é seguro executar o código do usuário. Embora seja perigoso chamar eval() sem pensar no código Python de um usuário, é possível executar o código CEL de um usuário com segurança. Como a CEL impede comportamentos que diminuiriam a performance, ela avalia com segurança na ordem de nanossegundos a microssegundos, sendo ideal para aplicativos com desempenho crítico.

A CEL avalia expressões, que são semelhantes a funções de linha única ou expressões lambda. Embora o CEL seja usado com frequência para decisões booleanas, ele também pode ser usado para construir objetos mais complexos, como mensagens JSON ou protobuf.

O CEL é adequado para seu projeto?

Como a CEL avalia uma expressão da AST em nanossegundos para microssegundos, o caso de uso ideal para a CEL são aplicativos com caminhos essenciais para o desempenho. A compilação do código CEL na AST não deve ser feita em caminhos críticos. O ideal é que os aplicativos executem a configuração com frequência e a modifiquem com pouca frequência.

Por exemplo, executar uma política de segurança com cada solicitação HTTP a um serviço é um caso de uso ideal para a CEL, porque a política de segurança muda raramente, e a CEL terá um impacto insignificante no tempo de resposta. Nesse caso, a CEL retorna um booleano, se a solicitação deve ser permitida ou não, mas poderia retornar uma mensagem mais complexa.

O que este codelab aborda?

A primeira etapa deste codelab explica a motivação para usar a CEL e seus conceitos básicos. O restante é dedicado a Exercícios de programação que abrangem casos de uso comuns. Para uma análise mais detalhada da linguagem, da semântica e dos recursos, consulte a definição da linguagem CEL no GitHub e os documentos do Go da CEL.

Este codelab é destinado a desenvolvedores que querem aprender CEL para usar serviços que já oferecem suporte a essa linguagem. Este codelab não aborda como integrar a CEL ao seu próprio projeto.

O que você vai aprender

  • Principais conceitos da CEL
  • Hello, World: como usar a CEL para avaliar uma string
  • Como criar variáveis
  • Como entender o curto-circuito da CEL em operações lógicas AND/OR
  • Como usar a CEL para criar JSON
  • Como usar a CEL para criar Protobuffers
  • Como criar macros
  • Como ajustar suas expressões CEL

O que é necessário

Pré-requisitos

Este codelab se baseia em um conhecimento básico de Protocol Buffers e Go Lang.

Se você não conhece os buffers de protocolo, o primeiro exercício vai dar uma ideia de como a CEL funciona. No entanto, como os exemplos mais avançados usam buffers de protocolo como entrada na CEL, eles podem ser mais difíceis de entender. Primeiro, siga um destes tutoriais. Os buffers de protocolo não são necessários para usar a CEL, mas são usados extensivamente neste codelab.

Para testar se o Go está instalado, execute:

go --help

2. Principais conceitos

Aplicativos

A CEL é de uso geral e tem sido usada em diversas aplicações, desde o roteamento de RPCs até a definição de políticas de segurança. A CEL é extensível, independente de aplicativos e otimizada para fluxos de trabalho de compilação única e avaliação múltipla.

Muitos serviços e aplicativos avaliam configurações declarativas. Por exemplo, o controle de acesso baseado em função (RBAC) é uma configuração declarativa que produz uma decisão de acesso com base em uma função e um conjunto de usuários. Se as configurações declarativas forem o caso de uso de 80%, a CEL será uma ferramenta útil para completar os 20% restantes quando os usuários precisarem de mais poder expressivo.

Compilação

Uma expressão é compilada em um ambiente. A etapa de compilação produz uma árvore de sintaxe abstrata (AST, na sigla em inglês) no formato protobuf. As expressões compiladas geralmente são armazenadas para uso futuro, mantendo a avaliação o mais rápida possível. Uma única expressão compilada pode ser avaliada com muitas entradas diferentes.

Expressões

Os usuários definem expressões, e os serviços e aplicativos definem o ambiente em que elas são executadas. Uma assinatura de função declara as entradas e é escrita fora da expressão CEL. A biblioteca de funções disponível para CEL é importada automaticamente.

No exemplo a seguir, a expressão usa um objeto de solicitação, e a solicitação inclui um token de declarações. A expressão retorna um booleano indicando se o token de declarações ainda é 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

Ambiente

Os ambientes são definidos por serviços. Serviços e aplicativos que incorporam a CEL declaram o ambiente de expressão. O ambiente é o conjunto de variáveis e funções que podem ser usadas em expressões.

As declarações baseadas em proto são usadas pelo verificador de tipos da CEL para garantir que todas as referências de identificadores e funções em uma expressão sejam declaradas e usadas corretamente.

Três fases da análise de uma expressão

Há três fases no processamento de uma expressão: análise, verificação e avaliação. O padrão mais comum para CEL é que um plano de controle analise e verifique expressões no momento da configuração e armazene a AST.

c71fc08068759f81.png

Em tempo de execução, o plano de dados recupera e avalia a AST repetidamente. A CEL é otimizada para eficiência de tempo de execução, mas a análise e a verificação não devem ser feitas em caminhos de código críticos para latência.

49ab7d8517143b66.png

A CEL é analisada de uma expressão legível por humanos para uma árvore de sintaxe abstrata usando uma gramática de analisador léxico / analisador ANTLR. A fase de análise emite uma árvore de sintaxe abstrata baseada em proto em que cada nó Expr na AST contém um ID inteiro usado para indexar metadados gerados durante a análise e a verificação. O syntax.proto produzido durante a análise representa fielmente a representação abstrata do que foi digitado na forma de string da expressão.

Depois que uma expressão é analisada, ela pode ser verificada no ambiente para garantir que todos os identificadores de variáveis e funções na expressão foram declarados e estão sendo usados corretamente. O verificador de tipos produz um checked.proto que inclui metadados de resolução de tipo, variável e função que podem melhorar muito a eficiência da avaliação.

O avaliador da CEL precisa de três coisas:

  • Vinculações de função para extensões personalizadas
  • Vinculações de variáveis
  • Uma AST a ser avaliada

As vinculações de função e variável precisam corresponder ao que foi usado para compilar a AST. Qualquer uma dessas entradas pode ser reutilizada em várias avaliações, como uma AST sendo avaliada em muitos conjuntos de vinculações de variáveis, ou as mesmas variáveis usadas em muitas ASTs, ou as vinculações de funções usadas durante o ciclo de vida de um processo (um caso comum).

3. Configurar

O código deste codelab está na pasta codelab do repositório cel-go. A solução está disponível na pasta codelab/solution do mesmo repositório.

Clone e acesse o repositório:

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

Execute o código usando go run:

go run .

Você verá esta resposta:

=== 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 ===

Onde estão os pacotes do CEL?

No editor, abra codelab/codelab.go. Você vai ver a função principal, que impulsiona a execução dos exercícios neste codelab, seguida por três blocos de funções auxiliares. O primeiro conjunto de helpers ajuda nas fases da avaliação de CEL:

  • Função Compile: analisa e verifica uma expressão de entrada em relação a um ambiente.
  • Função Eval: avalia um programa compilado em relação a uma entrada.
  • Função Report: imprime o resultado da avaliação de forma organizada.

Além disso, os helpers request e auth foram fornecidos para ajudar na construção de entradas para os vários exercícios.

Os exercícios vão se referir aos pacotes pelo nome curto. O mapeamento do pacote para o local de origem no repositório google/cel-go está abaixo, caso você queira saber mais detalhes:

Pacote

Local de origem

Descrição

cel

cel-go/cel

Interfaces de nível superior

ref

cel-go/common/types/ref

Interfaces de referência

tipos

cel-go/common/types

Valores do tipo de ambiente de execução

4. Olá, mundo!

Seguindo a tradição de todas as linguagens de programação, vamos começar criando e avaliando "Hello World!".

Configurar o ambiente

No editor, encontre a declaração de exercise1 e preencha o seguinte para configurar o ambiente:

// 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
}

Os aplicativos CEL avaliam uma expressão em relação a um ambiente. env, err := cel.NewEnv() configura o ambiente padrão.

O ambiente pode ser personalizado fornecendo as opções cel.EnvOption à chamada. Essas opções podem desativar macros, declarar variáveis e funções personalizadas etc.

O ambiente padrão da CEL é compatível com todos os tipos, operadores, funções e macros definidos na especificação da linguagem.

Analisar e verificar a expressão

Depois que o ambiente é configurado, as expressões podem ser analisadas e verificadas. Adicione o seguinte à sua função:

// 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
}

O valor iss retornado pelas chamadas Parse e Check é uma lista de problemas que podem ser erros. Se iss.Err() não for nulo, haverá um erro na sintaxe ou na semântica, e o programa não poderá continuar. Quando a expressão é bem formada, o resultado dessas chamadas é um cel.Ast executável.

Avalie a expressão

Depois que a expressão é analisada e verificada em um cel.Ast, ela pode ser convertida em um programa avaliável cujos vinculações de função e modos de avaliação podem ser personalizados com opções funcionais. Também é possível ler um cel.Ast de um proto usando as funções cel.CheckedExprToAst ou cel.ParsedExprToAst.

Depois que um cel.Program é planejado, ele pode ser avaliado em relação à entrada chamando Eval. O resultado do Eval vai conter o resultado, os detalhes da avaliação e o status do erro.

Adicione planejamento e chame 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()
}

Para simplificar, vamos omitir os casos de erro incluídos acima dos exercícios futuros.

Executar o código

Na linha de comando, execute o código novamente:

go run .

Você vai ver a seguinte saída, junto com marcadores de posição para os próximos exercícios.

=== Exercise 1: Hello World ===

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

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

5. Usar variáveis em uma função

A maioria dos aplicativos CEL declara variáveis que podem ser referenciadas em expressões. As declarações de variáveis especificam um nome e um tipo. O tipo de uma variável pode ser um tipo integrado do CEL, um tipo conhecido de buffer de protocolo ou qualquer tipo de mensagem protobuf, desde que o descritor também seja fornecido ao CEL.

Adicionar a função

No editor, encontre a declaração de exercise2 e adicione o seguinte:

// 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()
}

Executar novamente e entender o erro

Execute o programa novamente:

go run .

Você verá esta resposta:

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

O verificador de tipos produz um erro para o objeto de solicitação, que inclui convenientemente o snippet de origem em que o erro ocorre.

6. Declare as variáveis

Adicionar EnvOptions

No editor, vamos corrigir o erro resultante fornecendo uma declaração para o objeto de solicitação como uma mensagem do tipo google.rpc.context.AttributeContext.Request, assim:

// 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()
}

Executar novamente e entender o erro

Executar o programa novamente:

go run .

Você verá o seguinte erro:

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

Para usar variáveis que se referem a mensagens protobuf, o verificador de tipos também precisa conhecer o descritor de tipo.

Use cel.Types para determinar o descritor da solicitação na sua função:

// 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()
}

A nova execução foi concluída.

Execute o programa novamente:

go run .

Você verá o seguinte:

=== 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)

Para revisar, acabamos de declarar uma variável para um erro, atribuímos a ela um descritor de tipo e referenciamos a variável na avaliação da expressão.

7. AND/OR lógico

Um dos recursos mais exclusivos da CEL é o uso de operadores lógicos comutativos. Qualquer lado de uma ramificação condicional pode interromper a avaliação, mesmo diante de erros ou entrada parcial.

Em outras palavras, a CEL encontra uma ordem de avaliação que gera um resultado sempre que possível, ignorando erros ou até mesmo dados ausentes que podem ocorrer em outras ordens de avaliação. Os aplicativos podem usar essa propriedade para minimizar o custo da avaliação, adiando a coleta de entradas caras quando um resultado pode ser alcançado sem elas.

Vamos adicionar um exemplo de AND/OR e testar com entradas diferentes para entender como a CEL interrompe a avaliação.

Criar a função

No editor, adicione o seguinte conteúdo ao exercício 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"),
    ),
  )
}

Em seguida, inclua esta instrução OR, que vai retornar "true" se o usuário for membro do grupo admin ou tiver um identificador de e-mail específico:

// 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 fim, adicione o caso eval que avalia o usuário com um conjunto de declarações vazio:

// 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()))
}

Executar o código com o conjunto de declarações vazio

Ao executar o programa novamente, você vai ver esta nova saída:

=== 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)

Atualizar o caso de avaliação

Em seguida, atualize o caso de avaliação para transmitir um principal diferente com o conjunto de declarações vazio:

// 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()))
}

Executar o código com um tempo

Executando o programa novamente,

go run .

você vai receber o seguinte erro:

=== 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

No protobuf, sabemos quais campos e tipos esperar. Em valores de mapa e JSON, não sabemos se uma chave vai estar presente. Como não há um valor padrão seguro para uma chave ausente, a CEL usa o erro como padrão.

8. Funções personalizadas

Embora a CEL inclua muitas funções integradas, há ocasiões em que uma função personalizada é útil. Por exemplo, as funções personalizadas podem ser usadas para melhorar a experiência do usuário em condições comuns ou expor um estado sensível ao contexto.

Neste exercício, vamos mostrar como expor uma função para agrupar verificações usadas com frequência.

Chamar uma função personalizada

Primeiro, crie o código para configurar uma substituição chamada contains, que determina se uma chave existe em um mapa e tem um valor específico. Deixe os marcadores de posição para a definição e a vinculação da função:

// 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()
}  

Executar o código e entender o erro

Ao executar o código novamente, você vai receber o seguinte erro:

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

Para corrigir o erro, precisamos adicionar a função contains à lista de declarações que atualmente declara a variável de solicitação.

Declare um tipo parametrizado adicionando as três linhas a seguir. (Essa é a sobrecarga de função mais complicada para o 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()
}

Adicionar a função personalizada

Em seguida, vamos adicionar uma nova função "contains" que vai usar os 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()
}

Execute o programa para entender o erro

Faça o exercício. Você vai ver o seguinte erro sobre a função de ambiente de execução ausente:

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

Forneça a implementação da função à declaração NewEnv usando a função 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()
}

O programa vai ser executado:

=== 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)

O que acontece quando a reivindicação existe?

Para um crédito extra, tente definir a declaração de administrador na entrada para verificar se a sobrecarga de "contains" também retorna "true" quando a declaração existe. Você verá esta resposta:

=== 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 a pena inspecionar a própria função mapContainsKeyValue:

// 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 facilitar a extensão, a assinatura das funções personalizadas espera argumentos do tipo ref.Val. A facilidade de extensão adiciona um ônus ao implementador para garantir que todos os tipos de valor sejam processados corretamente. Quando os tipos ou a contagem de argumentos de entrada não correspondem à declaração da função, um erro no such overload precisa ser retornado.

O cel.FunctionBinding() adiciona uma proteção de tipo de tempo de execução para garantir que o contrato de tempo de execução corresponda à declaração verificada por tipo no ambiente.

9. Como criar JSON

A CEL também pode produzir saídas não booleanas, como JSON. Adicione o seguinte à sua função:

// 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()
}

Executar o código

Ao executar o código novamente, você vai receber o seguinte erro:

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

Adicione uma declaração para a variável now do tipo cel.TimestampType a cel.NewEnv() e execute novamente:

// 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()
}

Execute o código novamente. Ele vai ser executado corretamente:

=== 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}}

O programa é executado, mas o valor de saída out precisa ser convertido explicitamente em JSON. A representação interna da CEL nesse caso é conversível em JSON, já que se refere apenas a tipos que o JSON pode aceitar ou para os quais há um mapeamento de Proto para JSON conhecido.

// 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()
}

Depois que o tipo for convertido usando a função auxiliar valueToJSON no arquivo codelab.go, você vai ver esta saída 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. Como criar protótipos

A CEL pode criar mensagens protobuf para qualquer tipo de mensagem compilada no aplicativo. Adicione a função para criar um google.rpc.context.AttributeContext.Request de um 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()
}

Executar o código

Ao executar o código novamente, você vai receber o seguinte erro:

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

O contêiner é basicamente o equivalente a um namespace ou pacote, mas pode ser tão granular quanto um nome de mensagem protobuf. Os contêineres CEL usam as mesmas regras de resolução de namespace que Protobuf e C++ para determinar onde uma determinada variável, função ou nome de tipo é declarada.

Considerando o contêiner google.rpc.context.AttributeContext, o verificador de tipos e o avaliador vão tentar os seguintes nomes de identificadores para todas as variáveis, tipos e funções:

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

Para nomes absolutos, adicione um ponto antes da variável, do tipo ou da referência de função. No exemplo, a expressão .<id> só vai pesquisar o identificador <id> de nível superior sem verificar primeiro dentro do contêiner.

Tente especificar a opção cel.Container("google.rpc.context.AttributeContext") para o ambiente CEL e execute novamente:

// 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)),
  )

 ...
}

A seguinte saída será exibida:

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

… e muitos outros erros…

Em seguida, declare as variáveis jwt e now. O programa vai ser executado conforme o esperado:

=== 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}

Para um crédito extra, desencapsule o tipo chamando out.Value() na mensagem para ver como o resultado muda.

11. Macros

As macros podem ser usadas para manipular o programa CEL durante a análise. As macros correspondem a uma assinatura de chamada e manipulam a chamada de entrada e os argumentos dela para produzir uma nova AST de subexpressão.

As macros podem ser usadas para implementar uma lógica complexa na AST que não pode ser escrita diretamente em CEL. Por exemplo, a macro has permite o teste de presença de campo. As macros de compreensão, como "exists" e "all", substituem uma chamada de função por uma iteração limitada em uma lista ou mapa de entrada. Nenhum dos conceitos é possível no nível sintático, mas eles podem ser usados com expansões de macro.

Adicione e execute o próximo exercício:

// 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()
}

Você vai encontrar os seguintes erros:

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

… e muito mais…

Esses erros ocorrem porque as macros ainda não estão ativadas. Para ativar as macros, remova cel.ClearMacros() e execute novamente:

=== 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 são as macros compatíveis no momento:

Macro

Assinatura

Descrição

todas

r.all(var, cond)

Teste se cond é avaliado como verdadeiro para todas as variáveis no intervalo r.

existe

r.exists(var, cond)

Teste se cond é avaliado como verdadeiro para qualquer var no intervalo r.

exists_one

r.exists_one(var, cond)

Teste se cond é avaliado como verdadeiro para apenas uma var no intervalo r.

filtrar

r.filter(var, cond)

Para listas, crie uma nova lista em que cada elemento var no intervalo r satisfaça a condição cond. Para mapas, crie uma nova lista em que cada variável de chave no intervalo r satisfaz a condição cond.

mapa

r.map(var, expr)

Cria uma nova lista em que cada variável no intervalo r é transformada por expr.

r.map(var, cond, expr)

Igual ao mapa de dois argumentos, mas com um filtro condicional "cond" antes da transformação do valor.

tem

has(a.b)

Teste de presença de b no valor a : para mapas, definição de testes JSON. Para protos, testa um valor primitivo não padrão ou um campo de mensagem de conjunto.

Quando o argumento de intervalo r é do tipo map, var é a chave do mapa. Para valores do tipo list, var é o valor do elemento da lista. As macros all, exists, exists_one, filter e map realizam uma reescrita de AST que executa uma iteração para cada elemento limitada pelo tamanho da entrada.

As comprehensions limitadas garantem que os programas CEL não sejam Turing-completos, mas são avaliados em tempo superlinear em relação à entrada. Use essas macros com moderação ou não use. O uso intenso de comprehensions geralmente é um bom indicador de que uma função personalizada proporcionaria uma experiência do usuário e uma performance melhores.

12. Ajuste

No momento, há alguns recursos exclusivos do CEL-Go, mas que indicam planos futuros para outras implementações do CEL. O exercício a seguir mostra diferentes planos de programação para a mesma 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

Quando a flag de otimização está ativada, a CEL passa mais tempo criando literais de lista e mapa com antecedência e otimiza determinadas chamadas, como o operador "in", para ser um verdadeiro teste de associação de conjunto:

    // 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)

Quando o mesmo programa é avaliado muitas vezes com entradas diferentes, a otimização é uma boa escolha. No entanto, quando o programa será avaliado apenas uma vez, a otimização só vai adicionar sobrecarga.

Avaliação exaustiva

A avaliação exaustiva pode ser útil para depurar o comportamento de avaliação de expressões, já que fornece insights sobre o valor observado em cada etapa da avaliação.

    // 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)

Você vai ver uma lista do estado de avaliação de cada ID de expressão:

------ 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)

O ID da expressão 2 corresponde ao resultado do operador "in" na primeira ramificação, e o ID da expressão 11 corresponde ao operador "==" na segunda. Em uma avaliação normal, a expressão teria sido interrompida após o cálculo de 2. Se y não fosse uint, o estado mostraria dois motivos para a falha da expressão, e não apenas um.

13. O que foi abordado?

Se você precisar de um mecanismo de expressão, use a CEL. A CEL é ideal para projetos que precisam executar a configuração do usuário em que a performance é essencial.

Nos exercícios anteriores, esperamos que você tenha se acostumado a transmitir seus dados para a CEL e receber a saída ou a decisão de volta.

Esperamos que você tenha uma ideia do tipo de operações que pode realizar, desde uma decisão booleana até a geração de mensagens JSON e Protobuffer.

Esperamos que você tenha uma ideia de como trabalhar com as expressões e o que elas fazem. E entendemos as maneiras comuns de estender esse período.