1. Introduzione
Che cos'è CEL?
CEL è un linguaggio di espressione non Turing completo progettato per essere veloce, portatile e sicuro da eseguire. CEL può essere utilizzato da solo o incorporato in un prodotto più grande.
CEL è stato progettato come linguaggio in cui è sicuro eseguire il codice utente. Sebbene sia pericoloso chiamare ciecamente eval() sul codice Python di un utente, puoi eseguire in sicurezza il codice CEL di un utente. Inoltre, poiché CEL impedisce comportamenti che ne ridurrebbero le prestazioni, la valutazione avviene in modo sicuro nell'ordine di nanosecondi o microsecondi, il che lo rende ideale per le applicazioni in cui le prestazioni sono fondamentali.
CEL valuta le espressioni, che sono simili a funzioni a una sola riga o espressioni lambda. Sebbene il CEL venga comunemente utilizzato per le decisioni booleane, può essere utilizzato anche per creare oggetti più complessi come messaggi JSON o protobuf.
CEL è adatto al tuo progetto?
Poiché CEL valuta un'espressione dall'AST in nanosecondi e microsecondi, i casi d'uso ideali per CEL sono le applicazioni con percorsi critici per le prestazioni. La compilazione del codice CEL in AST non deve essere eseguita nei percorsi critici; le applicazioni ideali sono quelle in cui la configurazione viene eseguita spesso e modificata con relativa frequenza.
Ad esempio, l'esecuzione di una policy di sicurezza con ogni richiesta HTTP a un servizio è un caso d'uso ideale per CEL, perché la policy di sicurezza cambia raramente e CEL avrà un impatto trascurabile sul tempo di risposta. In questo caso, CEL restituisce un valore booleano, che indica se la richiesta deve essere consentita o meno, ma potrebbe restituire un messaggio più complesso.
Argomenti trattati in questo codelab
Il primo passaggio di questo codelab illustra la motivazione dell'utilizzo di CEL e i relativi concetti di base. Il resto è dedicato agli esercizi di programmazione che coprono i casi d'uso più comuni. Per un'analisi più approfondita del linguaggio, della semantica e delle funzionalità, consulta la definizione del linguaggio CEL su GitHub e la documentazione di CEL Go.
Questo codelab è rivolto agli sviluppatori che vogliono imparare CEL per utilizzare servizi che già lo supportano. Questo codelab non spiega come integrare CEL nel tuo progetto.
Cosa imparerai a fare
- Concetti principali di CEL
- Hello, World: utilizzo di CEL per valutare una stringa
- Creazione di variabili
- Comprendere il cortocircuito di CEL nelle operazioni logiche AND/OR
- Come utilizzare CEL per creare JSON
- Come utilizzare CEL per creare Protobuffer
- Creazione di macro
- Modi per ottimizzare le espressioni CEL
Che cosa ti serve
Prerequisiti
Questo codelab si basa su una comprensione di base di Protocol Buffers e Go Lang.
Se non hai familiarità con Protocol Buffers, il primo esercizio ti darà un'idea di come funziona CEL, ma poiché gli esempi più avanzati utilizzano Protocol Buffers come input in CEL, potrebbero essere più difficili da comprendere. Ti consigliamo di seguire prima uno di questi tutorial. Tieni presente che i protocol buffer non sono necessari per utilizzare CEL, ma vengono utilizzati ampiamente in questo codelab.
Puoi verificare che Go sia installato eseguendo il comando:
go --help
2. Concetti fondamentali
Applicazioni
CEL è di uso generale ed è stato utilizzato per diverse applicazioni, dal routing delle RPC alla definizione delle norme di sicurezza. CEL è estensibile, indipendente dall'applicazione e ottimizzato per i workflow di compilazione una sola volta e valutazione molte volte.
Molti servizi e applicazioni valutano le configurazioni dichiarative. Ad esempio, il controllo dell'accesso basato sui ruoli (RBAC) è una configurazione dichiarativa che produce una decisione di accesso dato un ruolo e un insieme di utenti. Se le configurazioni dichiarative rappresentano l'80% dei casi d'uso, CEL è uno strumento utile per completare il restante 20% quando gli utenti hanno bisogno di maggiore espressività.
Compilation
Un'espressione viene compilata in base a un ambiente. Il passaggio di compilazione produce un Abstract Syntax Tree (AST) in formato protobuf. Le espressioni compilate vengono generalmente archiviate per un utilizzo futuro per mantenere la valutazione il più rapida possibile. Una singola espressione compilata può essere valutata con molti input diversi.
Espressioni
Gli utenti definiscono le espressioni, mentre i servizi e le applicazioni definiscono l'ambiente in cui vengono eseguite. Una firma della funzione dichiara gli input ed è scritta al di fuori dell'espressione CEL. La libreria di funzioni disponibili per CEL viene importata automaticamente.
Nel seguente esempio, l'espressione accetta un oggetto richiesta e la richiesta include un token delle rivendicazioni. L'espressione restituisce un valore booleano che indica se il token delle rivendicazioni è ancora valido.
// 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
Gli ambienti sono definiti dai servizi. I servizi e le applicazioni che incorporano CEL dichiarano l'ambiente dell'espressione. L'ambiente è l'insieme di variabili e funzioni che possono essere utilizzate nelle espressioni.
Le dichiarazioni basate su proto vengono utilizzate dal controllo dei tipi CEL per garantire che tutti i riferimenti a identificatori e funzioni all'interno di un'espressione siano dichiarati e utilizzati correttamente.
Tre fasi dell'analisi di un'espressione
L'elaborazione di un'espressione prevede tre fasi: analisi, controllo e valutazione. Il pattern più comune per CEL prevede che un control plane analizzi e controlli le espressioni al momento della configurazione e memorizzi l'AST.

