Всем привет! Мы добавили новую фичу в наш инструмент моделирования — текстовые представления для моделей. В статье я покажу что это такое и расскажу про детали реализации.

Волшебница превращает модель в код
Волшебница превращает модель в код

TL;DR покажите мне видео

Инструкция как попробовать самому

Вы можете войти в систему с учетной записью Google, GitHub или Яндекс:

Вход в систему
Вход в систему

После аутентификации вы можете создать новый проект:

Форма создания проекта
Форма создания проекта

Затем создать модель:

Форма создания модели
Форма создания модели

Можно добавить несколько классов:

Пример модели классов
Пример модели классов

Наконец создаём текстовое представление для модели:

Форма создания представления для модели
Форма создания представления для модели

И видим такой код:

Текстовый редактор для моделей
Текстовый редактор для моделей

Не знаю как вы, но я просто балдею от того как у нас сделана локализация моделей, мне всегда этого не хватало в инструментах моделирования.

Текстовый редактор поддерживает ожидаемые вещи:

  • Подсветка синтаксиса

  • Синтаксическая валидация (более сложная семантическая валидация пока планируется)

  • Автодолополнение

  • Переход к определению

  • Переименование объектов с обновлением ссылок

  • Автоформатирование (спорный вопрос баг это или фича)

  • Сворачивание блоков кода

  • Совместное редактирование несколькими пользователями

  • Синхронизация с другими представлениями модели (дерево, диаграмма, форма). При редактировании кода обновляются навигатор, форма свойств и диаграмма. Также это работает и в обратную сторону. Вы можете редактировать диаграмму, параметры объектов на форме свойств, перетаскивать объекты в навигаторе, при этом будет обновляться код в текстовом редакторе. Причём, и у вас (вы можете открыть диаграмму в другой вкладке браузера), и у других пользователей, которые работают с этой моделью

Что пока не сделано

Самая приоритетная фича, которой сейчас не хватает, это межмодельные связи. Для моделей классов у нас есть библиотека стандартных типов данных. Сейчас на них нельзя ссылаться в текстовом редакторе, все типы приходится объявлять в самой модели.

Вторая проблема. Если редактировать модель одновременно в диаграммном и в текстовом редакторах (открыв их в разных вкладках браузера или у разных пользователей), то могут откатываться некоторые изменения. Например, добавляю в диаграммном редакторе атрибут для класса, но ещё не успел задать для него название. Эта модель отправляется в текстовый редактор, который не может корректно вставить в текст безымянный атрибут. В итоге текстовый редактор удаляет этот атрибут из модели. Для разных редакторов разные ошибки имеют разную критичность, нужно будет это выравнять.

И ещё один баг или фича. Модели хранятся на сервере в виде JSON, а не текста. В них теряется информация о форматировании. С одной стороны, это хорошо, потому что кастомное форматирование и не особо нужно. Но с другой стороны, при редактировании это может приводить к неожиданным эффектам. Например, классы и типы данных в модели хранятся в двух отдельных коллекциях в JSON (это не обязательно, но автор метамодели решил сделать так). Соответственно в модели сохраняется информация отдельно о последовательности классов и отдельно о последовательности типов данных. При преобразовании модели в текст сначала выводятся все классы, а затем все типы данных. Если вы в коде будете писать вперемежку классы и типы данных, то классы будут автоматически перескакивать в начало файла, а типы данных — в конец. Это вроде и хорошо, потому что в коде будет порядок, а вроде и неожиданно для пользователя.

Как это реализовано

Работает это следующим образом (мне нереально лень рисовать картинки, плюс и статья про текстовый редактор, поэтому простите, дальше будут тонны текста, никаких алгоритмических схемок, потоков данных и т. д.):

  1. Модель хранится в JSON формате в базе данных

  2. Модель передаётся на фронтенд

  3. Модель преобразуется в абстрактное синтаксическое дерево

  4. Абстрактное синтаксическое дерево преобразуется в текст

  5. Текст отображается в текстовом редакторе и пользователь может его редактировать

  6. Текст преобразуется обратно в абстрактное синтаксическое дерево

  7. Абстрактное синтаксическое дерево преобразуется обратно в модель

  8. Модель сохраняется в базу данных

