Перейти к основному содержимому

CEL

Этот документ был подготовлен при помощи ИИ

Текст может содержать неточности, устаревшие данные или технические ошибки. Пожалуйста, проверяйте критически важную информацию перед использованием.

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

Выражение на CEL в консоли и ответ на него: имена, собранные макросом

Как пишется выражение

Тело на 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в повторяющемся блоке документа — объект текущего повторения

Никаких других имён не связано. Несвязанное имя отмечается при компиляции выражения.

Приоритет операторов

Сначала связывающие сильнее всего. Операторы одной строки связывают слева направо, кроме унарных операторов и условного, которые связывают справа налево.

УровеньОператорыФорма
1a.f a.f(...) a[i] (a)выбор поля, вызов метода, индексация, группировка
2!a -aотрицание
3a * b a / b a % bмультипликативные
4a + b a - bаддитивные
5a < b a <= b a > b a >= b a == b a != b a in bотношения
6a && bконъюнкция
7a || bдизъюнкция
8c ? a : bусловный

&&, || и ? : вычисляют только те операнды, которые им нужны. Условный оператор — единственная управляющая конструкция: тело представляет собой одно выражение, в нём нет ни инструкций, ни циклов, ни присваивания.

Арифметические операторы

ВыражениеТипы операндовРезультатСмысл
a + bint, uint, doubleкак у операндовсумма
a + bstringstringконкатенация
a + bbytesbytesконкатенация
a + blistlistэлементы a, за которыми следуют элементы b
a + btimestamp и duration в любом порядкеtimestampмомент, сдвинутый на длительность
a + bdurationdurationсумма
a - bint, uint, doubleкак у операндовразность
a - btimestamp, timestampdurationвремя между двумя моментами
a - btimestamp, durationtimestampмомент, сдвинутый назад
a - bdurationdurationразность
a * bint, uint, doubleкак у операндовпроизведение
a / bint, uint, doubleкак у операндовчастное; целочисленное деление отбрасывает дробную часть в сторону нуля
a % bint, uintкак у операндовостаток
-aint, doubleкак у операндасмена знака

Оба операнда должны быть одного типа: не компилируется ни 1 + 1.0, ни 1 + 1u. Результат типа int или uint вне диапазона этого типа — ошибка вычисления, как и / или % с целочисленным нулём. Деление числа двойной точности на ноль даёт бесконечность либо NaN, если числитель тоже ноль.

Операторы сравнения и логические операторы

ВыражениеРезультатСмысл
a == bboolзначения равны
a != bboolзначения не равны
a < b, a <= b, a > b, a >= bboolупорядочение
!aboolотрицание
a && bboolвыполняются оба
a || bboolвыполняется хотя бы одно
c ? a : bтип a и ba, когда 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 listboolсписок содержит x
k in mapboolу отображения есть ключ k

Индекс за границами списка или ключ, которого нет в отображении, — ошибка вычисления. has() принимает выбор поля и ничего больше, поэтому has(a.f[0]) не компилируется.

Литералы

ЗаписьТипПримечания
42, -7, 0x1Fintдесятичный или шестнадцатеричный, 64-битный со знаком
42u, 0xFFuuint64-битный без знака
1.5, .5, 1e3, 1.5e-3double
true, falsebool
nullnull
"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)intint, uint, double (с отбрасыванием дробной части в сторону нуля), string, timestamp (секунды с начала эпохи)
uint(a)uintuint, int, double, string
double(a)doubledouble, int, uint, string
string(a)stringstring, int, uint, double, bytes, timestamp, duration
bool(a)boolbool, string
bytes(a)bytesbytes, 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)boolt встречается где-то в s
s.startsWith(t)bools начинается с t
s.endsWith(t)bools заканчивается на 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()timestampintгод
t.getMonth()timestampintмесяц, 0 — январь
t.getDayOfYear()timestampintдень года, отсчёт от нуля
t.getDayOfMonth()timestampintдень месяца, отсчёт от нуля
t.getDate()timestampintдень месяца, отсчёт от единицы
t.getDayOfWeek()timestampintдень недели, 0 — воскресенье
t.getHours()timestampintчас суток
t.getMinutes()timestampintминута часа
t.getSeconds()timestampintсекунда минуты
t.getMilliseconds()timestampintмиллисекунды внутри секунды
d.getHours()durationintсколько целых часов занимает длительность
d.getMinutes()durationintсколько целых минут занимает длительность
d.getSeconds()durationintсколько целых секунд занимает длительность
d.getMilliseconds()durationintмиллисекунды, оставшиеся после последней целой секунды

duration("1h30m").getMinutes() равно 90, а duration("1.5s").getMilliseconds() равно 500.

Операции над коллекциями

size применяется к строке, байтам, списку и отображению и записывается как вызов или как метод.

ВыражениеРезультатСмысл
c.size(), size(c)intчисло элементов или записей отображения
c[i]элементэлемент списка на позиции i

Макросы ниже принимают имя переменной, обозначающей текущий элемент, и выражение, которое эту переменную использует. Над отображением они перебирают его ключи. Переменная существует только внутри макроса; ничто другое в языке не вводит имён.

ВыражениеРезультатСмысл
c.all(e, p)boolp выполняется для каждого элемента
c.exists(e, p)boolp выполняется хотя бы для одного элемента
c.exists_one(e, p)boolp выполняется ровно для одного элемента
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 selfboolта же проверка
self.size(), size(self)intчисло характеристик
self.all(f, p)boolp выполняется для каждого имени характеристики

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, перечислены выше.