In fase di runtime, il piano dati recupera e valuta ripetutamente l'AST. CEL è ottimizzato per l'efficienza di runtime, ma l'analisi e il controllo non devono essere eseguiti in percorsi di codice critici per la latenza.

CEL viene analizzato da un'espressione leggibile da persone a un albero della sintassi astratta utilizzando una grammatica di analisi / lexer ANTLR. La fase di analisi genera un albero della sintassi astratta basato su proto in cui ogni nodo Expr dell'AST contiene un ID intero utilizzato per l'indicizzazione nei metadati generati durante l'analisi e il controllo. Il file syntax.proto prodotto durante l'analisi rappresenta fedelmente la rappresentazione astratta di ciò che è stato digitato nella forma di stringa dell'espressione.
Una volta analizzata un'espressione, questa può essere confrontata con l'ambiente per verificare che tutti gli identificatori di variabili e funzioni nell'espressione siano stati dichiarati e vengano utilizzati correttamente. Il type checker produce un file checked.proto che include metadati di risoluzione di tipi, variabili e funzioni che possono migliorare drasticamente l'efficienza della valutazione.
L'evaluator CEL ha bisogno di tre cose:
- Associazioni di funzioni per eventuali estensioni personalizzate
- Associazioni di variabili
- Un AST da valutare
Le associazioni di funzioni e variabili devono corrispondere a quelle utilizzate per compilare l'AST. Uno qualsiasi di questi input può essere riutilizzato in più valutazioni, ad esempio un AST valutato in molti insiemi di associazioni di variabili o le stesse variabili utilizzate in molti AST oppure le associazioni di funzioni utilizzate per l'intera durata di un processo (un caso comune).
3. Configura
Il codice per questo codelab si trova nella cartella codelab del repository cel-go. La soluzione è disponibile nella codelab/solution cartella dello stesso repository.
Clona il repository e passa alla directory:
git clone https://github.com/google/cel-go.git
cd cel-go/codelab
Esegui il codice utilizzando go run:
go run .
Dovresti vedere l'output seguente:
=== 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 ===
Dove si trovano i pacchetti CEL?
Nell'editor, apri codelab/codelab.go. Dovresti visualizzare la funzione principale che gestisce l'esecuzione degli esercizi in questo codelab, seguita da tre blocchi di funzioni di supporto. Il primo gruppo di helper assiste nelle fasi di valutazione di CEL:
- Funzione
Compile: analizza e controlla un'espressione di input rispetto a un ambiente - Funzione
Eval: valuta un programma compilato rispetto a un input - Funzione
Report: stampa in modo leggibile il risultato della valutazione
Inoltre, sono stati forniti gli helper request e auth per facilitare la costruzione dell'input per i vari esercizi.
Gli esercizi faranno riferimento ai pacchetti in base al nome breve del pacchetto. Se vuoi approfondire i dettagli, di seguito è riportata la mappatura dal pacchetto alla posizione di origine all'interno del repository google/cel-go:
Pacchetto | Posizione di origine | Descrizione |
cel | Interfacce di primo livello | |
ref | Interfacce di riferimento | |
tipi | Valori del tipo di runtime |
4. Ciao mondo!
Come da tradizione di tutti i linguaggi di programmazione, inizieremo creando e valutando "Hello World!".
Configura l'ambiente
Nell'editor, trova la dichiarazione di exercise1 e compila quanto segue per configurare l'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
}
Le applicazioni CEL valutano un'espressione in base a un ambiente. env, err := cel.NewEnv() configura l'ambiente standard.
L'ambiente può essere personalizzato fornendo le opzioni cel.EnvOption alla chiamata. Queste opzioni sono in grado di disattivare le macro, dichiarare variabili e funzioni personalizzate e così via.
L'ambiente CEL standard supporta tutti i tipi, gli operatori, le funzioni e le macro definiti all'interno della specifica del linguaggio.
Analizza e controlla l'espressione
Una volta configurato l'ambiente, le espressioni possono essere analizzate e controllate. Aggiungi quanto segue alla tua funzione:
// 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
}
Il valore iss restituito dalle chiamate Parse e Check è un elenco di problemi che potrebbero essere errori. Se iss.Err() non è nullo, è presente un errore nella sintassi o nella semantica e il programma non può procedere. Quando l'espressione è ben formata, il risultato di queste chiamate è un cel.Ast eseguibile.
Valuta l'espressione
Una volta analizzata e inserita in un cel.Ast, l'espressione può essere convertita in un programma valutabile le cui associazioni di funzioni e modalità di valutazione possono essere personalizzate con opzioni funzionali. Tieni presente che è anche possibile leggere un cel.Ast da un proto utilizzando le funzioni cel.CheckedExprToAst o cel.ParsedExprToAst.
Una volta pianificato un cel.Program, può essere valutato in base all'input chiamando Eval. Il risultato di Eval conterrà il risultato, i dettagli della valutazione e lo stato dell'errore.
Aggiungi pianificazione e chiama 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()
}
Per brevità, ometteremo i casi di errore inclusi sopra dagli esercizi futuri.
Esegui il codice
Nella riga di comando, esegui di nuovo il codice:
go run .
Dovresti vedere l'output seguente, insieme ai segnaposto per gli esercizi futuri.
=== Exercise 1: Hello World ===
------ input ------
(interpreter.emptyActivation)
------ result ------
value: Hello, World! (types.String)
5. Utilizzare le variabili in una funzione
La maggior parte delle applicazioni CEL dichiarerà variabili a cui è possibile fare riferimento all'interno delle espressioni. Le dichiarazioni di variabili specificano un nome e un tipo. Il tipo di una variabile può essere un tipo integrato CEL, un tipo noto di buffer di protocollo o qualsiasi tipo di messaggio protobuf, a condizione che il relativo descrittore venga fornito anche a CEL.
Aggiungere la funzione
Nell'editor, individua la dichiarazione di exercise2 e aggiungi quanto segue:
// 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()
}
Esegui di nuovo e comprendi l'errore
Esegui di nuovo il programma:
go run .
Dovresti vedere l'output seguente:
ERROR: <input>:1:1: undeclared reference to 'request' (in container '')
| request.auth.claims.group == 'admin'
| ^
Il controllo dei tipi produce un errore per l'oggetto richiesta, che include comodamente lo snippet di origine in cui si verifica l'errore.
6. Dichiarare le variabili
Aggiungi EnvOptions
Nell'editor, correggiamo l'errore risultante fornendo una dichiarazione per l'oggetto richiesta come messaggio di tipo google.rpc.context.AttributeContext.Request nel seguente modo:
// 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()
}
Esegui di nuovo e comprendi l'errore
Esegui di nuovo il programma:
go run .
Dovresti visualizzare il seguente errore:
ERROR: <input>:1:8: [internal] unexpected failed resolution of 'google.rpc.context.AttributeContext.Request'
| request.auth.claims.group == 'admin'
| .......^
Per utilizzare le variabili che fanno riferimento ai messaggi protobuf, il controllo dei tipi deve conoscere anche il descrittore del tipo.
Utilizza cel.Types per determinare il descrittore della richiesta nella funzione:
// 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()
}
Rerun successfully!
Esegui di nuovo il programma:
go run .
Dovresti visualizzare quanto segue:
=== 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)
Per riepilogare, abbiamo dichiarato una variabile per un errore, le abbiamo assegnato un descrittore di tipo e poi abbiamo fatto riferimento alla variabile nella valutazione dell'espressione.
7. AND/OR logico
Una delle funzionalità più esclusive di CEL è l'utilizzo di operatori logici commutativi. Entrambi i lati di una ramificazione condizionale possono interrompere la valutazione, anche in presenza di errori o input parziali.
In altre parole, CEL trova un ordine di valutazione che restituisce un risultato quando possibile, ignorando errori o anche dati mancanti che potrebbero verificarsi in altri ordini di valutazione. Le applicazioni possono fare affidamento su questa proprietà per ridurre al minimo il costo della valutazione, rimandando la raccolta di input costosi quando è possibile ottenere un risultato senza di essi.
Aggiungeremo un esempio AND/OR e poi lo proveremo con input diversi per capire come CEL interrompe la valutazione.
Crea la funzione
Nell'editor, aggiungi i seguenti contenuti all'esercizio 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"),
),
)
}
Successivamente, includi questa istruzione OR che restituirà il valore true se l'utente è membro del gruppo admin o ha un identificatore email specifico:
// 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)
}
Infine, aggiungi il caso eval che valuta l'utente con un insieme di rivendicazioni vuoto:
// 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()))
}
Esegui il codice con il set di rivendicazioni vuoto
Se esegui di nuovo il programma, dovresti vedere il seguente nuovo output:
=== 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)
Aggiornare la richiesta di valutazione
Successivamente, aggiorna lo scenario di valutazione in modo che venga superato da un principal diverso con il set di rivendicazioni vuoto:
// 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()))
}
Esegui il codice con un orario
Eseguendo di nuovo il programma,
go run .
dovresti visualizzare il seguente errore:
=== 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
In protobuf, sappiamo quali campi e tipi aspettarci. Nei valori di mappa e JSON, non sappiamo se una chiave sarà presente. Poiché non esiste un valore predefinito sicuro per una chiave mancante, CEL genera un errore per impostazione predefinita.
8. Funzioni personalizzate
Sebbene CEL includa molte funzioni integrate, in alcuni casi è utile una funzione personalizzata. Ad esempio, le funzioni personalizzate possono essere utilizzate per migliorare l'esperienza utente per condizioni comuni o esporre lo stato sensibile al contesto
In questo esercizio, esploreremo come esporre una funzione per raggruppare i controlli utilizzati più di frequente.
Chiamare una funzione personalizzata
Innanzitutto, crea il codice per configurare un override chiamato contains che determina se una chiave esiste in una mappa e ha un determinato valore. Lascia i segnaposto per la definizione della funzione e l'associazione della funzione:
// 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()
}
Esegui il codice e comprendi l'errore
Se esegui di nuovo il codice, dovresti visualizzare il seguente errore:
ERROR: <input>:1:29: found no matching overload for 'contains' applied to 'map(string, dyn).(string, string)'
| request.auth.claims.contains('group', 'admin')
| ............................^
Per correggere l'errore, dobbiamo aggiungere la funzione contains all'elenco delle dichiarazioni che attualmente dichiarano la variabile di richiesta.
Dichiara un tipo parametrizzato aggiungendo le seguenti tre righe. (Questa è la complessità massima di qualsiasi sovraccarico di funzioni per 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()
}
Aggiungere la funzione personalizzata
Successivamente, aggiungiamo una nuova funzione contains che utilizzerà i tipi parametrizzati:
// 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()
}
Esegui il programma per comprendere l'errore
Esegui l'esercizio. Dovresti visualizzare il seguente errore relativo alla funzione di runtime mancante:
------ result ------
error: no such overload: contains
Fornisci l'implementazione della funzione alla dichiarazione NewEnv utilizzando la funzione 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()
}
Il programma ora dovrebbe essere eseguito correttamente:
=== 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)
Cosa succede quando esiste la rivendicazione?
Per un credito extra, prova a impostare l'attestazione dell'amministratore sull'input per verificare che anche l'overload contains restituisca true quando l'attestazione esiste. Dovresti vedere l'output seguente:
=== 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)
Prima di andare avanti, vale la pena esaminare la funzione mapContainsKeyValue stessa:
// 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])
}
Per facilitare al massimo l'estensione, la firma delle funzioni personalizzate prevede che gli argomenti siano di tipo ref.Val. Il compromesso qui è che la facilità di estensione aggiunge un onere all'implementatore per garantire che tutti i tipi di valori vengano gestiti correttamente. Quando i tipi o il conteggio degli argomenti di input non corrispondono alla dichiarazione della funzione, deve essere restituito un errore no such overload.
cel.FunctionBinding() aggiunge una protezione del tipo di runtime per garantire che il contratto di runtime corrisponda alla dichiarazione con controllo del tipo nell'ambiente.
9. Creazione di JSON
CEL può anche produrre output non booleani, come JSON. Aggiungi quanto segue alla tua funzione:
// 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()
}
Esegui il codice
Se esegui di nuovo il codice, dovresti visualizzare il seguente errore:
ERROR: <input>:5:11: undeclared reference to 'now' (in container '')
| 'iat': now,
| ..........^
... and more ...
Aggiungi una dichiarazione per la variabile now di tipo cel.TimestampType a cel.NewEnv() ed esegui di nuovo:
// 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()
}
Esegui di nuovo il codice e dovrebbe funzionare:
=== 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}}
Il programma viene eseguito, ma il valore di output out deve essere convertito esplicitamente in JSON. In questo caso, la rappresentazione CEL interna è convertibile in JSON perché fa riferimento solo a tipi supportati da JSON o per i quali esiste un mapping da Proto a JSON noto.
// 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 volta convertito il tipo utilizzando la funzione helper valueToJSON all'interno del file codelab.go, dovresti visualizzare il seguente output aggiuntivo:
------ 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. Building Protos
CEL può creare messaggi protobuf per qualsiasi tipo di messaggio compilato nell'applicazione. Aggiungi la funzione per creare un google.rpc.context.AttributeContext.Request da un jwt di input
// 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()
}
Esegui il codice
Se esegui di nuovo il codice, dovresti visualizzare il seguente errore:
ERROR: <input>:2:10: undeclared reference to 'Request' (in container '')
| Request{
| .........^
Il container è fondamentalmente l'equivalente di uno spazio dei nomi o di un pacchetto, ma può essere anche granulare come un nome di messaggio protobuf. I contenitori CEL utilizzano le stesse regole di risoluzione dello spazio dei nomi di Protobuf e C++ per determinare dove viene dichiarata una determinata variabile, funzione o nome di tipo.
Dato il contenitore google.rpc.context.AttributeContext, il controllo dei tipi e il valutatore proveranno i seguenti nomi di identificatori per tutte le variabili, i tipi e le funzioni:
google.rpc.context.AttributeContext.<id>google.rpc.context.<id>google.rpc.<id>google.<id><id>
Per i nomi assoluti, anteponi un punto al riferimento alla variabile, al tipo o alla funzione. Nell'esempio, l'espressione .<id> cercherà solo l'identificatore <id> di primo livello senza prima eseguire il controllo all'interno del contenitore.
Prova a specificare l'opzione cel.Container("google.rpc.context.AttributeContext") per l'ambiente CEL ed esegui di nuovo:
// 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)),
)
...
}
Dovrebbe essere visualizzato il seguente output:
ERROR: <input>:4:16: undeclared reference to 'jwt' (in container 'google.rpc.context.AttributeContext')
| principal: jwt.iss + '/' + jwt.sub,
| ...............^
... e molti altri errori...
Poi dichiara le variabili jwt e now e il programma dovrebbe essere eseguito come previsto:
=== 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}
Per un ulteriore credito, devi anche decomprimere il tipo chiamando out.Value() sul messaggio per vedere come cambia il risultato.
11. Macro
Le macro possono essere utilizzate per manipolare il programma CEL in fase di analisi. Le macro corrispondono a una firma di chiamata e manipolano la chiamata di input e i relativi argomenti per produrre un nuovo AST di sottoespressione.
Le macro possono essere utilizzate per implementare una logica complessa nell'AST che non può essere scritta direttamente in CEL. Ad esempio, la macro has consente di testare la presenza dei campi. Le macro di comprensione come exists e all sostituiscono una chiamata di funzione con un'iterazione limitata su un elenco o una mappa di input. Nessuno dei due concetti è possibile a livello sintattico, ma sono possibili tramite l'espansione delle macro.
Aggiungi ed esegui l'esercizio successivo:
// 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()
}
Dovresti visualizzare i seguenti errori:
ERROR: <input>:1:25: undeclared reference to 'c' (in container '')
| jwt.extra_claims.exists(c, c.startsWith('group'))
| ........................^
… e molti altri…
Questi errori si verificano perché le macro non sono ancora abilitate. Per attivare le macro, rimuovi cel.ClearMacros() ed esegui di nuovo:
=== 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)
Queste sono le macro attualmente supportate:
Macro | Firma | Descrizione |
tutti | r.all(var, cond) | Verifica se cond restituisce true per tutte le variabili nell'intervallo r. |
esiste | r.exists(var, cond) | Verifica se cond restituisce true per qualsiasi var nell'intervallo r. |
exists_one | r.exists_one(var, cond) | Verifica se cond restituisce true per una sola variabile nell'intervallo r. |
filtro | r.filter(var, cond) | Per gli elenchi, crea un nuovo elenco in cui ogni elemento var nell'intervallo r soddisfa la condizione cond. Per le mappe, crea un nuovo elenco in cui ogni variabile chiave nell'intervallo r soddisfa la condizione cond. |
mappa | r.map(var, expr) | Crea un nuovo elenco in cui ogni variabile nell'intervallo r viene trasformata da expr. |
r.map(var, cond, expr) | Uguale a map con due argomenti, ma con un filtro cond condizionale prima della trasformazione del valore. | |
contiene | has(a.b) | Test di presenza per b sul valore a : per Maps, definizione dei test JSON. Per i proto, esegue il test di un valore primitivo non predefinito o di un campo di messaggio impostato. |
Quando l'argomento intervallo r è di tipo map, var sarà la chiave della mappa, mentre per i valori di tipo list, var sarà il valore dell'elemento dell'elenco. Le macro all, exists, exists_one, filter e map eseguono una riscrittura AST che esegue un'iterazione for-each delimitata dalle dimensioni dell'input.
Le comprensioni delimitate garantiscono che i programmi CEL non siano Turing completi, ma vengono valutati in tempo superlineare rispetto all'input. Utilizza queste macro con parsimonia o non utilizzarle affatto. L'uso intenso di comprensioni è in genere un buon indicatore del fatto che una funzione personalizzata fornirebbe un'esperienza utente e un rendimento migliori.
12. Ottimizzazione
Al momento esistono alcune funzionalità esclusive di CEL-Go, che però sono indicative dei piani futuri per altre implementazioni di CEL. Il seguente esercizio mostra diversi piani del programma per lo stesso 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 il flag di ottimizzazione è attivato, CEL impiega più tempo per creare la lista e mappare i valori letterali in anticipo e ottimizzare determinate chiamate, ad esempio l'operatore in, in modo che sia un vero test di appartenenza all'insieme:
// 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)
L'ottimizzazione è una buona scelta quando lo stesso programma viene valutato più volte in base a input diversi. Tuttavia, quando il programma verrà valutato una sola volta, l'ottimizzazione aggiungerà solo un sovraccarico.
Exhaustive Eval
La valutazione esaustiva può essere utile per il debug del comportamento di valutazione delle espressioni, in quanto fornisce informazioni sul valore osservato in ogni passaggio della valutazione dell'espressione.
// 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)
Dovresti visualizzare un elenco dello stato di valutazione dell'espressione per ogni ID espressione:
------ 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)
L'ID espressione 2 corrisponde al risultato dell'operatore in nel primo ramo, mentre l'ID espressione 11 corrisponde all'operatore == nel secondo. In una normale valutazione, l'espressione sarebbe stata interrotta dopo il calcolo di 2. Se y non fosse stato uint, lo stato avrebbe mostrato due motivi per cui l'espressione non sarebbe riuscita e non solo uno.
13. Quali argomenti sono stati trattati?
Se hai bisogno di un motore di espressioni, valuta l'utilizzo di CEL. CEL è ideale per i progetti che devono eseguire la configurazione utente in cui le prestazioni sono fondamentali.
Negli esercizi precedenti, ci auguriamo che tu abbia acquisito familiarità con il passaggio dei dati in CEL e con la restituzione dell'output o della decisione.
Ci auguriamo che tu abbia un'idea del tipo di operazioni che puoi eseguire, da una decisione booleana alla generazione di messaggi JSON e Protobuffer.
Ci auguriamo che tu abbia capito come utilizzare le espressioni e cosa fanno. e comprendiamo i modi comuni per estenderlo.