В общем архитектурный паттерн «Хоббит, или туда и обратно»...

Шаги 5 и 6 описаны в предыдущей статье. В этой статье более подробно остановимся на следующем:

  • Преобразование модели в абстрактное синтаксическое дерево и обратно (шаги 3 и 7)

  • Преобразование абстрактного синтаксического дерева в код (шаг 4)

  • Связь между метамоделями и грамматикой

Рассмотрим каждый пункт более детально.

Преобразование модели в абстрактное синтаксическое дерево (AST) и обратно

Это самая важная, сложная и интересная часть, практически квинтэссенция программирования — перекладывание JSONов! То ради чего джедаи от разработки годами оттачивают своё мастерство и получают тысячи денег...

И AST, и модель — это JSON, причём очень похожие по структуре, но всё‑таки не идентичные. Самое существенное отличие в том, что в модели у каждого объекта есть идентификатор (который нужен для межмодельных ссылок, для ссылок на модель из диаграмм). А в текстовом редакторе никаких идентификаторов у объектов может и не быть (будет просто название и всё), а значит их не будет и в AST.

Пример кода
classModel OnlineStore

@name('ru-RU', 'пользователь')
class User {

    @name('ru-RU', 'имя')
    attribute firstName String[0..1]

}

string String {

}
Пример AST
{
  "$type": "ClassModel",
  "name": "OnlineStore",
  "classes": [
    {
      "$type": "Class",
      "localizedName": [
        {
          "$type": "Ecore_EStringToStringMapEntry",
          "key": "ru-RU",
          "value": "пользователь"
        }
      ],
      "kind": {
        "$type": "ClassKind__Regular"
      },
      "name": "User",
      "properties": [
        {
          "$type": "Attribute",
          "localizedName": [
            {
              "$type": "Ecore_EStringToStringMapEntry",
              "key": "ru-RU",
              "value": "имя"
            }
          ],
          "name": "firstName",
          "dataType": {
            "$ref": "#/dataTypes@0",
            "$refText": "String"
          },
          "lower": 0,
          "upper": 1
        }
      ]
    }
  ],
  "dataTypes": [
    {
      "$type": "StringType",
      "name": "String"
    }
  ]
}
Пример модели
{
  "json": {
    "version": "1.0",
    "encoding": "utf-8"
  },
  "ns": {
    "classmodel": "https://example.com/class-model",
    "ecore": "http://www.eclipse.org/emf/2002/Ecore"
  },
  "content": [
    {
      "id": "0197f429-e5e0-743b-9330-d755229cecbb",
      "eClass": "classmodel:ClassModel",
      "data": {
        "name": "OnlineStore",
        "classes": [
          {
            "id": "0197f429-e5e0-743b-9330-dfccbe50ea8a",
            "eClass": "classmodel:Class",
            "data": {
              "localizedName": [
                {
                  "id": "0197f429-e5e0-743b-9330-e355d52c727d",
                  "eClass": "ecore:EStringToStringMapEntry",
                  "data": {
                    "key": "ru-RU",
                    "value": "пользователь"
                  }
                }
              ],
              "name": "User",
              "properties": [
                {
                  "id": "0197f429-e5e1-777f-9508-9562d8686ac3",
                  "eClass": "classmodel:Attribute",
                  "data": {
                    "localizedName": [
                      {
                        "id": "0197f429-e5e1-777f-9508-9940a4401452",
                        "eClass": "ecore:EStringToStringMapEntry",
                        "data": {
                          "key": "ru-RU",
                          "value": "имя"
                        }
                      }
                    ],
                    "name": "firstName",
                    "dataType": "0197f429-e5e1-777f-9508-fecb083bb4df",
                    "lower": 0,
                    "upper": 1
                  }
                }
              ]
            }
          }
        ],
        "dataTypes": [
          {
            "id": "0197f429-e5e1-777f-9508-fecb083bb4df",
            "eClass": "classmodel:StringType",
            "data": {
              "name": "String"
            }
          }
        ]
      }
    }
  ]
}

