1. Введение
Что такое CEL?
CEL — это язык выражений, не обладающий полной функцией Тьюринга, разработанный для обеспечения высокой скорости, переносимости и безопасности выполнения. CEL можно использовать как самостоятельно, так и встраивать в более крупные продукты.
CEL был разработан как язык, в котором безопасно выполнять пользовательский код. Хотя слепой вызов функции eval() для пользовательского кода на Python опасен, можно безопасно выполнить пользовательский код на CEL. А поскольку CEL предотвращает поведение, которое могло бы снизить его производительность, он безопасно вычисляет значения за время от наносекунд до микросекунд; это идеально подходит для приложений, критически важных с точки зрения производительности.
CEL вычисляет выражения, аналогичные однострочным функциям или лямбда-выражениям. Хотя CEL обычно используется для логических операций, его также можно применять для создания более сложных объектов, таких как сообщения JSON или protobuf.
Подходит ли CEL для вашего проекта?
Поскольку CEL вычисляет выражение из AST за наносекунды и микросекунды, идеальным вариантом применения CEL являются приложения с критически важными для производительности путями выполнения. Компиляция кода CEL в AST не должна выполняться в критически важных путях; идеальными приложениями являются те, в которых конфигурация выполняется часто и изменяется относительно редко.
Например, применение политики безопасности к каждому HTTP-запросу к сервису является идеальным вариантом использования CEL, поскольку политика безопасности меняется редко, и CEL окажет незначительное влияние на время ответа. В этом случае CEL возвращает логическое значение, указывающее, разрешен запрос или нет, но может также возвращать более сложное сообщение.
Что рассматривается в этом практическом занятии?
Первый этап этого практического занятия посвящен обоснованию использования CEL и его основным концепциям. Остальная часть посвящена упражнениям по программированию, охватывающим распространенные сценарии использования. Для более подробного ознакомления с языком, семантикой и возможностями см. определение языка CEL на GitHub и документацию CEL Go .
Данный практический курс предназначен для разработчиков, желающих изучить CEL, чтобы использовать сервисы, уже поддерживающие CEL. В этом курсе не рассматривается вопрос интеграции CEL в собственный проект.
Что вы узнаете
- Основные концепции из CEL
- Привет, мир: использование CEL для вычисления значения строки.
- Создание переменных
- Понимание механизма короткого замыкания CEL в логических операциях И/ИЛИ
- Как использовать CEL для создания JSON-файлов
- Как использовать CEL для создания протобуферов
- Создание макросов
- Способы настройки выражений CEL
Что вам понадобится
Предварительные требования
Данный практический урок основан на базовом понимании протокола Protocol Buffers и языка Go .
Если вы не знакомы с Protocol Buffers, первое упражнение даст вам представление о том, как работает CEL, но поскольку более сложные примеры используют Protocol Buffers в качестве входных данных для CEL, их может быть сложнее понять. Рекомендуем сначала пройти один из этих уроков . Обратите внимание, что Protocol Buffers не являются обязательными для использования CEL, но они широко используются в этом практическом занятии.
Проверить установку Go можно, выполнив следующую команду:
go --help
2. Ключевые понятия
Приложения
CEL — это универсальный язык программирования, используемый в самых разных областях, от маршрутизации RPC-вызовов до определения политики безопасности. CEL расширяем, независим от конкретных приложений и оптимизирован для рабочих процессов, в которых выполняется однократная компиляция и многократное выполнение.
Многие сервисы и приложения используют декларативные конфигурации. Например, управление доступом на основе ролей (RBAC) — это декларативная конфигурация, которая принимает решение о доступе, исходя из роли и набора пользователей. Если декларативные конфигурации составляют 80% случаев использования, то CEL является полезным инструментом для решения оставшихся 20%, когда пользователям требуется большая выразительность.
Компиляция
Выражение компилируется в среде выполнения. На этапе компиляции создается абстрактное синтаксическое дерево (AST) в формате protobuf. Скомпилированные выражения обычно сохраняются для дальнейшего использования, чтобы обеспечить максимально быструю оценку. Одно скомпилированное выражение может быть вычислено с множеством различных входных данных.
Выражения
Пользователи определяют выражения; сервисы и приложения определяют среду, в которой они выполняются. Сигнатура функции объявляет входные данные и записывается вне выражения CEL. Библиотека функций, доступных для CEL, импортируется автоматически.
В следующем примере выражение принимает объект запроса, который включает токен утверждений. Выражение возвращает логическое значение, указывающее, действителен ли токен утверждений.
// 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
Среда
Среды определяются сервисами . Сервисы и приложения, использующие CEL, объявляют среду выражений. Среда представляет собой набор переменных и функций, которые могут использоваться в выражениях.
Объявления на основе протоколов используются средством проверки типов CEL для обеспечения корректного объявления и использования всех ссылок на идентификаторы и функции внутри выражения.
Три этапа разбора выражения
Обработка выражения включает три этапа: разбор, проверка и вычисление. Наиболее распространенный подход в CEL заключается в том, что плоскость управления выполняет разбор и проверку выражений во время настройки и сохраняет абстрактное синтаксическое дерево (AST).

