CEL
Текст может содержать неточности, устаревшие данные или технические ошибки. Пожалуйста, проверяйте критически важную информацию перед использованием.
Common Expression Language — компактный язык выражений с операторами в духе C и небольшим набором макросов над коллекциями. Он читает модель и никогда её не изменяет.

Как пишется выражение
Тело на CEL — одно выражение. По характеристикам ведёт точка, операторы записаны как в C:
self.name != ""
self.outgoingRelationships.size() > 0 && self.name.startsWith("SVC-")
Равенство записывается как == и !=, логические операторы — &&, ||, !.
Стрелки -> в языке нет: операции над коллекциями — это макросы, которые
вызываются через точку, как любой другой метод.
Динамическая типизация
CEL компилируется до запуска, поэтому синтаксическая ошибка или тело, неспособное дать логическое значение там, где оно требуется, отмечаются в редакторе.
Навигация по характеристикам не проверяется. Контекстный объект типизирован
динамически, поэтому self.naem компилируется и завершается ошибкой только при
вычислении выражения — с сообщением, называющим отсутствующий ключ.
Автодополнения в CEL нет.
Видимые переменные
self — объект, на котором вычисляется выражение, типизированный динамически.
Какой это объект, зависит от того, где выражение выполняется, — см.
Консоль выражений.
Значения преобразуются в формы, понятные CEL. По вложенному объекту навигация
идёт так же, как по self, а многозначная характеристика становится списком.
Значение перечисления становится именем литерала в виде строки, поэтому тип
папки сравнивается как self.type == "business". Целые числа становятся целыми,
а дробные — числами двойной точности.
Примеры
Для ArchiMate, где self — элемент:
У элемента есть имя:
self.name != null && self.name != ""
Элемент с чем-то связан:
self.incomingRelationships.size() > 0 || self.outgoingRelationships.size() > 0
У каждого элемента есть свойство owner:
self.properties.exists(p, p.key == "owner")
Свойство owner задано ровно один раз и не пусто:
self.properties.exists_one(p, p.key == "owner")
&& self.properties.filter(p, p.key == "owner").all(p, p.value != "")
Каждая исходящая связь заканчивается на чём-то именованном:
self.outgoingRelationships.all(r, r.target.name != "")
Имена всего, от чего зависит этот элемент:
self.outgoingRelationships.map(r, r.target.name)
Имена соответствуют соглашению:
self.name.matches("^[A-Z][A-Za-z ]*$")
Документированные элементы должны быть именованы, недокументированные — освобождены от проверки:
self.documentation == "" ? true : self.name != ""
Для ArchiMate, где self — модель:
Каждая папка верхнего уровня именована:
self.folders.all(f, f.name != "")
Папка Business что-то содержит:
self.folders.exists(f, f.type == "business" && f.elements.size() > 0)
Дополнительные материалы
Спецификация Common Expression Language
Справочник
Переменные
| Переменная | Значение |
|---|---|
self | контекстный объект |
data | в выражении документа — список объектов, привязанных к документу |
| имя, указанное в поле Variable | в повторяющемся блоке документа — объект текущего повторения |
Никаких других имён не связано. Несвязанное имя отмечается при компиляции выражения.
Приоритет операторов
Сначала связывающие сильнее всего. Операторы одной строки связывают слева направо, кроме унарных операторов и условного, которые связывают справа налево.
| Уровень | Операторы | Форма |
|---|---|---|
| 1 | a.f a.f(...) a[i] (a) | выбор поля, вызов метода, индексация, группировка |
| 2 | !a -a | отрицание |
| 3 | a * b a / b a % b | мультипликативные |
| 4 | a + b a - b | аддитивные |
| 5 | a < b a <= b a > b a >= b a == b a != b a in b | отношения |
| 6 | a && b | конъюнкция |
| 7 | a || b | дизъюнкция |
| 8 | c ? a : b | условный |
&&, || и ? : вычисляют только те операнды, которые им нужны. Условный
оператор — единственная управляющая конструкция: тело представляет собой одно
выражение, в нём нет ни инструкций, ни циклов, ни присваивания.
Арифметические операторы
| Выражение | Типы операндов | Результат | Смысл |
|---|---|---|---|
a + b | int, uint, double | как у операндов | сумма |
a + b | string | string | конкатенация |
a + b | bytes | bytes | конкатенация |
a + b | list | list | элементы a, за которыми следуют элементы b |
a + b | timestamp и duration в любом порядке | timestamp | момент, сдвинутый на длительность |
a + b | duration | duration | сумма |
a - b | int, uint, double | как у операндов | разность |
a - b | timestamp, timestamp | duration | время между двумя моментами |
a - b | timestamp, duration | timestamp | момент, сдвинутый назад |
a - b | duration | duration | разность |
a * b | int, uint, double | как у операндов | произведение |
a / b | int, uint, double | как у операндов | частное; целочисленное деление отбрасывает дробную часть в сторону нуля |
a % b | int, uint | как у операндов | остаток |
-a | int, double | как у операнда | смена знака |
Оба операнда должны быть одного типа: не компилируется ни 1 + 1.0, ни
1 + 1u. Результат типа int или uint вне диапазона этого типа — ошибка
вычисления, как и / или % с целочисленным нулём. Деление числа двойной
точности на ноль даёт бесконечность либо NaN, если числитель тоже ноль.
Операторы сравнения и логические операторы
| Выражение | Результат | Смысл |
|---|---|---|
a == b | bool | значения равны |
a != b | bool | значения не равны |
a < b, a <= b, a > b, a >= b | bool | упорядочение |
!a | bool | отрицание |
a && b | bool | выполняются оба |
a || b | bool | выполняется хотя бы одно |
c ? a : b | тип a и b | a, когда c истинно, иначе b |
== и != принимают любые два значения одного типа, включая списки и
отображения, которые сравниваются поэлементно. Упорядочение принимает bool, int,
uint, double, string, bytes, timestamp и duration. Сравнение значений разных
типов не компилируется, включая 1 == 1.0; оберните операнд в dyn(), чтобы
отложить выбор операции до вычисления, — тогда dyn(1) == dyn(1.0) истинно.
Навигация и индексация
| Выражение | Результат | Смысл |
|---|---|---|
a.f | значение f | выбор поля объекта или отображения |
a["f"] | значение f | тот же выбор, записанный как индексация |
a[i] | элемент | элемент списка на позиции i, отсчёт от нуля |
m[k] | значение | значение, которое отображение хранит по ключу k |
has(a.f) | bool | у a есть поле с именем f |
x in list | bool | список содержит x |
k in map | bool | у отображения есть ключ k |
Индекс за границами списка или ключ, которого нет в отображении, — ошибка
вычисления. has() принимает выбор поля и ничего больше, поэтому has(a.f[0])
не компилируется.
Литералы
| Запись | Тип | Примечания |
|---|---|---|
42, -7, 0x1F | int | десятичный или шестнадцатеричный, 64-битный со знаком |
42u, 0xFFu | uint | 64-битный без знака |
1.5, .5, 1e3, 1.5e-3 | double | |
true, false | bool | |
null | null | |
"text", 'text' | string | |
"""text""", '''text''' | string | может занимать несколько строк |
r"a\d", R'a\d' | string | без обработки: обратная косая черта остаётся собой |
b"ab", b'\x41' | bytes | |
[1, 2] | list | элементы не обязаны быть одного типа |
{"a": 1, "b": 2} | map | повторяющийся ключ — ошибка вычисления |
Экранирующие последовательности в строках: \n, \r, \t, \a, \b, \f,
\v, \', \", \\, \`, \?, восьмеричная \101, шестнадцатеричная
\x41, а также кодовые точки \u00e9 и \U0001F600.
Разделитель групп цифр не входит в числовой литерал: пишите 1000, а не
1_000. Комментарий // продолжается до конца строки, а выражение можно
записать в несколько строк.
Следующие идентификаторы зарезервированы и не могут служить именем: as,
break, const, continue, else, false, for, function, if,
import, in, let, loop, namespace, null, package, return, true,
var, void, while. К характеристике с именем in, true, false или
null обращаются через индексацию, а не через точку — как в self["in"].
Имена типов и проверки типа
| Выражение | Результат | Смысл |
|---|---|---|
type(a) | type | тип значения |
dyn(a) | dyn | то же значение, тип которого определяется при вычислении |
type(a) сравнивается с именами типов int, uint, double, bool,
string, bytes, list, map, null_type, dyn и type. Объект модели
отвечает map. У timestamp и duration нет имени, которое можно записать в
выражении.
type(self.name) == string
Преобразования
| Выражение | Результат | Принимает |
|---|---|---|
int(a) | int | int, uint, double (с отбрасыванием дробной части в сторону нуля), string, timestamp (секунды с начала эпохи) |
uint(a) | uint | uint, int, double, string |
double(a) | double | double, int, uint, string |
string(a) | string | string, int, uint, double, bytes, timestamp, duration |
bool(a) | bool | bool, string |
bytes(a) | bytes | bytes, string |
timestamp(a) | timestamp | строку в форме RFC 3339 либо timestamp |
duration(a) | duration | строку вида "90s", "1h30m" или "1.5s" либо duration |
Строка, которая не разбирается, — ошибка вычисления.
int("12") + 1
timestamp("2020-03-04T05:06:07Z")
Строковые операции
| Выражение | Результат | Смысл |
|---|---|---|
s.size(), size(s) | int | число кодовых точек |
s.contains(t) | bool | t встречается где-то в s |
s.startsWith(t) | bool | s начинается с t |
s.endsWith(t) | bool | s заканчивается на t |
s.matches(p), matches(s, p) | bool | шаблон p совпадает где-то в s |
matches ищет совпадение внутри строки; чтобы потребовать совпадения целиком,
привяжите шаблон к ^ и $. Шаблоны используют синтаксис RE2, поэтому неверно
составленный шаблон — ошибка вычисления.
self.name.matches("^[A-Z][A-Za-z ]*$")
Операции над моментами времени и длительностями
Каждый метод доступа принимает единственным аргументом имя часового пояса IANA,
как в getHours("America/New_York"); без него отсчёт ведётся по UTC.
Длительность принимает только четыре отмеченных ниже метода доступа и игнорирует
аргумент.
| Выражение | Применяется к | Результат | Смысл |
|---|---|---|---|
t.getFullYear() | timestamp | int | год |
t.getMonth() | timestamp | int | месяц, 0 — январь |
t.getDayOfYear() | timestamp | int | день года, отсчёт от нуля |
t.getDayOfMonth() | timestamp | int | день месяца, отсчёт от нуля |
t.getDate() | timestamp | int | день месяца, отсчёт от единицы |
t.getDayOfWeek() | timestamp | int | день недели, 0 — воскресенье |
t.getHours() | timestamp | int | час суток |
t.getMinutes() | timestamp | int | минута часа |
t.getSeconds() | timestamp | int | секунда минуты |
t.getMilliseconds() | timestamp | int | миллисекунды внутри секунды |
d.getHours() | duration | int | сколько целых часов занимает длительность |
d.getMinutes() | duration | int | сколько целых минут занимает длительность |
d.getSeconds() | duration | int | сколько целых секунд занимает длительность |
d.getMilliseconds() | duration | int | миллисекунды, оставшиеся после последней целой секунды |
duration("1h30m").getMinutes() равно 90, а
duration("1.5s").getMilliseconds() равно 500.
Операции над коллекциями
size применяется к строке, байтам, списку и отображению и записывается как
вызов или как метод.
| Выражение | Результат | Смысл |
|---|---|---|
c.size(), size(c) | int | число элементов или записей отображения |
c[i] | элемент | элемент списка на позиции i |
Макросы ниже принимают имя переменной, обозначающей текущий элемент, и выражение, которое эту переменную использует. Над отображением они перебирают его ключи. Переменная существует только внутри макроса; ничто другое в языке не вводит имён.
| Выражение | Результат | Смысл |
|---|---|---|
c.all(e, p) | bool | p выполняется для каждого элемента |
c.exists(e, p) | bool | p выполняется хотя бы для одного элемента |
c.exists_one(e, p) | bool | p выполняется ровно для одного элемента |
c.filter(e, p) | list | элементы, для которых выполняется p |
c.map(e, v) | list | значение v для каждого элемента |
c.map(e, p, v) | list | значение v для элементов, для которых выполняется p |
self.outgoingRelationships.map(r, r.target.name)
self.eStructuralFeatures.map(f, f.derived, f.name)
Макросы вкладываются друг в друга, а их результаты — обычные списки:
self.folders.filter(f, f.type == "business").all(f, f.elements.size() > 0)
Объекты модели
Объект модели попадает в CEL как отображение: ключами служат имена структурных характеристик его метакласса, включая унаследованные и производные. Поэтому всё, что работает с отображением, работает и с объектом.
| Выражение | Результат | Смысл |
|---|---|---|
self.name | значение характеристики | характеристика с именем name |
self["name"] | значение характеристики | та же характеристика |
has(self.name) | bool | метакласс объявляет характеристику с именем name |
"name" in self | bool | та же проверка |
self.size(), size(self) | int | число характеристик |
self.all(f, p) | bool | p выполняется для каждого имени характеристики |
has() и in отвечают по любой характеристике, которую объявляет метакласс, —
задано у неё значение или нет.
Значения характеристик приходят в таком виде:
| Характеристика | Значение в CEL |
|---|---|
| ссылка или композиция, содержащая один объект | этот объект в виде отображения |
| многозначная характеристика | список, пустой, когда ничего не хранится |
| перечисление | строка, имя литерала |
| строковый атрибут | строка |
| символьный атрибут | строка из одного символа |
| логический атрибут | bool |
| целочисленный атрибут | int |
| дробный числовой атрибут | double |
У однозначной незаданной характеристики значения нет. Её чтение не даёт
результата — как и любое сравнение, построенное на этом чтении, в том числе с
null.
Навигация идёт только по характеристикам: до контейнеров, обратных ссылок и
связанных объектов вы добираетесь по имени характеристики, которую объявляет для
них метамодель, — например, incomingRelationships у элемента ArchiMate. Имя,
которое характеристикой текущего объекта не является, компилируется и падает при
вычислении с сообщением об отсутствующем ключе.
Синтаксис, который продукт не включает
Опциональный синтаксис — a.?f, {?"a": v}, значения optional и операции
orValue и optMap — не включён. Библиотеки расширений CEL тоже не включены,
поэтому недоступны и их функции: join, split, lowerAscii, upperAscii,
replace, indexOf, а также операции math и sets. Конструирование
сообщений protocol buffers и преобразование int в timestamp также
недоступны. Ничего сверх стандартного языка не добавлено: все функции и макросы,
доступные телу на CEL, перечислены выше.