При преобразовании кода в AST и затем в модель мы должны каким‑то образом восстановить исходные идентификаторы всех объектов, а для добавленных в коде объектов сгенерировать новые идентификаторы. Делается это следующим образом:

  • Преобразуем модель в AST, при этом копируем идентификаторы объектов. Текстовому редактору они не нужны, но нам понадобятся позже. Сохраняем AST в кэш

  • Преобразуем AST в код, здесь идентификаторы уже теряются

  • Пользователь редактирует код, преобразуем его в новый AST уже без идентификаторов

  • Сравниваем исходный AST (с идентификаторами) и новый AST (без идентификаторов), получаем список изменений (diff). Мы используем для этого JsonDiffPatch. Вообще, такое сравнение — это не самая тривиальная задача. Например, если вы в коде поменяете два атрибута местами, то как понять, что это именно обмен местами, а не удаление одного атрибута в начале и затем создание такого же в конце? Можно определить это по названию, т. е. если удалён и создан атрибут с таким же названием, значит это перемещение, но что если между этими действиями было ещё редактирование? Или если мы используем название в качестве идентификатора для объектов, то как отслеживать переименования? Это решается с помощью разных эвристик, которых нет в JsonDiffPatch, здесь есть что улучшить, но сейчас это как‑то работает

  • Применяем к исходному AST изменения. И теперь оно и с идентификаторами, и актуальное

  • Проходим по AST, генерируем идентификаторы для новых объектов

  • Преобразуем AST в модель

По ссылке минимальный демо пример преобразования кода в модель и обратно (исходный код):

Пример текстового редактора моделей
Пример текстового редактора моделей

Преобразование абстрактного синтаксического дерева в код

В движках для создания предметно‑ориентированных языков программирования обычно уделяется внимание парсингу кода в AST, но не так часто в них поддерживается обратное преобразование из AST в код. Мы используем Langium, и в текущей версии это не поддерживается. Хотя идейно он основан на Xtext, где эта фича есть, и вроде Langium делают те же люди и можно было бы дождаться когда они это запилят. Но в Xtext это сделано на столько заморочено, что мы решили не ждать и навайбкодить это сами.

Преобразование AST в код включает следующее:

  • Рекурсивно пройти по грамматике, параллельно пройти по AST и при этом генерить текст

  • Либо сразу, либо после форматировать код: добавлять пробелы и переносы строк где требуется

Первая часть относительно простая. А как задать правила форматирования кода — это нетривиальный вопрос. В Xtext для этого просто пишется код, и в Langium авторы пошли той же тропинкой в пропасть. Если бы нам нужно было реализовать один DSL, например, для моделей классов, то мы могли бы написать этот код. Но в нашем инструменте моделирования поддерживаются в том числе и пользовательские DSL, поэтому мы решили описывать правила форматирования в самой грамматике, подглядев эту идею в канувшем в лету EMFText.

Фрагмент грамматики с «аннотациями» для форматирования кода:

grammar CM

entry ClassModel:
    _NL? Localization* _NL?
    'classModel' name=ID
    (classes+=Class | dataTypes+=DataType)*;

Class:
    _NL? Localization* _NL?
    kind=ClassKind name=ID
    ('extends' generals+=[Class:ID] (',' generals+=[Class:ID])*)? ('{'
        properties+=Property*
    _NL? _NL? '}')?;

_NL returns string: '__NL__';