Во время выполнения плоскость данных многократно извлекает и оценивает абстрактное синтаксическое дерево (AST). CEL оптимизирован для повышения эффективности во время выполнения, но синтаксический анализ и проверка не должны выполняться в критически важных с точки зрения задержки участках кода.

CEL преобразуется из удобочитаемого выражения в абстрактное синтаксическое дерево с использованием грамматики лексера/парсера ANTLR . На этапе парсинга генерируется абстрактное синтаксическое дерево на основе прототипов, где каждый узел Expr в AST содержит целочисленный идентификатор, используемый для индексации метаданных, сгенерированных во время парсинга и проверки. Созданный во время парсинга файл syntax.proto точно представляет абстрактное представление того, что было введено в строковой форме выражения.
После анализа выражения его можно проверить на соответствие среде выполнения, чтобы убедиться, что все идентификаторы переменных и функций в выражении объявлены и используются правильно. Проверка типов создает файл checked.proto , который включает метаданные разрешения типов, переменных и функций, что может значительно повысить эффективность вычисления.
Для проведения оценки CEL необходимы 3 вещи:
- Привязки функций для любых пользовательских расширений
- Переменные привязки
- Тест на чувствительность к антибиотикам для оценки
Привязки функций и переменных должны соответствовать тем, которые использовались для компиляции AST. Любые из этих входных данных могут быть повторно использованы в нескольких вычислениях, например, AST может быть оценен с использованием множества наборов привязок переменных, или одни и те же переменные могут использоваться для многих AST, или привязки функций могут использоваться на протяжении всего жизненного цикла процесса (распространенный случай).
3. Настройка
Код для этого практического занятия находится в папке codelab репозитория cel-go . Решение доступно в папке codelab/solution того же репозитория.
Клонируйте репозиторий и перейдите в него с помощью команды `cd`:
git clone https://github.com/google/cel-go.git
cd cel-go/codelab
Запустите код с помощью команды go run :
go run .
Вы должны увидеть следующий результат:
=== 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 ===
Где находятся пакеты CEL?
В редакторе откройте codelab/codelab.go . Вы увидите основную функцию, которая управляет выполнением упражнений в этом коделабе, за которой следуют три блока вспомогательных функций. Первый набор вспомогательных функций помогает на этапах оценки CEL:
- Функция
Compile: анализирует и проверяет входное выражение на соответствие среде выполнения. - Функция
Eval: вычисляет и оценивает скомпилированную программу на основе входных данных. - Функция
Report: красиво выводит результаты оценки.
Кроме того, для облегчения формирования входных данных для различных упражнений были предоставлены вспомогательные средства request и auth .
В упражнениях пакеты будут обозначаться их краткими именами. Ниже приведена схема соответствия пакетов и их расположения в исходном коде репозитория google/cel-go если вы хотите углубиться в детали:
Упаковка | Местоположение источника | Описание |
цел | Интерфейсы верхнего уровня | |
ссылка | Справочные интерфейсы | |
типы | Значения типов времени выполнения |
4. Привет, мир!
Следуя традиции всех языков программирования, мы начнём с создания и выполнения программы "Hello World!".
Настройте среду
В редакторе найдите объявление exercise1 и заполните следующие поля для настройки среды:
// 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
}
Приложения CEL оценивают выражение в соответствии с окружением. env, err := cel.NewEnv() настраивает стандартное окружение.
Настройки среды можно изменить, указав параметры cel.EnvOption при вызове функции. Эти параметры позволяют отключать макросы, объявлять пользовательские переменные и функции и т. д.
Стандартная среда CEL поддерживает все типы, операторы, функции и макросы, определенные в спецификации языка .
Проанализируйте и проверьте выражение.
После настройки среды выражения можно анализировать и проверять. Добавьте в свою функцию следующее:
// 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
}
Значение iss возвращаемое вызовами Parse и Check представляет собой список возможных ошибок. Если iss.Err() не равно nil, значит, есть ошибка синтаксиса или семантики, и программа не может продолжить выполнение. Если выражение корректно сформировано, результатом этих вызовов является исполняемый cel.Ast .
Оцените выражение
После того как выражение будет проанализировано и преобразовано в объект cel.Ast , его можно преобразовать в исполняемую программу, привязки функций и режимы оценки которой можно настроить с помощью функциональных параметров. Обратите внимание, что также можно прочитать объект cel.Ast из прототипа, используя функции cel.CheckedExprToAst или cel.ParsedExprToAst .
После планирования cel.Program его можно оценить на входных данных, вызвав функцию Eval . Результат функции Eval будет содержать итоговый результат, подробности оценки и статус ошибки.
Добавить планирование и 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()
}
Для краткости мы опустим приведенные выше случаи ошибок в будущих упражнениях.
Выполните код
В командной строке повторно запустите код:
go run .
В результате вы должны увидеть следующий вывод, а также поля для будущих упражнений.
=== Exercise 1: Hello World ===
------ input ------
(interpreter.emptyActivation)
------ result ------
value: Hello, World! (types.String)
5. Использование переменных в функции
В большинстве приложений CEL объявляются переменные, на которые можно ссылаться в выражениях. Объявления переменных указывают имя и тип. Тип переменной может быть либо встроенным типом CEL , либо общеизвестным типом протокола ProtoBuffer, либо любым типом сообщения ProtoBuffer, при условии, что его дескриптор также предоставлен CEL.
Добавьте функцию
В редакторе найдите объявление exercise2 и добавьте следующее:
// 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()
}
Повторите запуск и разберитесь с ошибкой.
Перезапустите программу:
go run .
Вы должны увидеть следующий результат:
ERROR: <input>:1:1: undeclared reference to 'request' (in container '')
| request.auth.claims.group == 'admin'
| ^
Программа проверки типов выдает ошибку для объекта запроса, которая, что очень удобно, включает фрагмент исходного кода, где произошла ошибка.
6. Объявите переменные
Добавить EnvOptions
В редакторе исправим возникшую ошибку, указав в качестве объекта запроса сообщение типа google.rpc.context.AttributeContext.Request следующим образом:
// 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()
}
Повторите запуск и разберитесь с ошибкой.
Повторный запуск программы:
go run .
Вы должны увидеть следующую ошибку:
ERROR: <input>:1:8: [internal] unexpected failed resolution of 'google.rpc.context.AttributeContext.Request'
| request.auth.claims.group == 'admin'
| .......^
Для использования переменных, ссылающихся на сообщения protobuf, средство проверки типов должно также знать дескриптор типа.
Используйте cel.Types для определения дескриптора запроса в вашей функции:
// 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()
}
Повторный запуск прошёл успешно!
Запустите программу еще раз:
go run .
Вы должны увидеть следующее:
=== 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)
Напомним, что мы просто объявили переменную для ошибки, присвоили ей дескриптор типа, а затем сослались на эту переменную при вычислении выражения.
7. Логическое И/ИЛИ
Одна из уникальных особенностей CEL — использование коммутативных логических операторов. Любая из сторон условного перехода может прервать вычисление, даже при наличии ошибок или неполного ввода.
Другими словами, CEL находит порядок вычисления, который, по возможности, дает результат, игнорируя ошибки или даже недостающие данные, которые могут возникнуть в других порядках вычисления. Приложения могут полагаться на это свойство для минимизации затрат на вычисление, откладывая сбор дорогостоящих входных данных, когда результат может быть получен и без них.
Мы добавим пример с оператором И/ИЛИ, а затем попробуем его с разными входными данными, чтобы понять, как CEL сокращает время выполнения.
Создайте функцию
В редакторе добавьте в упражнение 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"),
),
)
}
Далее добавьте оператор ИЛИ, который вернет значение true, если пользователь либо является членом группы admin , либо имеет определенный адрес электронной почты:
// 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)
}
И наконец, добавьте сценарий eval , который проверяет пользователя с пустым набором утверждений:
// 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()))
}
Запустите код с пустым набором утверждений.
При повторном запуске программы вы должны увидеть следующий новый результат:
=== 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)
Обновите оценочный случай.
Далее обновите сценарий оценки, чтобы передать другой субъект с пустым набором утверждений:
// 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()))
}
Запустите код с указанием времени.
Повторный запуск программы,
go run .
Вы должны увидеть следующую ошибку:
=== 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
В protobuf мы знаем, какие поля и типы следует ожидать. В значениях map и json мы не знаем, будет ли присутствовать ключ. Поскольку для отсутствующего ключа нет безопасного значения по умолчанию, CEL по умолчанию выдает ошибку.
8. Пользовательские функции
Хотя CEL включает в себя множество встроенных функций, бывают случаи, когда полезна пользовательская функция. Например, пользовательские функции могут использоваться для улучшения пользовательского опыта в распространенных ситуациях или для отображения контекстно-зависимого состояния.
В этом упражнении мы рассмотрим, как сделать функцию доступной для объединения часто используемых проверок.
Вызов пользовательской функции
Сначала создайте код для настройки переопределения с именем contains , которое определяет, существует ли ключ в карте и имеет ли он определенное значение. Оставьте заполнители для определения функции и привязки функции:
// 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()
}
Запустите код и разберитесь с ошибкой.
При повторном запуске кода вы должны увидеть следующую ошибку:
ERROR: <input>:1:29: found no matching overload for 'contains' applied to 'map(string, dyn).(string, string)'
| request.auth.claims.contains('group', 'admin')
| ............................^
Для исправления ошибки необходимо добавить функцию contains в список объявлений, которые в настоящее время объявляют переменную request.
Объявите параметризованный тип, добавив следующие 3 строки. (Это настолько же сложно, насколько это вообще возможно для перегрузки функций в 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()
}
Добавить пользовательскую функцию
Далее мы добавим новую функцию `contains`, которая будет использовать параметризованные типы:
// 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()
}
Запустите программу, чтобы понять, в чем заключается ошибка.
Выполните упражнение. Вы должны увидеть следующую ошибку, связанную с отсутствующей функцией времени выполнения:
------ result ------
error: no such overload: contains
Предоставьте реализацию функции в объявлении NewEnv , используя функцию 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()
}
Теперь программа должна успешно запуститься:
=== 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)
Что происходит, когда такое требование существует?
В качестве дополнительного задания попробуйте установить утверждение администратора во входных данных, чтобы убедиться, что перегрузка метода contains также возвращает true, если утверждение существует. Вы должны увидеть следующий вывод:
=== 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)
Прежде чем двигаться дальше, стоит изучить саму функцию 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])
}
Для обеспечения максимальной простоты расширения сигнатура пользовательских функций ожидает аргументы типа ref.Val . Компромисс здесь заключается в том, что простота расширения накладывает на разработчика дополнительную нагрузку по обеспечению корректной обработки всех типов значений. Если типы входных аргументов или их количество не соответствуют объявлению функции, должна возвращаться ошибка " no such overload .
Метод cel.FunctionBinding() добавляет проверку типа во время выполнения, чтобы гарантировать соответствие контракта времени выполнения объявлению, прошедшему проверку типа в среде выполнения.
9. Создание JSON
CEL также может выдавать нелогические значения, например, в формате JSON. Добавьте в свою функцию следующее:
// 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()
}
Запустите код
При повторном запуске кода вы должны увидеть следующую ошибку:
ERROR: <input>:5:11: undeclared reference to 'now' (in container '')
| 'iat': now,
| ..........^
... and more ...
Добавьте объявление now типа cel.TimestampType в cel.NewEnv() и запустите программу снова:
// 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()
}
Запустите код повторно, и он должен выполниться успешно:
=== 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}}
Программа запускается, но выходное out необходимо явно преобразовать в JSON. В данном случае внутреннее представление CEL преобразуется в JSON, поскольку оно ссылается только на типы, которые поддерживает JSON или для которых существует известное сопоставление Proto с JSON .
// 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()
}
После преобразования типа с помощью вспомогательной функции valueToJSON в файле codelab.go вы должны увидеть следующий дополнительный вывод:
------ 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. Создание прототипов
CEL может создавать сообщения protobuf для любого типа сообщений, скомпилированного в приложение. Добавьте функцию для создания объекта google.rpc.context.AttributeContext.Request из входного jwt
// 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()
}
Запустите код
При повторном запуске кода вы должны увидеть следующую ошибку:
ERROR: <input>:2:10: undeclared reference to 'Request' (in container '')
| Request{
| .........^
Контейнер, по сути, эквивалентен пространству имен или пакету, но может быть даже настолько детализированным, как имя сообщения Protobuf. Контейнеры CEL используют те же правила разрешения пространств имен, что и Protobuf и C++, для определения места объявления данной переменной, функции или типа.
При наличии контейнера google.rpc.context.AttributeContext средство проверки типов и средство оценки будут пытаться использовать следующие имена идентификаторов для всех переменных, типов и функций:
-
google.rpc.context.AttributeContext.<id> -
google.rpc.context.<id> -
google.rpc.<id> -
google.<id> -
<id>
Для абсолютных имен добавьте перед ссылкой на переменную, тип или функцию точку. В примере выражение .<id> будет искать только идентификатор верхнего уровня <id> не проверяя предварительно содержимое контейнера.
Попробуйте указать параметр cel.Container("google.rpc.context.AttributeContext") для среды CEL и запустите снова:
// 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)),
)
...
}
В результате вы должны получить следующий результат:
ERROR: <input>:4:16: undeclared reference to 'jwt' (in container 'google.rpc.context.AttributeContext')
| principal: jwt.iss + '/' + jwt.sub,
| ...............^
...и множество других ошибок...
Далее объявите переменные jwt и now , и программа должна работать как ожидалось:
=== 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}
В качестве дополнительного задания вам также следует расшифровать тип, вызвав out.Value() для сообщения, чтобы увидеть, как изменится результат.
11. Макросы
Макросы можно использовать для управления программой CEL во время синтаксического анализа. Макросы сопоставляют сигнатуру вызова и изменяют входной вызов и его аргументы для создания нового подвыражения AST.
Макросы можно использовать для реализации сложной логики в абстрактном синтаксическом дереве (AST), которую нельзя написать напрямую на CEL. Например, макрос ` has позволяет проверять наличие полей. Макросы-заполнители, такие как `exists` и `map`, заменяют вызов функции ограниченной итерацией по входному списку или карте. Ни одна из этих концепций невозможна на синтаксическом уровне, но они реализуемы с помощью макрорасширений.
Добавьте и выполните следующее упражнение:
// 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()
}
Вы должны увидеть следующие ошибки:
ERROR: <input>:1:25: undeclared reference to 'c' (in container '')
| jwt.extra_claims.exists(c, c.startsWith('group'))
| ........................^
...и многое другое...
Эти ошибки возникают из-за того, что макросы еще не включены. Чтобы включить макросы, удалите cel.ClearMacros() и запустите программу снова:
=== 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)
В настоящее время поддерживаются следующие макросы:
Макро | Подпись | Описание |
все | r.all(var, cond) | Проверьте, выполняется ли условие `cond` для всех переменных в диапазоне `r`. |
существует | r.exists(var, cond) | Проверьте, выполняется ли условие `cond` для любой переменной в диапазоне `r`. |
существует_один | r.exists_one(var, cond) | Проверьте, выполняется ли условие `cond` истинно только для одной переменной в диапазоне `r`. |
фильтр | r.filter(var, cond) | Для списков создайте новый список, в котором каждый элемент var в диапазоне r удовлетворяет условию cond. Для карт создайте новый список, в котором каждый ключ var в диапазоне r удовлетворяет условию cond. |
карта | r.map(var, expr) | Создайте новый список, в котором каждая переменная в диапазоне r преобразуется с помощью выражения. |
r.map(var, cond, expr) | Аналогично функции `two-arg map`, но с условным фильтром `cond` перед преобразованием значения. | |
имеет | имеет(аб) | Проверка наличия b для значения a: Для карт JSON проверяет определение. Для протоколов проверяет нестандартное примитивное значение или поле сообщения a или set. |
Если аргумент range r имеет тип map , то var будет ключом map, а для значений типа list переменная var будет значением элемента списка. Макросы all , exists , exists_one , filter и map выполняют переписывание AST, которое выполняет итерацию for-each, ограниченную размером входных данных.
Ограниченные генераторы списков гарантируют, что программы на CEL не будут полными по Тьюрингу, но их вычисление происходит за сверхлинейное время относительно входных данных. Используйте эти макросы экономно или не используйте их вовсе. Интенсивное использование генераторов списков обычно является хорошим показателем того, что пользовательская функция обеспечит лучший пользовательский опыт и лучшую производительность.
12. Настройка
В настоящее время существует ряд функций, эксклюзивных для CEL-Go, но они указывают на планы по внедрению других реализаций CEL в будущем. В следующем примере показаны различные планы разработки одной и той же 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()
}
Оптимизировать
При включении флага оптимизации CEL будет тратить дополнительное время на предварительное построение литералов списков и отображений, а также оптимизировать определенные вызовы, такие как оператор in, чтобы они представляли собой истинную проверку принадлежности к множеству:
// 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)
Когда одна и та же программа многократно оценивается при разных входных данных, оптимизация является хорошим решением. Однако, если программа будет оцениваться только один раз, оптимизация лишь увеличит накладные расходы.
Исчерпывающая оценка
Полная оценка может быть полезна для отладки поведения при вычислении выражений, поскольку она позволяет получить представление о наблюдаемом значении на каждом этапе вычисления выражения.
// 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)
Для каждого идентификатора выражения вы должны увидеть список состояний оценки выражения:
------ 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)
Идентификатор выражения 2 соответствует результату оператора in в первой ветви, а идентификатор выражения 11 соответствует оператору == во второй. При нормальном вычислении выражение завершилось бы с ошибкой после вычисления 2. Если бы y не было целым числом (uint), то состояние показало бы две причины, по которым выражение завершилось бы с ошибкой, а не одну.
13. Что было рассмотрено?
Если вам нужен механизм обработки выражений, рассмотрите возможность использования CEL. CEL идеально подходит для проектов, требующих выполнения пользовательских настроек, где производительность имеет решающее значение.
Надеемся, что в предыдущих упражнениях вы освоили передачу данных в CEL и получение результата или принятого решения.
Мы надеемся, что вы получили представление о том, какие операции можно выполнять: от логических операций до генерации сообщений в формате JSON и Protobuffer.
Мы надеемся, что вы понимаете, как работать с выражениями и для чего они нужны. И мы знаем распространенные способы их расширения.