Там, где в грамматике указан _NL?, при генерации кода будет добавляться перевод строки. По хорошему мы должны были бы расширить язык описания грамматик новой конструкцией для описания правил форматирования, а не использовать для этого фейковое правило с захардкоженым названием. Но решили применить архитектурный паттерн «Фигак‑фигак и в продакшн».

Кроме переводов строк нужно задавать ещё правила добавления отступов и пробелов внутри текста. Но их мы просто захардкодили. Например, до или после скобок пробелы никогда не добавляются, до запятой пробел не ставим, а после запятой ставим. Для текста в фигурных скобках добавляется отступ. В общем случае для произвольных DSL это работать не будет и придётся всё‑таки расширять язык описания грамматики нормальными аннотациями для кодогенератора.

По ссылке минимальный демо пример преобразования кода в AST и обратно (исходный код):

Пример преобразования AST в код
Пример преобразования AST в код

Связь между метамоделями и грамматикой

Выше были ссылки на минимальные демо примеры. А как использовать всё это уже в реальном инструменте моделирования?

Для каждого языка моделирования (модели классов, C4, VAD, EPC, ...) у нас есть метамодель, которая описывает структуру соответствующих моделей (модель классов состоит из классов и типов данных, классы содержат свойства двух типов: атрибуты и ссылки и т. д.):

Метамодель для моделей классов
Метамодель для моделей классов

Чтобы добавить DSL (текстовую нотацию) для вашего языка моделирования достаточно добавить грамматику в поле Grammar на форме свойств. В будущих версиях мы сделаем адекватный редактор грамматики типа такого (исходный код):

Пример редактора грамматики и расширений для неё
Пример редактора грамматики и расширений для неё

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

Заключение

В нашем инструменте моделирования размывается граница между подходами «something as code» и «something as model», размывается граница между текстовыми DSL и визуальными языками моделирования. В будущих версиях немного причешем табличные представления для моделей и размоем границу между инструментами моделирования и учётными системами.

Что вы думаете по поводу всего этого?

Комментарии (9)


  1. VitaminND
    22.08.2025 05:49

    Сдается мне, что в ближайшее время подход «something as code» выживет, если только код будет генерить AI. Ибо на набивку кода вручную тратится много времени.

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


    1. Ares_ekb Автор
      22.08.2025 05:49

      А какая соседняя ветка? Не могу найти... У нас документация тоже генерится:

      Насчёт ИИ согласен, в принципе это один из сценариев для чего мы прикручивали текстовый редактор к моделеру. В целом, я думаю, что модели — это промежуточное звено между промптом пользователя и конечным результатом. Есть много областей, где не работает сценарий: «промпт → какая‑то ИИ магия → критичный код для управления атомной станцией». Но зато работает сценарий: «промпт → верхнеуровневая модель → более детальная модель → ещё более детальная модель → конечный артефакт». Каждая из промежуточных моделей может быть верифицирована пользователем или автоматически. Я уверен, что по крайней мере в критичных областях ИИ будет развиваться в эту сторону


  1. itGuevara
    22.08.2025 05:49

    1 Концепт заставок схожий.

    2 Подобное (diagramascode) в части инструментов складываю тут. Тоже в этом направлении пытаюсь "копать" (semanticBPM), но с семантическим уклоном (лопаткой с "семантической рукояткой", пример семантики Linked Data в медицине).
    Например, два фрагмента к будущей статьи:
    - Выше упомянуты две метамодели (концептуальные модели). Первая – «семантическая обертка» (DSL-sem) – основная, она задает типы объектов и их связей (взаимосвязей), например, говорит, что такие-то пары объектов могут иметь только такие-то типы связей (отношений). Примеры метамоделей: Togaf\ archimate или масса табличек из Aris (4000+ страничек с семантикой).

    Второй тип метамоделей, DSL-syn (условно, т.к. это метамодели синтаксиса, не семантики) – это формат обертки «графическая грамматика» (легенда условных знаков, как в картографии). Она определяет, как графический вид объектов и связей, так и специфику соединений, например, тип связи «следует» в VAD может исходить только с левой и правой стороны элемента «процесс», а «имеет исполнителя» только из нижней. ...

    - Варианты формирования схем:

    а) Классический: смысл "сразу в картинку", как в классических BPM (ARIS), т.е. "само перетаскивание" объекта из боковика template на "холст" (схему) задает (неявно) семантический триплет;

    б) «смысл в скрипте» и автопостроение (рендер), как в «Диаграмма как код», или более «длинный путь: смысл в DSL-sem -> DSL-syn -> DSL-synXY -> XML (как на схеме);

    в) комбинированный: классический, «Диаграмма как код» и наоборот: генерация по схеме кода (в идеале двухсторонняя связка);

    г) «самопальный» dot, т.е. собственный алгоритм размещения объектов на схеме. Стандартные алгоритмы dot \ mermaid, PlantUML и др. достаточно ограничены. Часто получить желаемое отображение невозможно. Иногда вообще нужно задать алгоритм разнесения схемы с бесконечного холста на несколько листов А4, сохраняя при этом визуализацию непрерывности алгоритма, например, через специальные маркеры перехода между листами, например, цифровые идентификаторы, продублированные на листах как точки перехода. В visio есть мастер орг-структур, который позволяет строить многостраничные иерархический схемы.

    «Самопальный» dot» - это собственный алгоритм авто-размещения объектов на холсте (схеме), включая даже такие «фишки» как ранжирование одного типа объекта, например, слева направо размещаются подразделения «по важности».

    3 Вопрос.

    Смотрим на Вашу схему VAD.

    Тезисно скажите. Как причесать VAD: исполнители и инструменты ровно под блоком "процесс" (VAD-кораблик), входящие и исходящие документы сверху, как тут. По VAD тут.
    4 Правильно я понимаю, что я в architeezy могу вначале сгенерить схему по коду, а потом ее же править вручную в графическом редакторе.

    5 Как-нибудь репозитарий объектов можно подключить, чтобы вести любой набор свойств каждого объекта на схеме? Если не полноценный репозитарий, то хотя бы как сделано в drawio\archi\visio, где пользовательские атрибуты хранятся в XML.

    6 Какой формат хранения? Описание XML.

    7 "github & DAC": github умеет в Mermaid, gitlab в plantUML. Как на github сделать рендеринг PlantUML или dot? Видимо можно проще, чем тут обсуждают.


    1. Ares_ekb Автор
      22.08.2025 05:49

      1) Для заставки я написал промпт «Волшебница превращает модель в код». Не ожидал, что ChatGPT поймёт слово «модель» таким образом, но переделывать не стал )

      2) У нас подход очень простой. Модель — это граф. Граф в математическом смысле (вершины, связи, атрибуты), не диаграмма. Граф хранится в JSON. Он может быть визуализирован разными способами:

      • в виде диаграммы (диаграмма это отдельный от модели JSON, в котором хранится информация о координатах и размерах фигур, цветах и т. д.)

      • в виде текста

      • в виде дерева

      • в виде таблицы

      • ...

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

      Наверное это ближе всего к варианту «в» из вашего описания

      3) Просто перетащить эти объекты на диаграмме. У нас диаграммы редактируемые в отличие от PlantUML. Конкретно эта модель не редактируется потому что для этого нужен доступ. Можно создать свой проект, в нём VAD модель и тогда она будет редактируемая

      4) У нас нет в явном виде генерации схемы по коду как в PlantUML. Я просто открываю модель в удобном для меня редакторе, при этом обновляются другие. Могу одновременно открыть одну и ту же модель в текстовом и диаграммном редакторах, вносить изменения в любом из них, при этом в реальном времени будет обновляться второй. Это видно в видео. Редактирую текст — сразу обновляется диаграмма. Редактирую диаграмму — сразу обновляется текст

      5) У нас полноценный репозиторий моделей. С API, с управлением доступом к моделям (дискреционный и мандатный на трёх уровнях: пространство моделирования, проект, модель), с прицелом на версионирование моделей (но пока нет)

      Для объектов и связей хранятся атрибуты. Для любых объектов можно задавать любые атрибуты, это настраивается на уровне метамодели. Модель с атрибутами хранится в JSON и доступна через API

      Можно ли подключать внешний репозиторий? Есть два варианта:

      • Проще всего реализовать интеграцию, чтобы из стороннего репозитория модели по API передавались в наш или наоборот забирались из нашего репозитория во внешний

      • Можно написать свой компонент хранения моделей. Он достаточно простой и изолированный

      6) Формат хранения моделей описан в стандарте OMG XML Metadata Interchange. Он поддерживается во многих инструментах моделирования. Но с поправкой на то, что заменили XML на JSON. Добавить API для моделей в XML формате не долго. Тем более что это и есть изначальный формат, просто с JSON удобнее работать

      7) У нас можно генерировать из моделей Markdown документы с Mermaid диаграммами. Выше пример. т. е. у нас рендерится Mermaid. PlantUML тоже можно будет прикрутить, но с ним больше сложностей, чем с Mermaid — нужно отправлять запросы на сервер (в отличие от Mermaid, который генерит диаграммы в браузере), это дополнительная морока, возможно поэтому он не поддерживается в GitHub


  1. itGuevara
    22.08.2025 05:49

    1. Возможно какой‑нибудь крупной компании понадобится инструмент моделирования и она вложится в развитие open source проекта

    Новостей нет?


    1. Ares_ekb Автор
      22.08.2025 05:49

      Новостей пока нет. Мне немного грустно от того, что мы вроде делаем интересный, нетривиальный проект. Но здесь статьи не очень заходят. Хотя я стараюсь добавлять технических деталей, делать демки с исходниками. Чтобы всё это реализовать, причесать код, написать статью нужно достаточно много усилий. Хорошо, что KPI, который я себе установил — это не количество плюсов к статье :)


      1. itGuevara
        22.08.2025 05:49

        мы вроде делаем интересный, нетривиальный проект. 

        Повернуть (позиционировать) бы его в сторону open source аналога businessstudio или силыюнион - раз уже есть у Вас репозитарий. Хотя бы минимум в open source выложить и возможно "народ за тобой (к тебе) потянется". Ценник на ARIS-подобное - сейчас "задран" и все там проприетарное. Однако спрос есть.

        "Минимум" - имеется ввиду, что корпорат сможет "затащить" к себе в периметр (обычно это ключевое условие для BPM\EA в "серьезных компаниях") и сделать нечто подобное (miniARIS) c процессными или архитектурными схемами (нотациями BPM \ EA), пусть небольшое, но жизнеспособное. А потом уже пойдут к Вам за развитием и \ или техподдержкой. "Первая доза" должна быть бесплатна. Первопроходцу - разработчику (нет пока опенсорсного BPMа) - должно повезти.


        1. Ares_ekb Автор
          22.08.2025 05:49

          Меня пока что-то пугает в этом шаге, хотя возможно зря

          сделать нечто подобное (miniARIS)

          Технически у нас это уже есть и гораздо больше. Я думаю не хватает примеров конкретных моделей...


          1. itGuevara
            22.08.2025 05:49

            Я думаю не хватает примеров конкретных моделей...

            Если будет выложен минимум в open source, то готов на нем собрать открытый каталог верхнеуровневых процессов типового банка (как минимум сотню VAD схем с паспортами процессов). Подобного ещё нигде не встречал. Только конечно инструмент должен быть удобным. Как минимум не хуже Visio в связке с excel (последнее как репозитарий). Visio кстати - базовая платформа для многих BPM систем: bpm-x, БС, enterprise explorer, iServer и др.

            Да, это предложение - для любого open source проекта класса BPM / ea.